Stage MC-0d: Stadium partitioned by quota slot, one lock per slot (FABRIC-3.10.8 §3.5)

- Ruling (Captain Bob): per-slot locks, not "own path needs no lock" --
  a lock only on the cross-slot side can't exclude an unlocked owner
  (Hermes release evicting a sender's cell while the sender admits).
  FABRIC-3.10.0 §3.6 amended.
- StadiumVMQuota.lock covers the slot's lists, reservoir, consumed, word
  row and its cells. Word dispatch holds only its own slot lock for the
  whole dispatch (*_in_slot forms); peek-then-pull is one step.
- g_lock_stadium: grant slot allocation (new slot published with release
  in_use) and the shared block table (ruled: the one shared-lock Stadium
  path). Taken before a slot lock.
- Bitmap bytes set/cleared atomically (8 cells, possibly 8 slots).
- Cross-slot writers moved under the owner's lock: Hermes decay
  (stadium_cell_decay), Hermes release ledger (stadium_evict_credit),
  session pin (stadium_cell_set_pin). stadium_conserved() one hold.
- Word and block cell lookups check the cell is still this slot's.
- Probe (removed), identical on all three ISAs: 10,746,599 of 10,746,616
  word dispatches took exactly one lock (own slot), the 17 others all row
  growths; every live VM conserved; Hermes audit failures 0.
- Findings reported: births/USE run a VM's words while another VM is the
  core's current VM (MC-3 claim must cover it); quota slots never
  released; owner admission can evict a live message cell (latent, 0).
- Standard sequence on amd64/aarch64/riscv64: dict_hash set and G
  byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-25 22:40:33 -04:00
co-authored by Claude Opus 5.5
parent 355909d46f
commit 02c4c7e620
22 changed files with 55429 additions and 133 deletions
+87
View File
@@ -319,3 +319,90 @@ These runs' `dict_hash` values differ from the table below only because the prob
- The G string is byte-identical to MC-0b2, MC-0b and MC-0a on all three ISAs.
- The ledger line is unchanged.
- No stop condition was hit. **MC-0c done.**
### 3.5 MC-0d — Stadium partitioned by quota slot (2026-09-25)
**Ruling (Captain Bob, 2026-09-25): one lock per quota slot.** FABRIC-3.10.0 §3.6 said a VM's own Stadium row and cells need no lock, with a Stadium lock only for cross-slot operations. That can't be made sound: a lock taken only on the cross-slot side can't exclude an owner that takes none. Concrete race:
- a receiver's drain runs `sk_hermes_release()` → `stadium_evict()` on the **sender's** message cell, which rewrites the sender's free list, resident list and reservoir;
- meanwhile the sender, running on another core, is popping that same free list in `stadium_admit()`.
`stadium_grant_quota()` splits a donor's free list the same way. The amendment is recorded at FABRIC-3.10.0 §3.6.
**Second ruling: the block table takes the shared Stadium lock.** `stadium_blocks.c` keeps one open-addressed table for every VM, keyed by (slot, LBN). `stadium_block_dispatch()` therefore takes `g_lock_stadium`. It is the one shared-lock Stadium path, recorded for MC-3 to measure; block access is rare next to word dispatch.
**Change.**
- **The slot lock.**
- `StadiumVMQuota` gains `lock`, a spinlock.h lock word.
- It covers the slot's free list, resident list, reservoir, consumed, word row and statistics, and the slot's cells: their headers, owner entries and resident-list links.
- Every public Stadium function locks the slot it touches. A cell operation locks the cell's owning slot (`lock_cell_owner()` re-reads the owner once locked).
- **Word dispatch** (`stadium_word_dispatch()`) holds its own VM's slot lock for the whole dispatch: row, cell, cooling, pull, and admission including its eviction fallback. It uses the new `*_in_slot` forms, so it takes no other lock (a row growth's `kmalloc` excepted).
- The reservoir peek-then-pull is now one step under that lock.
- **Global part: `g_lock_stadium`.**
- Quota-slot allocation in grants. Each new slot is filled first, then published with a release store of `in_use`; `quota_slot_for_vm()` reads `in_use` with acquire.
- The shared block table.
- It is taken before a slot lock, never after.
- **The residency bitmap:** a byte covers 8 cells, which can belong to different slots, so set and clear are atomic OR/AND.
- **Cross-slot writers moved into Stadium, under the owner's lock:**
- kernel-Hermes's decay: `stadium_cell_decay()`, the same `q48_mul` arithmetic, consumed recorded in the same step;
- Hermes's release ledger: `stadium_evict_credit()` returns the heat actually credited, read in the same step as the eviction, where it used to be read from the cell beforehand;
- `session_set_pinned()`'s pin bit: `stadium_cell_set_pin()`.
- **`stadium_conserved()`** reads its three terms under one hold, so the sum is of one instant.
- **Owner check** added to both dispatch paths' cell lookups: the cell must still be this slot's.
- Without it, a word or block cell evicted to this slot's free list could move to another VM in a grant and be admitted there with a matching identity. Word ids and LBNs are shared across VMs, so the old row would then heat the other VM's cell.
- This is latent on one core.
- **Unpartitioned reads that remain:** `sk_hermes_scan_held()` (reads message cells under the Hermes lock), `session.c`'s bounds check, and `kernel_main.c`'s decay self-test. None writes.
- **Lock order** (spinlock.h):
1. `hermes`
2. `switch`
3. `registry`
4. `stadium`
5. a slot lock
6. `doe`
7. `log`
8. `block`
9. `console`
10. `heap`
11. `vmm`
12. `pmm`
A VM's `dict_lock` sits above Stadium: `vm_core.c`'s heat bump dispatches the word under it. Nothing in the block path calls into Stadium, so MIGRATE's `blk_flush()` edge doesn't close a cycle.
**Probe** (temporary, removed before commit). It counted:
- the per-core lock acquisitions made during each `stadium_word_dispatch()`;
- slot-lock acquisitions made for a slot other than the running VM's own;
- admission-fallback evictions of message (DELIVER) cells;
- quota slots in use.
An `MC0D-PROBE` word in every VM printed these, with `stadium_conserved()` on **every** live VM and the Hermes audit-failure count. The results are identical on all three ISAs, count for count:
| ISA | log | word dispatches | exactly one lock (own slot) | more than one | …all at a row growth | conserved | Hermes audit failures | DELIVER-cell fallback evictions |
|---|---|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-212159` | 10,746,616 | 10,746,599 | 17 | 17 | 7/7 | 0 | 0 |
| aarch64 | `logs/20260925-213527` | 10,746,616 | 10,746,599 | 17 | 17 | 7/7 | 0 | 0 |
| riscv64 | `logs/20260925-214855` | 10,746,616 | 10,746,599 | 17 | 17 | 7/7 | 0 | 0 |
The table shows the final (rajames) reading. At Hera's prompt, before MINT, the readings were 9,687,103 dispatches with 14 exceptions (all row growths), 5/5 conserved, and 268 cross-slot slot locks.
**Findings, reported and not fixed:**
1. **The running VM isn't always the core's current VM.** Cross-slot slot locks went from 268 to 1,059,738 across MINT, WIREBIND, `USE` and `1 2 + .`, the same growth as word dispatches in that window.
- A birth runs the child's capsule on the parent's stack (capsule_birth.c), and `USE` runs another VM's lines, so these dispatches are for a VM other than `sk_percpu()->current_vm`.
- The Stadium locking is correct either way: it locks the dispatching VM's own slot.
- But MC-3's claim (`running_on`) must cover a VM executed this way, not only one switched to.
2. **Quota slots are never released.** `stadium.c` has no release path: `in_use` is set by grants and cleared only at boot.
- The probe counted 13 slots in use against 7 registry entries. Boot self-tests grant quotas to test ids (`kernel_main.c`), and a dead VM keeps its slot and cells.
- A long-running system eventually exhausts the RAM-sized slot table.
3. **An owner's word admission can evict a live message cell.** The admission fallback evicts the owner's least-dense resident, skipping only pinned and containing cells. Hermes admits messages unpinned into the sender's quota.
- After such an eviction, `msg->stadium_cell` is stale: the release would evict whatever occupies that cell next.
- This run counted 0 such evictions (the quotas never ran dry of free cells), so it is latent.
**Standard sequence** (clean build, with the probe reverted; the tree is byte-identical to the saved stage diff).
| ISA | log | O-A (G, PREC, LOAD) | M7.1a hash | MINT / WIREBIND / USE / `1 2 + .` | idle growth | UNKNOWN / faults |
|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-220202` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| aarch64 | `logs/20260925-221440` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| riscv64 | `logs/20260925-222740` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
- The G string is byte-identical to MC-0c and earlier on all three ISAs, so the O(ncells) audit scan now running under a slot lock didn't move it.
- The `dict_hash` set is unchanged, and the ledger line reads `stadium_conserved(self)=true`.
- No stop condition was hit. **MC-0d done, and with it MC-0: one core, fully locked, ready for MC-1.**