nonvacuity.sh 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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. #
  31. # The mod=11 cases do not need a rebuild -- they read the probe sources
  32. # directly -- so they are cheap, and they are the ones that matter most: the
  33. # table they guard is the one thing in this project that was wrong in the
  34. # documentation while the code was right, and a table that is wrong in the
  35. # code produces bytes that decode perfectly.
  36. #
  37. # Usage: tests/nonvacuity.sh (from shell/; leaves Runtime.mod restored)
  38. set -u
  39. cd "$(dirname "$0")/.." || exit 1
  40. GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
  41. SAVED=/tmp/opencode/nonvacuity.Runtime.mod
  42. PROBE=/tmp/opencode/nonvacuity.rtprobe
  43. DUMP=/tmp/opencode/nonvacuity.dump
  44. cp Runtime.mod "$SAVED" || exit 1
  45. trap 'cp "$SAVED" Runtime.mod; "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1' EXIT
  46. pass=0
  47. fail=0
  48. # mutate <file> <sed-expr> -- apply a deliberate breakage and INSIST it landed.
  49. #
  50. # Four cases in this file were already dead when first run, all the same way:
  51. # the helper they name had been renamed or reformatted since the case was
  52. # written, the sed matched nothing, the source was unchanged, and the check
  53. # correctly passed - so the harness reported "NOT NON-VACUOUS" and, worse, a
  54. # reader skimming the output could take "the check still passed" for a passing
  55. # test. A case that cannot fire is worse than no case: it is a claim of
  56. # coverage that was never tested.
  57. #
  58. # So the mutation is verified, not assumed. If the file is byte-identical
  59. # afterwards, that is reported as a FAILURE of the harness, naming the sed, and
  60. # the case is not run - because running it would only produce a meaningless
  61. # green. The message says what to do (fix the sed) rather than what it found.
  62. mutate () {
  63. mf=$1
  64. msed=$2
  65. cp "$mf" /tmp/opencode/nonvacuity.mut.bak
  66. sed -i "$msed" "$mf"
  67. if cmp -s "$mf" /tmp/opencode/nonvacuity.mut.bak; then
  68. echo " BROKEN CASE: the mutation did not change $mf"
  69. echo " sed: $msed"
  70. echo " the named code has probably been renamed or reformatted -"
  71. echo " fix this case, it is asserting nothing"
  72. fail=$((fail + 1))
  73. return 1
  74. fi
  75. return 0
  76. }
  77. # rebuild <label> -- re-emit the runtime and dump it
  78. rebuild () {
  79. "$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || return 1
  80. "$GM2" -fiso -o "$PROBE" tests/RtProbe.mod Runtime.o Posix.o \
  81. >/dev/null 2>&1 || return 1
  82. "$PROBE" > "$DUMP" || return 1
  83. return 0
  84. }
  85. # expect_red <label> <pattern> <checker-cmd...>
  86. # <pattern> is a grep the failure output must match, so a check cannot
  87. # "pass" by failing for some unrelated reason.
  88. expect_red () {
  89. label=$1
  90. want=$2
  91. shift 2
  92. if out=$("$@" 2>&1); then
  93. echo "NOT NON-VACUOUS: $label -- the check still passed"
  94. fail=$((fail + 1))
  95. elif ! printf '%s\n' "$out" | grep -qi "$want"; then
  96. echo "WRONG FAILURE: $label -- went red, but not for the stated reason"
  97. printf '%s\n' "$out" | sed 's/^/ /'
  98. fail=$((fail + 1))
  99. else
  100. echo " ok: $label"
  101. printf '%s\n' "$out" | grep -im1 "$want" | sed 's/^/ /'
  102. pass=$((pass + 1))
  103. fi
  104. }
  105. echo "== each mutation must turn the named check red"
  106. echo
  107. # --- 1. name-versus-decode -------------------------------------------
  108. # MovSiBx was `89 DC`, which is MOV SP,BX. Two bytes either way, decodes
  109. # cleanly, and no structural check can see it.
  110. cp "$SAVED" Runtime.mod
  111. mutate Runtime.mod 's|B (0DEH) END MovSiBx|B (0DCH) END MovSiBx|'
  112. expect_red "audit_helpers catches MovSiBx emitting MOV SP,BX" \
  113. "MovSiBx" python3 tests/audit_helpers.py
  114. # CmpSiBx had the identical mistake, which is how you know a single fix is
  115. # not enough -- the same misreading was written twice.
  116. cp "$SAVED" Runtime.mod
  117. mutate Runtime.mod 's|B (39H) ; B (0DEH) END CmpSiBx|B (39H) ; B (0DCH) END CmpSiBx|'
  118. expect_red "audit_helpers catches CmpSiBx emitting CMP SP,BX" \
  119. "CmpSiBx" python3 tests/audit_helpers.py
  120. # --- 2. golden, and entry goldens ------------------------------------
  121. # MovDlAl was `88 C0` = MOV AL,AL instead of MOV DL,AL. This is the case that
  122. # motivated runtime.golden: the sweep stayed in sync, every branch target
  123. # stayed on a boundary, no entry's first bytes moved, and the size did not
  124. # change. The target helper was MovAlDh when this case was written, which is
  125. # the fourth way a case here can rot - see the note on `mutate` below.
  126. cp "$SAVED" Runtime.mod
  127. mutate Runtime.mod 's|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C2H)|PROCEDURE MovDlAl ; BEGIN B (88H) ; B (0C0H)|'
  128. rebuild
  129. expect_red "runtime.golden catches MOV AL,AL" \
  130. "mov al,al" python3 tests/check_runtime.py "$DUMP"
  131. # initmem opened with the mis-emitted MovSiAx, so its entry golden was the
  132. # thing that noticed the prologue was a no-op.
  133. cp "$SAVED" Runtime.mod
  134. mutate Runtime.mod 's|B (0F0H) END MovSiAx|B (0C0H) END MovSiAx|'
  135. rebuild
  136. expect_red "check_runtime catches a broken initmem prologue" \
  137. "mov ax,ax" python3 tests/check_runtime.py "$DUMP"
  138. # --- 3. decode sweep / length ----------------------------------------
  139. # StDiDl was `88 97` = [BX+disp16],DL: mod=10, so the instruction needs a
  140. # disp16 it was not given, and the sweep loses sync two bytes later.
  141. cp "$SAVED" Runtime.mod
  142. mutate Runtime.mod 's|PROCEDURE StDiDl ; BEGIN B (88H) ; B (15H)|PROCEDURE StDiDl ; BEGIN B (88H) ; B (97H)|'
  143. rebuild
  144. expect_red "decode sweep catches a mod=10 byte move with no displacement" \
  145. "mov byte ptr \[bx+5b5fh\],dl" python3 tests/check_runtime.py "$DUMP"
  146. # --- 4. branch targets ------------------------------------------------
  147. # FixUp measures a rel8 from the end of the instruction, one byte past the
  148. # displacement field. Drop the +1 and every short branch lands one byte into
  149. # its target, which for a 3-byte instruction means the middle of it. The
  150. # bytes themselves are all perfectly well formed -- only the fixups are
  151. # wrong -- so this is the one failure mode the golden cannot be expected to
  152. # catch on its own.
  153. cp "$SAVED" Runtime.mod
  154. mutate Runtime.mod 's|rel := (t + 100H - (fix \[i\].place + 1)) MOD 100H|rel := (t + 100H - fix [i].place) MOD 100H|'
  155. rebuild
  156. expect_red "branch check catches rel8 fixups measured from the wrong byte" \
  157. "not an instruction boundary" \
  158. python3 tests/check_runtime.py "$DUMP"
  159. echo
  160. echo "== everything restored and green again"
  161. cp "$SAVED" Runtime.mod
  162. if rebuild; then
  163. if python3 tests/audit_helpers.py >/dev/null 2>&1 &&
  164. python3 tests/check_runtime.py "$DUMP" >/dev/null 2>&1; then
  165. echo " ok: both checks pass on the restored source"
  166. pass=$((pass + 1))
  167. else
  168. echo "NOT RESTORED: a check is red after restoring Runtime.mod"
  169. fail=$((fail + 1))
  170. fi
  171. else
  172. echo "NOT RESTORED: the runtime would not rebuild"
  173. fail=$((fail + 1))
  174. fi
  175. echo
  176. echo "== the mod=11 table (probe/modrm11.py)"
  177. # These mutate the probe's own sources, not the runtime, so there is no
  178. # rebuild in the loop. SAVED_PY / SAVED_S are restored after each case.
  179. SAVED_PY=/tmp/opencode/nonvacuity.modrm11.py
  180. SAVED_S=/tmp/opencode/nonvacuity.modrm11.s
  181. cp tests/probe/modrm11.py "$SAVED_PY" || exit 1
  182. cp tests/probe/modrm11.s "$SAVED_S" || exit 1
  183. M11="python3 tests/probe/modrm11.py"
  184. restore_probe () {
  185. cp "$SAVED_PY" tests/probe/modrm11.py
  186. cp "$SAVED_S" tests/probe/modrm11.s
  187. }
  188. # 1. one cell of the table moved
  189. mutate tests/probe/modrm11.py 's|"Si", "Di"\]$|"Bp", "Di"]|'
  190. expect_red "anchor pins a moved table cell" \
  191. "anchor ADD SI, 2" $M11
  192. restore_probe
  193. # 2. the table this project actually shipped: AX dropped off the front and a
  194. # duplicate BX invented at the end, which shifts every code down by one
  195. mutate tests/probe/modrm11.py 's|^REG = .*$|REG = ["Cx", "Dx", "Bx", "Sp", "Bp", "Si", "Di", "Bx"]|'
  196. expect_red "the table shifted by one (AX dropped, BX duplicated)" \
  197. "anchor MOV SP, BP" $M11
  198. restore_probe
  199. # 3. the .s edited to contradict the table. This is the case that shows why
  200. # the hard-coded EXPECT bytes exist: the assembler encodes the new claim
  201. # correctly, so comparing the .s against `as` alone can never fail here.
  202. mutate tests/probe/modrm11.s 's|movw %sp, %di # reg 100|movw %bp, %di # reg 100|'
  203. expect_red "probe source edited away from the recorded bytes" \
  204. "expected 89 E7" $M11
  205. restore_probe
  206. # 4. the 8-bit list edited, which is a different table from the word one
  207. mutate tests/probe/modrm11.s 's|movb %al, %dl # 88 C2 -> DL := AL|movb %al, %bl # was DL|'
  208. expect_red "the 8-bit register list edited" \
  209. "expected 88 C2" $M11
  210. restore_probe
  211. # 5. an anchor's recorded byte corrupted, so the anchor can no longer
  212. # corroborate itself
  213. mutate tests/probe/modrm11.py 's|"8B EC", "8B E5"|"8B ED", "8B E5"|'
  214. expect_red "anchor byte no longer matches the emitted code" \
  215. "expected 8B ED" $M11
  216. restore_probe
  217. if $M11 >/dev/null 2>&1; then
  218. echo " ok: modrm11.py passes on the restored probe sources"
  219. pass=$((pass + 1))
  220. else
  221. echo "NOT RESTORED: modrm11.py is red after restoring its sources"
  222. $M11 2>&1 | sed 's/^/ /'
  223. fail=$((fail + 1))
  224. fi
  225. echo
  226. echo "== the BP displacement rule (check_framedisp.py)"
  227. # This one is about Compiler.mod rather than the runtime, and it needs the
  228. # whole toolchain rebuilt (comtest, not rtprobe), so it gets its own rebuild.
  229. SAVED_C=/tmp/opencode/nonvacuity.Compiler.mod
  230. cp Compiler.mod "$SAVED_C" || exit 1
  231. rebuild_compiler () {
  232. $GM2 -fiso -c Compiler.mod >/dev/null 2>&1 || return 1
  233. $GM2 -fiso -fgen-module-list=tests/ct.lst -o /dev/null \
  234. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  235. >/dev/null 2>&1
  236. $GM2 -fiso -fuse-list=tests/ct.lst -o comtest \
  237. tests/ComTest.mod TextBuf.o Posix.o Compiler.o Runtime.o Linker.o \
  238. >/dev/null 2>&1 || return 1
  239. return 0
  240. }
  241. # 1. the original bug: `off MOD 100H`, always disp8. Restores exactly the code
  242. # that was there before EmBpDisp existed. t28's [BP+128] read becomes
  243. # [BP-128], which is the failure this whole check is named after.
  244. python3 - "$SAVED_C" <<'PYEOF'
  245. import sys
  246. p = 'Compiler.mod'
  247. s = open(p).read()
  248. old = """BEGIN
  249. IF off <= 127 THEN
  250. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  251. ELSE
  252. Ebyte (86H) ; Eword (off)
  253. END
  254. END EmBpDisp ;"""
  255. new = """VAR disp : CARDINAL ;
  256. BEGIN
  257. disp := off MOD 100H ;
  258. Ebyte (46H) ; Ebyte (VAL (BYTE, disp))
  259. END EmBpDisp ;"""
  260. assert old in s, "EmBpDisp body not found -- update this mutation"
  261. open(p, 'w').write(s.replace(old, new))
  262. PYEOF
  263. if rebuild_compiler; then
  264. expect_red "displacement truncation reads a different address" \
  265. "no 8B access at \[BP+128\]" python3 tests/check_framedisp.py
  266. else
  267. echo " FAIL: the compiler would not rebuild with the truncation"
  268. fail=$((fail + 1))
  269. fi
  270. cp "$SAVED_C" Compiler.mod
  271. # 2. the other half of the rule: always use the 4-byte form, ignoring the
  272. # <= 127 case. This is over-cautious rather than wrong, so the checker must
  273. # still be happy -- which is worth asserting, because a check that only
  274. # ever fails on a smaller encoding is a check that pins one answer instead
  275. # of the rule.
  276. python3 - <<'PYEOF'
  277. p = 'Compiler.mod'
  278. s = open(p).read()
  279. old = """ IF off <= 127 THEN
  280. Ebyte (46H) ; Ebyte (VAL (BYTE, off))
  281. ELSE
  282. Ebyte (86H) ; Eword (off)
  283. END"""
  284. new = """ Ebyte (86H) ; Eword (off)"""
  285. assert old in s, "EmBpDisp branch not found -- update this mutation"
  286. open(p, 'w').write(s.replace(old, new))
  287. PYEOF
  288. if rebuild_compiler; then
  289. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  290. echo " ok: always-disp16 is accepted, so the check pins the rule"
  291. echo " and not one particular encoding"
  292. pass=$((pass + 1))
  293. else
  294. echo " FAIL: check_framedisp rejects a safe, over-long encoding"
  295. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  296. fail=$((fail + 1))
  297. fi
  298. else
  299. echo " FAIL: the compiler would not rebuild with always-disp16"
  300. fail=$((fail + 1))
  301. fi
  302. cp "$SAVED_C" Compiler.mod
  303. if rebuild_compiler; then
  304. if python3 tests/check_framedisp.py >/dev/null 2>&1; then
  305. echo " ok: check_framedisp passes on the restored source"
  306. pass=$((pass + 1))
  307. else
  308. echo "NOT RESTORED: check_framedisp is red after restoring Compiler.mod"
  309. python3 tests/check_framedisp.py 2>&1 | sed 's/^/ /'
  310. fail=$((fail + 1))
  311. fi
  312. else
  313. echo "NOT RESTORED: the compiler would not rebuild"
  314. fail=$((fail + 1))
  315. fi
  316. echo
  317. echo "== the emitter-name audit of Compiler.mod (audit_helpers.py)"
  318. # These need no rebuild: the audit reads the SOURCE, not the built object, so
  319. # they are the cheapest cases here and they cover the module the audit used
  320. # not to look at at all. That is the point of the section: the audit reported
  321. # "every helper agrees with its name" for a module it had never examined, and
  322. # EmXchgAxCx was `93` (XCHG BX,AX) under a name that says XCHG AX,CX for the
  323. # whole life of the project. Two of these five are for faults that were real.
  324. SAVED_C2=/tmp/opencode/nonvacuity.Compiler.mod.2
  325. SAVED_R2=/tmp/opencode/nonvacuity.Runtime.mod.2
  326. cp Compiler.mod "$SAVED_C2" || exit 1
  327. cp Runtime.mod "$SAVED_R2" || exit 1
  328. restore_audit_sources () {
  329. cp "$SAVED_C2" Compiler.mod
  330. cp "$SAVED_R2" Runtime.mod
  331. }
  332. AUD="python3 tests/audit_helpers.py"
  333. # 1. THE fault. 91h is XCHG AX,CX; 93h is XCHG BX,AX. Both are one byte, so
  334. # the compile matrix never moved and the byte counts never moved.
  335. cp "$SAVED_C2" Compiler.mod
  336. mutate Compiler.mod 's|^ Ebyte (91H)$| Ebyte (93H)|'
  337. expect_red "audit catches XchgAxCx emitting XCHG BX,AX" \
  338. "exchanges Ax and Bx" $AUD
  339. restore_audit_sources
  340. # 2. the coverage check itself. A parameter list that find_helpers does not
  341. # accept is exactly how the real emitter was missed, and the inventory is
  342. # scanned separately on purpose so this can be caught. Without the
  343. # independent scan this case is silent, because both lists would come from
  344. # the same parser and agree that the helper does not exist.
  345. cp "$SAVED_C2" Compiler.mod
  346. mutate Compiler.mod 's|^PROCEDURE EmXchgAxCx () ;$|PROCEDURE EmXchgAxCx (why : CARDINAL) ;|'
  347. expect_red "audit reports an emitter it cannot reach, rather than skipping it" \
  348. "never examined it" $AUD
  349. restore_audit_sources
  350. # 3. EmXchgAxDx was named EmMoveAxDx, which said MOV where the bytes say XCHG.
  351. # 93h here is XCHG AX,BX - one letter away, the exact class of mistake the
  352. # name is supposed to make impossible.
  353. cp "$SAVED_C2" Compiler.mod
  354. mutate Compiler.mod 's|^ Ebyte (92H)$| Ebyte (93H)|'
  355. expect_red "audit catches XchgAxDx emitting XCHG BX,AX" \
  356. "XchgAxDx" $AUD
  357. restore_audit_sources
  358. # 4. CmpArgW0's [BP+2] written as the 386 SIB form, which decodes on a 8086 as
  359. # [SI+24h]. A real bug: the runtime was clearing the wrong memory.
  360. cp "$SAVED_R2" Runtime.mod
  361. mutate Runtime.mod 's| B (83H) ; B (7EH) ; B (2) ; B (0) ;| B (83H) ; B (7CH) ; B (24) ; B (0) ; B (0) ;|'
  362. expect_red "audit catches the [SI+24h] encoding of [BP+2]" \
  363. "memory base is 'si" $AUD
  364. restore_audit_sources
  365. # 5. MovAxSp is POP then PUSH, because MOV AX,[SP] does not exist on an 8086.
  366. # Dropping the POP leaves the stack one word short - a fault in the shape,
  367. # not in a byte value.
  368. cp "$SAVED_C2" Compiler.mod
  369. python3 - <<'PYEOF'
  370. p='Compiler.mod'; s=open(p).read()
  371. a=" Ebyte (58H) ; (* POP AX *)\n"
  372. assert s.count(a)==1, "EmMovAxSp POP line not found -- update this mutation"
  373. open(p,'w').write(s.replace(a, ""))
  374. PYEOF
  375. expect_red "audit catches MovAxSp with its POP missing" \
  376. "MovAxSp" $AUD
  377. restore_audit_sources
  378. # 6. The two-instruction shape: IDIV is CWD then IDIV, and dropping the CWD
  379. # leaves an un-sign-extended dividend in DX:AX. Both are still present as
  380. # a two-step spec, so a missing step has to be visible.
  381. cp "$SAVED_C2" Compiler.mod
  382. mutate Compiler.mod 's| Ebyte (99H) ; Ebyte (0F7H) ; Ebyte (0F9H)| Ebyte (0F7H) ; Ebyte (0F9H)|'
  383. expect_red "audit catches IDiv without the CWD that extends the dividend" \
  384. "IDivAxCx" $AUD
  385. restore_audit_sources
  386. if $AUD >/dev/null 2>&1; then
  387. echo " ok: the audit passes on both restored sources"
  388. pass=$((pass + 1))
  389. else
  390. echo "NOT RESTORED: the audit is red after restoring the sources"
  391. $AUD 2>&1 | sed 's/^/ /'
  392. fail=$((fail + 1))
  393. fi
  394. echo
  395. echo "== the .COM layout check, and the runtime size it now measures"
  396. # The checker used to RESTATE the runtime's size as a literal. It was wrong
  397. # by 41 bytes for an unknown time, and every one of the 30 .COM files "failed"
  398. # on a header read out of the code stream. A duplicated constant that has
  399. # drifted does not fail loudly; it re-reports the same falsehood, in which the
  400. # real failures hide. The size is now MEASURED from the image.
  401. #
  402. # These cases corrupt a real emitted .COM and require the checker to notice.
  403. # They need the images, so they are built once and copied; the checker has a
  404. # --check-only mode for exactly this, because its scratch directory is normally
  405. # deleted on exit and a check that has only ever seen the truth is not a check.
  406. KEEPDIR=/tmp/opencode/nonvacuity.com
  407. rm -rf "$KEEPDIR"
  408. TP_COM_KEEP=1 tests/run_com_tests.sh >/tmp/opencode/nonvacuity.com.log 2>&1
  409. KEEP=$(sed -n 's/^TP_COM_KEEP=1: images left in //p' \
  410. /tmp/opencode/nonvacuity.com.log | tail -1)
  411. if [ -z "$KEEP" ] || [ ! -d "$KEEP" ]; then
  412. echo " FAIL: could not obtain emitted .COM images for the layout cases"
  413. fail=$((fail + 1))
  414. else
  415. COMCHK="tests/run_com_tests.sh --check-only"
  416. # 0. The baseline. Every case below is a claim that a specific assertion
  417. # turns red, and none of them means anything if the copies of untouched
  418. # images already fail. (The stale RT_SZ produced exactly that: 30
  419. # failures that were not findings.) So this is asserted first, and a
  420. # failure here is reported as a broken baseline rather than a red test.
  421. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  422. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  423. if $COMCHK "$KEEPDIR" >/dev/null 2>&1; then
  424. echo " ok: baseline - untouched copies of the real images all pass"
  425. pass=$((pass + 1))
  426. else
  427. echo " FAIL: the baseline is already red, so the cases below prove"
  428. echo " nothing - fix the baseline before reading them"
  429. $COMCHK "$KEEPDIR" 2>&1 | grep FAIL | head -3 | sed 's/^/ /'
  430. fail=$((fail + 1))
  431. fi
  432. # 1. Break hdrDS so it no longer ties the header to its own offset. The
  433. # header must become UNFINDABLE and be reported as such - a checker that
  434. # fell back to a remembered offset would report a confident number here,
  435. # which is the failure mode the measurement was introduced to remove.
  436. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  437. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  438. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  439. import sys
  440. p = sys.argv[1]
  441. d = bytearray(open(p, 'rb').read())
  442. off = 435
  443. d[off + 4:off + 6] = (0x1234).to_bytes(2, 'little') # hdrDS, no longer self-consistent
  444. open(p, 'wb').write(bytes(d))
  445. PYEOF
  446. expect_red "a header that cannot be located is reported, not assumed" \
  447. "no program header found" $COMCHK "$KEEPDIR"
  448. # 2. A complete, self-consistent header four bytes later, so the measured
  449. # runtime size becomes 436 instead of 432. This is the positive half of
  450. # the same check: the derivation must FOLLOW the file, and the entry
  451. # jump assertion - expressed in terms of the measurement - must follow
  452. # it too, demanding 452 rather than 448.
  453. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  454. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  455. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  456. import sys
  457. p = sys.argv[1]
  458. d = bytearray(open(p, 'rb').read())
  459. off = 439
  460. w = [1, 464 + 0x100, off + 0x1000 + 0x100, off + 0x1000 + 0x100 + 4, 0, 0, 0, 0]
  461. for i, x in enumerate(w):
  462. d[off + 2 * i:off + 2 * i + 2] = x.to_bytes(2, 'little')
  463. open(p, 'wb').write(bytes(d))
  464. PYEOF
  465. expect_red "the measured runtime size follows the image (432 -> 436)" \
  466. "want 452" $COMCHK "$KEEPDIR"
  467. # 3. The entry jump's opcode. One byte, and the only assertion in the
  468. # project that can see where execution STARTS.
  469. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  470. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  471. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  472. import sys
  473. p = sys.argv[1]
  474. d = bytearray(open(p, 'rb').read())
  475. d[0] = 0xEA
  476. open(p, 'wb').write(bytes(d))
  477. PYEOF
  478. expect_red "the entry jump must be E9, not a near JMP" \
  479. "not the E9 of the entry jump" $COMCHK "$KEEPDIR"
  480. # 4. The entry jump's target, moved one instruction earlier. A .COM that
  481. # lands in the middle of the prologue runs, prints something and exits
  482. # cleanly, so no size or structure check can see this.
  483. rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
  484. cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
  485. python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
  486. import sys
  487. p = sys.argv[1]
  488. d = bytearray(open(p, 'rb').read())
  489. d[1:3] = (100).to_bytes(2, 'little')
  490. open(p, 'wb').write(bytes(d))
  491. PYEOF
  492. expect_red "the entry jump must land on the first instruction" \
  493. "entry jump rel16=100" $COMCHK "$KEEPDIR"
  494. rm -rf "$KEEPDIR"
  495. fi
  496. echo
  497. echo "non-vacuity: $pass ok, $fail failed"
  498. [ "$fail" -eq 0 ]