162306a36Sopenharmony_ci#!/usr/bin/env python3 262306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0 362306a36Sopenharmony_ci# 462306a36Sopenharmony_ci# Test that truncation of bprm->buf doesn't cause unexpected execs paths, along 562306a36Sopenharmony_ci# with various other pathological cases. 662306a36Sopenharmony_ciimport os, subprocess 762306a36Sopenharmony_ci 862306a36Sopenharmony_ci# Relevant commits 962306a36Sopenharmony_ci# 1062306a36Sopenharmony_ci# b5372fe5dc84 ("exec: load_script: Do not exec truncated interpreter path") 1162306a36Sopenharmony_ci# 6eb3c3d0a52d ("exec: increase BINPRM_BUF_SIZE to 256") 1262306a36Sopenharmony_ci 1362306a36Sopenharmony_ci# BINPRM_BUF_SIZE 1462306a36Sopenharmony_ciSIZE=256 1562306a36Sopenharmony_ci 1662306a36Sopenharmony_ciNAME_MAX=int(subprocess.check_output(["getconf", "NAME_MAX", "."])) 1762306a36Sopenharmony_ci 1862306a36Sopenharmony_citest_num=0 1962306a36Sopenharmony_ci 2062306a36Sopenharmony_cicode='''#!/usr/bin/perl 2162306a36Sopenharmony_ciprint "Executed interpreter! Args:\n"; 2262306a36Sopenharmony_ciprint "0 : '$0'\n"; 2362306a36Sopenharmony_ci$counter = 1; 2462306a36Sopenharmony_ciforeach my $a (@ARGV) { 2562306a36Sopenharmony_ci print "$counter : '$a'\n"; 2662306a36Sopenharmony_ci $counter++; 2762306a36Sopenharmony_ci} 2862306a36Sopenharmony_ci''' 2962306a36Sopenharmony_ci 3062306a36Sopenharmony_ci## 3162306a36Sopenharmony_ci# test - produce a binfmt_script hashbang line for testing 3262306a36Sopenharmony_ci# 3362306a36Sopenharmony_ci# @size: bytes for bprm->buf line, including hashbang but not newline 3462306a36Sopenharmony_ci# @good: whether this script is expected to execute correctly 3562306a36Sopenharmony_ci# @hashbang: the special 2 bytes for running binfmt_script 3662306a36Sopenharmony_ci# @leading: any leading whitespace before the executable path 3762306a36Sopenharmony_ci# @root: start of executable pathname 3862306a36Sopenharmony_ci# @target: end of executable pathname 3962306a36Sopenharmony_ci# @arg: bytes following the executable pathname 4062306a36Sopenharmony_ci# @fill: character to fill between @root and @target to reach @size bytes 4162306a36Sopenharmony_ci# @newline: character to use as newline, not counted towards @size 4262306a36Sopenharmony_ci# ... 4362306a36Sopenharmony_cidef test(name, size, good=True, leading="", root="./", target="/perl", 4462306a36Sopenharmony_ci fill="A", arg="", newline="\n", hashbang="#!"): 4562306a36Sopenharmony_ci global test_num, tests, NAME_MAX 4662306a36Sopenharmony_ci test_num += 1 4762306a36Sopenharmony_ci if test_num > tests: 4862306a36Sopenharmony_ci raise ValueError("more binfmt_script tests than expected! (want %d, expected %d)" 4962306a36Sopenharmony_ci % (test_num, tests)) 5062306a36Sopenharmony_ci 5162306a36Sopenharmony_ci middle = "" 5262306a36Sopenharmony_ci remaining = size - len(hashbang) - len(leading) - len(root) - len(target) - len(arg) 5362306a36Sopenharmony_ci # The middle of the pathname must not exceed NAME_MAX 5462306a36Sopenharmony_ci while remaining >= NAME_MAX: 5562306a36Sopenharmony_ci middle += fill * (NAME_MAX - 1) 5662306a36Sopenharmony_ci middle += '/' 5762306a36Sopenharmony_ci remaining -= NAME_MAX 5862306a36Sopenharmony_ci middle += fill * remaining 5962306a36Sopenharmony_ci 6062306a36Sopenharmony_ci dirpath = root + middle 6162306a36Sopenharmony_ci binary = dirpath + target 6262306a36Sopenharmony_ci if len(target): 6362306a36Sopenharmony_ci os.makedirs(dirpath, mode=0o755, exist_ok=True) 6462306a36Sopenharmony_ci open(binary, "w").write(code) 6562306a36Sopenharmony_ci os.chmod(binary, 0o755) 6662306a36Sopenharmony_ci 6762306a36Sopenharmony_ci buf=hashbang + leading + root + middle + target + arg + newline 6862306a36Sopenharmony_ci if len(newline) > 0: 6962306a36Sopenharmony_ci buf += 'echo this is not really perl\n' 7062306a36Sopenharmony_ci 7162306a36Sopenharmony_ci script = "binfmt_script-%s" % (name) 7262306a36Sopenharmony_ci open(script, "w").write(buf) 7362306a36Sopenharmony_ci os.chmod(script, 0o755) 7462306a36Sopenharmony_ci 7562306a36Sopenharmony_ci proc = subprocess.Popen(["./%s" % (script)], shell=True, 7662306a36Sopenharmony_ci stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 7762306a36Sopenharmony_ci stdout = proc.communicate()[0] 7862306a36Sopenharmony_ci 7962306a36Sopenharmony_ci if proc.returncode == 0 and b'Executed interpreter' in stdout: 8062306a36Sopenharmony_ci if good: 8162306a36Sopenharmony_ci print("ok %d - binfmt_script %s (successful good exec)" 8262306a36Sopenharmony_ci % (test_num, name)) 8362306a36Sopenharmony_ci else: 8462306a36Sopenharmony_ci print("not ok %d - binfmt_script %s succeeded when it should have failed" 8562306a36Sopenharmony_ci % (test_num, name)) 8662306a36Sopenharmony_ci else: 8762306a36Sopenharmony_ci if good: 8862306a36Sopenharmony_ci print("not ok %d - binfmt_script %s failed when it should have succeeded (rc:%d)" 8962306a36Sopenharmony_ci % (test_num, name, proc.returncode)) 9062306a36Sopenharmony_ci else: 9162306a36Sopenharmony_ci print("ok %d - binfmt_script %s (correctly failed bad exec)" 9262306a36Sopenharmony_ci % (test_num, name)) 9362306a36Sopenharmony_ci 9462306a36Sopenharmony_ci # Clean up crazy binaries 9562306a36Sopenharmony_ci os.unlink(script) 9662306a36Sopenharmony_ci if len(target): 9762306a36Sopenharmony_ci elements = binary.split('/') 9862306a36Sopenharmony_ci os.unlink(binary) 9962306a36Sopenharmony_ci elements.pop() 10062306a36Sopenharmony_ci while len(elements) > 1: 10162306a36Sopenharmony_ci os.rmdir("/".join(elements)) 10262306a36Sopenharmony_ci elements.pop() 10362306a36Sopenharmony_ci 10462306a36Sopenharmony_citests=27 10562306a36Sopenharmony_ciprint("TAP version 1.3") 10662306a36Sopenharmony_ciprint("1..%d" % (tests)) 10762306a36Sopenharmony_ci 10862306a36Sopenharmony_ci### FAIL (8 tests) 10962306a36Sopenharmony_ci 11062306a36Sopenharmony_ci# Entire path is well past the BINFMT_BUF_SIZE. 11162306a36Sopenharmony_citest(name="too-big", size=SIZE+80, good=False) 11262306a36Sopenharmony_ci# Path is right at max size, making it impossible to tell if it was truncated. 11362306a36Sopenharmony_citest(name="exact", size=SIZE, good=False) 11462306a36Sopenharmony_ci# Same as above, but with leading whitespace. 11562306a36Sopenharmony_citest(name="exact-space", size=SIZE, good=False, leading=" ") 11662306a36Sopenharmony_ci# Huge buffer of only whitespace. 11762306a36Sopenharmony_citest(name="whitespace-too-big", size=SIZE+71, good=False, root="", 11862306a36Sopenharmony_ci fill=" ", target="") 11962306a36Sopenharmony_ci# A good path, but it gets truncated due to leading whitespace. 12062306a36Sopenharmony_citest(name="truncated", size=SIZE+17, good=False, leading=" " * 19) 12162306a36Sopenharmony_ci# Entirely empty except for #! 12262306a36Sopenharmony_citest(name="empty", size=2, good=False, root="", 12362306a36Sopenharmony_ci fill="", target="", newline="") 12462306a36Sopenharmony_ci# Within size, but entirely spaces 12562306a36Sopenharmony_citest(name="spaces", size=SIZE-1, good=False, root="", fill=" ", 12662306a36Sopenharmony_ci target="", newline="") 12762306a36Sopenharmony_ci# Newline before binary. 12862306a36Sopenharmony_citest(name="newline-prefix", size=SIZE-1, good=False, leading="\n", 12962306a36Sopenharmony_ci root="", fill=" ", target="") 13062306a36Sopenharmony_ci 13162306a36Sopenharmony_ci### ok (19 tests) 13262306a36Sopenharmony_ci 13362306a36Sopenharmony_ci# The original test case that was broken by commit: 13462306a36Sopenharmony_ci# 8099b047ecc4 ("exec: load_script: don't blindly truncate shebang string") 13562306a36Sopenharmony_citest(name="test.pl", size=439, leading=" ", 13662306a36Sopenharmony_ci root="./nix/store/bwav8kz8b3y471wjsybgzw84mrh4js9-perl-5.28.1/bin", 13762306a36Sopenharmony_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") 13862306a36Sopenharmony_ci# One byte under size, leaving newline visible. 13962306a36Sopenharmony_citest(name="one-under", size=SIZE-1) 14062306a36Sopenharmony_ci# Two bytes under size, leaving newline visible. 14162306a36Sopenharmony_citest(name="two-under", size=SIZE-2) 14262306a36Sopenharmony_ci# Exact size, but trailing whitespace visible instead of newline 14362306a36Sopenharmony_citest(name="exact-trunc-whitespace", size=SIZE, arg=" ") 14462306a36Sopenharmony_ci# Exact size, but trailing space and first arg char visible instead of newline. 14562306a36Sopenharmony_citest(name="exact-trunc-arg", size=SIZE, arg=" f") 14662306a36Sopenharmony_ci# One bute under, with confirmed non-truncated arg since newline now visible. 14762306a36Sopenharmony_citest(name="one-under-full-arg", size=SIZE-1, arg=" f") 14862306a36Sopenharmony_ci# Short read buffer by one byte. 14962306a36Sopenharmony_citest(name="one-under-no-nl", size=SIZE-1, newline="") 15062306a36Sopenharmony_ci# Short read buffer by half buffer size. 15162306a36Sopenharmony_citest(name="half-under-no-nl", size=int(SIZE/2), newline="") 15262306a36Sopenharmony_ci# One byte under with whitespace arg. leaving wenline visible. 15362306a36Sopenharmony_citest(name="one-under-trunc-arg", size=SIZE-1, arg=" ") 15462306a36Sopenharmony_ci# One byte under with whitespace leading. leaving wenline visible. 15562306a36Sopenharmony_citest(name="one-under-leading", size=SIZE-1, leading=" ") 15662306a36Sopenharmony_ci# One byte under with whitespace leading and as arg. leaving newline visible. 15762306a36Sopenharmony_citest(name="one-under-leading-trunc-arg", size=SIZE-1, leading=" ", arg=" ") 15862306a36Sopenharmony_ci# Same as above, but with 2 bytes under 15962306a36Sopenharmony_citest(name="two-under-no-nl", size=SIZE-2, newline="") 16062306a36Sopenharmony_citest(name="two-under-trunc-arg", size=SIZE-2, arg=" ") 16162306a36Sopenharmony_citest(name="two-under-leading", size=SIZE-2, leading=" ") 16262306a36Sopenharmony_citest(name="two-under-leading-trunc-arg", size=SIZE-2, leading=" ", arg=" ") 16362306a36Sopenharmony_ci# Same as above, but with buffer half filled 16462306a36Sopenharmony_citest(name="two-under-no-nl", size=int(SIZE/2), newline="") 16562306a36Sopenharmony_citest(name="two-under-trunc-arg", size=int(SIZE/2), arg=" ") 16662306a36Sopenharmony_citest(name="two-under-leading", size=int(SIZE/2), leading=" ") 16762306a36Sopenharmony_citest(name="two-under-lead-trunc-arg", size=int(SIZE/2), leading=" ", arg=" ") 16862306a36Sopenharmony_ci 16962306a36Sopenharmony_ciif test_num != tests: 17062306a36Sopenharmony_ci raise ValueError("fewer binfmt_script tests than expected! (ran %d, expected %d" 17162306a36Sopenharmony_ci % (test_num, tests)) 172