FABRIC-3.10.8 K-9: remove a dead VM's physics node at KILL and reap

vm_physics_remove() retires the node (heat to the root) and frees it;
capsule_vm_kill() and capsule_vm_force_reap() call it, so WIREBIND/EJECT
kills and reaps now retire too (before, only the KILL word did). A dead
VM now leaves only its registry node (168 B). 3-ISA acceptance clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-26 10:03:26 -04:00
co-authored by Claude Opus 5.5
parent 1a3e21b51e
commit ff8e505308
13 changed files with 36747 additions and 11 deletions
+21
View File
@@ -721,3 +721,24 @@ riscv64's slowest completion halved (15.1 → 7.7 ms), consistent with requests
| **K-11** | Every lock gets a rank, in the header's order: hermes < switch < registry < VM `dict_lock` < VM `tuning_lock` < stadium < slot < doe < log < block < console < heap < vmm < pmm. Each core counts the locks it holds per rank. Taking a lock whose rank is not above every rank already held counts an **order violation**, recording both locks and the call site. A same-rank pair, such as two Stadium slots or two VMs' `dict_lock`s, is a violation too. It is logged by `sk_lock_report()` like the other lock failures. | A probe that takes heap and then registry logs exactly one `LOCK: order violation`, naming both. With the probe reverted, O-A, the standard sequence and a 20-VM churn log **0 order violations** on all three ISAs, or else each one is reported as a finding (not fixed unrequested). |
**Found, not changed:** `ARTEMIS-READY-ACK` (block 4129) was only ever called by the Hermes VM and has no caller now. `experiments/std79-doe/{analysis,report}-20260912/{combine.py,analysis.R}` read `hermes_heat_q48` from their own historical data; they are left as they are.
### 4.10 K-9 — a dead VM's physics node is removed (2026-09-26)
**Change.**
- New `vm_physics_remove(vm_id)` (`capsule_vm_physics.c`): retire (heat to the root, as before), then unlink and free the node. Hera is refused.
- `capsule_vm_kill()` and `capsule_vm_force_reap()` call it after releasing the quota slot and before the entry goes DEAD. That covers every death: the `KILL` word, WIREBIND/EJECT/UNCLEAN, and the reap.
- The `KILL` word's own `vm_physics_retire()` call is gone; it kept its console reset.
**Evidence** (K-8's heap probe, reapplied temporarily, amd64, `logs/20260926-092847`).
- Each worker cycle leaves **1 block, 168 bytes**, the registry node, in both measured cycles (K-8: 2 blocks, 232 bytes).
- After 10 worker kills, `VM-PHYSICS-STATUS` lists Hera, Hestia and Artemis only. `fleet_heat_sum=65536`, CONSERVED; Hera holds all of it.
**Standard sequence** (clean build, with the probe reverted; the tree is byte-identical to the saved stage diff).
| ISA | log | result |
|---|---|---|
| amd64 | `logs/20260926-093320` | hash `0x99b01134c3d3049c`, G A=40 B=40, HHH, all ok `3`, ledger unchanged, 0 B idle, 0 / 0 |
| aarch64 | `logs/20260926-094315` | same |
| riscv64 | `logs/20260926-095353` | same |
**K-9 closes the MC-0 finding "`vm_physics` nodes are never removed".** Each dead VM now leaves only its registry node, 168 bytes, by design.
+15 -1
View File
@@ -61,7 +61,8 @@ void vm_physics_init(VMUuid vm_id);
/**
* vm_physics_retire - Remove a killed VM, returning its heat to Hera
*
* Called from mama_word_kill before the registry entry is torn down.
* Called by vm_physics_remove() (FABRIC-3.10.8 K-9) before the registry
* entry is torn down; mama_word_kill used to call it directly.
* The dying VM's entire remaining heat is transferred up its
* parent_vm_id chain (VMRegistryEntry, capsule_run.h) to the fleet's
* single structural root, Hera -- no division, no per-survivor
@@ -73,6 +74,19 @@ void vm_physics_init(VMUuid vm_id);
*/
void vm_physics_retire(VMUuid vm_id);
/**
* vm_physics_remove - Retire a dead VM, then drop its node
*
* FABRIC-3.10.8 K-9: called from capsule_vm_kill() and
* capsule_vm_force_reap(), while the registry entry is still readable
* (retirement walks parent_vm_id). Heat goes to the root exactly as
* vm_physics_retire(); the node is then unlinked and freed, so a dead VM
* leaves nothing in the fleet list. Hera is refused.
*
* @param vm_id Registry VM ID of the VM being torn down
*/
void vm_physics_remove(VMUuid vm_id);
/**
* vm_physics_touch - Record real dispatch activity against a VM
*
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -45,6 +45,7 @@
#include "vm.h"
#include "platform_alloc.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_registry */
#include "starkernel/capsule_vm_physics.h" /* FABRIC-3.10.8 K-9 -- vm_physics_remove() */
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log, 64) and log.h's in-memory line
* length (renamed LOG_MSG_LINE_MAX, 256) are distinct names, so include
@@ -454,6 +455,10 @@ void capsule_vm_force_reap(VMUuid vm_id) {
* Hermes departure above, which returns its pending mail). */
(void)stadium_release_quota(vm_id);
/* FABRIC-3.10.8 K-9: its fleet-physics node -- heat to the root, node
* freed. Before the entry goes DEAD: retirement walks the registry. */
vm_physics_remove(vm_id);
capsule_parity_log_kill(vm_id, entry->name);
console_puts("REAP: ");
@@ -642,6 +647,9 @@ int capsule_vm_kill(const char *name) {
/* FABRIC-3.10.8 K-4: its Stadium quota slot is released. */
(void)stadium_release_quota(vm_id);
/* FABRIC-3.10.8 K-9: its fleet-physics node, as in force_reap above. */
vm_physics_remove(vm_id);
sk_spin_lock(&g_lock_registry);
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
@@ -300,6 +300,25 @@ void vm_physics_retire(VMUuid vm_id)
dying->physics.is_live = 0;
}
void vm_physics_remove(VMUuid vm_id)
{
vm_physics_node_t **link = &vm_physics_head;
vm_physics_node_t *n;
if (vm_uuid_is_hera(vm_id)) return; /* the root is never removed */
vm_physics_retire(vm_id); /* its heat to the root first; no-op if already retired */
while ((n = *link) != (void *)0) {
if (vm_uuid_equal(n->vm_id, vm_id)) {
*link = n->next;
kfree(n);
return;
}
link = &n->next;
}
}
void vm_physics_touch(VMUuid vm_id)
{
vm_physics_node_t *target = vm_physics_find(vm_id);
+4 -10
View File
@@ -598,16 +598,13 @@ void mama_word_kill(VM *vm)
for (i = 0; i < (uint32_t)u; i++) name_buf[i] = src[i];
name_buf[u] = '\0';
/* Resolve vm_id and fan out its heat to survivors before the registry
* entry is torn down -- capsule_vm_kill() only takes a name and
* leaves no live entry to query afterward. Excludes Hera: her
* self-referential parent_vm_id makes vm_physics_retire() treat her
* as her own unreachable root and zero the fleet's entire heat sum,
* before capsule_vm_kill()'s own Hera guard below ever runs. */
/* The dying VM's heat goes to the root inside capsule_vm_kill()
* (vm_physics_remove(), FABRIC-3.10.8 K-9), for every caller; this
* word used to retire it here, so WIREBIND/EJECT kills never did. */
{
VMRegistryEntry entry;
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): refuse here too, so the
* heat-fanout/active-VM cleanup below never runs against a VM
* active-VM cleanup below never runs against a VM
* that's about to be refused anyway. FABRIC-3.8.md §3.5 (Stage D):
* the rule itself now lives in one place, capsule_vm_kill_refusal(),
* rather than a copy of capsule_vm_kill()'s old blanket
@@ -623,9 +620,6 @@ void mama_word_kill(VM *vm)
}
if (capsule_vm_find_by_name_nocase(name_buf, &entry) == 0 &&
(entry.state == VM_STATE_LIVE || entry.state == VM_STATE_SWITCHED_OUT)) {
if (!vm_uuid_is_hera(entry.vm_id)) {
vm_physics_retire(entry.vm_id);
}
/* FABRIC-2.md §F.10, real bug found and reported 2026-08-27,
* fixed 2026-09-05: capsule_vm_kill() never touches
* g_repl_active_vm -- killing the VM the console is currently