binfmt_script.py 7.3 KB

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