nonvacuity.sh 80 KB

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