v-TP3-INTERRUPT-RESTORE.md 6.2 KB

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 exits 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.