1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# Test that truncation of bprm->buf doesn't cause unexpected execs paths, along 5*4882a593Smuzhiyun# with various other pathological cases. 6*4882a593Smuzhiyunimport os, subprocess 7*4882a593Smuzhiyun 8*4882a593Smuzhiyun# Relevant commits 9*4882a593Smuzhiyun# 10*4882a593Smuzhiyun# b5372fe5dc84 ("exec: load_script: Do not exec truncated interpreter path") 11*4882a593Smuzhiyun# 6eb3c3d0a52d ("exec: increase BINPRM_BUF_SIZE to 256") 12*4882a593Smuzhiyun 13*4882a593Smuzhiyun# BINPRM_BUF_SIZE 14*4882a593SmuzhiyunSIZE=256 15*4882a593Smuzhiyun 16*4882a593SmuzhiyunNAME_MAX=int(subprocess.check_output(["getconf", "NAME_MAX", "."])) 17*4882a593Smuzhiyun 18*4882a593Smuzhiyuntest_num=0 19*4882a593Smuzhiyun 20*4882a593Smuzhiyuncode='''#!/usr/bin/perl 21*4882a593Smuzhiyunprint "Executed interpreter! Args:\n"; 22*4882a593Smuzhiyunprint "0 : '$0'\n"; 23*4882a593Smuzhiyun$counter = 1; 24*4882a593Smuzhiyunforeach my $a (@ARGV) { 25*4882a593Smuzhiyun print "$counter : '$a'\n"; 26*4882a593Smuzhiyun $counter++; 27*4882a593Smuzhiyun} 28*4882a593Smuzhiyun''' 29*4882a593Smuzhiyun 30*4882a593Smuzhiyun## 31*4882a593Smuzhiyun# test - produce a binfmt_script hashbang line for testing 32*4882a593Smuzhiyun# 33*4882a593Smuzhiyun# @size: bytes for bprm->buf line, including hashbang but not newline 34*4882a593Smuzhiyun# @good: whether this script is expected to execute correctly 35*4882a593Smuzhiyun# @hashbang: the special 2 bytes for running binfmt_script 36*4882a593Smuzhiyun# @leading: any leading whitespace before the executable path 37*4882a593Smuzhiyun# @root: start of executable pathname 38*4882a593Smuzhiyun# @target: end of executable pathname 39*4882a593Smuzhiyun# @arg: bytes following the executable pathname 40*4882a593Smuzhiyun# @fill: character to fill between @root and @target to reach @size bytes 41*4882a593Smuzhiyun# @newline: character to use as newline, not counted towards @size 42*4882a593Smuzhiyun# ... 43*4882a593Smuzhiyundef test(name, size, good=True, leading="", root="./", target="/perl", 44*4882a593Smuzhiyun fill="A", arg="", newline="\n", hashbang="#!"): 45*4882a593Smuzhiyun global test_num, tests, NAME_MAX 46*4882a593Smuzhiyun test_num += 1 47*4882a593Smuzhiyun if test_num > tests: 48*4882a593Smuzhiyun raise ValueError("more binfmt_script tests than expected! (want %d, expected %d)" 49*4882a593Smuzhiyun % (test_num, tests)) 50*4882a593Smuzhiyun 51*4882a593Smuzhiyun middle = "" 52*4882a593Smuzhiyun remaining = size - len(hashbang) - len(leading) - len(root) - len(target) - len(arg) 53*4882a593Smuzhiyun # The middle of the pathname must not exceed NAME_MAX 54*4882a593Smuzhiyun while remaining >= NAME_MAX: 55*4882a593Smuzhiyun middle += fill * (NAME_MAX - 1) 56*4882a593Smuzhiyun middle += '/' 57*4882a593Smuzhiyun remaining -= NAME_MAX 58*4882a593Smuzhiyun middle += fill * remaining 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun dirpath = root + middle 61*4882a593Smuzhiyun binary = dirpath + target 62*4882a593Smuzhiyun if len(target): 63*4882a593Smuzhiyun os.makedirs(dirpath, mode=0o755, exist_ok=True) 64*4882a593Smuzhiyun open(binary, "w").write(code) 65*4882a593Smuzhiyun os.chmod(binary, 0o755) 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun buf=hashbang + leading + root + middle + target + arg + newline 68*4882a593Smuzhiyun if len(newline) > 0: 69*4882a593Smuzhiyun buf += 'echo this is not really perl\n' 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun script = "binfmt_script-%s" % (name) 72*4882a593Smuzhiyun open(script, "w").write(buf) 73*4882a593Smuzhiyun os.chmod(script, 0o755) 74*4882a593Smuzhiyun 75*4882a593Smuzhiyun proc = subprocess.Popen(["./%s" % (script)], shell=True, 76*4882a593Smuzhiyun stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 77*4882a593Smuzhiyun stdout = proc.communicate()[0] 78*4882a593Smuzhiyun 79*4882a593Smuzhiyun if proc.returncode == 0 and b'Executed interpreter' in stdout: 80*4882a593Smuzhiyun if good: 81*4882a593Smuzhiyun print("ok %d - binfmt_script %s (successful good exec)" 82*4882a593Smuzhiyun % (test_num, name)) 83*4882a593Smuzhiyun else: 84*4882a593Smuzhiyun print("not ok %d - binfmt_script %s succeeded when it should have failed" 85*4882a593Smuzhiyun % (test_num, name)) 86*4882a593Smuzhiyun else: 87*4882a593Smuzhiyun if good: 88*4882a593Smuzhiyun print("not ok %d - binfmt_script %s failed when it should have succeeded (rc:%d)" 89*4882a593Smuzhiyun % (test_num, name, proc.returncode)) 90*4882a593Smuzhiyun else: 91*4882a593Smuzhiyun print("ok %d - binfmt_script %s (correctly failed bad exec)" 92*4882a593Smuzhiyun % (test_num, name)) 93*4882a593Smuzhiyun 94*4882a593Smuzhiyun # Clean up crazy binaries 95*4882a593Smuzhiyun os.unlink(script) 96*4882a593Smuzhiyun if len(target): 97*4882a593Smuzhiyun elements = binary.split('/') 98*4882a593Smuzhiyun os.unlink(binary) 99*4882a593Smuzhiyun elements.pop() 100*4882a593Smuzhiyun while len(elements) > 1: 101*4882a593Smuzhiyun os.rmdir("/".join(elements)) 102*4882a593Smuzhiyun elements.pop() 103*4882a593Smuzhiyun 104*4882a593Smuzhiyuntests=27 105*4882a593Smuzhiyunprint("TAP version 1.3") 106*4882a593Smuzhiyunprint("1..%d" % (tests)) 107*4882a593Smuzhiyun 108*4882a593Smuzhiyun### FAIL (8 tests) 109*4882a593Smuzhiyun 110*4882a593Smuzhiyun# Entire path is well past the BINFMT_BUF_SIZE. 111*4882a593Smuzhiyuntest(name="too-big", size=SIZE+80, good=False) 112*4882a593Smuzhiyun# Path is right at max size, making it impossible to tell if it was truncated. 113*4882a593Smuzhiyuntest(name="exact", size=SIZE, good=False) 114*4882a593Smuzhiyun# Same as above, but with leading whitespace. 115*4882a593Smuzhiyuntest(name="exact-space", size=SIZE, good=False, leading=" ") 116*4882a593Smuzhiyun# Huge buffer of only whitespace. 117*4882a593Smuzhiyuntest(name="whitespace-too-big", size=SIZE+71, good=False, root="", 118*4882a593Smuzhiyun fill=" ", target="") 119*4882a593Smuzhiyun# A good path, but it gets truncated due to leading whitespace. 120*4882a593Smuzhiyuntest(name="truncated", size=SIZE+17, good=False, leading=" " * 19) 121*4882a593Smuzhiyun# Entirely empty except for #! 122*4882a593Smuzhiyuntest(name="empty", size=2, good=False, root="", 123*4882a593Smuzhiyun fill="", target="", newline="") 124*4882a593Smuzhiyun# Within size, but entirely spaces 125*4882a593Smuzhiyuntest(name="spaces", size=SIZE-1, good=False, root="", fill=" ", 126*4882a593Smuzhiyun target="", newline="") 127*4882a593Smuzhiyun# Newline before binary. 128*4882a593Smuzhiyuntest(name="newline-prefix", size=SIZE-1, good=False, leading="\n", 129*4882a593Smuzhiyun root="", fill=" ", target="") 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun### ok (19 tests) 132*4882a593Smuzhiyun 133*4882a593Smuzhiyun# The original test case that was broken by commit: 134*4882a593Smuzhiyun# 8099b047ecc4 ("exec: load_script: don't blindly truncate shebang string") 135*4882a593Smuzhiyuntest(name="test.pl", size=439, leading=" ", 136*4882a593Smuzhiyun root="./nix/store/bwav8kz8b3y471wjsybgzw84mrh4js9-perl-5.28.1/bin", 137*4882a593Smuzhiyun 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") 138*4882a593Smuzhiyun# One byte under size, leaving newline visible. 139*4882a593Smuzhiyuntest(name="one-under", size=SIZE-1) 140*4882a593Smuzhiyun# Two bytes under size, leaving newline visible. 141*4882a593Smuzhiyuntest(name="two-under", size=SIZE-2) 142*4882a593Smuzhiyun# Exact size, but trailing whitespace visible instead of newline 143*4882a593Smuzhiyuntest(name="exact-trunc-whitespace", size=SIZE, arg=" ") 144*4882a593Smuzhiyun# Exact size, but trailing space and first arg char visible instead of newline. 145*4882a593Smuzhiyuntest(name="exact-trunc-arg", size=SIZE, arg=" f") 146*4882a593Smuzhiyun# One bute under, with confirmed non-truncated arg since newline now visible. 147*4882a593Smuzhiyuntest(name="one-under-full-arg", size=SIZE-1, arg=" f") 148*4882a593Smuzhiyun# Short read buffer by one byte. 149*4882a593Smuzhiyuntest(name="one-under-no-nl", size=SIZE-1, newline="") 150*4882a593Smuzhiyun# Short read buffer by half buffer size. 151*4882a593Smuzhiyuntest(name="half-under-no-nl", size=int(SIZE/2), newline="") 152*4882a593Smuzhiyun# One byte under with whitespace arg. leaving wenline visible. 153*4882a593Smuzhiyuntest(name="one-under-trunc-arg", size=SIZE-1, arg=" ") 154*4882a593Smuzhiyun# One byte under with whitespace leading. leaving wenline visible. 155*4882a593Smuzhiyuntest(name="one-under-leading", size=SIZE-1, leading=" ") 156*4882a593Smuzhiyun# One byte under with whitespace leading and as arg. leaving newline visible. 157*4882a593Smuzhiyuntest(name="one-under-leading-trunc-arg", size=SIZE-1, leading=" ", arg=" ") 158*4882a593Smuzhiyun# Same as above, but with 2 bytes under 159*4882a593Smuzhiyuntest(name="two-under-no-nl", size=SIZE-2, newline="") 160*4882a593Smuzhiyuntest(name="two-under-trunc-arg", size=SIZE-2, arg=" ") 161*4882a593Smuzhiyuntest(name="two-under-leading", size=SIZE-2, leading=" ") 162*4882a593Smuzhiyuntest(name="two-under-leading-trunc-arg", size=SIZE-2, leading=" ", arg=" ") 163*4882a593Smuzhiyun# Same as above, but with buffer half filled 164*4882a593Smuzhiyuntest(name="two-under-no-nl", size=int(SIZE/2), newline="") 165*4882a593Smuzhiyuntest(name="two-under-trunc-arg", size=int(SIZE/2), arg=" ") 166*4882a593Smuzhiyuntest(name="two-under-leading", size=int(SIZE/2), leading=" ") 167*4882a593Smuzhiyuntest(name="two-under-lead-trunc-arg", size=int(SIZE/2), leading=" ", arg=" ") 168*4882a593Smuzhiyun 169*4882a593Smuzhiyunif test_num != tests: 170*4882a593Smuzhiyun raise ValueError("fewer binfmt_script tests than expected! (ran %d, expected %d" 171*4882a593Smuzhiyun % (test_num, tests)) 172