Fix silent disk overwrite of unrecognized Artemis disks

The generic block subsystem (blk_format_or_load_disk) auto-reformatted
any disk lacking its own low-level 'STFR' header at attach time, before
Artemis's Forth-level BLANK/LithosAnanke/Unrecognized classification
ever ran -- so ART-HALT-UNRECOG's "Disk preserved" message was false.

Split detection from commit: an unrecognized/blank disk is now left
PROVISIONAL (geometry computed in memory only, all writes refused)
until explicitly confirmed via the new blk_subsys_confirm_format() /
BLK-CONFIRM-FORMAT primitive. Artemis calls it from ART-FORMAT and
ART-RESUME, never from ART-HALT-UNRECOG.

Verified on amd64/aarch64/riscv64: parity intact (identical dict_hash),
normal recognized-disk resume + persist-read unaffected, and a
regenerated disk/artemis-unrecognized-test.img (the old copy had itself
been silently corrupted by this exact bug) now stays byte-for-byte
identical across a halted boot on amd64 and riscv64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-02 11:44:23 -04:00
co-authored by Claude Sonnet 5
parent cc6c8c43f3
commit 148c4aa12c
17 changed files with 1373064 additions and 4622 deletions
+84 -33
View File
@@ -132,6 +132,7 @@ struct blk_dev_slot {
blk_volume_meta_t vol_meta;
uint8_t vol_meta_dirty;
uint8_t bam_dirty; /* any bam entry dirty → needs BAM flush to disk */
uint8_t format_state; /* BLK_FMT_FORMATTED or BLK_FMT_PROVISIONAL */
cache_slot_t cache[DISK_CACHE_SLOTS];
/* BAM: one entry per user block, heap-allocated at attach time */
@@ -397,32 +398,11 @@ static int bam_flush_to_disk(blk_dev_slot_t *slot) {
/* ===== volume format / load (per disk slot) ===== */
static int blk_format_or_load_disk(blk_dev_slot_t *slot) {
uint8_t hdr[BLK_DEVICE_SECTOR] = {0};
(void) read_header_4k(slot, hdr);
volmeta_from_buf(&slot->vol_meta, hdr);
if (slot->vol_meta.magic == 0x53544652u && slot->vol_meta.version == 2) {
slot->vol_meta.total_devblocks = (uint64_t) udiv_floor(slot->total_blkio_blocks_1k, 4);
if (slot->vol_meta.bam_devblocks == 0) goto fresh_format;
uint64_t disk_user = (slot->vol_meta.total_blocks > BLK_DISK_SYS_RESERVED)
? (slot->vol_meta.total_blocks - BLK_DISK_SYS_RESERVED) : 0;
slot->user_blocks = (disk_user > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t) disk_user;
slot->bam = (blk_bam_entry_t *) calloc(slot->user_blocks, sizeof(blk_bam_entry_t));
if (!slot->bam) return BLK_ENOMEM;
slot->devblock_base_4k = slot->vol_meta.devblock_base;
return bam_sync_from_disk(slot);
}
if (slot->vol_meta.magic != 0 || slot->vol_meta.version != 0) {
log_message(LOG_WARN, "blk: unrecognised disk header (magic=0x%08x ver=%u) — fresh format",
slot->vol_meta.magic, slot->vol_meta.version);
}
fresh_format:
/* Compute fresh volume geometry in memory only. Pure function of device
* size — never touches the disk. Used both for the provisional in-memory
* state (detection) and, unchanged, as the values ultimately committed by
* blk_commit_format(). */
static void blk_compute_fresh_geometry(blk_dev_slot_t *slot) {
memset(&slot->vol_meta, 0, sizeof(slot->vol_meta));
slot->vol_meta.magic = 0x53544652u;
slot->vol_meta.version = 2;
@@ -444,24 +424,73 @@ fresh_format:
slot->user_blocks = (disk_user > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t) disk_user;
slot->devblock_base_4k = slot->vol_meta.devblock_base;
slot->bam = (blk_bam_entry_t *) calloc(slot->user_blocks, sizeof(blk_bam_entry_t));
if (!slot->bam) return BLK_ENOMEM;
slot->vol_meta.free_blocks = slot->vol_meta.total_blocks > BLK_DISK_SYS_RESERVED
? slot->vol_meta.total_blocks - BLK_DISK_SYS_RESERVED : 0;
}
/* Actually write the fresh format to disk (zero BAM pages, write header).
* Only reachable via blk_subsys_confirm_format() — never automatically. */
static int blk_commit_format(blk_dev_slot_t *slot) {
if (!slot->dev) return BLK_EINVAL;
if (slot->format_state == BLK_FMT_FORMATTED) return BLK_OK; /* already committed */
/* zero BAM pages on disk, then write header */
uint8_t z[1024] = {0};
for (uint32_t i = 0; i < slot->vol_meta.bam_devblocks; i++) {
uint32_t base1k = (slot->vol_meta.bam_start + i) * 4u;
for (uint32_t k = 0; k < 4; k++) (void) blkio_write(slot->dev, base1k + k, z);
}
uint8_t hdr[BLK_DEVICE_SECTOR];
volmeta_to_buf(&slot->vol_meta, hdr);
(void) write_header_4k(slot, hdr);
/* flush with reserved bits marked */
slot->bam_dirty = 1;
(void) bam_flush_to_disk(slot);
blkio_flush(slot->dev);
slot->format_state = BLK_FMT_FORMATTED;
return BLK_OK;
}
/* Detect the disk's low-level container state. NEVER writes to disk here —
* on anything other than a recognized STFR/v2 header, the slot is left
* PROVISIONAL with geometry computed in memory only, and all writes to it
* are refused (see blk_get_buffer/blk_update) until the disk's owner
* explicitly calls blk_subsys_confirm_format(). This is what lets a
* higher-level "unrecognized disk, halt" decision actually mean the disk
* was left untouched. */
static int blk_format_or_load_disk(blk_dev_slot_t *slot) {
uint8_t hdr[BLK_DEVICE_SECTOR] = {0};
(void) read_header_4k(slot, hdr);
volmeta_from_buf(&slot->vol_meta, hdr);
if (slot->vol_meta.magic == 0x53544652u && slot->vol_meta.version == 2 &&
slot->vol_meta.bam_devblocks != 0) {
slot->vol_meta.total_devblocks = (uint64_t) udiv_floor(slot->total_blkio_blocks_1k, 4);
uint64_t disk_user = (slot->vol_meta.total_blocks > BLK_DISK_SYS_RESERVED)
? (slot->vol_meta.total_blocks - BLK_DISK_SYS_RESERVED) : 0;
slot->user_blocks = (disk_user > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t) disk_user;
slot->bam = (blk_bam_entry_t *) calloc(slot->user_blocks, sizeof(blk_bam_entry_t));
if (!slot->bam) return BLK_ENOMEM;
slot->devblock_base_4k = slot->vol_meta.devblock_base;
slot->format_state = BLK_FMT_FORMATTED;
return bam_sync_from_disk(slot);
}
if (slot->vol_meta.magic != 0 || slot->vol_meta.version != 0) {
log_message(LOG_WARN, "blk: unrecognised disk header (magic=0x%08x ver=%u) — "
"deferring format decision to disk owner, disk untouched",
slot->vol_meta.magic, slot->vol_meta.version);
}
blk_compute_fresh_geometry(slot);
slot->bam = (blk_bam_entry_t *) calloc(slot->user_blocks, sizeof(blk_bam_entry_t));
if (!slot->bam) return BLK_ENOMEM;
slot->format_state = BLK_FMT_PROVISIONAL;
return BLK_OK;
}
@@ -527,7 +556,9 @@ int blk_subsys_attach_device(struct blkio_dev *dev) {
if (rc != BLK_OK) { if (slot->bam) free(slot->bam); free(slot); return rc; }
slot->vol_meta.mounted_time = blk_get_timestamp();
slot->vol_meta_dirty = 1;
/* Don't dirty a PROVISIONAL slot's header — nothing may be written to
* disk until the owner explicitly confirms the format. */
if (slot->format_state == BLK_FMT_FORMATTED) slot->vol_meta_dirty = 1;
chain_append(slot);
g.total_user_lbn += slot->user_blocks;
@@ -553,7 +584,10 @@ int blk_subsys_shutdown(void) {
blk_dev_slot_t *s = g.head;
while (s) {
if (s->dev) {
if (s->vol_meta_dirty) {
/* Never persist anything for a slot still awaiting an explicit
* format decision from its owner — defensive; should already
* be unreachable since nothing can dirty a PROVISIONAL slot. */
if (s->vol_meta_dirty && s->format_state == BLK_FMT_FORMATTED) {
uint8_t hdr[BLK_DEVICE_SECTOR];
volmeta_to_buf(&s->vol_meta, hdr);
(void) write_header_4k(s, hdr);
@@ -571,6 +605,18 @@ int blk_subsys_shutdown(void) {
return BLK_OK;
}
/* Commit the low-level format for the slot owning lbn. Called by the
* disk's higher-level owner once it has classified the disk content and
* decided it is safe to touch (e.g. Artemis's ART-BOOT-DETECT resolving
* to BLANK or a recognized marker). Must NOT be called on the path that
* halts for unrecognized content — that is what keeps the disk untouched. */
int blk_subsys_confirm_format(uint32_t lbn) {
if (!g.initialized) return BLK_ENODEV;
blk_dev_slot_t *slot = lbn_to_slot(lbn);
if (!slot || !slot->dev) return BLK_ERANGE;
return blk_commit_format(slot);
}
/* ===== block buffer access ===== */
uint8_t *blk_get_buffer(uint32_t block_num, int writable) {
@@ -599,6 +645,7 @@ uint8_t *blk_get_buffer(uint32_t block_num, int writable) {
}
/* disk */
if (writable && slot->format_state == BLK_FMT_PROVISIONAL) return NULL;
uint32_t rel_pbn = lbn_to_slot_pbn(slot, block_num);
uint32_t dev4k = slot_pbn_to_devblock(rel_pbn);
uint32_t pack = slot_pbn_pack_offset(rel_pbn);
@@ -627,6 +674,7 @@ int blk_update(uint32_t block_num) {
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
if (slot->format_state == BLK_FMT_PROVISIONAL) return BLK_ERESERVED;
uint32_t offset = block_num - slot->start_lbn;
slot->bam[offset].allocated = 1;
@@ -728,6 +776,7 @@ int blk_mark_allocated(uint32_t block_num) {
if (block_num < g.ram_user) return BLK_EINVAL;
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
if (slot->format_state == BLK_FMT_PROVISIONAL) return BLK_ERESERVED;
uint32_t offset = block_num - slot->start_lbn;
if (!slot->bam[offset].allocated) {
slot->bam[offset].allocated = 1;
@@ -745,6 +794,7 @@ int blk_mark_free(uint32_t block_num) {
if (block_num < g.ram_user) return BLK_EINVAL;
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
if (slot->format_state == BLK_FMT_PROVISIONAL) return BLK_ERESERVED;
uint32_t offset = block_num - slot->start_lbn;
if (slot->bam[offset].allocated) {
slot->bam[offset].allocated = 0;
@@ -765,7 +815,8 @@ int blk_allocate(uint32_t *block_num) {
blk_dev_slot_t *s = g.head;
while (s) {
if (s->raw_base || !s->dev || s->vol_meta.free_blocks == 0) { s = s->next; continue; }
if (s->raw_base || !s->dev || s->format_state == BLK_FMT_PROVISIONAL ||
s->vol_meta.free_blocks == 0) { s = s->next; continue; }
uint32_t limit = s->user_blocks;
uint32_t hint = (s->vol_meta.first_free > s->start_lbn)
+12
View File
@@ -279,6 +279,17 @@ void block_word_update(VM *vm) {
blk_update((uint32_t) blk);
}
/* BLK-CONFIRM-FORMAT ( lbn -- ) : commit the low-level disk container
* format for the slot owning lbn. Must be called by the disk's owner
* (e.g. Artemis) only after classifying disk content as safe to touch —
* never on the path that halts for unrecognized content. Until this is
* called, all writes to that disk slot are refused by the block layer. */
void block_word_confirm_format(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk_subsys_confirm_format((uint32_t) blk) != BLK_OK) { vm->error = 1; return; }
}
/* SAVE-BUFFERS ( -- ) : sync and write all dirty blocks */
void block_word_save_buffers(VM *vm) {
blk_vm_flush_all(vm);
@@ -457,6 +468,7 @@ void register_block_words(VM *vm) {
register_word(vm, "BLOCK", block_word_block);
register_word(vm, "BUFFER", block_word_buffer);
register_word(vm, "UPDATE", block_word_update);
register_word(vm, "BLK-CONFIRM-FORMAT", block_word_confirm_format);
register_word(vm, "SAVE-BUFFERS", block_word_save_buffers);
register_word(vm, "EMPTY-BUFFERS", block_word_empty_buffers);
register_word(vm, "FLUSH", block_word_flush);