nonvacuity.sh 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. #
  23. # The mod=11 cases do not need a rebuild -- they read the probe sources
  24. # directly -- so they are cheap, and they are the ones that matter most: the
  25. # table they guard is the one thing in this project that was wrong in the
  26. # documentation while the code was right, and a table that is wrong in the
  27. # code produces bytes that decode perfectly.
  28. #
  29. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  30. set -u
  31. cd "$(dirname "$0")/.." || exit 1
  32. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  33. SAVED=/tmp/opencode/nonvacuity.Runtime.mod
  34. PROBE=/tmp/opencode/nonvacuity.rtprobe
  35. DUMP=/tmp/opencode/nonvacuity.dump
  36. cp Runtime.mod "$SAVED" || exit 1
  37. trap 'cp "$SAVED" Runtime.mod; "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1' EXIT
  38. pass=0
  39. fail=0
  40. # rebuild <label> -- re-emit the runtime and dump it
  41. rebuild () {
  42. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  43. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  44. >/dev/null 2>&1 || return 1
  45. "$PROBE" > "$DUMP" || return 1
  46. return 0
  47. }
  48. # expect_red <label> <pattern> <checker-cmd...>
  49. # <pattern> is a grep the failure output must match, so a check cannot
  50. # "pass" by failing for some unrelated reason.
  51. expect_red () {
  52. label=$1
  53. want=$2
  54. shift 2
  55. if out=$("$@" 2>&1); then
  56. echo "NOT NON-VACUOUS: $label -- the check still passed"
  57. fail=$((fail + 1))
  58. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  59. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  60. printf '%s\n' "$out" | sed 's/^/ /'
  61. fail=$((fail + 1))
  62. else
  63. echo " ok: $label"
  64. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  65. pass=$((pass + 1))
  66. fi
  67. }
  68. echo "== each mutation must turn the named check red"
  69. echo
  70. # --- 1. name-versus-decode -------------------------------------------
  71. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  72. # cleanly, and no structural check can see it.
  73. cp "$SAVED" Runtime.mod
  74. sed -i 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|' Runtime.mod
  75. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  76. "MovSiBx" python3 tests/audit_helpers.py
  77. # CmpSiBx had the identical mistake, which is how you know a single fix is
  78. # not enough -- the same misreading was written twice.
  79. cp "$SAVED" Runtime.mod
  80. sed -i 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|' Runtime.mod
  81. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  82. "CmpSiBx" python3 tests/audit_helpers.py
  83. # --- 2. golden, and entry goldens ------------------------------------
  84. # MovAlDh was `8A C0` = MOV AL,AL instead of MOV AL,DH. This is the case
  85. # that motivated runtime.golden: the sweep stayed in sync, every branch
  86. # target stayed on a boundary, and no entry's first bytes moved.
  87. cp "$SAVED" Runtime.mod
  88. sed -i 's|B (0C6H) END MovAlDh|B (0C0H) END MovAlDh|' Runtime.mod
  89. rebuild
  90. expect_red "runtime.golden catches MOV AL,AL" \
  91. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  92. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  93. # thing that noticed the prologue was a no-op.
  94. cp "$SAVED" Runtime.mod
  95. sed -i 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|' Runtime.mod
  96. rebuild
  97. expect_red "check_runtime catches a broken initmem prologue" \
  98. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  99. # --- 3. decode sweep / length ----------------------------------------
  100. # StBxDl was `88 97` = [BX],DL with mod=10, so the instruction needs a
  101. # disp16 and the sweep loses sync two bytes later.
  102. cp "$SAVED" Runtime.mod
  103. sed -i 's|B (88H) ; B (17H) END StBxDl|B (88H) ; B (97H) END StBxDl|' Runtime.mod
  104. rebuild
  105. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  106. "585Bh" python3 tests/check_runtime.py "$DUMP"
  107. # --- 4. branch targets ------------------------------------------------
  108. # FixUp measures a rel8 from the end of the instruction, one byte past the
  109. # displacement field. Drop the +1 and every short branch lands one byte into
  110. # its target, which for a 3-byte instruction means the middle of it. The
  111. # bytes themselves are all perfectly well formed -- only the fixups are
  112. # wrong -- so this is the one failure mode the golden cannot be expected to
  113. # catch on its own.
  114. cp "$SAVED" Runtime.mod
  115. sed -i 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|' \
  116. Runtime.mod
  117. rebuild
  118. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  119. "not an instruction boundary" \
  120. python3 tests/check_runtime.py "$DUMP"
  121. echo
  122. echo "== everything restored and green again"
  123. cp "$SAVED" Runtime.mod
  124. if rebuild; then
  125. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  126. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  127. echo " ok: both checks pass on the restored source"
  128. pass=$((pass + 1))
  129. else
  130. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  131. fail=$((fail + 1))
  132. fi
  133. else
  134. echo "NOT RESTORED: the runtime would not rebuild"
  135. fail=$((fail + 1))
  136. fi
  137. echo
  138. echo "== the mod=11 table (probe/modrm11.py)"
  139. # These mutate the probe's own sources, not the runtime, so there is no
  140. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  141. SAVED_PY=/tmp/opencode/nonvacuity.modrm11.py
  142. SAVED_S=/tmp/opencode/nonvacuity.modrm11.s
  143. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  144. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  145. M11="python3 tests/probe/modrm11.py"
  146. restore_probe () {
  147. cp "$SAVED_PY" tests/probe/modrm11.py
  148. cp "$SAVED_S" tests/probe/modrm11.s
  149. }
  150. # 1. one cell of the table moved
  151. sed -i 's|"Si", "Di"\]$|"Bp", "Di"]|' tests/probe/modrm11.py
  152. expect_red "anchor pins a moved table cell" \
  153. "anchor ADD SI, 2" $M11
  154. restore_probe
  155. # 2. the table this project actually shipped: AX dropped off the front and a
  156. # duplicate BX invented at the end, which shifts every code down by one
  157. sed -i 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|' \
  158. tests/probe/modrm11.py
  159. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  160. "anchor MOV SP, BP" $M11
  161. restore_probe
  162. # 3. the .s edited to contradict the table. This is the case that shows why
  163. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  164. # correctly, so comparing the .s against `as` alone can never fail here.
  165. sed -i 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|' \
  166. tests/probe/modrm11.s
  167. expect_red "probe source edited away from the recorded bytes" \
  168. "expected 89 E7" $M11
  169. restore_probe
  170. # 4. the 8-bit list edited, which is a different table from the word one
  171. sed -i 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|' \
  172. tests/probe/modrm11.s
  173. expect_red "the 8-bit register list edited" \
  174. "expected 88 C2" $M11
  175. restore_probe
  176. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  177. # corroborate itself
  178. sed -i 's|"8B EC", "8B E5"|"8B ED", "8B E5"|' tests/probe/modrm11.py
  179. expect_red "anchor byte no longer matches the emitted code" \
  180. "expected 8B ED" $M11
  181. restore_probe
  182. if $M11 >/dev/null 2>&1; then
  183. echo " ok: modrm11.py passes on the restored probe sources"
  184. pass=$((pass + 1))
  185. else
  186. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  187. $M11 2>&1 | sed 's/^/ /'
  188. fail=$((fail + 1))
  189. fi
  190. echo
  191. echo "non-vacuity: $pass ok, $fail failed"
  192. [ "$fail" -eq 0 ]