Stage D: parked-idle VMs are killable, KILL unregisters; fix uninitialized pending_reap (FABRIC-3.8 §3.5, G6)
sk_vm_parked_idle(): saved context + exactly its own IDLE frame. capsule_vm_kill_refusal() is the single kill rule, shared by capsule_vm_kill() and the KILL word: refuse SWITCHED_OUT mid-task, parked with mail pending, or the VM running; allow parked-idle. capsule_vm_kill() now unregisters the switch slot (G6). Finding fixed: vm_registry_alloc() never initialized pending_reap (kmalloc does not zero); C-b's reap_pending walks every participant, so garbage reaped a healthy VM (seen live before any KILL). Now zeroed at alloc. Probe (reverted): baseline KILL refused, slots 3->4->4 (FAIL, proves G6); with Stage D dprobe parked idle, KILL dead, slots 3->4->3, PASS on amd64/ aarch64/riscv64. Acceptance: dict_hash unchanged, relay 3, idle growth 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
06bcab504d
commit
604ad7aadc
@@ -978,3 +978,77 @@ explicit instruction.
|
||||
her eligible).
|
||||
- Stage F's mechanism is now in C-b. Its remaining content is evidence only: work
|
||||
proceeds with the console idle, which this relay already shows for one message.
|
||||
- [x] **Stage D — kill of a parked-idle VM, and unregister on kill (§3.5, G6).** 2026-09-23.
|
||||
Captain Bob: *"go ahead with stage D."* §3.5's `parked_idle` flag is the frame predicate
|
||||
Stage FR already made possible, not a new field.
|
||||
|
||||
Changes:
|
||||
- `switch.c/.h`: new `sk_vm_parked_idle(vm)`. The VM has a saved context, and its frame
|
||||
stack is exactly one frame: its own `VM_FRAME_IDLE`. That describes a VM sitting at its
|
||||
trampoline with nothing in progress.
|
||||
- `capsule_birth.c/.h`: new `capsule_vm_kill_refusal(entry)`, the one place the rule
|
||||
lives. It refuses:
|
||||
- a `SWITCHED_OUT` VM that isn't parked idle (mid-task: a live frame awaits resuming);
|
||||
- one parked idle but with mail still pending (those messages hold their senders'
|
||||
Stadium heat, and nothing would drain them);
|
||||
- **the VM physically running**. A participant can now be the one executing `KILL`, and
|
||||
it can't free its own stack.
|
||||
|
||||
`capsule_vm_kill()` uses it and now calls `sk_vm_switch_signal_unregister()`, as
|
||||
`capsule_vm_force_reap()` already did. Before this, an ordinary `KILL` left the slot
|
||||
behind.
|
||||
- `mama_forth_words.c`: the `KILL` word used its own copy of the old blanket
|
||||
`SWITCHED_OUT` refusal. It now asks `capsule_vm_kill_refusal()`, and runs its heat
|
||||
retire and active-VM cleanup for any VM that will actually be killed, parked-idle ones
|
||||
included.
|
||||
|
||||
**Finding, fixed here: a C-b regression.**
|
||||
- `vm_registry_alloc()` never initialized `VMRegistryEntry.pending_reap`, and `kmalloc`
|
||||
doesn't zero memory, so every born VM started with whatever the allocation held.
|
||||
- This was latent since `FABRIC-3.md` §XXVIII Stage 4 (2026-09-14), because only the old
|
||||
checkpoint read it, on a timer-picked target.
|
||||
- C-b's `sk_vm_switch_reap_pending()` walks **every participant at every safe point**, so
|
||||
a garbage non-zero value reaps a healthy VM. Seen live in the first Stage D baseline
|
||||
(`logs/20260923-182435/amd64/`): `REAP: dprobe tombstone reclaimed` appears before any
|
||||
`KILL`.
|
||||
- Fixed at the root: `pending_reap = 0` in `vm_registry_alloc()`. It was the only
|
||||
`VMRegistryEntry` field left uninitialized.
|
||||
- The Tripod and `rajames~user` weren't reaped in C-b's runs only because their bytes
|
||||
happened to be 0.
|
||||
|
||||
**Evidence, as named in §4.** A temporary probe (`kernel_main.c`, reverted with
|
||||
`git checkout`):
|
||||
- It births `dprobe` from `workload-1-lite.4th` with `WORKER-BIRTH` and registers it by
|
||||
hand, standing in for Stage E.
|
||||
- It sends it `1 2 + DROP`. Hera runs `: DPX 1 DROP ; DPX`; that colon-word checkpoint,
|
||||
with a decision due from the mail, switches to `dprobe`. `dprobe` drains the message,
|
||||
finds nobody with work, and falls back to Hera.
|
||||
- It then checks that `dprobe` is parked idle, runs `S" dprobe" KILL`, and checks the
|
||||
slot count and registry state.
|
||||
|
||||
Runs:
|
||||
- **Baseline**, with the init fix but no Stage D, amd64 (`logs/20260923-182940/amd64/`):
|
||||
`KILL: refused -- switched-out`, `slots before=3 registered=4 after_kill=4
|
||||
parked_idle=1 dead=0`, **FAIL**. That proves G6.
|
||||
- A first run with Stage D (`-183452`) still failed identically. That is how the `KILL`
|
||||
word's own copy of the refusal was found; after that, the shared rule.
|
||||
- **With Stage D:** `logs/20260923-184026/amd64/`, `-184502/aarch64/` and
|
||||
`-184946/riscv64/` give `KILL: dprobe dead`, `slots before=3 registered=4 after_kill=3
|
||||
parked_idle=1 dead=1`, **PASS** on all three.
|
||||
|
||||
**Formal acceptance with the probe gone:** `logs/20260923-185515/amd64/`,
|
||||
`-190119/aarch64/`, `-190745/riscv64/`.
|
||||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD` and zero failures or faults.
|
||||
- The `dict_hash` set is unchanged.
|
||||
- The relay gives `3` through a real switch.
|
||||
- Idle log growth: 0 bytes.
|
||||
- Clean build on all three plus hosted, no new warnings.
|
||||
|
||||
**Reported, not changed (pre-existing):**
|
||||
- Killing a **LIVE** VM that still has kernel-Hermes mail queued leaves those messages in
|
||||
its queue. They hold their senders' Stadium heat forever, because nothing drains a dead
|
||||
VM's queue. Stage D refuses the parked case for exactly this reason, but the LIVE path
|
||||
has always had it.
|
||||
- WIREBIND's detach path calls `capsule_vm_kill()` directly, without the `KILL` word's
|
||||
`vm_physics_retire()`. I have not checked whether a detached identity's VM-physics heat
|
||||
is retired elsewhere.
|
||||
|
||||
Reference in New Issue
Block a user