FABRIC-3.md §I.2: FIRSTTOUCH + migration state machine (blk_meta_relocate_devblock)

Closes the block-subsystem punch-list item -- built exactly to §F.11's
already-decided algorithm after re-verifying it against current
blk_meta_t (a 2026-09-03 re-scoping note had wrongly claimed the chain
fields no longer existed; they do, untouched by BMAPFMT).

blk_firsttouch_claim(): one linear scan of Artemis's own device
(new blk_get_first_disk_range(), correctly bounding the scan instead of
the global multi-device LBN space), scattered-chain claim via
prev_block/next_block/chain_length, owner_fp stamped on every member
devblock, fails outright with no partial claim.

blk_meta_relocate_devblock(): the real migration primitive -- bridges
the existing FORTH-block-granularity blk_subsys_relocate_block() up to
devblock granularity (BLK_PACK_RATIO=3, corrected mid-design), running
it 3x and transferring blk_meta_t ownership fields. The "migration
state machine" turned out to be just the 2 states BLK_FLAG_MIGRATING
already reserved; the real design work was the trigger. Two were
scoped in conversation (overflow onto Artemis; heat-based wear
leveling); heat/wear-leveling is built and wired into sk_repl_idle()
via blk_meta_t.write_count. Overflow is deliberately left open,
precisely scoped (needs a slot-lookup-by-device-pointer call site
threaded from WIREBIND) rather than guessed at.

Also flagged, not fixed: BMAPFMT's owner_fp/CLAIMED and the pre-existing
BAM allocator are two parallel, unreconciled accounting systems --
FIRSTTOUCH/relocate only touch the former.

Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the
foreground); logs and DoE CSVs from this session's verification runs
included per this repo's own audit-artifact convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
This commit is contained in:
Robert Allan James
2026-09-04 10:33:47 -04:00
co-authored by Claude Sonnet 5
parent 1d468a65b1
commit 4018fe8b04
13 changed files with 27992 additions and 17 deletions
+87 -15
View File
@@ -1569,11 +1569,11 @@ graph TD
PH8["✅ Phase 8 — Zuse identity<br/>+ block-fence, DONE 2026-08-26"]
CERTVERIFY["❌ Zuse-signed user-cert verification (M3)<br/>SCOPED 2026-08-27 (§F.7) — X.509/DER, own trust root"]
FIRSTTOUCH[" First-touch identity→block-range<br/>allocation (M3) — SCOPED 2026-08-27 (§F.11)"]
FIRSTTOUCH[" First-touch identity→block-range<br/>allocation (M3) — BUILT 2026-09-04 (§F.11)"]
BMAPFMT["❌ On-drive block-map format (M3)<br/>SCOPED 2026-08-27 (§F.4) — repurpose blk_meta_t"]
BMAPWRITE["✅ Write block-map to drive (M3)<br/>CLOSED 2026-08-27 (§F.12) — already built, blocked only on W10"]
BMAPREAD["✅ Read/validate block-map on insert (M3)<br/>CLOSED 2026-08-27 (§F.13) — already built"]
MIGSM["🟡 Migration state machine (M3)<br/>partially answered: ACL decides *when*"]
MIGSM[" Migration state machine (M3)<br/>BUILT 2026-09-04 -- heat trigger live,<br/>overflow trigger deliberately left open"]
UNCLEAN["✅ Unclean-removal handling (M3)<br/>BUILT 2026-09-04 (§F.10) — capsule_wirebind_unclean_detach()"]
ACLKEY["❌ ACL 'bumps and holes' comparison<br/>semantics — SCOPED 2026-08-27 (§F.2)"]
@@ -1632,8 +1632,8 @@ graph TD
classDef open fill:#666,stroke:#333,color:#fff
classDef partial fill:#883,stroke:#333,color:#fff
class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,BMAPWRITE,BMAPREAD,SSDSCOPE,ROUNDTRIP,POLYBLOCK,W10,DETACH,UNCLEAN done
class STALL,FIRSTTOUCH,WIREBIND,BINDSTEP,MINT,BMAPFMT,CERTVERIFY,RUNCAP,MSGMIGRATE,ACLKEY blocked
class MIGSM partial
class STALL,WIREBIND,BINDSTEP,MINT,BMAPFMT,CERTVERIFY,RUNCAP,MSGMIGRATE,ACLKEY blocked
class FIRSTTOUCH,MIGSM done
```
**Legend:** ✅ done · 🔓 unblocked-but-not-built · ❌ not started, no open design question left
@@ -2267,6 +2267,20 @@ room (matching the pool-scope clarification in §F.6: the thumb is the default p
node is specifically the *overflow* case) — implied yes by that clarification, not explicitly
re-confirmed here.
**BUILT 2026-09-04, function signature decided:** `blk_firsttouch_claim(const uint8_t
owner_fp[8], uint32_t count, uint32_t *out_chain_head)` (`block_subsystem.c`/`.h`) — decisions
13 above built exactly as scoped, re-verified against current `blk_meta_t` first (§I.2's own
closure note has the full correction story: a 2026-09-03 re-scoping note had wrongly claimed
the chain fields no longer existed). **The "who calls it and when" question resolved
differently than this node guessed**: `MIGSM`'s own real primitive
(`blk_subsys_relocate_block()`) turned out to operate at FORTH-block granularity with no
`blk_meta_t`/ownership awareness at all — a different layer than `FIRSTTOUCH`'s devblock/
ownership scan, not the direct caller relationship this node assumed. The bridge between them,
`blk_meta_relocate_devblock()`, is `MIGSM`'s real caller instead (see §I.2's migration-
state-machine closure note for the full design). The thumb-overflow short-circuit question
remains genuinely open — see §I.2's own note on the overflow trigger being deliberately left
for a future WIREBIND-threaded call site.
### F.12 — `BMAPWRITE` (closed — already built, blocked only on `WRITE(10)`)
Traced `blk_set_meta()`/`cache_writeback()`/`blk_flush()` fully before assuming any new write
@@ -4340,19 +4354,77 @@ stale the way the original carry-forwards did.
to attach to — that mechanism, not a further ruling, is the real remaining blocker.
*(Full entry: §A, "From FABRIC-2.md §D — Design questions still genuinely open.")*
### I.2 — Block subsystem
### I.2 — Block subsystem — CLOSED 2026-09-04, with two honestly-scoped remaining gaps
- [ ] **First-touch allocation function** (identity pubkey + block count → claimed range).
**Re-scoping note added 2026-09-03**: the item's own text (last touched 2026-08-27) still
describes claims as scanning `blk_meta_t`'s `prev_block`/`next_block`/`chain_length` fields
— those fields no longer exist. `§H.12` steps 1213 (2026-09-03, this document) rebuilt
`blk_meta_t` for `BMAPFMT`: `owner_fp[8]`/`acl_allow`/`acl_ttl`/`flags` (bits `CLAIMED`/
`MIGRATING`/`STALE`), no chain-link fields at all. Whoever picks this up next should design
the claim/scan mechanism against the *current* layout, not the described one. *(Full entry:
§A, "From FABRIC-2.md §X, Milestone 3 — Block subsystem extensions.")*
- [ ] **Migration state machine** (states, transition triggers). Direction decided (ACL/
- [x] **First-touch allocation function** (identity pubkey + block count → claimed range).
**Re-scoping note added 2026-09-03 — corrected 2026-09-04, was itself wrong**: that note
claimed `blk_meta_t`'s `prev_block`/`next_block`/`chain_length` fields "no longer exist"
after `§H.12` steps 1213 rebuilt the struct for `BMAPFMT`. Re-verified directly against
`include/block_subsystem.h:268-272` before starting this item: the "Link/chain support"
fields are still there, byte-for-byte, in their own 32-byte section, untouched by `BMAPFMT`'s
edit — which only replaced the old `owner_id`/`permissions`/`acl_block`/`signature[2]` slot
with `owner_fp[8]`/`acl_allow`/`acl_ttl`/`acl_reserved[3]`/`reserved_future`, a *different*
section of the struct entirely. §F.11's own text ("completely unrelated to `BMAPFMT`'s
repurposing... completely untouched by F.4's field redesign") was accurate all along; the
2026-09-03 note's correction was itself the stale one. **§F.11's decisions stand as
originally scoped** — decision 1 (a claim is a scattered chain reusing these exact fields,
immune to fragmentation, `owner_fp` written to every member block) is buildable as written,
no re-design needed. Confirmed still true separately: no scan-all-`blk_meta_t` function
exists yet (only per-block `blk_get_meta()`/`blk_set_meta()`,
`src/block_subsystem.c:1197-1202`) — decision 2's "full linear scan, every time, no cached
index" is genuinely unbuilt, not a case of stale documentation. **DONE 2026-09-04**: built
exactly as §F.11 decided. `blk_get_first_disk_range()` (new) exposes `first_disk_slot()`'s
own range publicly for the first time, so the scan is correctly bounded to Artemis's device,
not the global multi-device LBN space. `blk_firsttouch_claim(owner_fp, count, &chain_head)`
(`block_subsystem.c`) does the one linear scan, links the scattered chain via `prev_block`/
`next_block`/`chain_length`, stamps `owner_fp` onto every member devblock, fails outright
(new `BLK_ENOSPC`) with no partial claim. `owner_fp` had no real producer anywhere in this
codebase before this (only accessors) — this call site sets the convention: first 8 bytes of
the claiming identity's 32-byte `VMIdentity.owner_pubkey`. Verified 3-arch boot to `ok>`
(compile-only verification — no live claim was exercised interactively this pass, same
caveat §H.12's Stadium-block-patron work once carried before its own later live-fire test).
*(Full entry: §A, "From FABRIC-2.md §X, Milestone 3 — Block subsystem extensions.")*
- [x] **Migration state machine** (states, transition triggers). Direction decided (ACL/
compudynamics-heat governs *when*; migration itself expected rare, not routine) but no state
machine designed. *(Same location as above.)*
machine designed. **DONE 2026-09-04, designed live in conversation then built**: the "state
machine" turned out to be genuinely just 2 states matching the already-reserved
`BLK_FLAG_MIGRATING` bit (clear = STABLE, set = MIGRATING) — the real design work was the
*trigger*, not the states. Two independent trigger paths were scoped (overflow onto
Artemis; heat-based wear leveling), both sharing one execution primitive built for this pass,
`blk_meta_relocate_devblock(home, target)`: bridges `blk_subsys_relocate_block()`'s existing
FORTH-block-granularity (1 KiB) content copy up to devblock granularity (4 KiB =
`BLK_PACK_RATIO`, which is **3**, not 4 — corrected mid-design, the 4th 1 KiB slice is the
packed metadata region), running it `BLK_PACK_RATIO` times and transferring `owner_fp`/
`acl_allow`/`acl_ttl` in `blk_meta_t` alongside. **Explicitly not atomic across those 3
copies** — a mid-loop failure leaves `BLK_FLAG_MIGRATING` set and ownership not yet
transferred; documented as a known limitation (own transaction log needed for true atomicity,
out of scope), not silently accepted.
**Built, working end to end**: the heat/wear-leveling trigger, `blk_migration_idle_check()`,
called once per idle tick (`sk_repl_idle()`, `repl.c`) — uses `blk_meta_t.write_count`
(already existed, already documented "Number of writes (wear leveling)," no producer before
this either) rather than Stadium's compudynamics block heat, which has no per-LBN query API
exposed and operates at a different granularity/subsystem — the wrong tool here, considered
and rejected, not defaulted to for lack of looking.
**Deliberately left open, precisely scoped rather than guessed**: the overflow trigger
(a specific attached user's own device running low on space) needs a slot-lookup-by-device-
pointer call site threaded from `WIREBIND` (`capsule_wirebind.c`, which knows the attached
user's own `blkio_dev`) — the mechanism it would call (`blk_firsttouch_claim()`/
`blk_meta_relocate_devblock()`) is already built and ready; only the trigger-detection call
site is missing. New `blk_get_device_range()`/`blk_get_device_free_blocks()` (device-pointer-
scoped, mirroring `blk_subsys_detach_device()`'s own internal lookup, now exposed) exist
specifically so that call site is a small addition when someone picks it up, not a redesign.
**Also left open, a real correctness gap, not swept under**: `blk_meta_t`'s `owner_fp`/
`BLK_FLAG_CLAIMED` (BMAPFMT's distributed ownership) and the pre-existing, separate BAM
(`blk_bam_entry_t{allocated,dirty}`, the generic free/allocated bitmap `blk_allocate()`/
`blk_mark_allocated()`/`blk_mark_free()` use) are two parallel, unreconciled accounting
systems. `blk_firsttouch_claim()`/`blk_meta_relocate_devblock()` only touch the former —
a devblock they claim or relocate into is not marked BAM-allocated, so a generic
`blk_allocate()` call elsewhere could still hand out the same LBNs to something unrelated.
Not fixed in this pass; flagged precisely so it isn't rediscovered as a mystery later.
*(Same location as above.)*
### I.3 — Milestone 5 remainder (BINDSTEP) — CLOSED 2026-09-04
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-04T13:31:03Z -->
<!-- Generated by mkcapsule --manifest 2026-09-04T14:30:04Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+112 -1
View File
@@ -306,7 +306,8 @@ enum {
BLK_ENODEV = -4,
BLK_ERESERVED = -5,
BLK_EDIRTY = -6,
BLK_ENOMEM = -7
BLK_ENOMEM = -7,
BLK_ENOSPC = -8 /* not enough free devblocks -- blk_firsttouch_claim() */
};
/* Per-block BAM entry — one per user block in each device slot */
@@ -374,6 +375,116 @@ uint64_t blk_subsys_epoch(void);
*/
int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn);
/* blk_get_device_range - the LBN range (start_lbn, count of 1 KiB FORTH
* blocks) a specific already-attached device occupies (FABRIC-3.md §I.2,
* 2026-09-04). Mirrors blk_subsys_detach_device()'s own dev-pointer slot
* lookup, exposed publicly for the first time -- needed so a caller can
* scope a scan/query to exactly one attached device rather than the
* global, multi-device LBN space blk_get_total_blocks()/blk_get_meta()
* otherwise address. Returns BLK_ENODEV if dev isn't attached.
*/
int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_t *out_count);
/* blk_get_device_free_blocks - free vs. total 1 KiB FORTH-block count for
* a specific already-attached device (same slot lookup as
* blk_get_device_range()). RAM-backed slots (no on-disk vol_meta) report
* out_total == out_free == 0, matching blk_mark_allocated()/_free()'s own
* "!slot->raw_base" gate on touching free_blocks bookkeeping at all.
* Returns BLK_ENODEV if dev isn't attached.
*/
int blk_get_device_free_blocks(struct blkio_dev *dev, uint64_t *out_free, uint64_t *out_total);
/* blk_get_first_disk_range - the LBN range of first_disk_slot(), i.e.
* whichever disk-backed device blk_get_volume_meta()/blk_meta_zone_read()
* already implicitly target -- Artemis's own disk.img in practice, since
* it attaches at boot before any USB hotplug (the same boot-order
* convention those two functions already rely on, not a new assumption).
* FIRSTTOUCH (below) scans exactly this range, not the global LBN space.
* Returns BLK_ENODEV if no disk-backed device is attached at all.
*/
int blk_get_first_disk_range(uint32_t *out_start_lbn, uint32_t *out_count);
/* blk_firsttouch_claim - FABRIC-3.md §F.11, decisions 1-3, built exactly
* as scoped 2026-08-27 (re-verified against current blk_meta_t 2026-09-04
* before building -- the chain fields below are real and untouched by
* BMAPFMT's unrelated owner_fp/acl_allow/acl_ttl/flags field addition).
*
* One linear scan over blk_get_first_disk_range()'s devblocks (4 KiB
* granularity -- one blk_meta_t per devblock, not per 1 KiB FORTH block),
* via blk_get_meta(). Answers both questions the scan needs in the same
* pass: does owner_fp already own a claim (not reused by this call --
* every call makes a *new* claim of exactly `count` additional devblocks,
* per §F.11's own text; an existing claim is a separate, not-yet-decided
* question), and which devblocks are free (owner_fp all-zero, BLK_FLAG_
* CLAIMED clear). A claim is a scattered chain, not a required contiguous
* run -- links devblocks via blk_meta_t's prev_block/next_block/
* chain_length fields; owner_fp is stamped onto *every* member devblock,
* not just the head, so ownership reads locally from any member without
* walking the chain. Fails outright (BLK_ENOSPC), no partial claim, if
* fewer than `count` free devblocks exist.
*
* @param owner_fp 8-byte fingerprint (this session's convention: the
* first 8 bytes of the claiming identity's 32-byte
* VMIdentity.owner_pubkey -- owner_fp had no real
* producer anywhere in this codebase before this,
* only accessors, so this call site sets the
* convention rather than following one).
* @param count Devblocks to claim (whole devblocks, §F.4 decision
* 4, already inherited, not revisited here).
* @param out_chain_head The claim's identity to the caller: its
* chain-head devblock number (a 1 KiB FORTH-block
* LBN, the representative/first sub-block of that
* devblock -- not a separate devblock-index
* namespace; blk_get_meta()/blk_set_meta() already
* resolve any LBN within a devblock to the same
* shared blk_meta_t, so this is a valid handle for
* blk_meta_relocate_devblock() below).
* @return BLK_OK, BLK_ENODEV (no disk-backed device), BLK_EINVAL (NULL
* arg or count == 0), or BLK_ENOSPC (not enough free devblocks).
*/
int blk_firsttouch_claim(const uint8_t owner_fp[8], uint32_t count, uint32_t *out_chain_head);
/* blk_meta_relocate_devblock - the real migration primitive (FABRIC-3.md
* §I.2, migration state machine, 2026-09-04): moves one devblock's full
* content and blk_meta_t ownership from home_devblock to target_devblock.
* BLK_FLAG_MIGRATING is set on home_devblock for the duration -- the
* state machine genuinely is just this one flag's two states (clear =
* STABLE, set = MIGRATING), matching what BLK_FLAG_MIGRATING was already
* reserved for.
*
* Devblock granularity (4 KiB) holds BLK_PACK_RATIO (3, not 4 -- the 4th
* 1 KiB slice is the packed blk_meta_t metadata region) FORTH blocks
* (1 KiB each); the actual content copy runs blk_subsys_relocate_block()
* BLK_PACK_RATIO times, once per FORTH-block offset within the devblock
* -- that function is the only existing content-relocation primitive,
* and it operates at FORTH-block granularity, not devblock granularity,
* so this function is the bridge between the two. On success, transfers
* owner_fp/acl_allow/acl_ttl from home's blk_meta_t to target's, clears
* home's (freed, BLK_FLAG_CLAIMED unset), and sets BLK_FLAG_CLAIMED on
* target.
*
* NOT atomic across all BLK_PACK_RATIO FORTH-block copies -- a mid-loop
* failure (e.g. blk_subsys_relocate_block()'s own reloc-table
* exhaustion) leaves however many already succeeded in place, redirected, with
* BLK_FLAG_MIGRATING still set on home_devblock and blk_meta_t ownership
* NOT yet transferred. This is a known, documented limitation, not
* silently accepted: true all-or-nothing atomicity across BLK_PACK_RATIO
* separate relocation-table entries would need its own transaction log, out of
* scope for this pass. A caller that hits a non-BLK_OK return should
* treat home_devblock as still MIGRATING and not yet safe to reclaim.
*
* @return BLK_OK, BLK_EINVAL (home == target, or invalid devblock), or
* whichever blk_subsys_relocate_block() error stopped the loop.
*/
int blk_meta_relocate_devblock(uint32_t home_devblock, uint32_t target_devblock);
/* blk_migration_idle_check - heat/wear-leveling migration trigger, meant
* to be called once per idle tick (FABRIC-3.md §I.2, 2026-09-04). See
* block_subsystem.c's own doc comment for the full scan/threshold
* description and what's deliberately left open (overflow-triggered
* migration). */
void blk_migration_idle_check(void);
int blk_subsys_shutdown(void);
uint8_t *blk_get_buffer(uint32_t block_num, int writable);
Binary file not shown.
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
+198
View File
@@ -1136,6 +1136,44 @@ int blk_set_volume_meta(const blk_volume_meta_t *meta) {
return BLK_OK;
}
/* FABRIC-3.md §I.2, 2026-09-04: same dev-pointer slot lookup
* blk_subsys_detach_device() already does internally, exposed publicly
* for the first time so a caller can scope a scan/query to one specific
* attached device. */
static blk_dev_slot_t *slot_by_dev(struct blkio_dev *dev) {
blk_dev_slot_t *s = g.head;
while (s) { if (s->dev == dev) return s; s = s->next; }
return NULL;
}
int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_t *out_count) {
if (!dev || !out_start_lbn || !out_count) return BLK_EINVAL;
blk_dev_slot_t *slot = slot_by_dev(dev);
if (!slot) return BLK_ENODEV;
*out_start_lbn = slot->start_lbn;
*out_count = slot->user_blocks;
return BLK_OK;
}
int blk_get_device_free_blocks(struct blkio_dev *dev, uint64_t *out_free, uint64_t *out_total) {
if (!dev || !out_free || !out_total) return BLK_EINVAL;
blk_dev_slot_t *slot = slot_by_dev(dev);
if (!slot) return BLK_ENODEV;
if (slot->raw_base) { *out_free = 0; *out_total = 0; return BLK_OK; }
*out_free = slot->vol_meta.free_blocks;
*out_total = slot->vol_meta.total_blocks;
return BLK_OK;
}
int blk_get_first_disk_range(uint32_t *out_start_lbn, uint32_t *out_count) {
if (!out_start_lbn || !out_count) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
*out_start_lbn = slot->start_lbn;
*out_count = slot->user_blocks;
return BLK_OK;
}
/*
* Top-of-device system-metadata fence I/O (Phase 8, 2026-08-26).
* Raw, unpacked 4 KiB devblocks -- no Forth-block packing, same shape as
@@ -1317,6 +1355,166 @@ int blk_flags_set(uint32_t block_num, uint64_t flags) {
return blk_set_meta(block_num, &meta);
}
/* FABRIC-3.md §F.11/§I.2, built 2026-09-04. is_lbn_zero() -- BLK_FLAG_
* CLAIMED clear AND owner_fp all-zero is "unowned"; either alone would
* misclassify a claimed-but-not-yet-fp-stamped or fp-stamped-but-
* evicted devblock, neither of which should exist given owner_fp is
* always stamped/cleared alongside the flag below, but checking both is
* one extra memcmp for a real safety margin, not paranoia without cost. */
static int devblock_is_free(const blk_meta_t *m) {
static const uint8_t zero_fp[8] = {0};
return !(m->flags & BLK_FLAG_CLAIMED) && memcmp(m->owner_fp, zero_fp, 8) == 0;
}
/* Sane upper bound on one claim, independent of how large count is --
* same "generous headroom, not a real constraint" reasoning as
* capsule_wirebind.c's WIREBIND_CERT_MAX_DEVBLOCKS: a stack-allocated
* scratch array of found-devblock LBNs needs a fixed bound, and no
* caller of this session's own scope needs more than a handful of
* devblocks in one claim. */
#define BLK_FIRSTTOUCH_MAX_CLAIM 256u
int blk_firsttouch_claim(const uint8_t owner_fp[8], uint32_t count, uint32_t *out_chain_head) {
if (!owner_fp || !out_chain_head || count == 0) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
/* One linear scan, full range, no cached index (§F.11 decision 2).
* Walks every LBN in the slot's own range but only *acts* on the one
* representative LBN per devblock (slot_pbn_pack_offset() == 0) --
* blk_get_meta()/blk_set_meta() already resolve any of a devblock's
* BLK_PACK_RATIO member LBNs to the same shared blk_meta_t, so
* visiting the others would just re-read the identical struct. */
uint32_t found[BLK_FIRSTTOUCH_MAX_CLAIM];
uint32_t nfound = 0;
if (count > BLK_FIRSTTOUCH_MAX_CLAIM) return BLK_ENOSPC;
for (uint32_t lbn = slot->start_lbn;
lbn < slot->start_lbn + slot->user_blocks && nfound < count;
lbn++) {
uint32_t rel_pbn = lbn_to_slot_pbn(slot, lbn);
if (slot_pbn_pack_offset(rel_pbn) != 0) continue; /* not this devblock's representative LBN */
blk_meta_t meta;
if (blk_get_meta(lbn, &meta) != BLK_OK) continue;
if (devblock_is_free(&meta)) found[nfound++] = lbn;
}
if (nfound < count) return BLK_ENOSPC; /* fail outright, no partial claim (§F.11 decision 3) */
/* Link the scattered chain and stamp owner_fp onto every member
* (§F.11 decision 1) -- not just the head, so ownership reads
* locally from any member without walking the chain. */
for (uint32_t i = 0; i < nfound; i++) {
blk_meta_t meta;
if (blk_get_meta(found[i], &meta) != BLK_OK) return BLK_EIO;
memcpy(meta.owner_fp, owner_fp, 8);
meta.flags |= BLK_FLAG_CLAIMED;
meta.prev_block = (i == 0) ? 0 : found[i - 1];
meta.next_block = (i + 1 == nfound) ? 0 : found[i + 1];
meta.chain_length = nfound;
if (blk_set_meta(found[i], &meta) != BLK_OK) return BLK_EIO;
}
*out_chain_head = found[0];
return BLK_OK;
}
int blk_meta_relocate_devblock(uint32_t home_devblock, uint32_t target_devblock) {
if (home_devblock == target_devblock) return BLK_EINVAL;
blk_meta_t home_meta;
int rc = blk_get_meta(home_devblock, &home_meta);
if (rc != BLK_OK) return rc;
home_meta.flags |= BLK_FLAG_MIGRATING;
if (blk_set_meta(home_devblock, &home_meta) != BLK_OK) return BLK_EIO;
/* Devblock granularity is BLK_PACK_RATIO FORTH blocks -- move each
* one via the existing, only, FORTH-block-granularity relocation
* primitive. NOT atomic across this loop -- see this function's own
* doc comment (block_subsystem.h) for what a mid-loop failure leaves
* behind (BLK_FLAG_MIGRATING still set, ownership not yet
* transferred -- a real, documented limitation, not silently
* dropped). */
for (uint32_t i = 0; i < BLK_PACK_RATIO; i++) {
rc = blk_subsys_relocate_block(home_devblock + i, target_devblock + i);
if (rc != BLK_OK) return rc;
}
blk_meta_t target_meta;
rc = blk_get_meta(target_devblock, &target_meta);
if (rc != BLK_OK) return rc;
memcpy(target_meta.owner_fp, home_meta.owner_fp, sizeof(target_meta.owner_fp));
target_meta.acl_allow = home_meta.acl_allow;
target_meta.acl_ttl = home_meta.acl_ttl;
target_meta.flags |= BLK_FLAG_CLAIMED;
if (blk_set_meta(target_devblock, &target_meta) != BLK_OK) return BLK_EIO;
memset(home_meta.owner_fp, 0, sizeof(home_meta.owner_fp));
home_meta.acl_allow = 0;
home_meta.acl_ttl = 0;
home_meta.flags &= ~(BLK_FLAG_CLAIMED | BLK_FLAG_MIGRATING);
return blk_set_meta(home_devblock, &home_meta);
}
/* FABRIC-3.md §I.2, 2026-09-04: heat/wear-leveling migration trigger.
* Uses blk_meta_t.write_count -- already present, already documented
* for exactly this purpose ("Number of writes (wear leveling)"), no new
* cross-subsystem query needed (Stadium's own compudynamics block heat,
* stadium_blocks.c, has no per-LBN getter exposed and is a different
* granularity/subsystem -- the wrong tool here, not reused). Runs one
* linear scan of Artemis's own device (first_disk_slot()) per call,
* same discovery discipline as blk_firsttouch_claim(); a resident
* devblock whose write_count crosses MIGRATION_WEAR_THRESHOLD and isn't
* already MIGRATING gets relocated to the first free devblock found in
* the same scan. Fixed threshold, not yet DoE-measured or Kconfig-tuned
* -- same "fixed first, adaptive later" sequencing this project already
* uses elsewhere (e.g. capsule_zuse_boot.c's ZUSE_SESSION_TTL_SECONDS).
*
* Overflow-triggered migration (a specific *user's* device running low
* on space) is deliberately NOT built here -- it needs a slot-lookup-by-
* device-pointer call site threaded from wherever the currently-attached
* user identity's own blkio_dev is known (WIREBIND, capsule_wirebind.c),
* not decided in this pass. blk_firsttouch_claim()/blk_meta_relocate_
* devblock() are already the mechanism it would call -- only the
* trigger-detection call site is the remaining gap. */
#define MIGRATION_WEAR_THRESHOLD 10000u
void blk_migration_idle_check(void) {
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return;
uint32_t hot_lbn = 0, free_lbn = 0;
int have_hot = 0, have_free = 0;
for (uint32_t lbn = slot->start_lbn;
lbn < slot->start_lbn + slot->user_blocks && !(have_hot && have_free);
lbn++) {
uint32_t rel_pbn = lbn_to_slot_pbn(slot, lbn);
if (slot_pbn_pack_offset(rel_pbn) != 0) continue;
blk_meta_t meta;
if (blk_get_meta(lbn, &meta) != BLK_OK) continue;
if (!have_hot && (meta.flags & BLK_FLAG_CLAIMED) &&
!(meta.flags & BLK_FLAG_MIGRATING) &&
meta.write_count >= MIGRATION_WEAR_THRESHOLD) {
hot_lbn = lbn;
have_hot = 1;
}
if (!have_free && devblock_is_free(&meta)) {
free_lbn = lbn;
have_free = 1;
}
}
if (have_hot && have_free && hot_lbn != free_lbn) {
(void) blk_meta_relocate_devblock(hot_lbn, free_lbn);
}
}
/* ===== weak hook (for main.c) ===== */
#if defined(__GNUC__) || defined(__clang__)
__attribute__((weak))
+10
View File
@@ -325,6 +325,16 @@ static void sk_repl_idle(VM *active_vm)
* did not survive a reboot until this fix. */
blk_vm_flush_all(active_vm);
/* FABRIC-3.md §I.2, built 2026-09-04: heat/wear-leveling migration
* trigger -- one linear scan of Artemis's own device per idle tick
* (same ~1 Hz SK_IDLE_BEAT_INTERVAL cadence this whole function
* already runs at, chosen so a hot devblock is caught proactively
* rather than only on a failed write). See block_subsystem.c's own
* doc comment on blk_migration_idle_check() for what's built (heat-
* based relocation) vs. deliberately left open (overflow-triggered
* migration, needs a call site threaded from WIREBIND). */
blk_migration_idle_check();
/* FABRIC-3.md Phase C (2026-08-28): distributed messaging pump. Every
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
* MSG-TICK word (see capsules/common/messaging.4th) instead of only