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