|
|
@@ -0,0 +1,153 @@
|
|
|
+v-TP3-INTERRUPT-RESTORE — Session summary
|
|
|
+========================================
|
|
|
+
|
|
|
+Tag: v-TP3-INTERRUPT-RESTORE
|
|
|
+Commit: (the commit that adds this file)
|
|
|
+Fixes: ff7b2dd "tests: nonvacuity restores Compiler.mod when the run is
|
|
|
+ interrupted" and 5e0312e "tests: the nonvacuity trap also restores the
|
|
|
+ mod=11 probe sources" — the two commits immediately before this one
|
|
|
+Covers: the nonvacuity harness's interrupt safety
|
|
|
+
|
|
|
+`v-TP3-STRCAT` (tag `v-TP3-STRCAT`, commit `1bed3bb`) is the string
|
|
|
+concatenation milestone this session began with; its write-up is
|
|
|
+`v-TP3-STRCAT.md`. This document covers the test-harness repair that finishing
|
|
|
+that milestone turned up, which was small in code and turned out to be two
|
|
|
+problems deep.
|
|
|
+
|
|
|
+The bug
|
|
|
+-------
|
|
|
+
|
|
|
+`shell/tests/nonvacuity.sh` mutates sources on purpose and has to put them back.
|
|
|
+It had an `EXIT` trap that restored `Runtime.mod`, `Exec86.mod`, `Shell.mod`,
|
|
|
+`tests/comimage.py` and `tests/comtest.py` — but not `Compiler.mod`, the file it
|
|
|
+mutates more than any other, because `SAVED_C` was defined at the BP-displacement
|
|
|
+section halfway down the file, well **after** the trap was set at line 153. An
|
|
|
+interrupted run therefore left `Compiler.mod` half-mutated, and `git status`
|
|
|
+showed a source that no case had finished with — the exact confusion that two
|
|
|
+agents colliding on one tree produces.
|
|
|
+
|
|
|
+Finding one: the trap had never fired for an interruption
|
|
|
+---------------------------------------------------------
|
|
|
+
|
|
|
+Moving `SAVED_C` above the trap was the obvious half. Testing it disproved the
|
|
|
+comment next to it:
|
|
|
+
|
|
|
+```
|
|
|
+bash -c 'trap "echo EXIT-TRAP" EXIT; sleep 30' &
|
|
|
+kill -TERM $! # prints nothing
|
|
|
+```
|
|
|
+
|
|
|
+An **unhandled signal does not run an `EXIT` trap at all** — bash dies on the
|
|
|
+spot. So `trap restore_all EXIT` had only ever protected against an `exit`
|
|
|
+*inside* the script; `Runtime.mod` and the rest were never protected against
|
|
|
+interruption either, despite the comment promising "an interrupted run cannot
|
|
|
+leave the layout half-written".
|
|
|
+
|
|
|
+The fix is that a handler which only `exit`s is enough, because `exit` from a
|
|
|
+trap **does** fire the `EXIT` trap — and it has to exit, or the run would go on
|
|
|
+using a tree it has just put back:
|
|
|
+
|
|
|
+```
|
|
|
+trap restore_all EXIT
|
|
|
+trap 'exit 129' HUP
|
|
|
+trap 'exit 130' INT
|
|
|
+trap 'exit 143' TERM
|
|
|
+```
|
|
|
+
|
|
|
+Finding two: not every signal behaves the same, and Ctrl-C does not stop the run
|
|
|
+-------------------------------------------------------------------------------
|
|
|
+
|
|
|
+Measured on bash 5.2.37, not assumed:
|
|
|
+
|
|
|
+| signal | does the handler fire? |
|
|
|
+|---|---|
|
|
|
+| `TERM` | yes — restores everything |
|
|
|
+| `HUP` | yes — restores everything |
|
|
|
+| `INT` | **usually never arrives** |
|
|
|
+
|
|
|
+For `INT` the rule is: a foreground child that dies of `SIGINT` makes bash treat
|
|
|
+the signal as the *child's* and **continue the script instead**. So **Ctrl-C
|
|
|
+does not abort `nonvacuity.sh` at all** — it kills the current case's child and
|
|
|
+the next case starts. `SIGTERM` is what actually stops it. The first version of
|
|
|
+the comment claimed `INT` worked; the interrupt test disproved it, and the
|
|
|
+comment now records what was measured.
|
|
|
+
|
|
|
+`tests/probe/modrm11.{py,s}`
|
|
|
+---------------------------
|
|
|
+
|
|
|
+The last file group, committed separately. The mod=11 table section mutates the
|
|
|
+probe's own sources and puts them back after each case, so a run that died
|
|
|
+between the two left them edited. `SAVED_PY` / `SAVED_S` moved to the top with
|
|
|
+the other six copies.
|
|
|
+
|
|
|
+`restore_all` copies them **directly** rather than calling `restore_probe`, which
|
|
|
+is defined halfway down the file: a trap firing before that definition would
|
|
|
+abort the rest of the restore with `command not found`, putting back the copies
|
|
|
+before it and silently leaving the ones after it. Same failure mode the whole
|
|
|
+exercise is about.
|
|
|
+
|
|
|
+The trap now covers all eight:
|
|
|
+
|
|
|
+ Runtime.mod Exec86.mod Shell.mod Compiler.mod
|
|
|
+ tests/comimage.py tests/comtest.py
|
|
|
+ tests/probe/modrm11.py tests/probe/modrm11.s
|
|
|
+
|
|
|
+Evidence
|
|
|
+--------
|
|
|
+
|
|
|
+Each claim came from killing a live run at the moment a given file was dirty:
|
|
|
+
|
|
|
+| file live | before | after |
|
|
|
+|---|---|---|
|
|
|
+| `Compiler.mod` | `DIRTY`, byte-different | `clean`, **byte-identical** |
|
|
|
+| `tests/probe/modrm11.py` | `DIRTY` | both probe sources **byte-identical** |
|
|
|
+
|
|
|
+The healthy path is unchanged: `non-vacuity: 78 ok, 0 failed`, and
|
|
|
+`run_all.sh` reports `OVERALL: ALL PASS` across all 15 suites.
|
|
|
+
|
|
|
+How to verify this work
|
|
|
+-----------------------
|
|
|
+
|
|
|
+ cd shell && tests/nonvacuity.sh # expect: non-vacuity: 78 ok, 0 failed
|
|
|
+ cd shell && tests/run_all.sh # expect: OVERALL: ALL PASS
|
|
|
+
|
|
|
+To reproduce the interrupt evidence — kill a run while a mutation is live and
|
|
|
+check the file comes back byte-identical:
|
|
|
+
|
|
|
+ cd shell
|
|
|
+ cp Compiler.mod /tmp/before
|
|
|
+ ./tests/nonvacuity.sh > /dev/null 2>&1 &
|
|
|
+ PID=$!
|
|
|
+ # wait until Compiler.mod differs, then:
|
|
|
+ kill -TERM $PID
|
|
|
+ wait $PID 2>/dev/null
|
|
|
+ cmp /tmp/before Compiler.mod && echo restored
|
|
|
+
|
|
|
+Note that `Compiler.mod` is restored by the trap but is a `.mod` source, so the
|
|
|
+trap's trailing `make` also rebuilds `Compiler.o` and `tpshell` from it — a
|
|
|
+restore that left a binary built from the mutation would still be reporting on
|
|
|
+the mutation.
|
|
|
+
|
|
|
+Next work
|
|
|
+---------
|
|
|
+
|
|
|
+Item 2 (the large one) is untouched: nested procedures / recursion, `var`
|
|
|
+parameters (the `SEG:OFF` push, RESUME-TP3.md 3.11), range/index checks
|
|
|
+(`TU_RANGE_CHECK`, `TU_INDEX_CHECK`), typed constants (3.14), `array` at its
|
|
|
+point of use (`t14`), `case` with subrange labels.
|
|
|
+
|
|
|
+Still `ENoLib` as unknown identifiers: the string standard functions `length`,
|
|
|
+`copy`, `pos`, `concat`, `delete`, `insert`, `str`. `+` and the six comparisons
|
|
|
+are done (`v-TP3-STRCAT`, `v-TP3-STRCMP`).
|
|
|
+
|
|
|
+Also still open:
|
|
|
+
|
|
|
+ - Harden the program-header parameter loop against non-advancing input
|
|
|
+ (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.
|
|
|
+ - FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried; it is the only
|
|
|
+ third opinion on the `INT 21h` shim.
|
|
|
+ - `SAVED_C2`/`SAVED_R2` (the 8086-opcodes section) and `SAVED_R3` are per-case
|
|
|
+ snapshots taken mid-file. They are **fine as they are**: the files they
|
|
|
+ guard (`Compiler.mod`, `Runtime.mod`) are already trap-covered, so an
|
|
|
+ interruption there is restored by `restore_all` regardless of which
|
|
|
+ per-case copy was live.
|