nonvacuity.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. #!/bin/sh
  2. # nonvacuity.sh -- prove the runtime checks can actually fail.
  3. #
  4. # A test that has never been seen red is not a test. This script breaks the
  5. # runtime on purpose, once per check, and asserts that the check goes red and
  6. # says something useful about the breakage. Then it restores the source and
  7. # asserts everything is green again.
  8. #
  9. # Each mutation below is a real bug that was in this file at some point, not an
  10. # invented one. That is the point: these are the mistakes we actually make
  11. # with 16-bit ModRM, so these are the ones the checks have to catch.
  12. #
  13. # audit_helpers.py name-versus-decode: catches a wrong ModRM that still
  14. # decodes cleanly
  15. # check_runtime.py golden: catches the same thing in the built
  16. # image
  17. # check_runtime.py decode sweep: catches a wrong instruction LENGTH
  18. # check_runtime.py branch targets: catches a wrong fixup
  19. # check_runtime.py entry goldens: catches a broken prologue
  20. # probe/modrm11.py the mod=11 table: catches the ModRM column itself
  21. # going wrong, which no amount of decoding will show
  22. # check_framedisp.py the BP disp rule: catches a displacement that reads
  23. # a different address than the symbol table named
  24. #
  25. # The mod=11 cases do not need a rebuild -- they read the probe sources
  26. # directly -- so they are cheap, and they are the ones that matter most: the
  27. # table they guard is the one thing in this project that was wrong in the
  28. # documentation while the code was right, and a table that is wrong in the
  29. # code produces bytes that decode perfectly.
  30. #
  31. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  32. set -u
  33. cd "$(dirname "$0")/.." || exit 1
  34. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  35. SAVED=/tmp/opencode/nonvacuity.Runtime.mod
  36. PROBE=/tmp/opencode/nonvacuity.rtprobe
  37. DUMP=/tmp/opencode/nonvacuity.dump
  38. cp Runtime.mod "$SAVED" || exit 1
  39. trap 'cp "$SAVED" Runtime.mod; "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1' EXIT
  40. pass=0
  41. fail=0
  42. # rebuild <label> -- re-emit the runtime and dump it
  43. rebuild () {
  44. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  45. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  46. >/dev/null 2>&1 || return 1
  47. "$PROBE" > "$DUMP" || return 1
  48. return 0
  49. }
  50. # expect_red <label> <pattern> <checker-cmd...>
  51. # <pattern> is a grep the failure output must match, so a check cannot
  52. # "pass" by failing for some unrelated reason.
  53. expect_red () {
  54. label=$1
  55. want=$2
  56. shift 2
  57. if out=$("$@" 2>&1); then
  58. echo "NOT NON-VACUOUS: $label -- the check still passed"
  59. fail=$((fail + 1))
  60. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  61. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  62. printf '%s\n' "$out" | sed 's/^/ /'
  63. fail=$((fail + 1))
  64. else
  65. echo " ok: $label"
  66. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  67. pass=$((pass + 1))
  68. fi
  69. }
  70. echo "== each mutation must turn the named check red"
  71. echo
  72. # --- 1. name-versus-decode -------------------------------------------
  73. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  74. # cleanly, and no structural check can see it.
  75. cp "$SAVED" Runtime.mod
  76. sed -i 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|' Runtime.mod
  77. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  78. "MovSiBx" python3 tests/audit_helpers.py
  79. # CmpSiBx had the identical mistake, which is how you know a single fix is
  80. # not enough -- the same misreading was written twice.
  81. cp "$SAVED" Runtime.mod
  82. sed -i 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|' Runtime.mod
  83. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  84. "CmpSiBx" python3 tests/audit_helpers.py
  85. # --- 2. golden, and entry goldens ------------------------------------
  86. # MovAlDh was `8A C0` = MOV AL,AL instead of MOV AL,DH. This is the case
  87. # that motivated runtime.golden: the sweep stayed in sync, every branch
  88. # target stayed on a boundary, and no entry's first bytes moved.
  89. cp "$SAVED" Runtime.mod
  90. sed -i 's|B (0C6H) END MovAlDh|B (0C0H) END MovAlDh|' Runtime.mod
  91. rebuild
  92. expect_red "runtime.golden catches MOV AL,AL" \
  93. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  94. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  95. # thing that noticed the prologue was a no-op.
  96. cp "$SAVED" Runtime.mod
  97. sed -i 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|' Runtime.mod
  98. rebuild
  99. expect_red "check_runtime catches a broken initmem prologue" \
  100. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  101. # --- 3. decode sweep / length ----------------------------------------
  102. # StBxDl was `88 97` = [BX],DL with mod=10, so the instruction needs a
  103. # disp16 and the sweep loses sync two bytes later.
  104. cp "$SAVED" Runtime.mod
  105. sed -i 's|B (88H) ; B (17H) END StBxDl|B (88H) ; B (97H) END StBxDl|' Runtime.mod
  106. rebuild
  107. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  108. "585Bh" python3 tests/check_runtime.py "$DUMP"
  109. # --- 4. branch targets ------------------------------------------------
  110. # FixUp measures a rel8 from the end of the instruction, one byte past the
  111. # displacement field. Drop the +1 and every short branch lands one byte into
  112. # its target, which for a 3-byte instruction means the middle of it. The
  113. # bytes themselves are all perfectly well formed -- only the fixups are
  114. # wrong -- so this is the one failure mode the golden cannot be expected to
  115. # catch on its own.
  116. cp "$SAVED" Runtime.mod
  117. sed -i 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|' \
  118. Runtime.mod
  119. rebuild
  120. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  121. "not an instruction boundary" \
  122. python3 tests/check_runtime.py "$DUMP"
  123. echo
  124. echo "== everything restored and green again"
  125. cp "$SAVED" Runtime.mod
  126. if rebuild; then
  127. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  128. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  129. echo " ok: both checks pass on the restored source"
  130. pass=$((pass + 1))
  131. else
  132. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  133. fail=$((fail + 1))
  134. fi
  135. else
  136. echo "NOT RESTORED: the runtime would not rebuild"
  137. fail=$((fail + 1))
  138. fi
  139. echo
  140. echo "== the mod=11 table (probe/modrm11.py)"
  141. # These mutate the probe's own sources, not the runtime, so there is no
  142. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  143. SAVED_PY=/tmp/opencode/nonvacuity.modrm11.py
  144. SAVED_S=/tmp/opencode/nonvacuity.modrm11.s
  145. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  146. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  147. M11="python3 tests/probe/modrm11.py"
  148. restore_probe () {
  149. cp "$SAVED_PY" tests/probe/modrm11.py
  150. cp "$SAVED_S" tests/probe/modrm11.s
  151. }
  152. # 1. one cell of the table moved
  153. sed -i 's|"Si", "Di"\]$|"Bp", "Di"]|' tests/probe/modrm11.py
  154. expect_red "anchor pins a moved table cell" \
  155. "anchor ADD SI, 2" $M11
  156. restore_probe
  157. # 2. the table this project actually shipped: AX dropped off the front and a
  158. # duplicate BX invented at the end, which shifts every code down by one
  159. sed -i 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|' \
  160. tests/probe/modrm11.py
  161. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  162. "anchor MOV SP, BP" $M11
  163. restore_probe
  164. # 3. the .s edited to contradict the table. This is the case that shows why
  165. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  166. # correctly, so comparing the .s against `as` alone can never fail here.
  167. sed -i 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|' \
  168. tests/probe/modrm11.s
  169. expect_red "probe source edited away from the recorded bytes" \
  170. "expected 89 E7" $M11
  171. restore_probe
  172. # 4. the 8-bit list edited, which is a different table from the word one
  173. sed -i 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|' \
  174. tests/probe/modrm11.s
  175. expect_red "the 8-bit register list edited" \
  176. "expected 88 C2" $M11
  177. restore_probe
  178. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  179. # corroborate itself
  180. sed -i 's|"8B EC", "8B E5"|"8B ED", "8B E5"|' tests/probe/modrm11.py
  181. expect_red "anchor byte no longer matches the emitted code" \
  182. "expected 8B ED" $M11
  183. restore_probe
  184. if $M11 >/dev/null 2>&1; then
  185. echo " ok: modrm11.py passes on the restored probe sources"
  186. pass=$((pass + 1))
  187. else
  188. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  189. $M11 2>&1 | sed 's/^/ /'
  190. fail=$((fail + 1))
  191. fi
  192. echo
  193. echo "== the BP displacement rule (check_framedisp.py)"
  194. # This one is about Compiler.mod rather than the runtime, and it needs the
  195. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  196. SAVED_C=/tmp/opencode/nonvacuity.Compiler.mod
  197. cp Compiler.mod "$SAVED_C" || exit 1
  198. rebuild_compiler () {
  199. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  200. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  201. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  202. >/dev/null 2>&1
  203. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  204. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  205. >/dev/null 2>&1 || return 1
  206. return 0
  207. }
  208. # 1. the original bug: `off MOD 100H`, always disp8. Restores exactly the code
  209. # that was there before EmBpDisp existed. t28's [BP+128] read becomes
  210. # [BP-128], which is the failure this whole check is named after.
  211. python3 - "$SAVED_C" <<'PYEOF'
  212. import sys
  213. p = 'Compiler.mod'
  214. s = open(p).read()
  215. old = """BEGIN
  216. IF off <= 127 THEN
  217. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  218. ELSE
  219. Ebyte (86H) ; Eword (off)
  220. END
  221. END EmBpDisp ;"""
  222. new = """VAR disp : CARDINAL ;
  223. BEGIN
  224. disp := off MOD 100H ;
  225. Ebyte (46H) ; Ebyte (VAL (BYTE, disp))
  226. END EmBpDisp ;"""
  227. assert old in s, "EmBpDisp body not found -- update this mutation"
  228. open(p, 'w').write(s.replace(old, new))
  229. PYEOF
  230. if rebuild_compiler; then
  231. expect_red "displacement truncation reads a different address" \
  232. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  233. else
  234. echo " FAIL: the compiler would not rebuild with the truncation"
  235. fail=$((fail + 1))
  236. fi
  237. cp "$SAVED_C" Compiler.mod
  238. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  239. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  240. # still be happy -- which is worth asserting, because a check that only
  241. # ever fails on a smaller encoding is a check that pins one answer instead
  242. # of the rule.
  243. python3 - <<'PYEOF'
  244. p = 'Compiler.mod'
  245. s = open(p).read()
  246. old = """ IF off <= 127 THEN
  247. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  248. ELSE
  249. Ebyte (86H) ; Eword (off)
  250. END"""
  251. new = """ Ebyte (86H) ; Eword (off)"""
  252. assert old in s, "EmBpDisp branch not found -- update this mutation"
  253. open(p, 'w').write(s.replace(old, new))
  254. PYEOF
  255. if rebuild_compiler; then
  256. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  257. echo " ok: always-disp16 is accepted, so the check pins the rule"
  258. echo " and not one particular encoding"
  259. pass=$((pass + 1))
  260. else
  261. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  262. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  263. fail=$((fail + 1))
  264. fi
  265. else
  266. echo " FAIL: the compiler would not rebuild with always-disp16"
  267. fail=$((fail + 1))
  268. fi
  269. cp "$SAVED_C" Compiler.mod
  270. if rebuild_compiler; then
  271. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  272. echo " ok: check_framedisp passes on the restored source"
  273. pass=$((pass + 1))
  274. else
  275. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  276. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  277. fail=$((fail + 1))
  278. fi
  279. else
  280. echo "NOT RESTORED: the compiler would not rebuild"
  281. fail=$((fail + 1))
  282. fi
  283. echo
  284. echo "non-vacuity: $pass ok, $fail failed"
  285. [ "$fail" -eq 0 ]