aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tools/testing/selftests/exec/binfmt_script
blob: 05f94a741c7aa05841940b030aebb93f325f8214 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# Test that truncation of bprm->buf doesn't cause unexpected execs paths, along
# with various other pathological cases.
import os, subprocess

# Relevant commits
#
# b5372fe5dc84 ("exec: load_script: Do not exec truncated interpreter path")
# 6eb3c3d0a52d ("exec: increase BINPRM_BUF_SIZE to 256")

# BINPRM_BUF_SIZE
SIZE=256

NAME_MAX=int(subprocess.check_output(["getconf", "NAME_MAX", "."]))

test_num=0

code='''#!/usr/bin/perl
print "Executed interpreter! Args:\n";
print "0 : '$0'\n";
$counter = 1;
foreach my $a (@ARGV) {
    print "$counter : '$a'\n";
    $counter++;
}
'''

##
# test - produce a binfmt_script hashbang line for testing
#
# @size:     bytes for bprm->buf line, including hashbang but not newline
# @good:     whether this script is expected to execute correctly
# @hashbang: the special 2 bytes for running binfmt_script
# @leading:  any leading whitespace before the executable path
# @root:     start of executable pathname
# @target:   end of executable pathname
# @arg:      bytes following the executable pathname
# @fill:     character to fill between @root and @target to reach @size bytes
# @newline:  character to use as newline, not counted towards @size
# ...
def test(name, size, good=True, leading="", root="./", target="/perl",
                     fill="A", arg="", newline="\n", hashbang="#!"):
    global test_num, tests, NAME_MAX
    test_num += 1
    if test_num > tests:
        raise ValueError("more binfmt_script tests than expected! (want %d, expected %d)"
                         % (test_num, tests))

    middle = ""
    remaining = size - len(hashbang) - len(leading) - len(root) - len(target) - len(arg)
    # The middle of the pathname must not exceed NAME_MAX
    while remaining >= NAME_MAX:
        middle += fill * (NAME_MAX - 1)
        middle += '/'
        remaining -= NAME_MAX
    middle += fill * remaining

    dirpath = root + middle
    binary = dirpath + target
    if len(target):
        os.makedirs(dirpath, mode=0o755, exist_ok=True)
        open(binary, "w").write(code)
        os.chmod(binary, 0o755)

    buf=hashbang + leading + root + middle + target + arg + newline
    if len(newline) > 0:
        buf += 'echo this is not really perl\n'

    script = "binfmt_script-%s" % (name)
    open(script, "w").write(buf)
    os.chmod(script, 0o755)

    proc = subprocess.Popen(["./%s" % (script)], shell=True,
                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = proc.communicate()[0]

    if proc.returncode == 0 and b'Executed interpreter' in stdout:
        if good:
            print("ok %d - binfmt_script %s (successful good exec)"
                  % (test_num, name))
        else:
            print("not ok %d - binfmt_script %s succeeded when it should have failed"
                  % (test_num, name))
    else:
        if good:
            print("not ok %d - binfmt_script %s failed when it should have succeeded (rc:%d)"
                  % (test_num, name, proc.returncode))
        else:
            print("ok %d - binfmt_script %s (correctly failed bad exec)"
                  % (test_num, name))

    # Clean up crazy binaries
    os.unlink(script)
    if len(target):
        elements = binary.split('/')
        os.unlink(binary)
        elements.pop()
        while len(elements) > 1:
            os.rmdir("/".join(elements))
            elements.pop()

tests=27
print("TAP version 1.3")
print("1..%d" % (tests))

### FAIL (8 tests)

# Entire path is well past the BINFMT_BUF_SIZE.
test(name="too-big",        size=SIZE+80, good=False)
# Path is right at max size, making it impossible to tell if it was truncated.
test(name="exact",          size=SIZE,    good=False)
# Same as above, but with leading whitespace.
test(name="exact-space",    size=SIZE,    good=False, leading=" ")
# Huge buffer of only whitespace.
test(name="whitespace-too-big", size=SIZE+71, good=False, root="",
                                              fill=" ", target="")
# A good path, but it gets truncated due to leading whitespace.
test(name="truncated",      size=SIZE+17, good=False, leading=" " * 19)
# Entirely empty except for #!
test(name="empty",          size=2,       good=False, root="",
                                          fill="", target="", newline="")
# Within size, but entirely spaces
test(name="spaces",         size=SIZE-1,  good=False, root="", fill=" ",
                                          target="", newline="")
# Newline before binary.
test(name="newline-prefix", size=SIZE-1,  good=False, leading="\n",
                                          root="", fill=" ", target="")

### ok (19 tests)

# The original test case that was broken by commit:
# 8099b047ecc4 ("exec: load_script: don't blindly truncate shebang string")
test(name="test.pl",        size=439, leading=" ",
     root="./nix/store/bwav8kz8b3y471wjsybgzw84mrh4js9-perl-5.28.1/bin",
     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")
# One byte under size, leaving newline visible.
test(name="one-under",           size=SIZE-1)
# Two bytes under size, leaving newline visible.
test(name="two-under",           size=SIZE-2)
# Exact size, but trailing whitespace visible instead of newline
test(name="exact-trunc-whitespace", size=SIZE, arg=" ")
# Exact size, but trailing space and first arg char visible instead of newline.
test(name="exact-trunc-arg",     size=SIZE, arg=" f")
# One bute under, with confirmed non-truncated arg since newline now visible.
test(name="one-under-full-arg",  size=SIZE-1, arg=" f")
# Short read buffer by one byte.
test(name="one-under-no-nl",     size=SIZE-1, newline="")
# Short read buffer by half buffer size.
test(name="half-under-no-nl",    size=int(SIZE/2), newline="")
# One byte under with whitespace arg. leaving wenline visible.
test(name="one-under-trunc-arg", size=SIZE-1, arg=" ")
# One byte under with whitespace leading. leaving wenline visible.
test(name="one-under-leading",   size=SIZE-1, leading=" ")
# One byte under with whitespace leading and as arg. leaving newline visible.
test(name="one-under-leading-trunc-arg",  size=SIZE-1, leading=" ", arg=" ")
# Same as above, but with 2 bytes under
test(name="two-under-no-nl",     size=SIZE-2, newline="")
test(name="two-under-trunc-arg", size=SIZE-2, arg=" ")
test(name="two-under-leading",   size=SIZE-2, leading=" ")
test(name="two-under-leading-trunc-arg",   size=SIZE-2, leading=" ", arg=" ")
# Same as above, but with buffer half filled
test(name="two-under-no-nl",     size=int(SIZE/2), newline="")
test(name="two-under-trunc-arg", size=int(SIZE/2), arg=" ")
test(name="two-under-leading",   size=int(SIZE/2), leading=" ")
test(name="two-under-lead-trunc-arg", size=int(SIZE/2), leading=" ", arg=" ")

if test_num != tests:
    raise ValueError("fewer binfmt_script tests than expected! (ran %d, expected %d"
                     % (test_num, tests))