18c2ecf20Sopenharmony_ci#!/usr/bin/env python3 28c2ecf20Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0 38c2ecf20Sopenharmony_ci# 48c2ecf20Sopenharmony_ci# Test that truncation of bprm->buf doesn't cause unexpected execs paths, along 58c2ecf20Sopenharmony_ci# with various other pathological cases. 68c2ecf20Sopenharmony_ciimport os, subprocess 78c2ecf20Sopenharmony_ci 88c2ecf20Sopenharmony_ci# Relevant commits 98c2ecf20Sopenharmony_ci# 108c2ecf20Sopenharmony_ci# b5372fe5dc84 ("exec: load_script: Do not exec truncated interpreter path") 118c2ecf20Sopenharmony_ci# 6eb3c3d0a52d ("exec: increase BINPRM_BUF_SIZE to 256") 128c2ecf20Sopenharmony_ci 138c2ecf20Sopenharmony_ci# BINPRM_BUF_SIZE 148c2ecf20Sopenharmony_ciSIZE=256 158c2ecf20Sopenharmony_ci 168c2ecf20Sopenharmony_ciNAME_MAX=int(subprocess.check_output(["getconf", "NAME_MAX", "."])) 178c2ecf20Sopenharmony_ci 188c2ecf20Sopenharmony_citest_num=0 198c2ecf20Sopenharmony_ci 208c2ecf20Sopenharmony_cicode='''#!/usr/bin/perl 218c2ecf20Sopenharmony_ciprint "Executed interpreter! Args:\n"; 228c2ecf20Sopenharmony_ciprint "0 : '$0'\n"; 238c2ecf20Sopenharmony_ci$counter = 1; 248c2ecf20Sopenharmony_ciforeach my $a (@ARGV) { 258c2ecf20Sopenharmony_ci print "$counter : '$a'\n"; 268c2ecf20Sopenharmony_ci $counter++; 278c2ecf20Sopenharmony_ci} 288c2ecf20Sopenharmony_ci''' 298c2ecf20Sopenharmony_ci 308c2ecf20Sopenharmony_ci## 318c2ecf20Sopenharmony_ci# test - produce a binfmt_script hashbang line for testing 328c2ecf20Sopenharmony_ci# 338c2ecf20Sopenharmony_ci# @size: bytes for bprm->buf line, including hashbang but not newline 348c2ecf20Sopenharmony_ci# @good: whether this script is expected to execute correctly 358c2ecf20Sopenharmony_ci# @hashbang: the special 2 bytes for running binfmt_script 368c2ecf20Sopenharmony_ci# @leading: any leading whitespace before the executable path 378c2ecf20Sopenharmony_ci# @root: start of executable pathname 388c2ecf20Sopenharmony_ci# @target: end of executable pathname 398c2ecf20Sopenharmony_ci# @arg: bytes following the executable pathname 408c2ecf20Sopenharmony_ci# @fill: character to fill between @root and @target to reach @size bytes 418c2ecf20Sopenharmony_ci# @newline: character to use as newline, not counted towards @size 428c2ecf20Sopenharmony_ci# ... 438c2ecf20Sopenharmony_cidef test(name, size, good=True, leading="", root="./", target="/perl", 448c2ecf20Sopenharmony_ci fill="A", arg="", newline="\n", hashbang="#!"): 458c2ecf20Sopenharmony_ci global test_num, tests, NAME_MAX 468c2ecf20Sopenharmony_ci test_num += 1 478c2ecf20Sopenharmony_ci if test_num > tests: 488c2ecf20Sopenharmony_ci raise ValueError("more binfmt_script tests than expected! (want %d, expected %d)" 498c2ecf20Sopenharmony_ci % (test_num, tests)) 508c2ecf20Sopenharmony_ci 518c2ecf20Sopenharmony_ci middle = "" 528c2ecf20Sopenharmony_ci remaining = size - len(hashbang) - len(leading) - len(root) - len(target) - len(arg) 538c2ecf20Sopenharmony_ci # The middle of the pathname must not exceed NAME_MAX 548c2ecf20Sopenharmony_ci while remaining >= NAME_MAX: 558c2ecf20Sopenharmony_ci middle += fill * (NAME_MAX - 1) 568c2ecf20Sopenharmony_ci middle += '/' 578c2ecf20Sopenharmony_ci remaining -= NAME_MAX 588c2ecf20Sopenharmony_ci middle += fill * remaining 598c2ecf20Sopenharmony_ci 608c2ecf20Sopenharmony_ci dirpath = root + middle 618c2ecf20Sopenharmony_ci binary = dirpath + target 628c2ecf20Sopenharmony_ci if len(target): 638c2ecf20Sopenharmony_ci os.makedirs(dirpath, mode=0o755, exist_ok=True) 648c2ecf20Sopenharmony_ci open(binary, "w").write(code) 658c2ecf20Sopenharmony_ci os.chmod(binary, 0o755) 668c2ecf20Sopenharmony_ci 678c2ecf20Sopenharmony_ci buf=hashbang + leading + root + middle + target + arg + newline 688c2ecf20Sopenharmony_ci if len(newline) > 0: 698c2ecf20Sopenharmony_ci buf += 'echo this is not really perl\n' 708c2ecf20Sopenharmony_ci 718c2ecf20Sopenharmony_ci script = "binfmt_script-%s" % (name) 728c2ecf20Sopenharmony_ci open(script, "w").write(buf) 738c2ecf20Sopenharmony_ci os.chmod(script, 0o755) 748c2ecf20Sopenharmony_ci 758c2ecf20Sopenharmony_ci proc = subprocess.Popen(["./%s" % (script)], shell=True, 768c2ecf20Sopenharmony_ci stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 778c2ecf20Sopenharmony_ci stdout = proc.communicate()[0] 788c2ecf20Sopenharmony_ci 798c2ecf20Sopenharmony_ci if proc.returncode == 0 and b'Executed interpreter' in stdout: 808c2ecf20Sopenharmony_ci if good: 818c2ecf20Sopenharmony_ci print("ok %d - binfmt_script %s (successful good exec)" 828c2ecf20Sopenharmony_ci % (test_num, name)) 838c2ecf20Sopenharmony_ci else: 848c2ecf20Sopenharmony_ci print("not ok %d - binfmt_script %s succeeded when it should have failed" 858c2ecf20Sopenharmony_ci % (test_num, name)) 868c2ecf20Sopenharmony_ci else: 878c2ecf20Sopenharmony_ci if good: 888c2ecf20Sopenharmony_ci print("not ok %d - binfmt_script %s failed when it should have succeeded (rc:%d)" 898c2ecf20Sopenharmony_ci % (test_num, name, proc.returncode)) 908c2ecf20Sopenharmony_ci else: 918c2ecf20Sopenharmony_ci print("ok %d - binfmt_script %s (correctly failed bad exec)" 928c2ecf20Sopenharmony_ci % (test_num, name)) 938c2ecf20Sopenharmony_ci 948c2ecf20Sopenharmony_ci # Clean up crazy binaries 958c2ecf20Sopenharmony_ci os.unlink(script) 968c2ecf20Sopenharmony_ci if len(target): 978c2ecf20Sopenharmony_ci elements = binary.split('/') 988c2ecf20Sopenharmony_ci os.unlink(binary) 998c2ecf20Sopenharmony_ci elements.pop() 1008c2ecf20Sopenharmony_ci while len(elements) > 1: 1018c2ecf20Sopenharmony_ci os.rmdir("/".join(elements)) 1028c2ecf20Sopenharmony_ci elements.pop() 1038c2ecf20Sopenharmony_ci 1048c2ecf20Sopenharmony_citests=27 1058c2ecf20Sopenharmony_ciprint("TAP version 1.3") 1068c2ecf20Sopenharmony_ciprint("1..%d" % (tests)) 1078c2ecf20Sopenharmony_ci 1088c2ecf20Sopenharmony_ci### FAIL (8 tests) 1098c2ecf20Sopenharmony_ci 1108c2ecf20Sopenharmony_ci# Entire path is well past the BINFMT_BUF_SIZE. 1118c2ecf20Sopenharmony_citest(name="too-big", size=SIZE+80, good=False) 1128c2ecf20Sopenharmony_ci# Path is right at max size, making it impossible to tell if it was truncated. 1138c2ecf20Sopenharmony_citest(name="exact", size=SIZE, good=False) 1148c2ecf20Sopenharmony_ci# Same as above, but with leading whitespace. 1158c2ecf20Sopenharmony_citest(name="exact-space", size=SIZE, good=False, leading=" ") 1168c2ecf20Sopenharmony_ci# Huge buffer of only whitespace. 1178c2ecf20Sopenharmony_citest(name="whitespace-too-big", size=SIZE+71, good=False, root="", 1188c2ecf20Sopenharmony_ci fill=" ", target="") 1198c2ecf20Sopenharmony_ci# A good path, but it gets truncated due to leading whitespace. 1208c2ecf20Sopenharmony_citest(name="truncated", size=SIZE+17, good=False, leading=" " * 19) 1218c2ecf20Sopenharmony_ci# Entirely empty except for #! 1228c2ecf20Sopenharmony_citest(name="empty", size=2, good=False, root="", 1238c2ecf20Sopenharmony_ci fill="", target="", newline="") 1248c2ecf20Sopenharmony_ci# Within size, but entirely spaces 1258c2ecf20Sopenharmony_citest(name="spaces", size=SIZE-1, good=False, root="", fill=" ", 1268c2ecf20Sopenharmony_ci target="", newline="") 1278c2ecf20Sopenharmony_ci# Newline before binary. 1288c2ecf20Sopenharmony_citest(name="newline-prefix", size=SIZE-1, good=False, leading="\n", 1298c2ecf20Sopenharmony_ci root="", fill=" ", target="") 1308c2ecf20Sopenharmony_ci 1318c2ecf20Sopenharmony_ci### ok (19 tests) 1328c2ecf20Sopenharmony_ci 1338c2ecf20Sopenharmony_ci# The original test case that was broken by commit: 1348c2ecf20Sopenharmony_ci# 8099b047ecc4 ("exec: load_script: don't blindly truncate shebang string") 1358c2ecf20Sopenharmony_citest(name="test.pl", size=439, leading=" ", 1368c2ecf20Sopenharmony_ci root="./nix/store/bwav8kz8b3y471wjsybgzw84mrh4js9-perl-5.28.1/bin", 1378c2ecf20Sopenharmony_ci arg=" -I/nix/store/x6yyav38jgr924nkna62q3pkp0dgmzlx-perl5.28.1-File-Slurp-9999.25/lib/perl5/site_perl -I/nix/store/ha8v67sl8dac92r9z07vzr4gv1y9nwqz-perl5.28.1-Net-DBus-1.1.0/lib/perl5/site_perl -I/nix/store/dcrkvnjmwh69ljsvpbdjjdnqgwx90a9d-perl5.28.1-XML-Parser-2.44/lib/perl5/site_perl -I/nix/store/rmji88k2zz7h4zg97385bygcydrf2q8h-perl5.28.1-XML-Twig-3.52/lib/perl5/site_perl") 1388c2ecf20Sopenharmony_ci# One byte under size, leaving newline visible. 1398c2ecf20Sopenharmony_citest(name="one-under", size=SIZE-1) 1408c2ecf20Sopenharmony_ci# Two bytes under size, leaving newline visible. 1418c2ecf20Sopenharmony_citest(name="two-under", size=SIZE-2) 1428c2ecf20Sopenharmony_ci# Exact size, but trailing whitespace visible instead of newline 1438c2ecf20Sopenharmony_citest(name="exact-trunc-whitespace", size=SIZE, arg=" ") 1448c2ecf20Sopenharmony_ci# Exact size, but trailing space and first arg char visible instead of newline. 1458c2ecf20Sopenharmony_citest(name="exact-trunc-arg", size=SIZE, arg=" f") 1468c2ecf20Sopenharmony_ci# One bute under, with confirmed non-truncated arg since newline now visible. 1478c2ecf20Sopenharmony_citest(name="one-under-full-arg", size=SIZE-1, arg=" f") 1488c2ecf20Sopenharmony_ci# Short read buffer by one byte. 1498c2ecf20Sopenharmony_citest(name="one-under-no-nl", size=SIZE-1, newline="") 1508c2ecf20Sopenharmony_ci# Short read buffer by half buffer size. 1518c2ecf20Sopenharmony_citest(name="half-under-no-nl", size=int(SIZE/2), newline="") 1528c2ecf20Sopenharmony_ci# One byte under with whitespace arg. leaving wenline visible. 1538c2ecf20Sopenharmony_citest(name="one-under-trunc-arg", size=SIZE-1, arg=" ") 1548c2ecf20Sopenharmony_ci# One byte under with whitespace leading. leaving wenline visible. 1558c2ecf20Sopenharmony_citest(name="one-under-leading", size=SIZE-1, leading=" ") 1568c2ecf20Sopenharmony_ci# One byte under with whitespace leading and as arg. leaving newline visible. 1578c2ecf20Sopenharmony_citest(name="one-under-leading-trunc-arg", size=SIZE-1, leading=" ", arg=" ") 1588c2ecf20Sopenharmony_ci# Same as above, but with 2 bytes under 1598c2ecf20Sopenharmony_citest(name="two-under-no-nl", size=SIZE-2, newline="") 1608c2ecf20Sopenharmony_citest(name="two-under-trunc-arg", size=SIZE-2, arg=" ") 1618c2ecf20Sopenharmony_citest(name="two-under-leading", size=SIZE-2, leading=" ") 1628c2ecf20Sopenharmony_citest(name="two-under-leading-trunc-arg", size=SIZE-2, leading=" ", arg=" ") 1638c2ecf20Sopenharmony_ci# Same as above, but with buffer half filled 1648c2ecf20Sopenharmony_citest(name="two-under-no-nl", size=int(SIZE/2), newline="") 1658c2ecf20Sopenharmony_citest(name="two-under-trunc-arg", size=int(SIZE/2), arg=" ") 1668c2ecf20Sopenharmony_citest(name="two-under-leading", size=int(SIZE/2), leading=" ") 1678c2ecf20Sopenharmony_citest(name="two-under-lead-trunc-arg", size=int(SIZE/2), leading=" ", arg=" ") 1688c2ecf20Sopenharmony_ci 1698c2ecf20Sopenharmony_ciif test_num != tests: 1708c2ecf20Sopenharmony_ci raise ValueError("fewer binfmt_script tests than expected! (ran %d, expected %d" 1718c2ecf20Sopenharmony_ci % (test_num, tests)) 172