nonvacuity.sh 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  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. # audit_helpers.py coverage: catches a helper that has silently
  16. # dropped OUT of the audit, which is a green report about
  17. # a subject nobody looked at
  18. # run_com_tests.sh the .COM layout: catches a header that cannot be
  19. # located, a runtime size that disagrees with the image,
  20. # and an entry jump that starts in the wrong place
  21. # check_runtime.py golden: catches the same thing in the built
  22. # image
  23. # check_runtime.py decode sweep: catches a wrong instruction LENGTH
  24. # check_runtime.py branch targets: catches a wrong fixup
  25. # check_runtime.py entry goldens: catches a broken prologue
  26. # probe/modrm11.py the mod=11 table: catches the ModRM column itself
  27. # going wrong, which no amount of decoding will show
  28. # check_framedisp.py the BP disp rule: catches a displacement that reads
  29. # a different address than the symbol table named
  30. # rt_exec.py the BP contract: catches an entry that borrows BP to
  31. # reach its argument and does not hand it back. The bytes
  32. # are well formed, the golden is satisfied, the audit says
  33. # every helper emits what its name says, and the machine
  34. # triple-faults on the second call - so nothing short of
  35. # running it, or of stating the register contract
  36. # explicitly, can see it.
  37. # check_8086.py the 8086 opcodes: catches a conditional branch or a
  38. # SETcc emitted as `0F 8x'/`0F 9x', which are 386-only.
  39. # Nothing else here can: qemu-system-i386's lowest CPU
  40. # model is 486, where both are ordinary instructions, and
  41. # FCML's -m16 mode is a 386 too. Clause H additionally
  42. # catches the branch polarity being inverted, which is
  43. # still a legal opcode and therefore invisible to every
  44. # shape-based check.
  45. # run_com_exec.py behaviour: catches a*b that emits an ADD, `>'
  46. # and `>=' swapped, REPEAT..UNTIL that stops after one
  47. # pass, and two procedures whose parameters collide.
  48. # All four sat in fixtures that COMPILED and were never
  49. # RUN, with every byte-level check green.
  50. #
  51. # The mod=11 cases do not need a rebuild -- they read the probe sources
  52. # directly -- so they are cheap, and they are the ones that matter most: the
  53. # table they guard is the one thing in this project that was wrong in the
  54. # documentation while the code was right, and a table that is wrong in the
  55. # code produces bytes that decode perfectly.
  56. #
  57. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  58. set -u
  59. cd "$(dirname "$0")/.." || exit 1
  60. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  61. SAVED=/tmp/opencode/nonvacuity.Runtime.mod
  62. PROBE=/tmp/opencode/nonvacuity.rtprobe
  63. DUMP=/tmp/opencode/nonvacuity.dump
  64. cp Runtime.mod "$SAVED" || exit 1
  65. trap 'cp "$SAVED" Runtime.mod; "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1' EXIT
  66. pass=0
  67. fail=0
  68. # mutate <file> <sed-expr> -- apply a deliberate breakage and INSIST it landed.
  69. #
  70. # Four cases in this file were already dead when first run, all the same way:
  71. # the helper they name had been renamed or reformatted since the case was
  72. # written, the sed matched nothing, the source was unchanged, and the check
  73. # correctly passed - so the harness reported "NOT NON-VACUOUS" and, worse, a
  74. # reader skimming the output could take "the check still passed" for a passing
  75. # test. A case that cannot fire is worse than no case: it is a claim of
  76. # coverage that was never tested.
  77. #
  78. # So the mutation is verified, not assumed. If the file is byte-identical
  79. # afterwards, that is reported as a FAILURE of the harness, naming the sed, and
  80. # the case is not run - because running it would only produce a meaningless
  81. # green. The message says what to do (fix the sed) rather than what it found.
  82. mutate () {
  83. mf=$1
  84. msed=$2
  85. cp "$mf" /tmp/opencode/nonvacuity.mut.bak
  86. sed -i "$msed" "$mf"
  87. if cmp -s "$mf" /tmp/opencode/nonvacuity.mut.bak; then
  88. echo " BROKEN CASE: the mutation did not change $mf"
  89. echo " sed: $msed"
  90. echo " the named code has probably been renamed or reformatted -"
  91. echo " fix this case, it is asserting nothing"
  92. fail=$((fail + 1))
  93. return 1
  94. fi
  95. return 0
  96. }
  97. # rebuild <label> -- re-emit the runtime and dump it
  98. rebuild () {
  99. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  100. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  101. >/dev/null 2>&1 || return 1
  102. "$PROBE" > "$DUMP" || return 1
  103. return 0
  104. }
  105. # expect_red <label> <pattern> <checker-cmd...>
  106. # <pattern> is a grep the failure output must match, so a check cannot
  107. # "pass" by failing for some unrelated reason.
  108. expect_red () {
  109. label=$1
  110. want=$2
  111. shift 2
  112. if out=$("$@" 2>&1); then
  113. echo "NOT NON-VACUOUS: $label -- the check still passed"
  114. fail=$((fail + 1))
  115. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  116. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  117. printf '%s\n' "$out" | sed 's/^/ /'
  118. fail=$((fail + 1))
  119. else
  120. echo " ok: $label"
  121. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  122. pass=$((pass + 1))
  123. fi
  124. }
  125. echo "== each mutation must turn the named check red"
  126. echo
  127. # --- 1. name-versus-decode -------------------------------------------
  128. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  129. # cleanly, and no structural check can see it.
  130. cp "$SAVED" Runtime.mod
  131. mutate Runtime.mod 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|'
  132. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  133. "MovSiBx" python3 tests/audit_helpers.py
  134. # CmpSiBx had the identical mistake, which is how you know a single fix is
  135. # not enough -- the same misreading was written twice.
  136. cp "$SAVED" Runtime.mod
  137. mutate Runtime.mod 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|'
  138. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  139. "CmpSiBx" python3 tests/audit_helpers.py
  140. # --- 2. golden, and entry goldens ------------------------------------
  141. # MovDlAl was `88 C0` = MOV AL,AL instead of MOV DL,AL. This is the case that
  142. # motivated runtime.golden: the sweep stayed in sync, every branch target
  143. # stayed on a boundary, no entry's first bytes moved, and the size did not
  144. # change. The target helper was MovAlDh when this case was written, which is
  145. # the fourth way a case here can rot - see the note on `mutate` below.
  146. cp "$SAVED" Runtime.mod
  147. mutate Runtime.mod 's|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H)|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C0H)|'
  148. rebuild
  149. expect_red "runtime.golden catches MOV AL,AL" \
  150. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  151. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  152. # thing that noticed the prologue was a no-op.
  153. cp "$SAVED" Runtime.mod
  154. mutate Runtime.mod 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|'
  155. rebuild
  156. expect_red "check_runtime catches a broken initmem prologue" \
  157. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  158. # --- 3. decode sweep / length ----------------------------------------
  159. # StDiDl was `88 97` = [BX+disp16],DL: mod=10, so the instruction needs a
  160. # disp16 it was not given, and the sweep loses sync two bytes later.
  161. cp "$SAVED" Runtime.mod
  162. mutate Runtime.mod 's|PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H)|PROCEDURE StDiDl ; BEGIN B (88H) ; B (97H)|'
  163. rebuild
  164. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  165. "mov byte ptr \[bx+5b5fh\],dl" python3 tests/check_runtime.py "$DUMP"
  166. # --- 4. branch targets ------------------------------------------------
  167. # FixUp measures a rel8 from the end of the instruction, one byte past the
  168. # displacement field. Drop the +1 and every short branch lands one byte into
  169. # its target, which for a 3-byte instruction means the middle of it. The
  170. # bytes themselves are all perfectly well formed -- only the fixups are
  171. # wrong -- so this is the one failure mode the golden cannot be expected to
  172. # catch on its own.
  173. cp "$SAVED" Runtime.mod
  174. mutate Runtime.mod 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|'
  175. rebuild
  176. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  177. "not an instruction boundary" \
  178. python3 tests/check_runtime.py "$DUMP"
  179. echo
  180. echo "== everything restored and green again"
  181. cp "$SAVED" Runtime.mod
  182. if rebuild; then
  183. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  184. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  185. echo " ok: both checks pass on the restored source"
  186. pass=$((pass + 1))
  187. else
  188. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  189. fail=$((fail + 1))
  190. fi
  191. else
  192. echo "NOT RESTORED: the runtime would not rebuild"
  193. fail=$((fail + 1))
  194. fi
  195. echo
  196. echo "== the mod=11 table (probe/modrm11.py)"
  197. # These mutate the probe's own sources, not the runtime, so there is no
  198. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  199. SAVED_PY=/tmp/opencode/nonvacuity.modrm11.py
  200. SAVED_S=/tmp/opencode/nonvacuity.modrm11.s
  201. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  202. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  203. M11="python3 tests/probe/modrm11.py"
  204. restore_probe () {
  205. cp "$SAVED_PY" tests/probe/modrm11.py
  206. cp "$SAVED_S" tests/probe/modrm11.s
  207. }
  208. # 1. one cell of the table moved
  209. mutate tests/probe/modrm11.py 's|"Si", "Di"\]$|"Bp", "Di"]|'
  210. expect_red "anchor pins a moved table cell" \
  211. "anchor ADD SI, 2" $M11
  212. restore_probe
  213. # 2. the table this project actually shipped: AX dropped off the front and a
  214. # duplicate BX invented at the end, which shifts every code down by one
  215. mutate tests/probe/modrm11.py 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|'
  216. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  217. "anchor MOV SP, BP" $M11
  218. restore_probe
  219. # 3. the .s edited to contradict the table. This is the case that shows why
  220. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  221. # correctly, so comparing the .s against `as` alone can never fail here.
  222. mutate tests/probe/modrm11.s 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|'
  223. expect_red "probe source edited away from the recorded bytes" \
  224. "expected 89 E7" $M11
  225. restore_probe
  226. # 4. the 8-bit list edited, which is a different table from the word one
  227. mutate tests/probe/modrm11.s 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|'
  228. expect_red "the 8-bit register list edited" \
  229. "expected 88 C2" $M11
  230. restore_probe
  231. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  232. # corroborate itself
  233. mutate tests/probe/modrm11.py 's|"8B EC", "8B E5"|"8B ED", "8B E5"|'
  234. expect_red "anchor byte no longer matches the emitted code" \
  235. "expected 8B ED" $M11
  236. restore_probe
  237. if $M11 >/dev/null 2>&1; then
  238. echo " ok: modrm11.py passes on the restored probe sources"
  239. pass=$((pass + 1))
  240. else
  241. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  242. $M11 2>&1 | sed 's/^/ /'
  243. fail=$((fail + 1))
  244. fi
  245. echo
  246. echo "== the BP displacement rule (check_framedisp.py)"
  247. # This one is about Compiler.mod rather than the runtime, and it needs the
  248. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  249. SAVED_C=/tmp/opencode/nonvacuity.Compiler.mod
  250. cp Compiler.mod "$SAVED_C" || exit 1
  251. rebuild_compiler () {
  252. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  253. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  254. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  255. >/dev/null 2>&1
  256. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  257. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  258. >/dev/null 2>&1 || return 1
  259. return 0
  260. }
  261. # 1. the original bug: `off MOD 100H`, always disp8. Restores exactly the code
  262. # that was there before EmBpDisp existed. t28's [BP+128] read becomes
  263. # [BP-128], which is the failure this whole check is named after.
  264. python3 - "$SAVED_C" <<'PYEOF'
  265. import sys
  266. p = 'Compiler.mod'
  267. s = open(p).read()
  268. old = """BEGIN
  269. IF off <= 127 THEN
  270. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  271. ELSE
  272. Ebyte (86H) ; Eword (off)
  273. END
  274. END EmBpDisp ;"""
  275. new = """VAR disp : CARDINAL ;
  276. BEGIN
  277. disp := off MOD 100H ;
  278. Ebyte (46H) ; Ebyte (VAL (BYTE, disp))
  279. END EmBpDisp ;"""
  280. assert old in s, "EmBpDisp body not found -- update this mutation"
  281. open(p, 'w').write(s.replace(old, new))
  282. PYEOF
  283. if rebuild_compiler; then
  284. expect_red "displacement truncation reads a different address" \
  285. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  286. else
  287. echo " FAIL: the compiler would not rebuild with the truncation"
  288. fail=$((fail + 1))
  289. fi
  290. cp "$SAVED_C" Compiler.mod
  291. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  292. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  293. # still be happy -- which is worth asserting, because a check that only
  294. # ever fails on a smaller encoding is a check that pins one answer instead
  295. # of the rule.
  296. python3 - <<'PYEOF'
  297. p = 'Compiler.mod'
  298. s = open(p).read()
  299. old = """ IF off <= 127 THEN
  300. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  301. ELSE
  302. Ebyte (86H) ; Eword (off)
  303. END"""
  304. new = """ Ebyte (86H) ; Eword (off)"""
  305. assert old in s, "EmBpDisp branch not found -- update this mutation"
  306. open(p, 'w').write(s.replace(old, new))
  307. PYEOF
  308. if rebuild_compiler; then
  309. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  310. echo " ok: always-disp16 is accepted, so the check pins the rule"
  311. echo " and not one particular encoding"
  312. pass=$((pass + 1))
  313. else
  314. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  315. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  316. fail=$((fail + 1))
  317. fi
  318. else
  319. echo " FAIL: the compiler would not rebuild with always-disp16"
  320. fail=$((fail + 1))
  321. fi
  322. cp "$SAVED_C" Compiler.mod
  323. if rebuild_compiler; then
  324. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  325. echo " ok: check_framedisp passes on the restored source"
  326. pass=$((pass + 1))
  327. else
  328. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  329. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  330. fail=$((fail + 1))
  331. fi
  332. else
  333. echo "NOT RESTORED: the compiler would not rebuild"
  334. fail=$((fail + 1))
  335. fi
  336. # --- 3. the operator bugs the nine dead fixtures were hiding ------------
  337. echo
  338. echo "== the four bugs the nine never-executed fixtures were hiding"
  339. echo
  340. # A different KIND of case from everything above. The others break the code
  341. # and assert a byte-level check notices; these break the code and assert a
  342. # BEHAVIOURAL check notices, which is the only kind that could have found them.
  343. # All four shipped with a green compile matrix, a passing .COM layout check, a
  344. # passing golden and a passing emitter audit:
  345. #
  346. # OpMul = 1 `a * b` emitted ADD AX,CX. `*' and `+' both numbered
  347. # their operator 1, and BinOpEmit cannot see which
  348. # precedence level called it, so every multiplication
  349. # dispatched to the addition. The constant-folding arm was
  350. # correct, which is why `n * n' with n a CONST was right and
  351. # `a * a' with a a variable was not -- and t08_const is the
  352. # only fixture that ever multiplied.
  353. # 9Dh / 9FH `>' got SETGE and `>=' got SETG: swapped, one letter
  354. # apart in the mnemonic. Only a==b could see it.
  355. # JNZ -> body REPEAT..UNTIL looped back while the condition was TRUE,
  356. # which is WHILE, so the body ran once and stopped.
  357. #
  358. # They are mutated back to the original defect and run_com_exec.py must go red
  359. # on the exact fixture that pins the behaviour. The fourth (HideLocals) is a
  360. # scoping bug rather than an operator bug; it was hiding in the same place.
  361. mutate_compiler () { # reuse mutate's verified-change discipline on Compiler.mod
  362. mf=Compiler.mod
  363. msed=$1
  364. cp "$SAVED_C" /tmp/opencode/nonvacuity.mut2.bak
  365. sed -i "$msed" "$mf"
  366. if cmp -s "$mf" /tmp/opencode/nonvacuity.mut2.bak; then
  367. echo " BROKEN CASE: the mutation did not change Compiler.mod"
  368. echo " sed: $msed"
  369. echo " the named code has probably been renamed or reformatted -"
  370. echo " fix this case, it is asserting nothing"
  371. fail=$((fail + 1))
  372. return 1
  373. fi
  374. return 0
  375. }
  376. # The SETcc swap and the HideLocals removal are done in python rather than
  377. # with sed: both need to match source text containing `*` and `(` in a way that
  378. # is tedious and fragile as a regex, and a case whose only failure mode is a
  379. # malformed sed is a case that silently asserts nothing.
  380. #
  381. # And a python helper fails in a way sed does not: a syntax error in the helper
  382. # is a non-zero exit, `if mutate_foo; then` is simply false, and the case is
  383. # SKIPPED -- with no failure counted and nothing on stdout but whatever python
  384. # printed. That is how the SETcc case spent its first run: an apostrophe in an
  385. # assert message ("the `>' arm") closed the string early, python died, the
  386. # compiler was never broken, and the suite still reported 0 failed. A skipped
  387. # case and a passing case look the same in the total. So each helper below
  388. # fails LOUDLY: a non-zero exit from python is reported as a BROKEN CASE and
  389. # counted, never swallowed.
  390. #
  391. # Each one also counts its targets before replacing. `assert s != before' only
  392. # says the file changed; with two edits it would pass if just one of them
  393. # landed, and with two identical HideLocals call sites it would happily delete
  394. # the wrong one -- still a changed file, still a working compiler, still green
  395. # for the wrong reason.
  396. mutate_cc_swap () {
  397. if python3 - <<'PYX'
  398. p = 'Compiler.mod'
  399. s = open(p).read()
  400. GT = 'EmSetcc (9FH) ; (* > SETG *)' # the greater-than arm
  401. GE = 'EmSetcc (9DH) ; (* >= SETGE *)' # the greater-equal arm
  402. assert s.count(GT) == 1, 'expected 1 greater-than arm, found %d' % s.count(GT)
  403. assert s.count(GE) == 1, 'expected 1 greater-equal arm, found %d' % s.count(GE)
  404. s = s.replace(GT, GT.replace('9FH', '9DH'))
  405. s = s.replace(GE, GE.replace('9DH', '9FH'))
  406. open(p, 'w').write(s)
  407. PYX
  408. then
  409. return 0
  410. fi
  411. echo " BROKEN CASE: the SETcc swap did not apply"
  412. echo " the two EmSetcc arms are probably renamed or reformatted -"
  413. echo " fix this case, it is asserting nothing"
  414. fail=$((fail + 1))
  415. return 1
  416. }
  417. mutate_no_hidelocals () {
  418. if python3 - <<'PYX'
  419. p = 'Compiler.mod'
  420. s = open(p).read()
  421. # Two HideLocals calls exist. Only the body-exit one may go: deleting the
  422. # FORWARD one instead would still change the file, still rebuild, and still
  423. # leave t13_proc compiling, so the case would go green for the wrong reason.
  424. HL = ' HideLocals (nestMark) ; (* parameters and locals stop here *)\n'
  425. assert s.count(HL) == 1, 'expected 1 body-exit HideLocals, found %d' % s.count(HL)
  426. open(p, 'w').write(s.replace(HL, ''))
  427. PYX
  428. then
  429. return 0
  430. fi
  431. echo " BROKEN CASE: HideLocals was not removed"
  432. echo " the call or its comment has probably been reformatted -"
  433. echo " fix this case, it is asserting nothing"
  434. fail=$((fail + 1))
  435. return 1
  436. }
  437. cp "$SAVED_C" Compiler.mod
  438. if mutate_compiler 's|^ op := OpMul ; DropCh| op := OpAdd ; DropCh|'; then
  439. if rebuild_compiler; then
  440. expect_red "execution catches '*' emitting an ADD (t08_const, n*n)" \
  441. "t08_const" python3 tests/run_com_exec.py t08_const
  442. # t08 only ever multiplied two CONSTANTS, which is the one path that was
  443. # never wrong, because BinOpEmit folds it. So the case above is close
  444. # to vacuous: it proves the mutation changed the binary, not that the
  445. # emitted multiply is covered. The check that matters needs a
  446. # VARIABLE operand, and no shipped fixture has one -- which is why the
  447. # bug survived at all. So this writes a throwaway fixture that
  448. # multiplies a variable, runs it, and asserts the multiply is right.
  449. # The fixture is deleted afterwards; it is here to close the coverage
  450. # hole, not to become a permanent test (that is what a real fixture
  451. # with a `*' in it would be for).
  452. cat > tests/fixtures/zzmul.pas <<'ZZEOF'
  453. program zzmul;
  454. var a : integer ;
  455. begin
  456. a := 7 ;
  457. writeln (a * 6)
  458. end.
  459. ZZEOF
  460. printf '42\r\n' > tests/fixtures/zzmul.out
  461. expect_red "execution catches '*' on a VARIABLE (the unfolded path)" \
  462. "zzmul" python3 tests/run_com_exec.py zzmul
  463. rm -f tests/fixtures/zzmul.pas tests/fixtures/zzmul.out
  464. else
  465. echo " FAIL: the compiler would not rebuild with OpAdd for '*'"
  466. fail=$((fail + 1))
  467. fi
  468. fi
  469. cp "$SAVED_C" Compiler.mod
  470. cp "$SAVED_C" Compiler.mod
  471. if mutate_cc_swap; then
  472. if rebuild_compiler; then
  473. expect_red "execution catches '>' and '>=' swapped (t09_if)" \
  474. "t09_if" python3 tests/run_com_exec.py t09_if
  475. else
  476. echo " FAIL: the compiler would not rebuild with the SETcc swap"
  477. fail=$((fail + 1))
  478. fi
  479. fi
  480. cp "$SAVED_C" Compiler.mod
  481. cp "$SAVED_C" Compiler.mod
  482. if mutate_compiler 's| DropC (EmJcc (84H, L1)) ; (\* JZ -> body again \*)| zj := EmJcc (85H, L1) ;|'; then
  483. if rebuild_compiler; then
  484. expect_red "execution catches REPEAT..UNTIL exiting after one pass (t12)" \
  485. "t12_repeat" python3 tests/run_com_exec.py t12_repeat
  486. else
  487. echo " FAIL: the compiler would not rebuild with the JNZ repeat"
  488. fail=$((fail + 1))
  489. fi
  490. fi
  491. cp "$SAVED_C" Compiler.mod
  492. # The fourth: sibling procedures shared one parameter namespace, because a
  493. # finished procedure's symbols were left at a level Search still accepts.
  494. # Removing HideLocals puts two procedures' `a : integer' back in collision.
  495. cp "$SAVED_C" Compiler.mod
  496. if mutate_no_hidelocals; then
  497. if rebuild_compiler; then
  498. expect_red "a duplicate parameter in two procedures is a compile error again" \
  499. "ERROR 41" python3 tests/run_com_exec.py t13_proc
  500. else
  501. echo " FAIL: the compiler would not rebuild without HideLocals"
  502. fail=$((fail + 1))
  503. fi
  504. else
  505. echo " FAIL: could not remove HideLocals to test the scoping fix"
  506. fail=$((fail + 1))
  507. fi
  508. cp "$SAVED_C" Compiler.mod
  509. if rebuild_compiler; then
  510. if python3 tests/run_com_exec.py >/dev/null 2>&1; then
  511. echo " ok: all 31 executed fixtures pass on the restored compiler"
  512. pass=$((pass + 1))
  513. else
  514. echo "NOT RESTORED: run_com_exec.py is red after restoring Compiler.mod"
  515. python3 tests/run_com_exec.py 2>&1 | grep -i fail | head -3 | sed 's/^/ /'
  516. fail=$((fail + 1))
  517. fi
  518. else
  519. echo "NOT RESTORED: the compiler would not rebuild"
  520. fail=$((fail + 1))
  521. fi
  522. echo
  523. echo "== 8086 legality of the conditional lowering (check_8086.py)"
  524. # This whole section exists because of a fault that every other check in the
  525. # project was blind to, and being blunt about WHY is the point of listing it.
  526. #
  527. # EmJcc and EmSetcc emitted `0F 8x rel16' and `0F 9x rel8'. `0F' is a
  528. # 386-and-later opcode prefix; the 8086 has none. So EVERY conditional branch
  529. # and EVERY comparison in EVERY compiled program was an illegal instruction on
  530. # the machine this compiler targets. And all of the following were green while
  531. # it was: the compile matrix, the .COM layout checker, the runtime golden, the
  532. # emitter audit, and 30 fixtures executing under qemu to the right answers.
  533. #
  534. # Two reasons, and the second is the one worth keeping:
  535. # 1. qemu-system-i386 has no 8086 model. Its lowest is 486, where `0F 84' is
  536. # a perfectly ordinary JZ. So the execution oracle CANNOT see this class
  537. # of fault, ever.
  538. # 2. FCML is this project's INDEPENDENT disassembler, and FCML's -m16 mode is
  539. # a 386. The one tool whose job is to say "this is not a real instruction"
  540. # was architecturally guaranteed to agree with the bug.
  541. #
  542. # So a check was needed that asks the question nothing else was asking. The
  543. # five mutations below are the mutations that check has to catch, and each was
  544. # run by hand and confirmed red BEFORE this section existed.
  545. #
  546. # M4 and M5 are the interesting pair. M4 is a fault this project actually
  547. # introduced while fixing the above: EmJcc jumps TO its target, but the 8086
  548. # shape steps OVER a 3-byte EJMP, so the naive port inverts every conditional
  549. # in every program. It was caught by execution, not by any byte check. M5 is
  550. # the partial version -- the inversion dropped for the IF and CASE sites only,
  551. # kept for FOR -- and MEASURING that is what produced clause H, because the
  552. # check was blind to M5 as first written: IF declares nibble 4, CASE declares
  553. # 5, they negate into each other, and both are declared, so nothing complained
  554. # while every conditional in every program took the wrong path. FOR declares
  555. # C and F, whose negations D and E are declared for nothing at all, so the
  556. # whole-suite version was caught by luck.
  557. #
  558. # Note these need `rebuild_compiler' only to refresh comtest; check_8086.py
  559. # relinks the fixtures itself. Getting that wrong is how this section's first
  560. # draft reported a mutation as VACUOUS: the build was skipped, a stale comtest
  561. # ran the GOOD compiler, and the check passed. A helper that exits non-zero
  562. # makes `if mutate_x; then' false and the case is silently skipped, which looks
  563. # exactly like a pass -- so rebuild_compiler is checked, not assumed.
  564. # M1: the original defect, restored verbatim.
  565. cp "$SAVED_C" Compiler.mod
  566. if python3 - <<'PYX'
  567. p = 'Compiler.mod'
  568. s = open(p).read()
  569. old = """ Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)
  570. Ebyte (03H) ; (* rel8: step over the 3-byte EJMP *)
  571. RETURN EmJmpNear (target) (* target = 0 => forward, see above *)"""
  572. new = """ Ebyte (0FH) ; Ebyte (cc) ; Eword (0) ;
  573. IF target = 0 THEN
  574. RETURN nPatch
  575. END ;
  576. RETURN 0"""
  577. assert s.count(old) == 1, 'EmJcc body found %d times -- update this mutation' % s.count(old)
  578. open(p, 'w').write(s.replace(old, new))
  579. PYX
  580. then
  581. if rebuild_compiler; then
  582. expect_red "check_8086 catches a 0F 8x branch (the original bug)" \
  583. "0F 84" python3 tests/check_8086.py
  584. else
  585. echo " FAIL: the compiler would not rebuild with the 0F branch"
  586. fail=$((fail + 1))
  587. fi
  588. else
  589. echo " BROKEN CASE: the 0F branch mutation did not apply"
  590. fail=$((fail + 1))
  591. fi
  592. cp "$SAVED_C" Compiler.mod
  593. # M2: the other original defect, SETcc plus the MOV AH,0 it needed.
  594. cp "$SAVED_C" Compiler.mod
  595. if python3 - <<'PYX'
  596. p = 'Compiler.mod'
  597. s = open(p).read()
  598. old = """ Ebyte (0B8H) ; Eword (1) ; (* MOV AX,#0001 *)
  599. Ebyte (JccShort (cc)) ; (* taken when the comparison HOLDS *)
  600. Ebyte (01H) ; (* rel8: step over the DEC AX *)
  601. Ebyte (48H) (* DEC AX *)"""
  602. new = """ Ebyte (0FH) ; Ebyte (cc) ; Ebyte (0C0H) ;
  603. EmMovAh0 ()"""
  604. assert s.count(old) == 1, 'EmSetcc body found %d times -- update this mutation' % s.count(old)
  605. open(p, 'w').write(s.replace(old, new))
  606. PYX
  607. then
  608. if rebuild_compiler; then
  609. expect_red "check_8086 catches a 0F 9x SETcc (the original bug)" \
  610. "0F 9F C0" python3 tests/check_8086.py
  611. else
  612. echo " FAIL: the compiler would not rebuild with the 0F SETcc"
  613. fail=$((fail + 1))
  614. fi
  615. else
  616. echo " BROKEN CASE: the 0F SETcc mutation did not apply"
  617. fail=$((fail + 1))
  618. fi
  619. cp "$SAVED_C" Compiler.mod
  620. # M4: the polarity inversion, everywhere. Note the expected text is clause H's,
  621. # not a shape complaint: every byte here is a legal 8086 shape, which is the
  622. # entire reason a separate clause had to be written.
  623. cp "$SAVED_C" Compiler.mod
  624. if python3 - <<'PYX'
  625. p = 'Compiler.mod'
  626. s = open(p).read()
  627. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  628. new = ' Ebyte (JccShort (cc)) ; (* MUTATION: inversion dropped *)'
  629. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  630. open(p, 'w').write(s.replace(old, new))
  631. PYX
  632. then
  633. if rebuild_compiler; then
  634. expect_red "clause H catches the branch polarity inverted everywhere" \
  635. "but reading the source says" python3 tests/check_8086.py
  636. else
  637. echo " FAIL: the compiler would not rebuild with the inversion dropped"
  638. fail=$((fail + 1))
  639. fi
  640. else
  641. echo " BROKEN CASE: the polarity mutation did not apply"
  642. fail=$((fail + 1))
  643. fi
  644. cp "$SAVED_C" Compiler.mod
  645. # M5: the same inversion dropped for the IF and CASE sites ONLY. This one is
  646. # the reason clause H exists: it was run by hand and the check stayed GREEN,
  647. # and FOR's C/F nibbles were the only reason the whole-suite version (M4)
  648. # happened to be caught.
  649. cp "$SAVED_C" Compiler.mod
  650. if python3 - <<'PYX'
  651. p = 'Compiler.mod'
  652. s = open(p).read()
  653. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  654. new = """ IF (cc = 84H) OR (cc = 85H) THEN
  655. Ebyte (JccShort (cc)) (* MUTATION: no inversion here *)
  656. ELSE
  657. Ebyte (JccShortInv (cc))
  658. END ;"""
  659. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  660. open(p, 'w').write(s.replace(old, new))
  661. PYX
  662. then
  663. if rebuild_compiler; then
  664. expect_red "clause H catches the inversion dropped for IF and CASE only" \
  665. "t22_case" python3 tests/check_8086.py
  666. else
  667. echo " FAIL: the compiler would not rebuild with the partial inversion"
  668. fail=$((fail + 1))
  669. fi
  670. else
  671. echo " BROKEN CASE: the partial-inversion mutation did not apply"
  672. fail=$((fail + 1))
  673. fi
  674. cp "$SAVED_C" Compiler.mod
  675. # And the check on the RESTORED compiler, so a green above cannot come from a
  676. # mutation that failed to take and left the real defect in place.
  677. if rebuild_compiler; then
  678. if python3 tests/check_8086.py >/dev/null 2>&1; then
  679. echo " ok: check_8086 green on the restored compiler"
  680. pass=$((pass + 1))
  681. else
  682. echo "NOT RESTORED: check_8086.py is red after restoring Compiler.mod"
  683. python3 tests/check_8086.py 2>&1 | grep -m3 -- ' - ' | sed 's/^/ /'
  684. fail=$((fail + 1))
  685. fi
  686. else
  687. echo "NOT RESTORED: the compiler would not rebuild"
  688. fail=$((fail + 1))
  689. fi
  690. echo
  691. echo "== the emitter-name audit of Compiler.mod (audit_helpers.py)"
  692. # These need no rebuild: the audit reads the SOURCE, not the built object, so
  693. # they are the cheapest cases here and they cover the module the audit used
  694. # not to look at at all. That is the point of the section: the audit reported
  695. # "every helper agrees with its name" for a module it had never examined, and
  696. # EmXchgAxCx was `93` (XCHG BX,AX) under a name that says XCHG AX,CX for the
  697. # whole life of the project. Two of these five are for faults that were real.
  698. SAVED_C2=/tmp/opencode/nonvacuity.Compiler.mod.2
  699. SAVED_R2=/tmp/opencode/nonvacuity.Runtime.mod.2
  700. cp Compiler.mod "$SAVED_C2" || exit 1
  701. cp Runtime.mod "$SAVED_R2" || exit 1
  702. restore_audit_sources () {
  703. cp "$SAVED_C2" Compiler.mod
  704. cp "$SAVED_R2" Runtime.mod
  705. }
  706. AUD="python3 tests/audit_helpers.py"
  707. # 1. THE fault. 91h is XCHG AX,CX; 93h is XCHG BX,AX. Both are one byte, so
  708. # the compile matrix never moved and the byte counts never moved.
  709. cp "$SAVED_C2" Compiler.mod
  710. mutate Compiler.mod 's|^ Ebyte (91H)$| Ebyte (93H)|'
  711. expect_red "audit catches XchgAxCx emitting XCHG BX,AX" \
  712. "exchanges Ax and Bx" $AUD
  713. restore_audit_sources
  714. # 2. the coverage check itself. A parameter list that find_helpers does not
  715. # accept is exactly how the real emitter was missed, and the inventory is
  716. # scanned separately on purpose so this can be caught. Without the
  717. # independent scan this case is silent, because both lists would come from
  718. # the same parser and agree that the helper does not exist.
  719. cp "$SAVED_C2" Compiler.mod
  720. mutate Compiler.mod 's|^PROCEDURE EmXchgAxCx () ;$|PROCEDURE EmXchgAxCx (why : CARDINAL) ;|'
  721. expect_red "audit reports an emitter it cannot reach, rather than skipping it" \
  722. "never examined it" $AUD
  723. restore_audit_sources
  724. # 3. EmXchgAxDx was named EmMoveAxDx, which said MOV where the bytes say XCHG.
  725. # 93h here is XCHG AX,BX - one letter away, the exact class of mistake the
  726. # name is supposed to make impossible.
  727. cp "$SAVED_C2" Compiler.mod
  728. mutate Compiler.mod 's|^ Ebyte (92H)$| Ebyte (93H)|'
  729. expect_red "audit catches XchgAxDx emitting XCHG BX,AX" \
  730. "XchgAxDx" $AUD
  731. restore_audit_sources
  732. # 4. CmpArgW0's [BP+2] written as the 386 SIB form, which decodes on a 8086 as
  733. # [SI+24h]. A real bug: the runtime was clearing the wrong memory.
  734. cp "$SAVED_R2" Runtime.mod
  735. mutate Runtime.mod 's| B (83H) ; B (7EH) ; B (2) ; B (0) ;| B (83H) ; B (7CH) ; B (24) ; B (0) ; B (0) ;|'
  736. expect_red "audit catches the [SI+24h] encoding of [BP+2]" \
  737. "memory base is 'si" $AUD
  738. restore_audit_sources
  739. # 5. MovAxSp is POP then PUSH, because MOV AX,[SP] does not exist on an 8086.
  740. # Dropping the POP leaves the stack one word short - a fault in the shape,
  741. # not in a byte value.
  742. cp "$SAVED_C2" Compiler.mod
  743. python3 - <<'PYEOF'
  744. p='Compiler.mod'; s=open(p).read()
  745. a=" Ebyte (58H) ; (* POP AX *)\n"
  746. assert s.count(a)==1, "EmMovAxSp POP line not found -- update this mutation"
  747. open(p,'w').write(s.replace(a, ""))
  748. PYEOF
  749. expect_red "audit catches MovAxSp with its POP missing" \
  750. "MovAxSp" $AUD
  751. restore_audit_sources
  752. # 6. The two-instruction shape: IDIV is CWD then IDIV, and dropping the CWD
  753. # leaves an un-sign-extended dividend in DX:AX. Both are still present as
  754. # a two-step spec, so a missing step has to be visible.
  755. cp "$SAVED_C2" Compiler.mod
  756. mutate Compiler.mod 's| Ebyte (99H) ; Ebyte (0F7H) ; Ebyte (0F9H)| Ebyte (0F7H) ; Ebyte (0F9H)|'
  757. expect_red "audit catches IDiv without the CWD that extends the dividend" \
  758. "IDivAxCx" $AUD
  759. restore_audit_sources
  760. if $AUD >/dev/null 2>&1; then
  761. echo " ok: the audit passes on both restored sources"
  762. pass=$((pass + 1))
  763. else
  764. echo "NOT RESTORED: the audit is red after restoring the sources"
  765. $AUD 2>&1 | sed 's/^/ /'
  766. fail=$((fail + 1))
  767. fi
  768. echo
  769. echo "== the BP contract rt_exec.py checks before it starts a machine"
  770. # wrchar and wrbool both borrowed BP to reach their argument -- [SP] is not
  771. # encodable in 16-bit mode -- and neither saved it. The driver's cursor into
  772. # the case record lives in BP precisely because BP is the one register an entry
  773. # may keep, so "wrchar borrowed it and did not give it back" sent the second
  774. # call to a garbage address, the machine triple-faulted, and the run printed the
  775. # record header twice and hung. Both the golden and the audit call that shape
  776. # CORRECT: the bytes are well formed, every branch is on a boundary, the size
  777. # is unchanged, and the decode says exactly what it says. So the rule is now
  778. # checked directly, and these two cases are what make that check more than a
  779. # claim.
  780. #
  781. # rt_exec.py needs the whole runtime rebuilt and then boots 36 machines, so this
  782. # section is the slow one. The baseline comes first and is asserted: a case
  783. # that mutates a red tree proves nothing.
  784. SAVED_R3=/tmp/opencode/nonvacuity.Runtime.mod.3
  785. cp Runtime.mod "$SAVED_R3" || exit 1
  786. if rebuild; then
  787. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  788. echo " ok: baseline - rt_exec.py passes on the unmutated runtime"
  789. pass=$((pass + 1))
  790. else
  791. echo " FAIL: the baseline is already red, so the cases below prove"
  792. echo " nothing - fix the baseline before reading them"
  793. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  794. fail=$((fail + 1))
  795. fi
  796. else
  797. echo " FAIL: could not rebuild the runtime for the baseline"
  798. fail=$((fail + 1))
  799. fi
  800. # 1. THE fault: drop the PUSH, exactly as EmitWrChar was written. This is the
  801. # shape the behavioural case found, so the byte-level check must find it too
  802. # -- a check that only the expensive test can trip is a check that has not
  803. # been made to earn its place.
  804. cp "$SAVED_R3" Runtime.mod
  805. python3 - <<'PYEOF'
  806. p = 'Runtime.mod'
  807. s = open(p).read()
  808. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n'
  809. assert s.count(a) == 1, "EmitWrChar prologue not found exactly once"
  810. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n'))
  811. PYEOF
  812. if rebuild; then
  813. expect_red "the BP contract catches wrchar borrowing BP unsaved" \
  814. "borrows BP but does not open with" python3 tests/rt_exec.py
  815. else
  816. echo " FAIL: the runtime would not rebuild with the PUSH removed"
  817. fail=$((fail + 1))
  818. fi
  819. cp "$SAVED_R3" Runtime.mod
  820. # 2. The mirror image: push it and never pop it. A different one-instruction
  821. # omission with the same consequence for a caller, and the reason the rule
  822. # asks for the 5D and not just the 55.
  823. cp "$SAVED_R3" Runtime.mod
  824. mutate Runtime.mod 's|^ MovSpBp ; PopBp ;$| MovSpBp ;|'
  825. if rebuild; then
  826. expect_red "the BP contract catches a BP that is pushed and never popped" \
  827. "pushes BP but never pops it" python3 tests/rt_exec.py
  828. else
  829. echo " FAIL: the runtime would not rebuild with the POP removed"
  830. fail=$((fail + 1))
  831. fi
  832. cp "$SAVED_R3" Runtime.mod
  833. # 3. The scan's other silent failure: a rule that matches nothing looks exactly
  834. # like a rule that passes. The pattern the check looks for is changed to one
  835. # the blob does not contain -- MOV BP,DI, which the runtime has no reason to
  836. # emit -- so the check has examined thirteen entries and matched nothing and
  837. # MUST say so rather than report a clean sweep. This is the general shape of
  838. # the fault this project keeps making: a check whose SUBJECT has drifted
  839. # reports a confident answer about the wrong thing.
  840. cp "$SAVED_R3" Runtime.mod
  841. SAVED_X=/tmp/opencode/nonvacuity.rt_exec.py
  842. cp tests/rt_exec.py "$SAVED_X" || exit 1
  843. mutate tests/rt_exec.py 's|^MOV_BP_SP = b"\\x8b\\xec" .*$|MOV_BP_SP = b"\\x8b\\xed" # MOV BP,DI: never emitted|' \
  844. expect_red "a check that matched nothing is a failure, not a pass" \
  845. "matched nothing" python3 tests/rt_exec.py
  846. cp "$SAVED_X" tests/rt_exec.py
  847. if rebuild; then
  848. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  849. echo " ok: rt_exec.py passes on the restored source"
  850. pass=$((pass + 1))
  851. else
  852. echo "NOT RESTORED: rt_exec.py is red after restoring Runtime.mod"
  853. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  854. fail=$((fail + 1))
  855. fi
  856. else
  857. echo "NOT RESTORED: the runtime would not rebuild"
  858. fail=$((fail + 1))
  859. fi
  860. # 4. The GOLDEN and the entry goldens, which are the other half of the same
  861. # rule. rt_exec.py states the contract; check_runtime.py pins the bytes.
  862. # They are separate mechanisms and the case below is what shows the golden
  863. # one works on its own -- a re-baseline of runtime.golden would otherwise
  864. # have absorbed the new prologues silently, and the next person to bless it
  865. # would have no way to know the PUSH and POP were ever missing.
  866. cp "$SAVED_R3" Runtime.mod
  867. python3 - <<'PYEOF'
  868. p = 'Runtime.mod'
  869. s = open(p).read()
  870. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n MovAlArg4 ;\n MovSpBp ; PopBp ;\n'
  871. assert s.count(a) == 1, "EmitWrChar frame not found exactly once"
  872. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n MovAlArg2 ;\n MovSpBp ;\n'))
  873. PYEOF
  874. if rebuild; then
  875. expect_red "the entry golden catches wrchar without its PUSH BP" \
  876. "entry wrchar starts 8B EC" python3 tests/check_runtime.py "$DUMP"
  877. else
  878. echo " FAIL: the runtime would not rebuild with wrchar's frame removed"
  879. fail=$((fail + 1))
  880. fi
  881. cp "$SAVED_R3" Runtime.mod
  882. echo
  883. echo "== the .COM layout check, and the runtime size it now measures"
  884. # The checker used to RESTATE the runtime's size as a literal. It was wrong
  885. # by 41 bytes for an unknown time, and every one of the 30 .COM files "failed"
  886. # on a header read out of the code stream. A duplicated constant that has
  887. # drifted does not fail loudly; it re-reports the same falsehood, in which the
  888. # real failures hide. The size is now MEASURED from the image.
  889. #
  890. # These cases corrupt a real emitted .COM and require the checker to notice.
  891. # They need the images, so they are built once and copied; the checker has a
  892. # --check-only mode for exactly this, because its scratch directory is normally
  893. # deleted on exit and a check that has only ever seen the truth is not a check.
  894. KEEPDIR=/tmp/opencode/nonvacuity.com
  895. rm -rf "$KEEPDIR"
  896. TP_COM_KEEP=1 tests/run_com_tests.sh >/tmp/opencode/nonvacuity.com.log 2>&1
  897. KEEP=$(sed -n 's/^TP_COM_KEEP=1: images left in //p' \
  898. /tmp/opencode/nonvacuity.com.log | tail -1)
  899. if [ -z "$KEEP" ] || [ ! -d "$KEEP" ]; then
  900. echo " FAIL: could not obtain emitted .COM images for the layout cases"
  901. fail=$((fail + 1))
  902. else
  903. COMCHK="tests/run_com_tests.sh --check-only"
  904. # 0. The baseline. Every case below is a claim that a specific assertion
  905. # turns red, and none of them means anything if the copies of untouched
  906. # images already fail. (The stale RT_SZ produced exactly that: 30
  907. # failures that were not findings.) So this is asserted first, and a
  908. # failure here is reported as a broken baseline rather than a red test.
  909. #
  910. # The baseline is also WHERE THE HEADER OFFSET COMES FROM. Cases 1 and 2
  911. # used to carry it as the literal 435 and 439, which were ENT_SZ + the
  912. # runtime size at the time -- 432. Two 4-byte changes to the runtime
  913. # later, both cases were editing the wrong bytes: 435 had become four
  914. # bytes inside the runtime itself, so case 1 had stopped testing "the
  915. # header cannot be found" and had started testing "the checker also
  916. # notices you scribbled on the code", and case 2 had become a no-op
  917. # that wrote the header where it already was. Both still went red, for
  918. # reasons the messages did not name, which is the whole problem: a
  919. # hard-coded offset is a claim about the world that expires silently.
  920. #
  921. # So the offset is read back out of the checker's own report on the
  922. # untouched images, and the expected numbers below are ARITHMETIC ON IT.
  923. # If the runtime grows again, these cases still test what they say.
  924. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  925. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  926. BASE_OUT=$($COMCHK "$KEEPDIR" 2>&1)
  927. if [ $? -eq 0 ]; then
  928. echo " ok: baseline - untouched copies of the real images all pass"
  929. pass=$((pass + 1))
  930. else
  931. echo " FAIL: the baseline is already red, so the cases below prove"
  932. echo " nothing - fix the baseline before reading them"
  933. printf '%s\n' "$BASE_OUT" | grep FAIL | head -3 | sed 's/^/ /'
  934. fail=$((fail + 1))
  935. fi
  936. BASE_RT=$(printf '%s\n' "$BASE_OUT" \
  937. | sed -n 's/.*measured runtime size: \([0-9][0-9]*\) bytes.*/\1/p' \
  938. | head -1)
  939. if [ -z "$BASE_RT" ]; then
  940. echo " FAIL: the checker did not report the runtime size it measured,"
  941. echo " so the cases below cannot find the header to edit"
  942. fail=$((fail + 1))
  943. BASE_RT=0
  944. fi
  945. # The layout constants are ENT_SZ 3 and HDR_SZ 16 (run_com_tests.sh), so the
  946. # header sits at hdrOff = 3 + rtSz and the entry jump's displacement must be
  947. # (hdrOff + 16) - 3. The wanted number is therefore computed from the header
  948. # OFFSET, not from rtSz: the two differ by 3, and getting that backwards
  949. # produces a plausible-looking expectation three bytes out, which is how
  950. # this case came to expect "want 452" and then be reported as not proving
  951. # what it said.
  952. BASE_OFF=$((3 + BASE_RT))
  953. # Case 2 claims a runtime four bytes LONGER, so the header - and with it the
  954. # demanded jump target - moves four bytes further on.
  955. SHIFTED_WANT=$((BASE_OFF + 4 + 16 - 3))
  956. echo " (measured runtime size $BASE_RT, header at image offset $BASE_OFF)"
  957. # 1. Break hdrDS so it no longer ties the header to its own offset. The
  958. # header must become UNFINDABLE and be reported as such - a checker that
  959. # fell back to a remembered offset would report a confident number here,
  960. # which is the failure mode the measurement was introduced to remove.
  961. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  962. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  963. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" <<'PYEOF'
  964. import sys
  965. p, off = sys.argv[1], int(sys.argv[2])
  966. d = bytearray(open(p, 'rb').read())
  967. d[off + 4:off + 6] = (0x1234).to_bytes(2, 'little') # hdrDS, no longer self-consistent
  968. open(p, 'wb').write(bytes(d))
  969. PYEOF
  970. expect_red "a header that cannot be located is reported, not assumed" \
  971. "no program header found" $COMCHK "$KEEPDIR"
  972. # 2. A complete, self-consistent header four bytes later, so the measured
  973. # runtime size becomes $((BASE_RT + 4)) instead of $BASE_RT. This is the
  974. # positive half of the same check: the derivation must FOLLOW the file,
  975. # and the entry jump assertion - expressed in terms of the measurement -
  976. # must follow it too, demanding $SHIFTED_WANT rather than $((BASE_OFF + 16 - 3)).
  977. #
  978. # hdrCS is copied from the header already in the file rather than written
  979. # as a literal. It used to be the literal 464+100h, which was the image
  980. # length when the image was 720 bytes; four bytes of runtime later it was
  981. # 464+100h against a 724-byte image, so this case was ALSO failing on
  982. # hdrCS, for a reason three lines below the one it was written to test.
  983. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  984. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  985. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" "$SHIFTED_WANT" <<'PYEOF'
  986. import sys
  987. p, off, want = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
  988. d = bytearray(open(p, 'rb').read())
  989. hdrCS = int.from_bytes(d[off + 2:off + 4], 'little') # unchanged: still the image end
  990. off += 4
  991. ds = off + 0x1000 + 0x100
  992. w = [1, hdrCS, ds, ds + 4, 0, 0, 0, 0]
  993. for i, x in enumerate(w):
  994. d[off + 2 * i:off + 2 * i + 2] = x.to_bytes(2, 'little')
  995. open(p, 'wb').write(bytes(d))
  996. PYEOF
  997. expect_red "the measured runtime size follows the image ($BASE_RT -> $((BASE_RT + 4)))" \
  998. "want $SHIFTED_WANT" $COMCHK "$KEEPDIR"
  999. # 3. The entry jump's opcode. One byte, and the only assertion in the
  1000. # project that can see where execution STARTS.
  1001. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1002. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1003. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  1004. import sys
  1005. p = sys.argv[1]
  1006. d = bytearray(open(p, 'rb').read())
  1007. d[0] = 0xEA
  1008. open(p, 'wb').write(bytes(d))
  1009. PYEOF
  1010. expect_red "the entry jump must be E9, not a near JMP" \
  1011. "not the E9 of the entry jump" $COMCHK "$KEEPDIR"
  1012. # 4. The entry jump's target, moved one instruction earlier. A .COM that
  1013. # lands in the middle of the prologue runs, prints something and exits
  1014. # cleanly, so no size or structure check can see this.
  1015. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1016. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1017. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  1018. import sys
  1019. p = sys.argv[1]
  1020. d = bytearray(open(p, 'rb').read())
  1021. d[1:3] = (100).to_bytes(2, 'little')
  1022. open(p, 'wb').write(bytes(d))
  1023. PYEOF
  1024. expect_red "the entry jump must land on the first instruction" \
  1025. "entry jump rel16=100" $COMCHK "$KEEPDIR"
  1026. rm -rf "$KEEPDIR"
  1027. fi
  1028. echo
  1029. echo "non-vacuity: $pass ok, $fail failed"
  1030. [ "$fail" -eq 0 ]