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