nonvacuity.sh 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518
  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, and both halves of a name that admits
  15. # an operand at all - `34 02' where the name promises
  16. # `34 01', and `35 01' where the row's opcode gate admits
  17. # only 34h. Both rows are new, and a row nobody has seen
  18. # reject anything accepts whatever it is shown.
  19. # audit_helpers.py coverage: catches a helper that has silently
  20. # dropped OUT of the audit, which is a green report about
  21. # a subject nobody looked at
  22. # run_com_tests.sh the .COM layout: catches a header that cannot be
  23. # located, a runtime size that disagrees with the image,
  24. # and an entry jump that starts in the wrong place
  25. # check_runtime.py golden: catches the same thing in the built
  26. # image
  27. # check_runtime.py decode sweep: catches a wrong instruction LENGTH
  28. # check_runtime.py branch targets: catches a wrong fixup
  29. # check_runtime.py entry goldens: catches a broken prologue
  30. # probe/modrm11.py the mod=11 table: catches the ModRM column itself
  31. # going wrong, which no amount of decoding will show
  32. # check_framedisp.py the BP disp rule: catches a displacement that reads
  33. # a different address than the symbol table named
  34. # tests/comimage.py the image layout: catches a sweep region built from a
  35. # literal instead of from two independent readings of the
  36. # file - the old RT_SZ went on reporting PASS while its
  37. # region began inside the runtime - and the layout itself
  38. # being written down a second time, now that one file
  39. # defines it and all three readers import it
  40. # rt_exec.py the BP contract: catches an entry that borrows BP to
  41. # reach its argument and does not hand it back. The bytes
  42. # are well formed, the golden is satisfied, the audit says
  43. # every helper emits what its name says, and the machine
  44. # triple-faults on the second call - so nothing short of
  45. # running it, or of stating the register contract
  46. # explicitly, can see it.
  47. # check_8086.py the 8086 opcodes: catches a conditional branch or a
  48. # SETcc emitted as `0F 8x'/`0F 9x', which are 386-only.
  49. # Nothing else here can: qemu-system-i386's lowest CPU
  50. # model is 486, where both are ordinary instructions, and
  51. # FCML's -m16 mode is a 386 too. Clause H additionally
  52. # catches the branch polarity being inverted, which is
  53. # still a legal opcode and therefore invisible to every
  54. # shape-based check.
  55. # run_com_exec.py behaviour: catches a*b that emits an ADD, `>'
  56. # and `>=' swapped, REPEAT..UNTIL that stops after one
  57. # pass, two procedures whose parameters collide, a left
  58. # operand overwritten by the right one's code (SaveLeft),
  59. # and a boolean `not' lowered as the integer one. Every
  60. # one of them sat in a fixture that COMPILED and was
  61. # never RUN, with every byte-level check green.
  62. # run_exec86.py behaviour (Exec86): catches the interpreter's OWN
  63. # reading of the machine: JE inverted in Exec86's Cond,
  64. # which swaps the two arms of every `=' in every program
  65. # while the emitted bytes, the sizes and every
  66. # byte-level check stay green. The image is fine; only
  67. # the thing reading it is wrong, so nothing that looks at
  68. # the image can fail this one.
  69. # runtest.py the R key: catches CmdRun poking nothing into
  70. # the interpreter at all, which faults on the first step
  71. # and prints "interpreter fault" instead of the guest's
  72. # answer. It is the only case here that needs a rebuilt
  73. # SHELL rather than a rebuilt compiler or runtime.
  74. #
  75. # The mod=11 cases do not need a rebuild -- they read the probe sources
  76. # directly -- so they are cheap, and they are the ones that matter most: the
  77. # table they guard is the one thing in this project that was wrong in the
  78. # documentation while the code was right, and a table that is wrong in the
  79. # code produces bytes that decode perfectly.
  80. #
  81. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  82. set -u
  83. cd "$(dirname "$0")/.." || exit 1
  84. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  85. SAVED=../tmp/nonvacuity.Runtime.mod
  86. PROBE=../tmp/nonvacuity.rtprobe
  87. DUMP=../tmp/nonvacuity.dump
  88. # Exec86.mod is UNTRACKED, so unlike Runtime.mod git cannot put a botched
  89. # mutation back: the copy taken here is the only correct one in existence.
  90. # Shell.mod is tracked but is mutated by the R-key case below, and a trap that
  91. # restored only the sources would leave tpshell BUILT FROM the mutation - so
  92. # the trap rebuilds too. All three copies live in the project's own tmp/ and
  93. # are taken here, before the first mutation, rather than beside each case.
  94. #
  95. # comimage.py and comtest.py are the image layout and one of its three readers;
  96. # both are mutated by cases further down, and neither is recoverable from git
  97. # once the mutation is staged over - so they join the trap here for the same
  98. # reason, and an interrupted run cannot leave the layout half-written.
  99. mkdir -p ../tmp
  100. SAVED_E=../tmp/nonvacuity.Exec86.mod
  101. SAVED_SH=../tmp/nonvacuity.Shell.mod
  102. SAVED_CI=../tmp/nonvacuity.comimage.py
  103. SAVED_T=../tmp/nonvacuity.comtest.py
  104. cp Runtime.mod "$SAVED" || exit 1
  105. cp Exec86.mod "$SAVED_E" || exit 1
  106. cp Shell.mod "$SAVED_SH" || exit 1
  107. cp tests/comimage.py "$SAVED_CI" || exit 1
  108. cp tests/comtest.py "$SAVED_T" || exit 1
  109. restore_all () {
  110. cp "$SAVED" Runtime.mod
  111. cp "$SAVED_E" Exec86.mod
  112. cp "$SAVED_SH" Shell.mod
  113. cp "$SAVED_CI" tests/comimage.py
  114. cp "$SAVED_T" tests/comtest.py
  115. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1
  116. "$GM2" -fiso -c Exec86.mod >/dev/null 2>&1
  117. # The shell is rebuilt as well: a test that runs against a binary built
  118. # from a half-restored tree is reporting on the mutation, not on the code.
  119. make >/dev/null 2>&1
  120. }
  121. trap restore_all EXIT
  122. pass=0
  123. fail=0
  124. # mutate <file> <sed-expr> -- apply a deliberate breakage and INSIST it landed.
  125. #
  126. # Four cases in this file were already dead when first run, all the same way:
  127. # the helper they name had been renamed or reformatted since the case was
  128. # written, the sed matched nothing, the source was unchanged, and the check
  129. # correctly passed - so the harness reported "NOT NON-VACUOUS" and, worse, a
  130. # reader skimming the output could take "the check still passed" for a passing
  131. # test. A case that cannot fire is worse than no case: it is a claim of
  132. # coverage that was never tested.
  133. #
  134. # So the mutation is verified, not assumed. If the file is byte-identical
  135. # afterwards, that is reported as a FAILURE of the harness, naming the sed, and
  136. # the case is not run - because running it would only produce a meaningless
  137. # green. The message says what to do (fix the sed) rather than what it found.
  138. mutate () {
  139. mf=$1
  140. msed=$2
  141. cp "$mf" ../tmp/nonvacuity.mut.bak
  142. sed -i "$msed" "$mf"
  143. if cmp -s "$mf" ../tmp/nonvacuity.mut.bak; then
  144. echo " BROKEN CASE: the mutation did not change $mf"
  145. echo " sed: $msed"
  146. echo " the named code has probably been renamed or reformatted -"
  147. echo " fix this case, it is asserting nothing"
  148. fail=$((fail + 1))
  149. return 1
  150. fi
  151. return 0
  152. }
  153. # rebuild <label> -- re-emit the runtime and dump it
  154. rebuild () {
  155. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  156. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  157. >/dev/null 2>&1 || return 1
  158. "$PROBE" > "$DUMP" || return 1
  159. return 0
  160. }
  161. # expect_red <label> <pattern> <checker-cmd...>
  162. # <pattern> is a grep the failure output must match, so a check cannot
  163. # "pass" by failing for some unrelated reason.
  164. expect_red () {
  165. label=$1
  166. want=$2
  167. shift 2
  168. if out=$("$@" 2>&1); then
  169. echo "NOT NON-VACUOUS: $label -- the check still passed"
  170. fail=$((fail + 1))
  171. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  172. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  173. printf '%s\n' "$out" | sed 's/^/ /'
  174. fail=$((fail + 1))
  175. else
  176. echo " ok: $label"
  177. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  178. pass=$((pass + 1))
  179. fi
  180. }
  181. echo "== each mutation must turn the named check red"
  182. echo
  183. # --- 1. name-versus-decode -------------------------------------------
  184. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  185. # cleanly, and no structural check can see it.
  186. cp "$SAVED" Runtime.mod
  187. mutate Runtime.mod 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|'
  188. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  189. "MovSiBx" python3 tests/audit_helpers.py
  190. # CmpSiBx had the identical mistake, which is how you know a single fix is
  191. # not enough -- the same misreading was written twice.
  192. cp "$SAVED" Runtime.mod
  193. mutate Runtime.mod 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|'
  194. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  195. "CmpSiBx" python3 tests/audit_helpers.py
  196. # --- 2. golden, and entry goldens ------------------------------------
  197. # MovDlAl was `88 C0` = MOV AL,AL instead of MOV DL,AL. This is the case that
  198. # motivated runtime.golden: the sweep stayed in sync, every branch target
  199. # stayed on a boundary, no entry's first bytes moved, and the size did not
  200. # change. The target helper was MovAlDh when this case was written, which is
  201. # the fourth way a case here can rot - see the note on `mutate` below.
  202. cp "$SAVED" Runtime.mod
  203. mutate Runtime.mod 's|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H)|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C0H)|'
  204. rebuild
  205. expect_red "runtime.golden catches MOV AL,AL" \
  206. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  207. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  208. # thing that noticed the prologue was a no-op.
  209. cp "$SAVED" Runtime.mod
  210. mutate Runtime.mod 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|'
  211. rebuild
  212. expect_red "check_runtime catches a broken initmem prologue" \
  213. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  214. # --- 3. decode sweep / length ----------------------------------------
  215. # StDiDl was `88 97` = [BX+disp16],DL: mod=10, so the instruction needs a
  216. # disp16 it was not given, and the sweep loses sync two bytes later.
  217. cp "$SAVED" Runtime.mod
  218. mutate Runtime.mod 's|PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H)|PROCEDURE StDiDl ; BEGIN B (88H) ; B (97H)|'
  219. rebuild
  220. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  221. "mov byte ptr \[bx+5b5fh\],dl" python3 tests/check_runtime.py "$DUMP"
  222. # --- 4. branch targets ------------------------------------------------
  223. # FixUp measures a rel8 from the end of the instruction, one byte past the
  224. # displacement field. Drop the +1 and every short branch lands one byte into
  225. # its target, which for a 3-byte instruction means the middle of it. The
  226. # bytes themselves are all perfectly well formed -- only the fixups are
  227. # wrong -- so this is the one failure mode the golden cannot be expected to
  228. # catch on its own.
  229. cp "$SAVED" Runtime.mod
  230. mutate Runtime.mod 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|'
  231. rebuild
  232. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  233. "not an instruction boundary" \
  234. python3 tests/check_runtime.py "$DUMP"
  235. echo
  236. echo "== everything restored and green again"
  237. cp "$SAVED" Runtime.mod
  238. if rebuild; then
  239. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  240. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  241. echo " ok: both checks pass on the restored source"
  242. pass=$((pass + 1))
  243. else
  244. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  245. fail=$((fail + 1))
  246. fi
  247. else
  248. echo "NOT RESTORED: the runtime would not rebuild"
  249. fail=$((fail + 1))
  250. fi
  251. echo
  252. echo "== the mod=11 table (probe/modrm11.py)"
  253. # These mutate the probe's own sources, not the runtime, so there is no
  254. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  255. SAVED_PY=../tmp/nonvacuity.modrm11.py
  256. SAVED_S=../tmp/nonvacuity.modrm11.s
  257. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  258. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  259. M11="python3 tests/probe/modrm11.py"
  260. restore_probe () {
  261. cp "$SAVED_PY" tests/probe/modrm11.py
  262. cp "$SAVED_S" tests/probe/modrm11.s
  263. }
  264. # 1. one cell of the table moved
  265. mutate tests/probe/modrm11.py 's|"Si", "Di"\]$|"Bp", "Di"]|'
  266. expect_red "anchor pins a moved table cell" \
  267. "anchor ADD SI, 2" $M11
  268. restore_probe
  269. # 2. the table this project actually shipped: AX dropped off the front and a
  270. # duplicate BX invented at the end, which shifts every code down by one
  271. mutate tests/probe/modrm11.py 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|'
  272. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  273. "anchor MOV SP, BP" $M11
  274. restore_probe
  275. # 3. the .s edited to contradict the table. This is the case that shows why
  276. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  277. # correctly, so comparing the .s against `as` alone can never fail here.
  278. mutate tests/probe/modrm11.s 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|'
  279. expect_red "probe source edited away from the recorded bytes" \
  280. "expected 89 E7" $M11
  281. restore_probe
  282. # 4. the 8-bit list edited, which is a different table from the word one
  283. mutate tests/probe/modrm11.s 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|'
  284. expect_red "the 8-bit register list edited" \
  285. "expected 88 C2" $M11
  286. restore_probe
  287. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  288. # corroborate itself
  289. mutate tests/probe/modrm11.py 's|"8B EC", "8B E5"|"8B ED", "8B E5"|'
  290. expect_red "anchor byte no longer matches the emitted code" \
  291. "expected 8B ED" $M11
  292. restore_probe
  293. if $M11 >/dev/null 2>&1; then
  294. echo " ok: modrm11.py passes on the restored probe sources"
  295. pass=$((pass + 1))
  296. else
  297. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  298. $M11 2>&1 | sed 's/^/ /'
  299. fail=$((fail + 1))
  300. fi
  301. echo
  302. echo "== the BP displacement rule (check_framedisp.py)"
  303. # This one is about Compiler.mod rather than the runtime, and it needs the
  304. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  305. SAVED_C=../tmp/nonvacuity.Compiler.mod
  306. cp Compiler.mod "$SAVED_C" || exit 1
  307. rebuild_compiler () {
  308. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  309. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  310. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  311. >/dev/null 2>&1
  312. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  313. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  314. >/dev/null 2>&1 || return 1
  315. return 0
  316. }
  317. # 1. the original bug: `off MOD 100H`, always disp8. Restores exactly the code
  318. # that was there before EmBpDisp existed. t28's [BP+128] read becomes
  319. # [BP-128], which is the failure this whole check is named after.
  320. python3 - "$SAVED_C" <<'PYEOF'
  321. import sys
  322. p = 'Compiler.mod'
  323. s = open(p).read()
  324. old = """BEGIN
  325. IF off <= 127 THEN
  326. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  327. ELSE
  328. Ebyte (86H) ; Eword (off)
  329. END
  330. END EmBpDisp ;"""
  331. new = """VAR disp : CARDINAL ;
  332. BEGIN
  333. disp := off MOD 100H ;
  334. Ebyte (46H) ; Ebyte (VAL (BYTE, disp))
  335. END EmBpDisp ;"""
  336. assert old in s, "EmBpDisp body not found -- update this mutation"
  337. open(p, 'w').write(s.replace(old, new))
  338. PYEOF
  339. if rebuild_compiler; then
  340. expect_red "displacement truncation reads a different address" \
  341. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  342. else
  343. echo " FAIL: the compiler would not rebuild with the truncation"
  344. fail=$((fail + 1))
  345. fi
  346. cp "$SAVED_C" Compiler.mod
  347. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  348. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  349. # still be happy -- which is worth asserting, because a check that only
  350. # ever fails on a smaller encoding is a check that pins one answer instead
  351. # of the rule.
  352. python3 - <<'PYEOF'
  353. p = 'Compiler.mod'
  354. s = open(p).read()
  355. old = """ IF off <= 127 THEN
  356. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  357. ELSE
  358. Ebyte (86H) ; Eword (off)
  359. END"""
  360. new = """ Ebyte (86H) ; Eword (off)"""
  361. assert old in s, "EmBpDisp branch not found -- update this mutation"
  362. open(p, 'w').write(s.replace(old, new))
  363. PYEOF
  364. if rebuild_compiler; then
  365. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  366. echo " ok: always-disp16 is accepted, so the check pins the rule"
  367. echo " and not one particular encoding"
  368. pass=$((pass + 1))
  369. else
  370. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  371. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  372. fail=$((fail + 1))
  373. fi
  374. else
  375. echo " FAIL: the compiler would not rebuild with always-disp16"
  376. fail=$((fail + 1))
  377. fi
  378. cp "$SAVED_C" Compiler.mod
  379. if rebuild_compiler; then
  380. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  381. echo " ok: check_framedisp passes on the restored source"
  382. pass=$((pass + 1))
  383. else
  384. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  385. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  386. fail=$((fail + 1))
  387. fi
  388. else
  389. echo "NOT RESTORED: the compiler would not rebuild"
  390. fail=$((fail + 1))
  391. fi
  392. # --- the region check_framedisp sweeps, and the one file it comes from -----
  393. echo
  394. echo "== the image layout, measured once (tests/comimage.py)"
  395. # check_framedisp.py decodes instructions over a REGION of the image, and a
  396. # wrong region does not stop it: the byte patterns it searches for are in the
  397. # image wherever they happen to be, and a decode that starts mid-instruction
  398. # reached the same offsets anyway. That is how RT_SZ = 391 - a literal that had
  399. # drifted from the runtime it described - went on reporting PASS while its sweep
  400. # began inside the runtime, part way through an instruction. Nothing in the
  401. # check could see where its own region began.
  402. #
  403. # So the region is two independent readings of the file - where the entry jump
  404. # says execution starts, and where the program header says the runtime ends -
  405. # and the two must AGREE or the region is undetermined. Both readings, and the
  406. # layout itself, live in tests/comimage.py, which is also where comtest.py and
  407. # the independent checker in run_com_tests.sh now get the copy each of them
  408. # used to keep: a duplicated constant that has silently drifted is not an
  409. # independent check, it is a second source of truth that lies, and it lies in
  410. # the direction of looking like the code under test is broken.
  411. #
  412. # None of this needs a rebuild - comimage.py is Python the checkers import -
  413. # so these cases are cheap, and they turn the READERS red one at a time.
  414. # 1. entry_target answers with the literal the region used to be built from.
  415. # The two readings now disagree, and there is no measured region to sweep.
  416. python3 - <<'PYEOF'
  417. p = 'tests/comimage.py'
  418. s = open(p).read()
  419. old = ' return ENT_SZ + int.from_bytes(d[1:ENT_SZ], "little", signed=True)'
  420. new = ' return ENT_SZ + 391 # the literal RT_SZ was'
  421. assert s.count(old) == 1, "entry_target's return not found -- update this mutation"
  422. open(p, 'w').write(s.replace(old, new))
  423. PYEOF
  424. expect_red "a sweep region that is not where execution starts is rejected" \
  425. "neither region is swept" python3 tests/check_framedisp.py
  426. cp "$SAVED_CI" tests/comimage.py
  427. # 2 and 3. The header's own equation, one byte out, so the header cannot be
  428. # located at all - the case where a checker that fell back on a remembered
  429. # offset would report a confident number instead. Two readers, two
  430. # different sentences: each must say in its own words that it does not know
  431. # where the program's code begins.
  432. python3 - <<'PYEOF'
  433. p = 'tests/comimage.py'
  434. s = open(p).read()
  435. old = ' if w(HDR_DS_WORD) != off + 0x1000 + LOAD_BIAS: # hdrDS'
  436. new = ' if w(HDR_DS_WORD) != off + 0x1000 + LOAD_BIAS + 1: # hdrDS'
  437. assert s.count(old) == 1, "the hdrDS equation not found -- update this mutation"
  438. open(p, 'w').write(s.replace(old, new))
  439. PYEOF
  440. expect_red "check_framedisp reports a header it cannot locate" \
  441. "no program header found" python3 tests/check_framedisp.py
  442. expect_red "check_8086 reports the same unlocatable header" \
  443. "could not locate the code region" python3 tests/check_8086.py
  444. cp "$SAVED_CI" tests/comimage.py
  445. # 4-7. The single copy itself, now that there is one. A second find_header,
  446. # a second HDR_SZ, a caller that reaches the helper through another
  447. # consumer instead of through comimage, and - the case that exists
  448. # because of rt_exec.py's "matched nothing" lesson - the definition the
  449. # audit counts. A rule that matches nothing looks exactly like a rule
  450. # that passes, so a deleted definition must be a red report rather than a
  451. # silent green one.
  452. python3 - <<'PYEOF'
  453. p = 'tests/comtest.py'
  454. s = open(p).read()
  455. old = 'def check_com(path, expected_src_len):'
  456. new = ('def find_header(d):\n'
  457. ' return None # a second copy of the helper\n'
  458. '\n\n'
  459. 'def check_com(path, expected_src_len):')
  460. assert s.count(old) == 1, "check_com not found -- update this mutation"
  461. open(p, 'w').write(s.replace(old, new))
  462. PYEOF
  463. expect_red "the audit reports a find_header copied out of comimage" \
  464. "comtest.py defines its own find_header" python3 tests/check_comimage.py
  465. cp "$SAVED_T" tests/comtest.py
  466. python3 - <<'PYEOF'
  467. p = 'tests/comtest.py'
  468. s = open(p).read()
  469. old = 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,'
  470. new = ('HDR_SZ = 16 # a second copy of the layout\n'
  471. 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,')
  472. assert s.count(old) == 1, "the comimage import not found -- update this mutation"
  473. open(p, 'w').write(s.replace(old, new))
  474. PYEOF
  475. expect_red "the audit reports a layout constant defined elsewhere" \
  476. "comtest.py defines its own HDR_SZ" python3 tests/check_comimage.py
  477. cp "$SAVED_T" tests/comtest.py
  478. python3 - <<'PYEOF'
  479. p = 'tests/comimage.py'
  480. s = open(p).read()
  481. old = 'def find_header(d):'
  482. new = 'def _find_header(d): # the definition is gone'
  483. assert s.count(old) == 1, "def find_header not found -- update this mutation"
  484. open(p, 'w').write(s.replace(old, new))
  485. PYEOF
  486. expect_red "the audit counts its own subject rather than matching nothing" \
  487. "defines find_header 0 times" python3 tests/check_comimage.py
  488. cp "$SAVED_CI" tests/comimage.py
  489. # 7. The import dropped while the CALLS stay - the shape a refactor makes when
  490. # one reader starts taking the helper from another reader instead. It would
  491. # NameError the moment comtest ran, but the audit says so first, and in the
  492. # vocabulary of the layout rather than in the vocabulary of Python: the
  493. # failure this is guarding is "two sources of truth", not "undefined name".
  494. python3 - <<'PYEOF'
  495. p = 'tests/comtest.py'
  496. s = open(p).read()
  497. old = ('from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,\n'
  498. ' HDR_HEAP_WORD, find_header)')
  499. new = '# MUTATION: the shared import is gone, but the calls remain\n'
  500. assert s.count(old) == 1, "the comimage import not found -- update this mutation"
  501. open(p, 'w').write(s.replace(old, new))
  502. PYEOF
  503. expect_red "the audit reports a caller that does not take it from comimage" \
  504. "comtest.py calls find_header without importing it from comimage" \
  505. python3 tests/check_comimage.py
  506. cp "$SAVED_T" tests/comtest.py
  507. # Green again, on all three readers at once: the case above mutated the one
  508. # file all of them import, so restoring it has to satisfy each of them, not
  509. # just the one that was last run.
  510. for c in tests/check_framedisp.py tests/check_comimage.py tests/check_8086.py; do
  511. if python3 "$c" >/dev/null 2>&1; then
  512. echo " ok: $c passes on the restored sources"
  513. pass=$((pass + 1))
  514. else
  515. echo "NOT RESTORED: $c is red after restoring comimage.py and comtest.py"
  516. python3 "$c" 2>&1 | tail -3 | sed 's/^/ /'
  517. fail=$((fail + 1))
  518. fi
  519. done
  520. # --- 3. the operator bugs the nine dead fixtures were hiding ------------
  521. echo
  522. echo "== the bugs the never-executed fixtures were hiding"
  523. echo
  524. # A different KIND of case from everything above. The others break the code
  525. # and assert a byte-level check notices; these break the code and assert a
  526. # BEHAVIOURAL check notices, which is the only kind that could have found them.
  527. # Each of the original four shipped with a green compile matrix, a passing .COM
  528. # layout check, a passing golden and a passing emitter audit:
  529. #
  530. # OpMul = 1 `a * b` emitted ADD AX,CX. `*' and `+' both numbered
  531. # their operator 1, and BinOpEmit cannot see which
  532. # precedence level called it, so every multiplication
  533. # dispatched to the addition. The constant-folding arm was
  534. # correct, which is why `n * n' with n a CONST was right and
  535. # `a * a' with a a variable was not -- and t08_const is the
  536. # only fixture that ever multiplied.
  537. # 9Dh / 9FH `>' got SETGE and `>=' got SETG: swapped, one letter
  538. # apart in the mnemonic. Only a==b could see it.
  539. # JNZ -> body REPEAT..UNTIL looped back while the condition was TRUE,
  540. # which is WHILE, so the body ran once and stopped.
  541. #
  542. # They are mutated back to the original defect and run_com_exec.py must go red
  543. # on the exact fixture that pins the behaviour. The fourth (HideLocals) is a
  544. # scoping bug rather than an operator bug; it was hiding in the same place.
  545. #
  546. # Two joined them when the operand-lifetime and `not' fixes landed, each run
  547. # red by hand before it was written down here, and each with the same property
  548. # as the four above -- green everywhere except execution:
  549. #
  550. # SaveLeft's park `(p > q) or (q > p)' evaluated `(q > p) or (q > p)'. A
  551. # kind-2 value exists only in AX, and the right operand's
  552. # code is emitted before the two are ever brought together,
  553. # so without the push the left one is simply gone.
  554. # neglevel's split `not' on a boolean emitted the INTEGER `not', which left
  555. # 0FFFEh where a boolean belongs, and wrbool reads anything
  556. # non-zero as TRUE -- so `not (a = a)' answered TRUE and so
  557. # did every other `not'.
  558. mutate_compiler () { # reuse mutate's verified-change discipline on Compiler.mod
  559. mf=Compiler.mod
  560. msed=$1
  561. cp "$SAVED_C" ../tmp/nonvacuity.mut2.bak
  562. sed -i "$msed" "$mf"
  563. if cmp -s "$mf" ../tmp/nonvacuity.mut2.bak; then
  564. echo " BROKEN CASE: the mutation did not change Compiler.mod"
  565. echo " sed: $msed"
  566. echo " the named code has probably been renamed or reformatted -"
  567. echo " fix this case, it is asserting nothing"
  568. fail=$((fail + 1))
  569. return 1
  570. fi
  571. return 0
  572. }
  573. # The SETcc swap and the HideLocals removal are done in python rather than
  574. # with sed: both need to match source text containing `*` and `(` in a way that
  575. # is tedious and fragile as a regex, and a case whose only failure mode is a
  576. # malformed sed is a case that silently asserts nothing.
  577. #
  578. # And a python helper fails in a way sed does not: a syntax error in the helper
  579. # is a non-zero exit, `if mutate_foo; then` is simply false, and the case is
  580. # SKIPPED -- with no failure counted and nothing on stdout but whatever python
  581. # printed. That is how the SETcc case spent its first run: an apostrophe in an
  582. # assert message ("the `>' arm") closed the string early, python died, the
  583. # compiler was never broken, and the suite still reported 0 failed. A skipped
  584. # case and a passing case look the same in the total. So each helper below
  585. # fails LOUDLY: a non-zero exit from python is reported as a BROKEN CASE and
  586. # counted, never swallowed.
  587. #
  588. # Each one also counts its targets before replacing. `assert s != before' only
  589. # says the file changed; with two edits it would pass if just one of them
  590. # landed, and with two identical HideLocals call sites it would happily delete
  591. # the wrong one -- still a changed file, still a working compiler, still green
  592. # for the wrong reason.
  593. mutate_cc_swap () {
  594. if python3 - <<'PYX'
  595. p = 'Compiler.mod'
  596. s = open(p).read()
  597. GT = 'EmSetcc (9FH) ; (* > SETG *)' # the greater-than arm
  598. GE = 'EmSetcc (9DH) ; (* >= SETGE *)' # the greater-equal arm
  599. assert s.count(GT) == 1, 'expected 1 greater-than arm, found %d' % s.count(GT)
  600. assert s.count(GE) == 1, 'expected 1 greater-equal arm, found %d' % s.count(GE)
  601. s = s.replace(GT, GT.replace('9FH', '9DH'))
  602. s = s.replace(GE, GE.replace('9DH', '9FH'))
  603. open(p, 'w').write(s)
  604. PYX
  605. then
  606. return 0
  607. fi
  608. echo " BROKEN CASE: the SETcc swap did not apply"
  609. echo " the two EmSetcc arms are probably renamed or reformatted -"
  610. echo " fix this case, it is asserting nothing"
  611. fail=$((fail + 1))
  612. return 1
  613. }
  614. mutate_no_hidelocals () {
  615. if python3 - <<'PYX'
  616. p = 'Compiler.mod'
  617. s = open(p).read()
  618. # Two HideLocals calls exist. Only the body-exit one may go: deleting the
  619. # FORWARD one instead would still change the file, still rebuild, and still
  620. # leave t13_proc compiling, so the case would go green for the wrong reason.
  621. HL = ' HideLocals (nestMark) ; (* parameters and locals stop here *)\n'
  622. assert s.count(HL) == 1, 'expected 1 body-exit HideLocals, found %d' % s.count(HL)
  623. open(p, 'w').write(s.replace(HL, ''))
  624. PYX
  625. then
  626. return 0
  627. fi
  628. echo " BROKEN CASE: HideLocals was not removed"
  629. echo " the call or its comment has probably been reformatted -"
  630. echo " fix this case, it is asserting nothing"
  631. fail=$((fail + 1))
  632. return 1
  633. }
  634. cp "$SAVED_C" Compiler.mod
  635. if mutate_compiler 's|^ op := OpMul ; DropCh| op := OpAdd ; DropCh|'; then
  636. if rebuild_compiler; then
  637. expect_red "execution catches '*' emitting an ADD (t08_const, n*n)" \
  638. "t08_const" python3 tests/run_com_exec.py t08_const
  639. # t08 only ever multiplied two CONSTANTS, which is the one path that was
  640. # never wrong, because BinOpEmit folds it. So the case above is close
  641. # to vacuous: it proves the mutation changed the binary, not that the
  642. # emitted multiply is covered. The check that matters needs a
  643. # VARIABLE operand, and no shipped fixture has one -- which is why the
  644. # bug survived at all. So this writes a throwaway fixture that
  645. # multiplies a variable, runs it, and asserts the multiply is right.
  646. # The fixture is deleted afterwards; it is here to close the coverage
  647. # hole, not to become a permanent test (that is what a real fixture
  648. # with a `*' in it would be for).
  649. cat > tests/fixtures/zzmul.pas <<'ZZEOF'
  650. program zzmul;
  651. var a : integer ;
  652. begin
  653. a := 7 ;
  654. writeln (a * 6)
  655. end.
  656. ZZEOF
  657. printf '42\r\n' > tests/fixtures/zzmul.out
  658. expect_red "execution catches '*' on a VARIABLE (the unfolded path)" \
  659. "zzmul" python3 tests/run_com_exec.py zzmul
  660. rm -f tests/fixtures/zzmul.pas tests/fixtures/zzmul.out
  661. else
  662. echo " FAIL: the compiler would not rebuild with OpAdd for '*'"
  663. fail=$((fail + 1))
  664. fi
  665. fi
  666. cp "$SAVED_C" Compiler.mod
  667. cp "$SAVED_C" Compiler.mod
  668. if mutate_cc_swap; then
  669. if rebuild_compiler; then
  670. expect_red "execution catches '>' and '>=' swapped (t09_if)" \
  671. "t09_if" python3 tests/run_com_exec.py t09_if
  672. else
  673. echo " FAIL: the compiler would not rebuild with the SETcc swap"
  674. fail=$((fail + 1))
  675. fi
  676. fi
  677. cp "$SAVED_C" Compiler.mod
  678. cp "$SAVED_C" Compiler.mod
  679. if mutate_compiler 's| DropC (EmJcc (84H, L1)) ; (\* JZ -> body again \*)| zj := EmJcc (85H, L1) ;|'; then
  680. if rebuild_compiler; then
  681. expect_red "execution catches REPEAT..UNTIL exiting after one pass (t12)" \
  682. "t12_repeat" python3 tests/run_com_exec.py t12_repeat
  683. else
  684. echo " FAIL: the compiler would not rebuild with the JNZ repeat"
  685. fail=$((fail + 1))
  686. fi
  687. fi
  688. cp "$SAVED_C" Compiler.mod
  689. # The fourth: sibling procedures shared one parameter namespace, because a
  690. # finished procedure's symbols were left at a level Search still accepts.
  691. # Removing HideLocals puts two procedures' `a : integer' back in collision.
  692. cp "$SAVED_C" Compiler.mod
  693. if mutate_no_hidelocals; then
  694. if rebuild_compiler; then
  695. expect_red "a duplicate parameter in two procedures is a compile error again" \
  696. "ERROR 41" python3 tests/run_com_exec.py t13_proc
  697. else
  698. echo " FAIL: the compiler would not rebuild without HideLocals"
  699. fail=$((fail + 1))
  700. fi
  701. else
  702. echo " FAIL: could not remove HideLocals to test the scoping fix"
  703. fail=$((fail + 1))
  704. fi
  705. # M6: the LEFT operand, parked for the right one. kind 2 means "this value
  706. # exists in AX and nowhere else", and the right-hand operand's code is emitted
  707. # between recognizing the operator and using both operands - so without
  708. # SaveLeft's push the left value is overwritten before it is ever read, and
  709. # LoadPair's kind-4 shape can never trigger. `(p > q) or (q > p)' then
  710. # evaluates `(q > p) or (q > p)'. Every byte, every size and the whole compile
  711. # matrix stay green: nothing is malformed, the value is simply the wrong one.
  712. cp "$SAVED_C" Compiler.mod
  713. if python3 - <<'PYX'
  714. p = 'Compiler.mod'
  715. s = open(p).read()
  716. old = """BEGIN
  717. IF left.kind = 2 THEN
  718. EmPushAx () ;
  719. left.kind := 4 (* 4 = on the top of the stack *)
  720. END
  721. END SaveLeft ;"""
  722. new = """BEGIN
  723. (* MUTATION: the park is unreachable, so nothing survives the parse *)
  724. IF left.kind = 99 THEN
  725. EmPushAx () ;
  726. left.kind := 4
  727. END
  728. END SaveLeft ;"""
  729. assert s.count(old) == 1, 'SaveLeft body found %d times -- update this mutation' % s.count(old)
  730. open(p, 'w').write(s.replace(old, new))
  731. PYX
  732. then
  733. if rebuild_compiler; then
  734. expect_red "execution catches a left operand clobbered by the right one" \
  735. "t34_arith" python3 tests/run_com_exec.py t34_arith
  736. else
  737. echo " FAIL: the compiler would not rebuild without SaveLeft's push"
  738. fail=$((fail + 1))
  739. fi
  740. else
  741. echo " BROKEN CASE: the SaveLeft mutation did not apply"
  742. fail=$((fail + 1))
  743. fi
  744. cp "$SAVED_C" Compiler.mod
  745. # M7: the type split in TPSRC9's neglevel, wrong half. A boolean NOT lowered
  746. # as the INTEGER one leaves 0FFFEh/0FFFFh, and wrbool tests [BP+4] <> 0 - so
  747. # `not (a = a)' answers TRUE, and so does every other `not'. One instruction,
  748. # same length, same sizes, and the compile matrix cannot see it because both
  749. # halves emit well-formed code for a type the parser already accepted.
  750. cp "$SAVED_C" Compiler.mod
  751. if python3 - <<'PYX'
  752. p = 'Compiler.mod'
  753. s = open(p).read()
  754. old = """ IF r.cls = TBool THEN
  755. LoadAtom (r) ;
  756. EmXorAl01 () ;"""
  757. new = """ IF r.cls = TBool THEN
  758. LoadAtom (r) ;
  759. EmNotAx () ; (* MUTATION: integer NOT on a boolean *)"""
  760. assert s.count(old) == 1, 'the TBool arm of ParseNeg found %d times -- update this mutation' % s.count(old)
  761. open(p, 'w').write(s.replace(old, new))
  762. PYX
  763. then
  764. if rebuild_compiler; then
  765. expect_red "execution catches a boolean NOT lowered as an integer one" \
  766. "t35_not" python3 tests/run_com_exec.py t35_not
  767. else
  768. echo " FAIL: the compiler would not rebuild with EmNotAx for a boolean"
  769. fail=$((fail + 1))
  770. fi
  771. else
  772. echo " BROKEN CASE: the neglevel type-split mutation did not apply"
  773. fail=$((fail + 1))
  774. fi
  775. cp "$SAVED_C" Compiler.mod
  776. if rebuild_compiler; then
  777. if python3 tests/run_com_exec.py >/dev/null 2>&1; then
  778. # No count: the matrix grows every time a fixture is added, and a
  779. # number here would be right only until the next one.
  780. echo " ok: every executed fixture passes on the restored compiler"
  781. pass=$((pass + 1))
  782. else
  783. echo "NOT RESTORED: run_com_exec.py is red after restoring Compiler.mod"
  784. python3 tests/run_com_exec.py 2>&1 | grep -i fail | head -3 | sed 's/^/ /'
  785. fail=$((fail + 1))
  786. fi
  787. else
  788. echo "NOT RESTORED: the compiler would not rebuild"
  789. fail=$((fail + 1))
  790. fi
  791. echo
  792. echo "== the second execution oracle (run_exec86.py)"
  793. # Everything above runs the image under qemu-system-i386. This one runs the
  794. # SAME image inside shell/Exec86.mod, this project's own in-process 8086
  795. # interpreter, and requires its output to agree with BOTH the hand-derived .out
  796. # and qemu, byte for byte. Two execution checks that could only ever agree
  797. # with each other would be one check: the point of this one is that it was
  798. # written against the 8086's own reference rather than against qemu, whose
  799. # lowest CPU model is a 486 and whose `0F 84' is an ordinary JZ.
  800. #
  801. # So the only mutation worth writing here is one qemu cannot make at all - the
  802. # interpreter's own reading of the machine. Cond is that reading: nibble 4 is
  803. # JE, and inverting it swaps the two arms of every `=' in every program. The
  804. # emitted bytes, the sizes and the compile matrix are untouched, because
  805. # nothing is emitted here - the fault is in who interprets it.
  806. #
  807. # Exec86.mod is recompiled explicitly rather than left to the harness:
  808. # ensure_exec86run() notices the source changed and RELINKS, but does not
  809. # recompile it, so a mutated source with a stale object would link the good
  810. # interpreter straight back in and stay green - which is the trap its own
  811. # docstring records, and the reason this case compiles first.
  812. #
  813. # SAVED_E itself was taken at the top of this file, next to the EXIT trap that
  814. # puts it back.
  815. if python3 - <<'PYX'
  816. p = 'Exec86.mod'
  817. s = open(p).read()
  818. old = "| 4H : r := ZF"
  819. new = "| 4H : r := NOT ZF (* MUTATION: JE inverted *)"
  820. assert s.count(old) == 1, 'the JE arm of Cond found %d times -- update this mutation' % s.count(old)
  821. open(p, 'w').write(s.replace(old, new))
  822. PYX
  823. then
  824. if $GM2 -fiso -c Exec86.mod >/dev/null 2>&1; then
  825. expect_red "the interpreter's own oracle catches JE inverted" \
  826. "t33_cmpops" python3 tests/run_exec86.py t33_cmpops
  827. else
  828. echo " FAIL: Exec86.mod would not compile with JE inverted"
  829. fail=$((fail + 1))
  830. fi
  831. else
  832. echo " BROKEN CASE: the Cond JE mutation did not apply"
  833. fail=$((fail + 1))
  834. fi
  835. cp "$SAVED_E" Exec86.mod
  836. # And the check on the RESTORED interpreter, because a green above could also
  837. # come from a mutation that never took and an object left mutated by a run
  838. # that died in between.
  839. if $GM2 -fiso -c Exec86.mod >/dev/null 2>&1; then
  840. if python3 tests/run_exec86.py >/dev/null 2>&1; then
  841. echo " ok: run_exec86 green on the restored interpreter"
  842. pass=$((pass + 1))
  843. else
  844. echo "NOT RESTORED: run_exec86.py is red after restoring Exec86.mod"
  845. python3 tests/run_exec86.py 2>&1 | grep -i "fail" | head -3 | sed 's/^/ /'
  846. fail=$((fail + 1))
  847. fi
  848. else
  849. echo "NOT RESTORED: Exec86.mod would not recompile"
  850. fail=$((fail + 1))
  851. fi
  852. echo
  853. echo "== the R key: compile, poke, run, report (runtest.py)"
  854. # The only check in the project that exercises CmdRun. Everything above looks
  855. # at bytes, or at what qemu says the image does; this one drives the shell
  856. # through a pty the way a person would, presses R, and compares the GUEST's
  857. # output against the fixture's hand-derived .out - so it fails on things no
  858. # byte check can see and no file records either, because R writes no file at
  859. # all (which is itself asserted).
  860. #
  861. # The mutation is the poke loop's count. With n = 0 nothing is copied into
  862. # the interpreter, loadHi stays where Clear86 left it - 0100h - and Run86
  863. # faults on its very first step, "execution left the loaded image", before the
  864. # guest has executed a single instruction. That is fast and deterministic, which
  865. # matters: the obvious alternative (poking the image somewhere else) leaves the
  866. # machine executing zeros and buys a step-limit timeout instead of a finding.
  867. cp "$SAVED_SH" Shell.mod
  868. if python3 - <<'PYX'
  869. p = 'Shell.mod'
  870. s = open(p).read()
  871. old = " n := LinkSize () ;"
  872. new = " n := 0 ; (* MUTATION: nothing is poked *)"
  873. assert s.count(old) == 1, 'the poke count in CmdRun found %d times -- update this mutation' % s.count(old)
  874. open(p, 'w').write(s.replace(old, new))
  875. PYX
  876. then
  877. if make > ../tmp/nonvacuity.make.log 2>&1; then
  878. expect_red "the R check catches an image that was never poked" \
  879. "AH=4Ch exit" python3 tests/runtest.py
  880. else
  881. echo " FAIL: the shell would not rebuild with the poke loop neutered"
  882. tail -5 ../tmp/nonvacuity.make.log | sed 's/^/ /'
  883. fail=$((fail + 1))
  884. fi
  885. else
  886. echo " BROKEN CASE: the CmdRun poke mutation did not apply"
  887. fail=$((fail + 1))
  888. fi
  889. cp "$SAVED_SH" Shell.mod
  890. if make > ../tmp/nonvacuity.make.log 2>&1; then
  891. if python3 tests/runtest.py >/dev/null 2>&1; then
  892. echo " ok: runtest green on the restored shell"
  893. pass=$((pass + 1))
  894. else
  895. echo "NOT RESTORED: runtest.py is red after restoring Shell.mod"
  896. python3 tests/runtest.py 2>&1 | grep -i "fail" | head -3 | sed 's/^/ /'
  897. fail=$((fail + 1))
  898. fi
  899. else
  900. echo "NOT RESTORED: the shell would not rebuild"
  901. tail -5 ../tmp/nonvacuity.make.log | sed 's/^/ /'
  902. fail=$((fail + 1))
  903. fi
  904. echo
  905. echo "== 8086 legality of the conditional lowering (check_8086.py)"
  906. # This whole section exists because of a fault that every other check in the
  907. # project was blind to, and being blunt about WHY is the point of listing it.
  908. #
  909. # EmJcc and EmSetcc emitted `0F 8x rel16' and `0F 9x rel8'. `0F' is a
  910. # 386-and-later opcode prefix; the 8086 has none. So EVERY conditional branch
  911. # and EVERY comparison in EVERY compiled program was an illegal instruction on
  912. # the machine this compiler targets. And all of the following were green while
  913. # it was: the compile matrix, the .COM layout checker, the runtime golden, the
  914. # emitter audit, and the whole execution suite answering correctly under qemu.
  915. #
  916. # Two reasons, and the second is the one worth keeping:
  917. # 1. qemu-system-i386 has no 8086 model. Its lowest is 486, where `0F 84' is
  918. # a perfectly ordinary JZ. So the execution oracle CANNOT see this class
  919. # of fault, ever.
  920. # 2. FCML is this project's INDEPENDENT disassembler, and FCML's -m16 mode is
  921. # a 386. The one tool whose job is to say "this is not a real instruction"
  922. # was architecturally guaranteed to agree with the bug.
  923. #
  924. # So a check was needed that asks the question nothing else was asking. The
  925. # five mutations below are the mutations that check has to catch, and each was
  926. # run by hand and confirmed red BEFORE this section existed.
  927. #
  928. # M4 and M5 are the interesting pair. M4 is a fault this project actually
  929. # introduced while fixing the above: EmJcc jumps TO its target, but the 8086
  930. # shape steps OVER a 3-byte EJMP, so the naive port inverts every conditional
  931. # in every program. It was caught by execution, not by any byte check. M5 is
  932. # the partial version -- the inversion dropped for the IF and CASE sites only,
  933. # kept for FOR -- and MEASURING that is what produced clause H, because the
  934. # check was blind to M5 as first written: IF declares nibble 4, CASE declares
  935. # 5, they negate into each other, and both are declared, so nothing complained
  936. # while every conditional in every program took the wrong path. FOR declares
  937. # C and F, whose negations D and E are declared for nothing at all, so the
  938. # whole-suite version was caught by luck.
  939. #
  940. # Note these need `rebuild_compiler' only to refresh comtest; check_8086.py
  941. # relinks the fixtures itself. Getting that wrong is how this section's first
  942. # draft reported a mutation as VACUOUS: the build was skipped, a stale comtest
  943. # ran the GOOD compiler, and the check passed. A helper that exits non-zero
  944. # makes `if mutate_x; then' false and the case is silently skipped, which looks
  945. # exactly like a pass -- so rebuild_compiler is checked, not assumed.
  946. # M1: the original defect, restored verbatim.
  947. cp "$SAVED_C" Compiler.mod
  948. if python3 - <<'PYX'
  949. p = 'Compiler.mod'
  950. s = open(p).read()
  951. old = """ Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)
  952. Ebyte (03H) ; (* rel8: step over the 3-byte EJMP *)
  953. RETURN EmJmpNear (target) (* target = 0 => forward, see above *)"""
  954. new = """ Ebyte (0FH) ; Ebyte (cc) ; Eword (0) ;
  955. IF target = 0 THEN
  956. RETURN nPatch
  957. END ;
  958. RETURN 0"""
  959. assert s.count(old) == 1, 'EmJcc body found %d times -- update this mutation' % s.count(old)
  960. open(p, 'w').write(s.replace(old, new))
  961. PYX
  962. then
  963. if rebuild_compiler; then
  964. expect_red "check_8086 catches a 0F 8x branch (the original bug)" \
  965. "0F 84" python3 tests/check_8086.py
  966. else
  967. echo " FAIL: the compiler would not rebuild with the 0F branch"
  968. fail=$((fail + 1))
  969. fi
  970. else
  971. echo " BROKEN CASE: the 0F branch mutation did not apply"
  972. fail=$((fail + 1))
  973. fi
  974. cp "$SAVED_C" Compiler.mod
  975. # M2: the other original defect, SETcc plus the MOV AH,0 it needed.
  976. cp "$SAVED_C" Compiler.mod
  977. if python3 - <<'PYX'
  978. p = 'Compiler.mod'
  979. s = open(p).read()
  980. old = """ Ebyte (0B8H) ; Eword (1) ; (* MOV AX,#0001 *)
  981. Ebyte (JccShort (cc)) ; (* taken when the comparison HOLDS *)
  982. Ebyte (01H) ; (* rel8: step over the DEC AX *)
  983. Ebyte (48H) (* DEC AX *)"""
  984. new = """ Ebyte (0FH) ; Ebyte (cc) ; Ebyte (0C0H) ;
  985. EmMovAh0 ()"""
  986. assert s.count(old) == 1, 'EmSetcc body found %d times -- update this mutation' % s.count(old)
  987. open(p, 'w').write(s.replace(old, new))
  988. PYX
  989. then
  990. if rebuild_compiler; then
  991. expect_red "check_8086 catches a 0F 9x SETcc (the original bug)" \
  992. "0F 9F C0" python3 tests/check_8086.py
  993. else
  994. echo " FAIL: the compiler would not rebuild with the 0F SETcc"
  995. fail=$((fail + 1))
  996. fi
  997. else
  998. echo " BROKEN CASE: the 0F SETcc mutation did not apply"
  999. fail=$((fail + 1))
  1000. fi
  1001. cp "$SAVED_C" Compiler.mod
  1002. # M4: the polarity inversion, everywhere. Note the expected text is clause H's,
  1003. # not a shape complaint: every byte here is a legal 8086 shape, which is the
  1004. # entire reason a separate clause had to be written.
  1005. cp "$SAVED_C" Compiler.mod
  1006. if python3 - <<'PYX'
  1007. p = 'Compiler.mod'
  1008. s = open(p).read()
  1009. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  1010. new = ' Ebyte (JccShort (cc)) ; (* MUTATION: inversion dropped *)'
  1011. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  1012. open(p, 'w').write(s.replace(old, new))
  1013. PYX
  1014. then
  1015. if rebuild_compiler; then
  1016. expect_red "clause H catches the branch polarity inverted everywhere" \
  1017. "but reading the source says" python3 tests/check_8086.py
  1018. else
  1019. echo " FAIL: the compiler would not rebuild with the inversion dropped"
  1020. fail=$((fail + 1))
  1021. fi
  1022. else
  1023. echo " BROKEN CASE: the polarity mutation did not apply"
  1024. fail=$((fail + 1))
  1025. fi
  1026. cp "$SAVED_C" Compiler.mod
  1027. # M5: the same inversion dropped for the IF and CASE sites ONLY. This one is
  1028. # the reason clause H exists: it was run by hand and the check stayed GREEN,
  1029. # and FOR's C/F nibbles were the only reason the whole-suite version (M4)
  1030. # happened to be caught.
  1031. cp "$SAVED_C" Compiler.mod
  1032. if python3 - <<'PYX'
  1033. p = 'Compiler.mod'
  1034. s = open(p).read()
  1035. old = ' Ebyte (JccShortInv (cc)) ; (* Jcc_s, taken when cc does NOT hold *)'
  1036. new = """ IF (cc = 84H) OR (cc = 85H) THEN
  1037. Ebyte (JccShort (cc)) (* MUTATION: no inversion here *)
  1038. ELSE
  1039. Ebyte (JccShortInv (cc))
  1040. END ;"""
  1041. assert s.count(old) == 1, 'EmJcc Ebyte found %d times -- update this mutation' % s.count(old)
  1042. open(p, 'w').write(s.replace(old, new))
  1043. PYX
  1044. then
  1045. if rebuild_compiler; then
  1046. expect_red "clause H catches the inversion dropped for IF and CASE only" \
  1047. "t22_case" python3 tests/check_8086.py
  1048. else
  1049. echo " FAIL: the compiler would not rebuild with the partial inversion"
  1050. fail=$((fail + 1))
  1051. fi
  1052. else
  1053. echo " BROKEN CASE: the partial-inversion mutation did not apply"
  1054. fail=$((fail + 1))
  1055. fi
  1056. cp "$SAVED_C" Compiler.mod
  1057. # And the check on the RESTORED compiler, so a green above cannot come from a
  1058. # mutation that failed to take and left the real defect in place.
  1059. if rebuild_compiler; then
  1060. if python3 tests/check_8086.py >/dev/null 2>&1; then
  1061. echo " ok: check_8086 green on the restored compiler"
  1062. pass=$((pass + 1))
  1063. else
  1064. echo "NOT RESTORED: check_8086.py is red after restoring Compiler.mod"
  1065. python3 tests/check_8086.py 2>&1 | grep -m3 -- ' - ' | sed 's/^/ /'
  1066. fail=$((fail + 1))
  1067. fi
  1068. else
  1069. echo "NOT RESTORED: the compiler would not rebuild"
  1070. fail=$((fail + 1))
  1071. fi
  1072. echo
  1073. echo "== the emitter-name audit of Compiler.mod (audit_helpers.py)"
  1074. # These need no rebuild: the audit reads the SOURCE, not the built object, so
  1075. # they are the cheapest cases here and they cover the module the audit used
  1076. # not to look at at all. That is the point of the section: the audit reported
  1077. # "every helper agrees with its name" for a module it had never examined, and
  1078. # EmXchgAxCx was `93` (XCHG BX,AX) under a name that says XCHG AX,CX for the
  1079. # whole life of the project. Two of these five are for faults that were real.
  1080. SAVED_C2=../tmp/nonvacuity.Compiler.mod.2
  1081. SAVED_R2=../tmp/nonvacuity.Runtime.mod.2
  1082. cp Compiler.mod "$SAVED_C2" || exit 1
  1083. cp Runtime.mod "$SAVED_R2" || exit 1
  1084. restore_audit_sources () {
  1085. cp "$SAVED_C2" Compiler.mod
  1086. cp "$SAVED_R2" Runtime.mod
  1087. }
  1088. AUD="python3 tests/audit_helpers.py"
  1089. # 1. THE fault. 91h is XCHG AX,CX; 93h is XCHG BX,AX. Both are one byte, so
  1090. # the compile matrix never moved and the byte counts never moved.
  1091. cp "$SAVED_C2" Compiler.mod
  1092. mutate Compiler.mod 's|^ Ebyte (91H)$| Ebyte (93H)|'
  1093. expect_red "audit catches XchgAxCx emitting XCHG BX,AX" \
  1094. "exchanges Ax and Bx" $AUD
  1095. restore_audit_sources
  1096. # 2. the coverage check itself. A parameter list that find_helpers does not
  1097. # accept is exactly how the real emitter was missed, and the inventory is
  1098. # scanned separately on purpose so this can be caught. Without the
  1099. # independent scan this case is silent, because both lists would come from
  1100. # the same parser and agree that the helper does not exist.
  1101. cp "$SAVED_C2" Compiler.mod
  1102. mutate Compiler.mod 's|^PROCEDURE EmXchgAxCx () ;$|PROCEDURE EmXchgAxCx (why : CARDINAL) ;|'
  1103. expect_red "audit reports an emitter it cannot reach, rather than skipping it" \
  1104. "never examined it" $AUD
  1105. restore_audit_sources
  1106. # 3. EmXchgAxDx was named EmMoveAxDx, which said MOV where the bytes say XCHG.
  1107. # 93h here is XCHG AX,BX - one letter away, the exact class of mistake the
  1108. # name is supposed to make impossible.
  1109. cp "$SAVED_C2" Compiler.mod
  1110. mutate Compiler.mod 's|^ Ebyte (92H)$| Ebyte (93H)|'
  1111. expect_red "audit catches XchgAxDx emitting XCHG BX,AX" \
  1112. "XchgAxDx" $AUD
  1113. restore_audit_sources
  1114. # 4. CmpArgW0's [BP+2] written as the 386 SIB form, which decodes on a 8086 as
  1115. # [SI+24h]. A real bug: the runtime was clearing the wrong memory.
  1116. cp "$SAVED_R2" Runtime.mod
  1117. mutate Runtime.mod 's| B (83H) ; B (7EH) ; B (2) ; B (0) ;| B (83H) ; B (7CH) ; B (24) ; B (0) ; B (0) ;|'
  1118. expect_red "audit catches the [SI+24h] encoding of [BP+2]" \
  1119. "memory base is 'si" $AUD
  1120. restore_audit_sources
  1121. # 5. MovAxSp is POP then PUSH, because MOV AX,[SP] does not exist on an 8086.
  1122. # Dropping the POP leaves the stack one word short - a fault in the shape,
  1123. # not in a byte value.
  1124. cp "$SAVED_C2" Compiler.mod
  1125. python3 - <<'PYEOF'
  1126. p='Compiler.mod'; s=open(p).read()
  1127. a=" Ebyte (58H) ; (* POP AX *)\n"
  1128. assert s.count(a)==1, "EmMovAxSp POP line not found -- update this mutation"
  1129. open(p,'w').write(s.replace(a, ""))
  1130. PYEOF
  1131. expect_red "audit catches MovAxSp with its POP missing" \
  1132. "MovAxSp" $AUD
  1133. restore_audit_sources
  1134. # 6. The two-instruction shape: IDIV is CWD then IDIV, and dropping the CWD
  1135. # leaves an un-sign-extended dividend in DX:AX. Both are still present as
  1136. # a two-step spec, so a missing step has to be visible.
  1137. cp "$SAVED_C2" Compiler.mod
  1138. mutate Compiler.mod 's| Ebyte (99H) ; Ebyte (0F7H) ; Ebyte (0F9H)| Ebyte (0F7H) ; Ebyte (0F9H)|'
  1139. expect_red "audit catches IDiv without the CWD that extends the dividend" \
  1140. "IDivAxCx" $AUD
  1141. restore_audit_sources
  1142. # 7. The byte under EmXorAl01's name, and the row in PATTERNS that was added
  1143. # to admit it. `34 01' is XOR AL,#01 - the boolean NOT, and the one emitter
  1144. # in Compiler.mod with no ModRM byte at all. A grammar row nobody has ever
  1145. # seen reject anything cannot be trusted to accept only the truth, so here
  1146. # is the rejection: `34 02' is the same length, decodes just as cleanly, and
  1147. # `not x' would flip bit 1 instead of bit 0.
  1148. cp "$SAVED_C2" Compiler.mod
  1149. mutate Compiler.mod 's|^ Ebyte (34H) ; Ebyte (01H)| Ebyte (34H) ; Ebyte (02H)|'
  1150. expect_red "audit catches EmXorAl01 emitting xor al,#2 under a name saying #1" \
  1151. "XorAl01" $AUD
  1152. restore_audit_sources
  1153. # 7b. The other half of that row: the opcode gate. 34h is XOR AL,#imm and
  1154. # 35h is XOR AX,#imm - a word, not a byte, under a name that says AL. The
  1155. # row is pinned to {034h} on purpose, and a pin that has never been asked
  1156. # to hold is a pin. The report is the one thing the row above cannot
  1157. # produce: with no reading at all, the helper falls out of the grammar.
  1158. cp "$SAVED_C2" Compiler.mod
  1159. mutate Compiler.mod 's|^ Ebyte (34H) ; Ebyte (01H)| Ebyte (35H) ; Ebyte (01H)|'
  1160. expect_red "the opcode gate rejects XOR AX under an XOR AL name" \
  1161. "no name pattern accepts it" $AUD
  1162. restore_audit_sources
  1163. if $AUD >/dev/null 2>&1; then
  1164. echo " ok: the audit passes on both restored sources"
  1165. pass=$((pass + 1))
  1166. else
  1167. echo "NOT RESTORED: the audit is red after restoring the sources"
  1168. $AUD 2>&1 | sed 's/^/ /'
  1169. fail=$((fail + 1))
  1170. fi
  1171. echo
  1172. echo "== the BP contract rt_exec.py checks before it starts a machine"
  1173. # wrchar and wrbool both borrowed BP to reach their argument -- [SP] is not
  1174. # encodable in 16-bit mode -- and neither saved it. The driver's cursor into
  1175. # the case record lives in BP precisely because BP is the one register an entry
  1176. # may keep, so "wrchar borrowed it and did not give it back" sent the second
  1177. # call to a garbage address, the machine triple-faulted, and the run printed the
  1178. # record header twice and hung. Both the golden and the audit call that shape
  1179. # CORRECT: the bytes are well formed, every branch is on a boundary, the size
  1180. # is unchanged, and the decode says exactly what it says. So the rule is now
  1181. # checked directly, and these two cases are what make that check more than a
  1182. # claim.
  1183. #
  1184. # rt_exec.py needs the whole runtime rebuilt and then boots 36 machines, so this
  1185. # section is the slow one. The baseline comes first and is asserted: a case
  1186. # that mutates a red tree proves nothing.
  1187. SAVED_R3=../tmp/nonvacuity.Runtime.mod.3
  1188. cp Runtime.mod "$SAVED_R3" || exit 1
  1189. if rebuild; then
  1190. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  1191. echo " ok: baseline - rt_exec.py passes on the unmutated runtime"
  1192. pass=$((pass + 1))
  1193. else
  1194. echo " FAIL: the baseline is already red, so the cases below prove"
  1195. echo " nothing - fix the baseline before reading them"
  1196. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  1197. fail=$((fail + 1))
  1198. fi
  1199. else
  1200. echo " FAIL: could not rebuild the runtime for the baseline"
  1201. fail=$((fail + 1))
  1202. fi
  1203. # 1. THE fault: drop the PUSH, exactly as EmitWrChar was written. This is the
  1204. # shape the behavioural case found, so the byte-level check must find it too
  1205. # -- a check that only the expensive test can trip is a check that has not
  1206. # been made to earn its place.
  1207. cp "$SAVED_R3" Runtime.mod
  1208. python3 - <<'PYEOF'
  1209. p = 'Runtime.mod'
  1210. s = open(p).read()
  1211. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n'
  1212. assert s.count(a) == 1, "EmitWrChar prologue not found exactly once"
  1213. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n'))
  1214. PYEOF
  1215. if rebuild; then
  1216. expect_red "the BP contract catches wrchar borrowing BP unsaved" \
  1217. "borrows BP but does not open with" python3 tests/rt_exec.py
  1218. else
  1219. echo " FAIL: the runtime would not rebuild with the PUSH removed"
  1220. fail=$((fail + 1))
  1221. fi
  1222. cp "$SAVED_R3" Runtime.mod
  1223. # 2. The mirror image: push it and never pop it. A different one-instruction
  1224. # omission with the same consequence for a caller, and the reason the rule
  1225. # asks for the 5D and not just the 55.
  1226. cp "$SAVED_R3" Runtime.mod
  1227. mutate Runtime.mod 's|^ MovSpBp ; PopBp ;$| MovSpBp ;|'
  1228. if rebuild; then
  1229. expect_red "the BP contract catches a BP that is pushed and never popped" \
  1230. "pushes BP but never pops it" python3 tests/rt_exec.py
  1231. else
  1232. echo " FAIL: the runtime would not rebuild with the POP removed"
  1233. fail=$((fail + 1))
  1234. fi
  1235. cp "$SAVED_R3" Runtime.mod
  1236. # 3. The scan's other silent failure: a rule that matches nothing looks exactly
  1237. # like a rule that passes. The pattern the check looks for is changed to one
  1238. # the blob does not contain -- MOV BP,DI, which the runtime has no reason to
  1239. # emit -- so the check has examined thirteen entries and matched nothing and
  1240. # MUST say so rather than report a clean sweep. This is the general shape of
  1241. # the fault this project keeps making: a check whose SUBJECT has drifted
  1242. # reports a confident answer about the wrong thing.
  1243. cp "$SAVED_R3" Runtime.mod
  1244. SAVED_X=../tmp/nonvacuity.rt_exec.py
  1245. cp tests/rt_exec.py "$SAVED_X" || exit 1
  1246. mutate tests/rt_exec.py 's|^MOV_BP_SP = b"\\x8b\\xec" .*$|MOV_BP_SP = b"\\x8b\\xed" # MOV BP,DI: never emitted|' \
  1247. expect_red "a check that matched nothing is a failure, not a pass" \
  1248. "matched nothing" python3 tests/rt_exec.py
  1249. cp "$SAVED_X" tests/rt_exec.py
  1250. if rebuild; then
  1251. if python3 tests/rt_exec.py >/dev/null 2>&1; then
  1252. echo " ok: rt_exec.py passes on the restored source"
  1253. pass=$((pass + 1))
  1254. else
  1255. echo "NOT RESTORED: rt_exec.py is red after restoring Runtime.mod"
  1256. python3 tests/rt_exec.py 2>&1 | tail -3 | sed 's/^/ /'
  1257. fail=$((fail + 1))
  1258. fi
  1259. else
  1260. echo "NOT RESTORED: the runtime would not rebuild"
  1261. fail=$((fail + 1))
  1262. fi
  1263. # 4. The GOLDEN and the entry goldens, which are the other half of the same
  1264. # rule. rt_exec.py states the contract; check_runtime.py pins the bytes.
  1265. # They are separate mechanisms and the case below is what shows the golden
  1266. # one works on its own -- a re-baseline of runtime.golden would otherwise
  1267. # have absorbed the new prologues silently, and the next person to bless it
  1268. # would have no way to know the PUSH and POP were ever missing.
  1269. cp "$SAVED_R3" Runtime.mod
  1270. python3 - <<'PYEOF'
  1271. p = 'Runtime.mod'
  1272. s = open(p).read()
  1273. a = ' M ("wrchar") ;\n PushBp ; MovBpSp ;\n MovAlArg4 ;\n MovSpBp ; PopBp ;\n'
  1274. assert s.count(a) == 1, "EmitWrChar frame not found exactly once"
  1275. open(p, 'w').write(s.replace(a, ' M ("wrchar") ;\n MovBpSp ;\n MovAlArg2 ;\n MovSpBp ;\n'))
  1276. PYEOF
  1277. if rebuild; then
  1278. expect_red "the entry golden catches wrchar without its PUSH BP" \
  1279. "entry wrchar starts 8B EC" python3 tests/check_runtime.py "$DUMP"
  1280. else
  1281. echo " FAIL: the runtime would not rebuild with wrchar's frame removed"
  1282. fail=$((fail + 1))
  1283. fi
  1284. cp "$SAVED_R3" Runtime.mod
  1285. echo
  1286. echo "== the .COM layout check, and the runtime size it now measures"
  1287. # The checker used to RESTATE the runtime's size as a literal. It had drifted
  1288. # from the runtime it described, so the header was read out of the code stream
  1289. # and EVERY linked fixture "failed" on it - a whole file of "failures" that were
  1290. # not findings at all. A duplicated constant that has drifted does not fail
  1291. # loudly; it re-reports the same falsehood, in which the real failures hide.
  1292. # The size is now MEASURED from the image.
  1293. #
  1294. # These cases corrupt a real emitted .COM and require the checker to notice.
  1295. # They need the images, so they are built once and copied; the checker has a
  1296. # --check-only mode for exactly this, because its scratch directory is normally
  1297. # deleted on exit and a check that has only ever seen the truth is not a check.
  1298. KEEPDIR=../tmp/nonvacuity.com
  1299. rm -rf "$KEEPDIR"
  1300. TP_COM_KEEP=1 tests/run_com_tests.sh >../tmp/nonvacuity.com.log 2>&1
  1301. KEEP=$(sed -n 's/^TP_COM_KEEP=1: images left in //p' \
  1302. ../tmp/nonvacuity.com.log | tail -1)
  1303. if [ -z "$KEEP" ] || [ ! -d "$KEEP" ]; then
  1304. echo " FAIL: could not obtain emitted .COM images for the layout cases"
  1305. fail=$((fail + 1))
  1306. else
  1307. COMCHK="tests/run_com_tests.sh --check-only"
  1308. # 0. The baseline. Every case below is a claim that a specific assertion
  1309. # turns red, and none of them means anything if the copies of untouched
  1310. # images already fail. (The stale RT_SZ produced exactly that: a whole
  1311. # file of "failures" that were not findings.) So this is asserted first,
  1312. # and a failure here is reported as a broken baseline rather than a red
  1313. # test.
  1314. #
  1315. # The baseline is also WHERE THE HEADER OFFSET COMES FROM. Cases 1 and 2
  1316. # used to carry it as the literal 435 and 439, which were ENT_SZ + the
  1317. # runtime size at the time -- 432. Two 4-byte changes to the runtime
  1318. # later, both cases were editing the wrong bytes: 435 had become four
  1319. # bytes inside the runtime itself, so case 1 had stopped testing "the
  1320. # header cannot be found" and had started testing "the checker also
  1321. # notices you scribbled on the code", and case 2 had become a no-op
  1322. # that wrote the header where it already was. Both still went red, for
  1323. # reasons the messages did not name, which is the whole problem: a
  1324. # hard-coded offset is a claim about the world that expires silently.
  1325. #
  1326. # So the offset is read back out of the checker's own report on the
  1327. # untouched images, and the expected numbers below are ARITHMETIC ON IT.
  1328. # If the runtime grows again, these cases still test what they say.
  1329. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1330. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1331. BASE_OUT=$($COMCHK "$KEEPDIR" 2>&1)
  1332. if [ $? -eq 0 ]; then
  1333. echo " ok: baseline - untouched copies of the real images all pass"
  1334. pass=$((pass + 1))
  1335. else
  1336. echo " FAIL: the baseline is already red, so the cases below prove"
  1337. echo " nothing - fix the baseline before reading them"
  1338. printf '%s\n' "$BASE_OUT" | grep FAIL | head -3 | sed 's/^/ /'
  1339. fail=$((fail + 1))
  1340. fi
  1341. BASE_RT=$(printf '%s\n' "$BASE_OUT" \
  1342. | sed -n 's/.*measured runtime size: \([0-9][0-9]*\) bytes.*/\1/p' \
  1343. | head -1)
  1344. if [ -z "$BASE_RT" ]; then
  1345. echo " FAIL: the checker did not report the runtime size it measured,"
  1346. echo " so the cases below cannot find the header to edit"
  1347. fail=$((fail + 1))
  1348. BASE_RT=0
  1349. fi
  1350. # The layout constants are ENT_SZ 3 and HDR_SZ 16 (tests/comimage.py), so the
  1351. # header sits at hdrOff = 3 + rtSz and the entry jump's displacement must be
  1352. # (hdrOff + 16) - 3. The wanted number is therefore computed from the header
  1353. # OFFSET, not from rtSz: the two differ by 3, and getting that backwards
  1354. # produces a plausible-looking expectation three bytes out, which is how
  1355. # this case came to expect "want 452" and then be reported as not proving
  1356. # what it said.
  1357. BASE_OFF=$((3 + BASE_RT))
  1358. # Case 2 claims a runtime four bytes LONGER, so the header - and with it the
  1359. # demanded jump target - moves four bytes further on.
  1360. SHIFTED_WANT=$((BASE_OFF + 4 + 16 - 3))
  1361. echo " (measured runtime size $BASE_RT, header at image offset $BASE_OFF)"
  1362. # 1. Break hdrDS so it no longer ties the header to its own offset. The
  1363. # header must become UNFINDABLE and be reported as such - a checker that
  1364. # fell back to a remembered offset would report a confident number here,
  1365. # which is the failure mode the measurement was introduced to remove.
  1366. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1367. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1368. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" <<'PYEOF'
  1369. import sys
  1370. p, off = sys.argv[1], int(sys.argv[2])
  1371. d = bytearray(open(p, 'rb').read())
  1372. d[off + 4:off + 6] = (0x1234).to_bytes(2, 'little') # hdrDS, no longer self-consistent
  1373. open(p, 'wb').write(bytes(d))
  1374. PYEOF
  1375. expect_red "a header that cannot be located is reported, not assumed" \
  1376. "no program header found" $COMCHK "$KEEPDIR"
  1377. # 2. A complete, self-consistent header four bytes later, so the measured
  1378. # runtime size becomes $((BASE_RT + 4)) instead of $BASE_RT. This is the
  1379. # positive half of the same check: the derivation must FOLLOW the file,
  1380. # and the entry jump assertion - expressed in terms of the measurement -
  1381. # must follow it too, demanding $SHIFTED_WANT rather than $((BASE_OFF + 16 - 3)).
  1382. #
  1383. # hdrCS is copied from the header already in the file rather than written
  1384. # as a literal. It used to be the literal 464+100h, which was the image
  1385. # length when the image was 720 bytes; four bytes of runtime later it was
  1386. # 464+100h against a 724-byte image, so this case was ALSO failing on
  1387. # hdrCS, for a reason three lines below the one it was written to test.
  1388. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1389. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1390. python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" "$SHIFTED_WANT" <<'PYEOF'
  1391. import sys
  1392. p, off, want = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
  1393. d = bytearray(open(p, 'rb').read())
  1394. hdrCS = int.from_bytes(d[off + 2:off + 4], 'little') # unchanged: still the image end
  1395. off += 4
  1396. ds = off + 0x1000 + 0x100
  1397. w = [1, hdrCS, ds, ds + 4, 0, 0, 0, 0]
  1398. for i, x in enumerate(w):
  1399. d[off + 2 * i:off + 2 * i + 2] = x.to_bytes(2, 'little')
  1400. open(p, 'wb').write(bytes(d))
  1401. PYEOF
  1402. expect_red "the measured runtime size follows the image ($BASE_RT -> $((BASE_RT + 4)))" \
  1403. "want $SHIFTED_WANT" $COMCHK "$KEEPDIR"
  1404. # 3. The entry jump's opcode. One byte, and the only assertion in the
  1405. # project that can see where execution STARTS.
  1406. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1407. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1408. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  1409. import sys
  1410. p = sys.argv[1]
  1411. d = bytearray(open(p, 'rb').read())
  1412. d[0] = 0xEA
  1413. open(p, 'wb').write(bytes(d))
  1414. PYEOF
  1415. expect_red "the entry jump must be E9, not a near JMP" \
  1416. "not the E9 of the entry jump" $COMCHK "$KEEPDIR"
  1417. # 4. The entry jump's target, moved one instruction earlier. A .COM that
  1418. # lands in the middle of the prologue runs, prints something and exits
  1419. # cleanly, so no size or structure check can see this.
  1420. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  1421. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  1422. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  1423. import sys
  1424. p = sys.argv[1]
  1425. d = bytearray(open(p, 'rb').read())
  1426. d[1:3] = (100).to_bytes(2, 'little')
  1427. open(p, 'wb').write(bytes(d))
  1428. PYEOF
  1429. expect_red "the entry jump must land on the first instruction" \
  1430. "entry jump rel16=100" $COMCHK "$KEEPDIR"
  1431. rm -rf "$KEEPDIR"
  1432. fi
  1433. echo
  1434. echo "non-vacuity: $pass ok, $fail failed"
  1435. [ "$fail" -eq 0 ]