Add per-slot CRC + campaign crash-resume (DOE-RESUME-COUNT)
FABRIC-3.md §XXXV.12: closes two real gaps found by walking the disk-to- PDF pipeline end to end. (1) doe_trial_slot_t had no integrity check of its own -- 64 slots share one devblock via a read-modify-write, so a crash mid-write to any slot could silently corrupt its siblings with nothing to catch it. Added an 8-byte CRC (same discipline the control header already had), computed/verified the same way. (2) The campaign itself had no crash-resume -- a reboot always restarted the whole 18-trial loop from index 0 even with good persisted data already on disk. Since the trial matrix is fully deterministic from (seed, n-reps), resuming needs no persisted "which trials ran" state, just a count: DOE-RESUME-COUNT (new kernel primitive, same pattern as DOE-PERSIST-TRIAL) walks the ring counting CRC-valid records for a given ISA; MU-EXEC-CAMPAIGN's signature changes to accept a start-idx and seeds MU-RUN-ID/the DO loop from it. Each RUN-*-CAMPAIGN entry word now calls DOE-RESUME-COUNT before launching. Both block-namespace-checked to fit existing headroom before committing to the design -- no new blocks needed. Real bug caught by the build, not shipped: an implicit compiler-inserted alignment byte before slot_crc silently broke the hand-computed trailing pad size (doe_trial_slot_size_check failed to compile). Fixed with an explicit alignment field, matching log_slot_t's own zero-implicit- padding discipline. extract_doe_region.py now verifies both CRCs with the REAL CRC-64/XZ algorithm from block_subsystem.c's compute_crc64() (ported and verified bit-for-bit against a native C reference this session), replacing §XXXV.11's incorrect guessed implementation. Verified: clean compile on all 3 ISAs (amd64/aarch64/riscv64 -- amd64 verified via isolated build only, not boot, since the real per-ISA campaign is currently live on the only QEMU instance this project's own rule allows), both capsules pass mkcapsule --lint, CRC64 bit-exact via native reference. Live boot-verification explicitly deferred until that campaign finishes or is interrupted -- not claimed as tested when it wasn't. Found (not missed) a real compatibility consequence: the running campaign's data is in the old pre-CRC format, so it reads as "corrupt" under the new extractor and DOE-RESUME-COUNT will resume it from 0, not mid-campaign -- no data loss, cheap redundant re-run of ~1-2 trials. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c535252907
commit
51940f49d0
@@ -42,6 +42,17 @@ static uint64_t doe_region_ctrl_compute_crc(const doe_region_ctrl_t *ctrl) {
|
||||
return compute_crc64((const uint8_t *)ctrl, crc_span);
|
||||
}
|
||||
|
||||
/* §XXXV.12: a devblock holds DOE_SLOTS_PER_DEVBLOCK slots via one
|
||||
* read-modify-write, so a crash mid-write can corrupt siblings of the
|
||||
* slot actually being written. This is how a reader (doe_region_
|
||||
* count_for_isa(), and any future host-side extractor) tells a genuine
|
||||
* record from that corruption -- same offsetof-span discipline as the
|
||||
* control header's own CRC above. */
|
||||
static uint64_t doe_region_slot_compute_crc(const doe_trial_slot_t *slot) {
|
||||
size_t crc_span = offsetof(doe_trial_slot_t, slot_crc);
|
||||
return compute_crc64((const uint8_t *)slot, crc_span);
|
||||
}
|
||||
|
||||
/* Reads the control header; returns 0 and *out populated only if magic,
|
||||
* version, and CRC all check out. Any other result (blank, foreign,
|
||||
* corrupt, or a read failure) is treated identically by the caller: "not
|
||||
@@ -94,6 +105,20 @@ static int doe_region_write_slot(uint32_t slot_index, const doe_trial_slot_t *sl
|
||||
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Returns 0 and *out populated only if the slot's own CRC checks out --
|
||||
* a corrupted sibling slot (§XXXV.12's own devblock-sharing risk) reads
|
||||
* back as a failure here, not as a plausible-looking garbage record. */
|
||||
static int doe_region_read_slot(uint32_t slot_index, doe_trial_slot_t *out) {
|
||||
uint8_t devblock_buf[4096];
|
||||
uint32_t dft = doe_region_slot_devblock_from_top(slot_index);
|
||||
uint32_t slot_in_devblock = slot_index % DOE_SLOTS_PER_DEVBLOCK;
|
||||
|
||||
if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1;
|
||||
memcpy(out, devblock_buf + (size_t)slot_in_devblock * DOE_SLOT_SIZE, sizeof(*out));
|
||||
if (doe_region_slot_compute_crc(out) != out->slot_crc) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
|
||||
uint32_t mode, uint32_t fail_count,
|
||||
uint64_t fleet_k_q48, uint32_t fleet_conserved,
|
||||
@@ -139,6 +164,8 @@ int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
|
||||
if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX;
|
||||
if (isa && isa_n) memcpy(slot.isa, isa, isa_n);
|
||||
|
||||
slot.slot_crc = doe_region_slot_compute_crc(&slot);
|
||||
|
||||
if (doe_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1;
|
||||
|
||||
ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots;
|
||||
@@ -146,3 +173,32 @@ int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
|
||||
|
||||
return doe_region_ctrl_write(&ctrl) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
uint32_t doe_region_count_for_isa(const char *isa, uint32_t isa_len) {
|
||||
doe_region_ctrl_t ctrl;
|
||||
if (doe_region_ctrl_read(&ctrl) != 0) return 0;
|
||||
|
||||
/* Build the full DOE_SLOT_ISA_MAX-byte zero-padded tag exactly as
|
||||
* doe_region_append() stores it, so this compares the whole field
|
||||
* (including trailing zero padding) -- not just a prefix, which would
|
||||
* wrongly match e.g. "amd" against a stored "amd64". */
|
||||
char want[DOE_SLOT_ISA_MAX];
|
||||
memset(want, 0, sizeof(want));
|
||||
uint32_t isa_n = isa_len;
|
||||
if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX;
|
||||
if (isa && isa_n) memcpy(want, isa, isa_n);
|
||||
|
||||
uint32_t total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
|
||||
uint32_t count = 0;
|
||||
uint32_t idx = ctrl.head_slot;
|
||||
for (uint32_t i = 0; i < ctrl.record_count; i++) {
|
||||
doe_trial_slot_t slot;
|
||||
if (doe_region_read_slot(idx, &slot) == 0) {
|
||||
if (memcmp(slot.isa, want, DOE_SLOT_ISA_MAX) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
idx = (idx + 1u) % total_slots;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -352,6 +352,39 @@ static void log_word_doe_persist_trial(VM *vm)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── DOE-RESUME-COUNT ( isa-addr isa-u -- n ) ── */
|
||||
/*
|
||||
* Kernel-only. §XXXV.12 campaign crash-resume: counts CRC-valid, live
|
||||
* records in Artemis's DoE ring matching the given ISA tag. Called by
|
||||
* each RUN-*-CAMPAIGN entry word (per-isa-doe.4th) before MU-EXEC-
|
||||
* CAMPAIGN, so a re-boot after a crash resumes at the first trial that
|
||||
* doesn't already have a good persisted record, instead of restarting
|
||||
* the whole campaign from trial 0.
|
||||
*/
|
||||
#ifdef __STARKERNEL__
|
||||
static void log_word_doe_resume_count(VM *vm)
|
||||
{
|
||||
if (vm->dsp < 1) {
|
||||
console_println("DOE-RESUME-COUNT: stack underflow");
|
||||
vm->error = 1;
|
||||
return;
|
||||
}
|
||||
cell_t isa_u = vm_pop(vm);
|
||||
cell_t isa_addr = vm_pop(vm);
|
||||
|
||||
if (isa_u < 0 || isa_addr < 0 || (isa_addr + isa_u) > (cell_t)VM_MEMORY_SIZE) {
|
||||
console_println("DOE-RESUME-COUNT: isa string address out of range");
|
||||
vm->error = 1;
|
||||
vm_push(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *isa = (const char *)&vm->memory[isa_addr];
|
||||
uint32_t n = doe_region_count_for_isa(isa, (uint32_t)isa_u);
|
||||
vm_push(vm, (cell_t)n);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── Registration ────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
@@ -364,7 +397,7 @@ static void log_word_doe_persist_trial(VM *vm)
|
||||
* String literals: LOG-ERROR" LOG-WARN" LOG-INFO" LOG-TEST" LOG-DEBUG"
|
||||
* Stack string: LOG-ERROR-STR LOG-WARN-STR LOG-INFO-STR
|
||||
* LOG-TEST-STR LOG-DEBUG-STR
|
||||
* Kernel-only: (LOG-APPEND-RAW) DOE-PERSIST-TRIAL
|
||||
* Kernel-only: (LOG-APPEND-RAW) DOE-PERSIST-TRIAL DOE-RESUME-COUNT
|
||||
*/
|
||||
void register_log_words(VM *vm)
|
||||
{
|
||||
@@ -402,5 +435,6 @@ void register_log_words(VM *vm)
|
||||
#ifdef __STARKERNEL__
|
||||
register_word(vm, "(LOG-APPEND-RAW)", log_word_append_raw);
|
||||
register_word(vm, "DOE-PERSIST-TRIAL", log_word_doe_persist_trial);
|
||||
register_word(vm, "DOE-RESUME-COUNT", log_word_doe_resume_count);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user