Files
LithosAnanake/src/block_subsystem.c
T
Robert Allan JamesandClaude Sonnet 5 b031b802e3 Rename FABRIC series: FABRIC.md->0, FABRIC-2.md->1, FABRIC-3.md->2, FABRIC-4.md unchanged
FABRIC.md -> FABRIC-0.md
FABRIC-2.md -> FABRIC-1.md
FABRIC-3.md -> FABRIC-2.md (the current/living document)
FABRIC-4.md unchanged (new #3 to follow separately)

Every cross-reference repo-wide updated to match, including doc-comment
citations inside kernel source (.c/.h) files -- done via an ordered
placeholder substitution (FABRIC-3.md->placeholder2, FABRIC-2.md->
placeholder1, FABRIC.md->placeholder0, then placeholders resolved to
final names) in a single pass per file to avoid double-shifting
already-renamed references.

One line in capsules/font.4th grew past the 64-char block-format limit
as a side effect of the longer filename; shortened it and reverified
with mkcapsule --lint (34/34 pass) before rebuilding.

Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the
foreground) after the fix; 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
2026-09-04 11:22:51 -04:00

1526 lines
59 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
Block Subsystem v2 — Unified LBN Device Chain
-----------------------------------------------
Architecture (unified block address space):
- LBN 0..2047: FAST RAM (volatile, g.ram_base)
- LBN 2048..x: RAMDRIVE (raw RAM buffer, volatile; first attached device)
- LBN x..y: DISK IMG (virtio-blk, persistent; second attached device)
- LBN y+: USB / future devices (chained, including hot-attach/detach)
Devices are registered via blk_subsys_add_raw_device() (volatile RAM buffer)
or blk_subsys_attach_device() (formatted disk). Each allocates a
blk_dev_slot_t node on the heap and appends it to g.head linked list.
BAM design:
- Each device slot owns a heap-allocated blk_bam_entry_t[] array, one
entry per user block. No global shared bitset.
- blk_dev_slot_t is the kernel/Artemis decoupling boundary:
the kernel hands a blkio_dev* to blk_subsys_attach_device(); Artemis
owns everything below that call.
blkio NOTE:
- blkio backends operate on 1 KiB units.
- One 4 KiB "devblock" == 4 consecutive 1 KiB blkio blocks.
License: See LICENSE file. No warranty.
*/
#include "../include/blkio.h"
#include "../include/block_subsystem.h"
#include "../include/log.h"
#include "../include/platform_time.h"
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
/* ===== compile-time config ===== */
#ifndef BLK_FORTH_SYS_RESERVED
# define BLK_FORTH_SYS_RESERVED 32u
#endif
#ifndef BLK_DISK_SYS_RESERVED
# define BLK_DISK_SYS_RESERVED 32u
#endif
#define META_REGION_OFFSET (BLK_PACK_RATIO * BLK_FORTH_SIZE)
#define META_REGION_SIZE (BLK_DEVICE_SECTOR - META_REGION_OFFSET)
#define META_PER_BLOCK (META_REGION_SIZE / BLK_PACK_RATIO)
#define DISK_CACHE_SLOTS 8
/* Single-block relocation (Milestone 2h+). One reserved 4 KiB devblock:
* a 4-byte count prefix plus up to BLK_RELOC_MAX_ENTRIES 8-byte
* {home_lbn, actual_lbn} pairs (500*8+4 = 4004 <= 4096). Relocations are
* expected rare, not routine -- see FABRIC-1.md's own design writeup --
* so a linear-scanned fixed array is deliberate, matching this file's
* existing tolerance for small bounded scans (DISK_CACHE_SLOTS above,
* BLK_VM_SLOTS in block_words.c are the same shape). */
#define BLK_RELOC_MAX_ENTRIES 500u
typedef struct {
uint32_t home_lbn;
uint32_t actual_lbn;
} blk_reloc_entry_t;
static inline size_t minzu(size_t a, size_t b) { return a < b ? a : b; }
static inline uint32_t udiv_floor(uint32_t a, uint32_t b) { return a / b; }
/* ===== CRC64-ISO ===== */
static uint64_t crc64_table[256];
static int crc64_inited = 0;
static void crc64_init(void) {
if (crc64_inited) return;
const uint64_t poly = 0x42F0E1EBA9EA3693ULL;
for (int i = 0; i < 256; ++i) {
uint64_t crc = (uint64_t) i;
for (int j = 0; j < 8; ++j)
crc = (crc & 1) ? ((crc >> 1) ^ poly) : (crc >> 1);
crc64_table[i] = crc;
}
crc64_inited = 1;
}
uint64_t compute_crc64(const uint8_t *data, size_t len) {
if (!crc64_inited) crc64_init();
uint64_t crc = 0xFFFFFFFFFFFFFFFFULL;
for (size_t i = 0; i < len; i++) {
uint8_t idx = (uint8_t)(crc ^ data[i]);
crc = crc64_table[idx] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFFFFFFFFFULL;
}
/* ===== physical BAM bitset helpers (temporary buffers only) ===== */
static inline int pbam_test(const uint8_t *bm, uint32_t i) { return (bm[i>>3] >> (i&7)) & 1; }
static inline void pbam_set (uint8_t *bm, uint32_t i) { bm[i>>3] |= (uint8_t)(1u << (i&7)); }
static inline void pbam_clr (uint8_t *bm, uint32_t i) { bm[i>>3] &= (uint8_t)~(1u << (i&7)); }
/* ===== cache slot ===== */
typedef struct {
uint32_t devblock;
uint8_t data[BLK_DEVICE_SECTOR];
blk_meta_t meta[BLK_PACK_RATIO];
uint8_t valid;
uint8_t loaded;
uint8_t dirty;
uint8_t meta_dirty;
} cache_slot_t;
/* ===== device slot (linked list node — kernel/Artemis decoupling boundary) ===== */
typedef struct blk_dev_slot blk_dev_slot_t;
struct blk_dev_slot {
uint32_t start_lbn; /* first LBN served by this slot */
uint32_t user_blocks; /* count of LBNs served */
/* raw (ramdrive) path — raw_base != NULL, dev == NULL */
uint8_t *raw_base;
/* disk path — dev != NULL, raw_base == NULL */
struct blkio_dev *dev;
uint32_t total_blkio_blocks_1k;
uint32_t devblock_base_4k;
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 */
blk_bam_entry_t *bam;
blk_dev_slot_t *next;
};
/* ===== global state ===== */
static struct {
VM *vm;
uint8_t *ram_base;
size_t ram_size;
uint8_t dirty_ram[BLK_RAM_BLOCKS];
uint32_t ram_user; /* user-visible RAM LBNs = BLK_RAM_BLOCKS - BLK_FORTH_SYS_RESERVED */
uint64_t total_user_lbn; /* total user-visible LBNs across RAM + all device slots */
blk_dev_slot_t *head; /* linked list of device slots */
/* Bumped on every attach/detach (Milestone 2h). Freeing a slot then
* immediately allocating a new one for a same-LBN re-attach can hand
* back the *same* heap address (confirmed live: this kernel's own
* first-fit kmalloc, src/starkernel/memory/kmalloc.c, does exactly
* this for a free() followed immediately by a same-size calloc(),
* with nothing else allocated in between) -- so a raw
* pointer comparison against a cached blk_get_buffer() result cannot
* reliably detect "this LBN's device changed underneath a caller
* holding a stale cached pointer." A monotonic epoch can't be fooled
* by address reuse the way a pointer comparison was found to be (see
* block_words.c's blk_vm_find(), the one place outside this file that
* caches a blk_get_buffer() result across calls). */
uint64_t epoch;
/* Single-block relocation exception table (Milestone 2h+, in-memory
* mirror of whichever attached disk's own on-disk copy is canonical
* -- see reloc_flush_to_disk()/reloc_load_from_disk() below).
* Subsystem-global, not per-device: an LBN can be relocated to any
* other LBN regardless of which devices happen to be involved. */
blk_reloc_entry_t reloc[BLK_RELOC_MAX_ENTRIES];
uint32_t reloc_count;
int initialized;
} g = {0};
/* Redirect lbn through the relocation table if it's been moved elsewhere.
* The single choke point every public LBN-consuming entry point below
* calls first -- see FABRIC-1.md's design writeup for why this is an
* LBN->LBN redirect rather than a new storage-allocation mechanism, and
* why it's safe for every downstream function (BAM offset math, cache
* lookup, lbn_to_slot() itself) to stay completely unaware a substitution
* happened. Linear scan -- see BLK_RELOC_MAX_ENTRIES's own doc comment
* for why that's the right tradeoff here. */
static uint32_t resolve_lbn(uint32_t lbn) {
for (uint32_t i = 0; i < g.reloc_count; i++) {
if (g.reloc[i].home_lbn == lbn) return g.reloc[i].actual_lbn;
}
return lbn;
}
/* ===== slot routing ===== */
static blk_dev_slot_t *lbn_to_slot(uint32_t lbn) {
blk_dev_slot_t *s = g.head;
while (s) {
if (lbn >= s->start_lbn && lbn < s->start_lbn + s->user_blocks)
return s;
s = s->next;
}
return NULL;
}
/* append a new slot to the tail of the device chain */
static void chain_append(blk_dev_slot_t *slot) {
if (!g.head) { g.head = slot; return; }
blk_dev_slot_t *t = g.head;
while (t->next) t = t->next;
t->next = slot;
}
/* ===== per-slot disk helpers ===== */
static inline uint32_t devblock4k_to_lba1k(blk_dev_slot_t *slot, uint32_t dev4k) {
return (slot->devblock_base_4k + dev4k) * 4u;
}
static inline uint32_t lbn_to_slot_pbn(blk_dev_slot_t *slot, uint32_t lbn) {
return BLK_DISK_SYS_RESERVED + (lbn - slot->start_lbn);
}
static inline uint32_t slot_pbn_to_devblock(uint32_t rel_pbn) { return rel_pbn / BLK_PACK_RATIO; }
static inline uint32_t slot_pbn_pack_offset(uint32_t rel_pbn) { return rel_pbn % BLK_PACK_RATIO; }
static int read_devblock_4k(blk_dev_slot_t *slot, uint32_t dev4k, uint8_t *buf4k) {
uint32_t base = devblock4k_to_lba1k(slot, dev4k);
for (uint32_t i = 0; i < 4; i++) {
uint32_t lba = base + i;
if (lba >= slot->total_blkio_blocks_1k) { memset(buf4k + i*1024u, 0, 1024u); continue; }
int rc = blkio_read(slot->dev, lba, buf4k + i*1024u);
if (rc != BLKIO_OK) memset(buf4k + i*1024u, 0, 1024u);
}
return BLK_OK;
}
static int write_devblock_4k(blk_dev_slot_t *slot, uint32_t dev4k, const uint8_t *buf4k) {
uint32_t base = devblock4k_to_lba1k(slot, dev4k);
for (uint32_t i = 0; i < 4; i++) {
uint32_t lba = base + i;
if (lba >= slot->total_blkio_blocks_1k) return BLK_EIO;
if (blkio_write(slot->dev, lba, buf4k + i*1024u) != BLKIO_OK) return BLK_EIO;
}
return BLK_OK;
}
/* ===== cache management (per slot) ===== */
static void meta_from_slice(blk_meta_t *dst, const uint8_t *slice) {
memset(dst, 0, sizeof(*dst));
memcpy(dst, slice, minzu(sizeof(*dst), (size_t) META_PER_BLOCK));
}
static void meta_to_slice(const blk_meta_t *src, uint8_t *slice) {
memset(slice, 0, META_PER_BLOCK);
memcpy(slice, src, minzu(sizeof(*src), (size_t) META_PER_BLOCK));
}
static int cache_writeback(blk_dev_slot_t *slot, cache_slot_t *s) {
if (!s || !slot->dev || !(s->dirty || s->meta_dirty)) return BLK_OK;
if (s->meta_dirty) {
uint8_t *mr = s->data + META_REGION_OFFSET;
for (uint32_t j = 0; j < BLK_PACK_RATIO; j++)
meta_to_slice(&s->meta[j], mr + j * META_PER_BLOCK);
}
int rc = write_devblock_4k(slot, s->devblock, s->data);
if (rc != BLK_OK) return rc;
s->dirty = s->meta_dirty = 0;
return BLK_OK;
}
static cache_slot_t *cache_get_slot(blk_dev_slot_t *slot, uint32_t dev4k) {
for (int i = 0; i < DISK_CACHE_SLOTS; i++)
if (slot->cache[i].valid && slot->cache[i].devblock == dev4k)
return &slot->cache[i];
for (int i = 0; i < DISK_CACHE_SLOTS; i++) {
if (!slot->cache[i].valid) {
cache_slot_t *s = &slot->cache[i];
memset(s, 0, sizeof(*s));
s->valid = 1; s->devblock = dev4k;
return s;
}
}
/* evict slot[0] FIFO */
(void) cache_writeback(slot, &slot->cache[0]);
for (int i = 0; i < DISK_CACHE_SLOTS - 1; i++) slot->cache[i] = slot->cache[i+1];
memset(&slot->cache[DISK_CACHE_SLOTS-1], 0, sizeof(cache_slot_t));
slot->cache[DISK_CACHE_SLOTS-1].valid = 1;
slot->cache[DISK_CACHE_SLOTS-1].devblock = dev4k;
return &slot->cache[DISK_CACHE_SLOTS-1];
}
static cache_slot_t *cache_load_devblock(blk_dev_slot_t *slot, uint32_t dev4k) {
cache_slot_t *s = cache_get_slot(slot, dev4k);
if (!s) return NULL;
if (!s->loaded) {
(void) read_devblock_4k(slot, dev4k, s->data);
uint8_t *mr = s->data + META_REGION_OFFSET;
for (uint32_t i = 0; i < BLK_PACK_RATIO; i++) {
meta_from_slice(&s->meta[i], mr + i * META_PER_BLOCK);
if (s->meta[i].magic != 0x424C4B5F5354524BULL) {
memset(&s->meta[i], 0, sizeof(s->meta[i]));
s->meta[i].magic = 0x424C4B5F5354524BULL;
}
}
s->loaded = 1;
}
return s;
}
/* ===== header I/O ===== */
static inline void volmeta_from_buf(blk_volume_meta_t *out, const uint8_t *buf) {
memset(out, 0, sizeof(*out));
memcpy(out, buf, minzu(sizeof(*out), (size_t) BLK_DEVICE_SECTOR));
}
static inline void volmeta_to_buf(const blk_volume_meta_t *in, uint8_t *buf) {
memset(buf, 0, BLK_DEVICE_SECTOR);
memcpy(buf, in, minzu(sizeof(*in), (size_t) BLK_DEVICE_SECTOR));
}
static int read_header_4k(blk_dev_slot_t *slot, uint8_t *buf4k) {
for (uint32_t i = 0; i < 4; i++) {
if (blkio_read(slot->dev, i, buf4k + i*1024u) != BLKIO_OK)
memset(buf4k + i*1024u, 0, 1024u);
}
return BLK_OK;
}
static int write_header_4k(blk_dev_slot_t *slot, const uint8_t *buf4k) {
for (uint32_t i = 0; i < 4; i++)
if (blkio_write(slot->dev, i, buf4k + i*1024u) != BLKIO_OK) return BLK_EIO;
return BLK_OK;
}
/* ===== volume sizing ===== */
static uint32_t choose_B(uint64_t total_devblocks_4k) {
if (total_devblocks_4k <= 2) return 1;
uint64_t B = (3ULL*(total_devblocks_4k - 1ULL) + 32767ULL) / 32768ULL;
if (B == 0) B = 1;
if (B > 0xFFFFFFFFu) B = 0xFFFFFFFFu;
return (uint32_t) B;
}
static void compute_totals_from_B(blk_volume_meta_t *m) {
uint64_t B = m->bam_devblocks;
uint64_t R = m->reloc_devblocks; /* reserved relocation-table region, see blk_volume_meta_t's own doc comment */
uint64_t F = m->meta_fence_blocks; /* top-of-device system-metadata fence, see blk_volume_meta_t's own doc comment */
m->tracked_blocks = 32768ULL * B;
uint64_t payload4k = (m->total_devblocks > (1+B+R+F)) ? (m->total_devblocks - 1 - B - R - F) : 0;
uint64_t storable = 3ULL * payload4k;
m->total_blocks = (m->tracked_blocks < storable) ? m->tracked_blocks : storable;
/* NOTE: first_free/last_allocated are intentionally NOT set here. They
* are absolute Forth LBN hints (readers in blk_allocate()/blk_mark_free()
* subtract slot->start_lbn), which this pure-geometry function cannot
* know -- it has no slot/LBN-base context. Setting them to a physical
* BAM index here made the first-free hint point at the wrong block; the
* slot's owner establishes them from slot->start_lbn instead (see
* blk_compute_fresh_geometry()). */
}
/* ===== physical BAM I/O (sync to/from slot->bam[]) ===== */
/*
* Read physical BAM from disk; populate slot->bam[i].allocated for user blocks.
* Physical bit [BLK_DISK_SYS_RESERVED + i] → slot->bam[i].allocated.
*/
static int bam_sync_from_disk(blk_dev_slot_t *slot) {
blk_volume_meta_t *m = &slot->vol_meta;
if (!slot->dev || m->bam_devblocks == 0) return BLK_EINVAL;
size_t psize = (size_t)4096u * m->bam_devblocks;
uint8_t *pbam = (uint8_t *) calloc(1, psize);
if (!pbam) return BLK_ENOMEM;
for (uint32_t i = 0; i < m->bam_devblocks; i++) {
uint32_t base1k = (m->bam_start + i) * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_read(slot->dev, base1k + k, pbam + i*4096u + k*1024u) != BLKIO_OK)
memset(pbam + i*4096u + k*1024u, 0, 1024u);
}
}
for (uint32_t i = 0; i < slot->user_blocks; i++)
slot->bam[i].allocated = pbam_test(pbam, BLK_DISK_SYS_RESERVED + i) ? 1 : 0;
free(pbam);
slot->bam_dirty = 0;
return BLK_OK;
}
/*
* Write slot->bam[i].allocated back to the physical BAM on disk (read-modify-write).
* Preserves reserved bits. Only runs when slot->bam_dirty is set.
*/
static int bam_flush_to_disk(blk_dev_slot_t *slot) {
if (!slot->dev || !slot->bam_dirty) return BLK_OK;
blk_volume_meta_t *m = &slot->vol_meta;
if (m->bam_devblocks == 0) return BLK_OK;
size_t psize = (size_t)4096u * m->bam_devblocks;
uint8_t *pbam = (uint8_t *) calloc(1, psize);
if (!pbam) return BLK_ENOMEM;
/* read current on-disk BAM */
for (uint32_t i = 0; i < m->bam_devblocks; i++) {
uint32_t base1k = (m->bam_start + i) * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_read(slot->dev, base1k + k, pbam + i*4096u + k*1024u) != BLKIO_OK)
memset(pbam + i*4096u + k*1024u, 0, 1024u);
}
}
/* reserved physical bits always set */
for (uint32_t i = 0; i < BLK_DISK_SYS_RESERVED; i++) pbam_set(pbam, i);
/* copy user allocation state */
for (uint32_t i = 0; i < slot->user_blocks; i++) {
if (slot->bam[i].allocated) pbam_set(pbam, BLK_DISK_SYS_RESERVED + i);
else pbam_clr(pbam, BLK_DISK_SYS_RESERVED + i);
}
/* write back */
for (uint32_t i = 0; i < m->bam_devblocks; i++) {
uint32_t base1k = (m->bam_start + i) * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_write(slot->dev, base1k + k, pbam + i*4096u + k*1024u) != BLKIO_OK) {
free(pbam); return BLK_EIO;
}
}
}
blkio_flush(slot->dev);
free(pbam);
slot->bam_dirty = 0;
return BLK_OK;
}
/* ===== relocation-exception table I/O (Milestone 2h+, mirrors the BAM I/O
* functions' own absolute-devblock-addressing shape above) =====
*
* Wire format: raw byte layout of the in-memory g.reloc[]/g.reloc_count
* state -- a 4-byte count prefix, then that many packed {uint32_t home_lbn;
* uint32_t actual_lbn;} pairs, all within one 4 KiB devblock (only the
* first is ever read/written -- blk_compute_fresh_geometry() always
* reserves exactly one; a volume with reloc_devblocks > 1 isn't produced
* by this driver today, so the rest would silently go unused, matching
* "won't migrate too much" scale). slot is the relocation-table owner
* (first_disk_slot(), see blk_subsys_attach_device()) -- both functions
* are no-ops (not errors) when there's no owner yet, or the owner
* predates reloc capacity (reloc_devblocks == 0, e.g. disk/artemis.img's
* existing fixture, formatted before this feature existed). */
static int reloc_load_from_disk(blk_dev_slot_t *slot) {
if (!slot || !slot->dev) return BLK_EINVAL;
blk_volume_meta_t *m = &slot->vol_meta;
if (m->reloc_devblocks == 0) { g.reloc_count = 0; return BLK_OK; }
uint8_t buf4k[4096];
uint32_t base1k = m->reloc_start * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_read(slot->dev, base1k + k, buf4k + k*1024u) != BLKIO_OK)
memset(buf4k + k*1024u, 0, 1024u);
}
uint32_t count;
memcpy(&count, buf4k, sizeof(count));
if (count > BLK_RELOC_MAX_ENTRIES) count = 0; /* corrupt/foreign data guard */
memcpy(g.reloc, buf4k + sizeof(count), (size_t) count * sizeof(blk_reloc_entry_t));
g.reloc_count = count;
return BLK_OK;
}
static int reloc_flush_to_disk(blk_dev_slot_t *slot) {
if (!slot || !slot->dev) return BLK_OK; /* no owner yet -- nothing to persist to */
blk_volume_meta_t *m = &slot->vol_meta;
if (m->reloc_devblocks == 0) return BLK_OK; /* this device predates reloc capacity */
uint8_t buf4k[4096] = {0};
memcpy(buf4k, &g.reloc_count, sizeof(g.reloc_count));
memcpy(buf4k + sizeof(g.reloc_count), g.reloc,
(size_t) g.reloc_count * sizeof(blk_reloc_entry_t));
uint32_t base1k = m->reloc_start * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_write(slot->dev, base1k + k, buf4k + k*1024u) != BLKIO_OK) return BLK_EIO;
}
blkio_flush(slot->dev);
return BLK_OK;
}
/* ===== volume format / load (per disk slot) ===== */
/* 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;
slot->vol_meta.total_volumes = 1;
slot->vol_meta.reserved_ram_lo = BLK_FORTH_SYS_RESERVED;
slot->vol_meta.reserved_disk_lo = BLK_DISK_SYS_RESERVED;
strncpy(slot->vol_meta.label, "StarForth Volume", sizeof(slot->vol_meta.label) - 1);
slot->vol_meta.total_devblocks = (uint64_t) udiv_floor(slot->total_blkio_blocks_1k, 4);
slot->vol_meta.bam_start = 1;
slot->vol_meta.bam_devblocks = choose_B(slot->vol_meta.total_devblocks);
slot->vol_meta.reloc_start = slot->vol_meta.bam_start + slot->vol_meta.bam_devblocks;
/* One devblock (4 KiB): a 4-byte count prefix + up to BLK_RELOC_MAX_ENTRIES
* 8-byte {home_lbn, actual_lbn} pairs -- see this file's own doc comment
* on BLK_RELOC_MAX_ENTRIES. Reserved unconditionally on every fresh
* format, not sized to demand -- relocations are rare, but knowing in
* advance whether a volume *can* ever receive one is simpler than a
* variable-size region that might need to grow later. */
slot->vol_meta.reloc_devblocks = 1;
slot->vol_meta.devblock_base = slot->vol_meta.reloc_start + slot->vol_meta.reloc_devblocks;
slot->vol_meta.meta_fence_blocks = BLK_META_FENCE_INIT;
compute_totals_from_B(&slot->vol_meta);
/* Allocation hints are absolute Forth LBNs (see the readers' note at
* compute_totals_from_B()). This slot's first user-visible block is at
* slot->start_lbn, so that is the true "next free" hint on a fresh
* volume -- anything else (e.g. a physical BAM index) made the hint
* point into the wrong block and left blk_allocate() guessing. */
slot->vol_meta.first_free = (uint64_t) slot->start_lbn;
slot->vol_meta.last_allocated = (uint64_t) slot->start_lbn - 1u;
if (sf_has_rtc()) slot->vol_meta.created_time = sf_realtime_ns();
else slot->vol_meta.created_time = sf_monotonic_ns();
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->devblock_base_4k = slot->vol_meta.devblock_base;
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 */
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);
}
/* Zero the reloc region too -- a zeroed 4-byte count prefix reads back
* as "0 entries", the correct empty-table default. */
for (uint32_t i = 0; i < slot->vol_meta.reloc_devblocks; i++) {
uint32_t base1k = (slot->vol_meta.reloc_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;
}
/* ===== timestamp helper ===== */
static inline uint64_t blk_get_timestamp(void) {
if (sf_has_rtc()) return sf_realtime_ns();
return sf_monotonic_ns();
}
/* ===== public API ===== */
int blk_subsys_init(VM *vm, uint8_t *ram_base, size_t ram_size) {
if (!vm || !ram_base || ram_size < ((size_t) BLK_RAM_BLOCKS * BLK_FORTH_SIZE))
return BLK_EINVAL;
memset(&g, 0, sizeof(g));
g.vm = vm;
g.ram_base = ram_base;
g.ram_size = ram_size;
g.ram_user = (BLK_RAM_BLOCKS > BLK_FORTH_SYS_RESERVED)
? (BLK_RAM_BLOCKS - BLK_FORTH_SYS_RESERVED) : 0u;
g.total_user_lbn = g.ram_user;
g.head = NULL;
g.initialized = 1;
return BLK_OK;
}
int blk_subsys_add_raw_device(uint8_t *buf, uint32_t nblocks) {
if (!g.initialized) return BLK_ENODEV;
if (!buf || nblocks == 0) return BLK_EINVAL;
blk_dev_slot_t *slot = (blk_dev_slot_t *) calloc(1, sizeof(*slot));
if (!slot) return BLK_ENOMEM;
slot->bam = (blk_bam_entry_t *) calloc(nblocks, sizeof(blk_bam_entry_t));
if (!slot->bam) { free(slot); return BLK_ENOMEM; }
slot->start_lbn = (uint32_t) g.total_user_lbn;
slot->user_blocks = nblocks;
slot->raw_base = buf;
chain_append(slot);
g.total_user_lbn += nblocks;
g.epoch++;
log_message(LOG_INFO, "blk: raw device LBN %u..%u (%u blocks)",
slot->start_lbn, slot->start_lbn + nblocks - 1, nblocks);
return BLK_OK;
}
static blk_dev_slot_t *first_disk_slot(void); /* defined below; used here to identify the
* relocation-table owner right after attach */
int blk_subsys_attach_device(struct blkio_dev *dev) {
if (!g.initialized) return BLK_ENODEV;
if (!dev) return BLK_EINVAL;
blk_dev_slot_t *slot = (blk_dev_slot_t *) calloc(1, sizeof(*slot));
if (!slot) return BLK_ENOMEM;
slot->dev = dev;
blkio_info_t info = {0};
if (blkio_info(dev, &info) != BLKIO_OK) { free(slot); return BLK_EIO; }
slot->total_blkio_blocks_1k = info.total_blocks;
slot->start_lbn = (uint32_t) g.total_user_lbn;
int rc = blk_format_or_load_disk(slot);
if (rc != BLK_OK) { if (slot->bam) free(slot->bam); free(slot); return rc; }
slot->vol_meta.mounted_time = blk_get_timestamp();
/* 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;
g.epoch++;
/* Milestone 2h+: load the relocation-exception table from whichever
* disk-backed device is now the canonical owner (first_disk_slot(),
* the same "which device is canonical" answer blk_get_volume_meta()/
* blk_set_volume_meta() already use) -- but only the first time that
* device becomes the owner. A later-attached second disk-backed
* device (e.g. a USB drive attaching after Artemis's own disk) must
* NOT overwrite the already-loaded table with its own (likely empty)
* one. See FABRIC-1.md's design writeup for why "first attached wins"
* is a pragmatic default, not a general multi-primary-device answer. */
if (slot->dev && first_disk_slot() == slot) {
(void) reloc_load_from_disk(slot);
}
log_message(LOG_INFO,
"blk: disk '%s' v2 LBN %u..%u (%u user blocks); "
"devblocks=%llu bam=%u base=%u total=%llu free=%llu fence=%u",
slot->vol_meta.label,
slot->start_lbn, slot->start_lbn + slot->user_blocks - 1,
slot->user_blocks,
(unsigned long long) slot->vol_meta.total_devblocks,
slot->vol_meta.bam_devblocks, slot->vol_meta.devblock_base,
(unsigned long long) slot->vol_meta.total_blocks,
(unsigned long long) slot->vol_meta.free_blocks,
slot->vol_meta.meta_fence_blocks);
return BLK_OK;
}
/* Milestone 2h hot-detach. Deliberately refuses anything but the current
* chain tail: block_subsystem.c's own architecture doc (top of this file)
* has USB/future devices as the *last* link specifically so a removal
* never has to renumber any other slot's start_lbn -- a mid-chain removal
* would corrupt every later slot's LBN range, so this is refused outright
* rather than attempted.
*
* Deliberately discards rather than flushes any dirty cache/BAM/vol_meta
* state: the device is physically gone by the time this runs (called only
* after a real PORTSC disconnect), so a flush attempt cannot succeed --
* pretending to try would just call blkio_write() against a vanished
* device for no benefit. Revisit if a future graceful-unmount path (as
* opposed to a surprise removal) wants a best-effort flush first; today
* every removal this driver can observe is a surprise removal.
*/
int blk_subsys_detach_device(struct blkio_dev *dev) {
if (!g.initialized) return BLK_ENODEV;
if (!dev) return BLK_EINVAL;
blk_dev_slot_t *prev = NULL;
blk_dev_slot_t *s = g.head;
while (s && s->dev != dev) { prev = s; s = s->next; }
if (!s) return BLK_ENODEV;
if (s->next) {
log_message(LOG_WARN, "blk: refusing detach of non-tail device (LBN %u..%u)",
s->start_lbn, s->start_lbn + s->user_blocks - 1);
return BLK_EINVAL;
}
log_message(LOG_INFO, "blk: detaching disk '%s' LBN %u..%u (%u user blocks)",
s->vol_meta.label, s->start_lbn, s->start_lbn + s->user_blocks - 1,
s->user_blocks);
if (prev) prev->next = NULL; else g.head = NULL;
g.total_user_lbn -= s->user_blocks;
g.epoch++;
blkio_close(dev);
if (s->bam) free(s->bam);
free(s);
return BLK_OK;
}
/* Milestone 2h+ single-block relocation -- see FABRIC-1.md's design
* writeup for the full reasoning. Mechanical primitive only: this
* function does not decide *whether* a relocation should happen (ACL's
* job) or validate that target_lbn is genuinely owned by whoever is
* asking (also ACL's job) -- it just executes one, correctly, once told
* to. */
int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn) {
if (!g.initialized) return BLK_ENODEV;
if (home_lbn == target_lbn) return BLK_EINVAL;
/* Refuse re-relocating an already-relocated home_lbn, or relocating
* onto an LBN that's itself someone else's relocation source --
* both would need chain-following this function deliberately doesn't
* support; call blk_subsys_unrelocate()-style bookkeeping (not yet
* needed, not yet built) first if that's ever required. */
for (uint32_t i = 0; i < g.reloc_count; i++) {
if (g.reloc[i].home_lbn == home_lbn || g.reloc[i].home_lbn == target_lbn)
return BLK_EINVAL;
}
if (g.reloc_count >= BLK_RELOC_MAX_ENTRIES) return BLK_ENOMEM;
if (!blk_is_valid(home_lbn) || !blk_is_valid(target_lbn)) return BLK_ERANGE;
/* Stage through a local buffer rather than copying directly from one
* blk_get_buffer() result to another -- obtaining the target buffer
* can trigger a cache eviction (cache_get_slot()'s FIFO shift) that
* silently invalidates a pointer already held into the *same*
* device's cache array, if home_lbn and target_lbn happen to share a
* device. Not a hypothetical: this is exactly the class of stale
* pointer this file's own blk_vm_evict() comment already warns about. */
uint8_t *src = blk_get_buffer(home_lbn, 0);
if (!src) return BLK_EIO;
uint8_t staged[BLK_FORTH_SIZE];
memcpy(staged, src, BLK_FORTH_SIZE);
uint8_t *dst = blk_get_buffer(target_lbn, 1);
if (!dst) return BLK_EIO;
memcpy(dst, staged, BLK_FORTH_SIZE);
if (blk_update(target_lbn) != BLK_OK) return BLK_EIO;
/* Free home_lbn's original backing block -- must happen before the
* redirect is inserted below, while resolve_lbn(home_lbn) still
* resolves to itself; inserting the redirect first would make this
* call free target_lbn instead. */
(void) blk_mark_free(home_lbn);
g.reloc[g.reloc_count].home_lbn = home_lbn;
g.reloc[g.reloc_count].actual_lbn = target_lbn;
g.reloc_count++;
g.epoch++;
(void) reloc_flush_to_disk(first_disk_slot());
log_message(LOG_INFO, "blk: relocated LBN %u -> %u", home_lbn, target_lbn);
return BLK_OK;
}
uint64_t blk_subsys_epoch(void) {
return g.epoch;
}
int blk_subsys_shutdown(void) {
if (!g.initialized) return BLK_OK;
blk_flush(0);
blk_dev_slot_t *s = g.head;
while (s) {
if (s->dev) {
/* 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);
blkio_flush(s->dev);
}
blkio_flush(s->dev);
}
blk_dev_slot_t *next = s->next;
if (s->bam) free(s->bam);
free(s);
s = next;
}
memset(&g, 0, sizeof(g));
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) {
if (!g.initialized) return NULL;
block_num = resolve_lbn(block_num);
/* RAM */
if (block_num < g.ram_user) {
uint32_t pbn = block_num + BLK_FORTH_SYS_RESERVED;
if (pbn >= BLK_RAM_BLOCKS) return NULL;
if (writable) g.dirty_ram[pbn] = 1;
return g.ram_base + (size_t) pbn * BLK_FORTH_SIZE;
}
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return NULL;
uint32_t offset = block_num - slot->start_lbn;
/* raw (ramdrive) */
if (slot->raw_base) {
if (writable) {
slot->bam[offset].allocated = 1;
slot->bam[offset].dirty = 1;
}
return slot->raw_base + (size_t) offset * BLK_FORTH_SIZE;
}
/* 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);
cache_slot_t *c = cache_load_devblock(slot, dev4k);
if (!c) return NULL;
if (writable) c->dirty = 1;
return c->data + pack * BLK_FORTH_SIZE;
}
uint8_t *blk_get_empty_buffer(uint32_t block_num) {
uint8_t *p = blk_get_buffer(block_num, 1);
if (p) memset(p, 0, BLK_FORTH_SIZE);
return p;
}
int blk_update(uint32_t block_num) {
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(block_num);
/* RAM */
if (block_num < g.ram_user) {
uint32_t pbn = block_num + BLK_FORTH_SYS_RESERVED;
if (pbn >= BLK_RAM_BLOCKS) return BLK_ERANGE;
g.dirty_ram[pbn] = 1;
return BLK_OK;
}
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;
slot->bam[offset].dirty = 1;
slot->bam_dirty = 1;
/* raw: done */
if (slot->raw_base) return BLK_OK;
/* disk: update CRC + timestamps in cache */
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);
cache_slot_t *c = cache_load_devblock(slot, dev4k);
if (!c) return BLK_EIO;
uint8_t *blkdata = c->data + pack * BLK_FORTH_SIZE;
c->meta[pack].checksum = compute_crc64(blkdata, BLK_FORTH_SIZE);
uint64_t now = blk_get_timestamp();
/* cache_load_devblock() already force-stamps magic on every load, so
* testing magic==0 here is dead code that silently dropped the
* first-write created_time stamp. A block that has never been written
* before has created_time==0 (the loaded slot was memset); an existing
* block carries its stamped-on-first-write non-zero created_time. Use
* that as the first-write signal so a fresh block records its true
* creation time. */
if (c->meta[pack].created_time == 0) {
c->meta[pack].magic = 0x424C4B5F5354524BULL;
c->meta[pack].created_time = now;
}
c->meta[pack].modified_time = now;
c->meta_dirty = c->dirty = 1;
if (slot->vol_meta.free_blocks > 0 &&
!slot->bam[offset].allocated) { /* was free before this call */
slot->vol_meta.free_blocks--;
slot->vol_meta_dirty = 1;
}
return BLK_OK;
}
int blk_flush(uint32_t block_num) {
if (!g.initialized) return BLK_ENODEV;
if (block_num > 0) block_num = resolve_lbn(block_num); /* 0 is the "flush all" sentinel */
if (block_num > 0) {
if (block_num < g.ram_user) {
uint32_t pbn = block_num + BLK_FORTH_SYS_RESERVED;
if (pbn < BLK_RAM_BLOCKS) g.dirty_ram[pbn] = 0;
return BLK_OK;
}
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
uint32_t offset = block_num - slot->start_lbn;
slot->bam[offset].dirty = 0;
if (slot->raw_base) return BLK_OK;
uint32_t rel_pbn = lbn_to_slot_pbn(slot, block_num);
uint32_t dev4k = slot_pbn_to_devblock(rel_pbn);
for (int i = 0; i < DISK_CACHE_SLOTS; i++) {
cache_slot_t *c = &slot->cache[i];
if (c->valid && c->devblock == dev4k && (c->dirty || c->meta_dirty)) {
int rc = cache_writeback(slot, c);
if (rc != BLK_OK) return rc;
blkio_flush(slot->dev);
(void) bam_flush_to_disk(slot);
return BLK_OK;
}
}
return BLK_OK;
}
/* flush all */
blk_dev_slot_t *s = g.head;
while (s) {
if (s->dev) {
for (int i = 0; i < DISK_CACHE_SLOTS; i++) {
cache_slot_t *c = &s->cache[i];
if (c->valid && (c->dirty || c->meta_dirty))
(void) cache_writeback(s, c);
}
blkio_flush(s->dev);
(void) bam_flush_to_disk(s);
}
/* clear all dirty flags for this slot */
for (uint32_t i = 0; i < s->user_blocks; i++) s->bam[i].dirty = 0;
s = s->next;
}
memset(g.dirty_ram, 0, sizeof(g.dirty_ram));
return BLK_OK;
}
/* ===== BAM wrappers ===== */
int blk_is_allocated(uint32_t block_num) {
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(block_num);
if (block_num < g.ram_user) return BLK_EINVAL;
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return 0;
return slot->bam[block_num - slot->start_lbn].allocated ? 1 : 0;
}
int blk_mark_allocated(uint32_t block_num) {
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(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;
slot->bam_dirty = 1;
if (!slot->raw_base && slot->vol_meta.free_blocks) {
slot->vol_meta.free_blocks--;
slot->vol_meta_dirty = 1;
}
}
return BLK_OK;
}
int blk_mark_free(uint32_t block_num) {
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(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;
slot->bam_dirty = 1;
if (!slot->raw_base) {
slot->vol_meta.free_blocks++;
if ((uint64_t) block_num < slot->vol_meta.first_free)
slot->vol_meta.first_free = block_num;
slot->vol_meta_dirty = 1;
}
}
return BLK_OK;
}
int blk_allocate(uint32_t *block_num) {
if (!block_num) return BLK_EINVAL;
if (!g.initialized) return BLK_ENODEV;
blk_dev_slot_t *s = g.head;
while (s) {
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)
? (s->vol_meta.first_free - s->start_lbn) : 0;
if (hint >= limit) hint = 0;
for (uint32_t i = 0; i < limit; i++) {
uint32_t k = (hint + i) % limit;
if (!s->bam[k].allocated) {
s->bam[k].allocated = 1;
s->bam_dirty = 1;
if (s->vol_meta.free_blocks) s->vol_meta.free_blocks--;
uint32_t lbn = s->start_lbn + k;
s->vol_meta.last_allocated = lbn;
s->vol_meta.first_free = lbn + 1;
s->vol_meta_dirty = 1;
*block_num = lbn;
return BLK_OK;
}
}
s = s->next;
}
return BLK_ERANGE;
}
/* ===== info / meta ===== */
static blk_dev_slot_t *first_disk_slot(void) {
blk_dev_slot_t *s = g.head;
while (s) { if (s->dev) return s; s = s->next; }
return NULL;
}
int blk_get_volume_meta(blk_volume_meta_t *meta) {
if (!meta) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
*meta = slot->vol_meta;
return BLK_OK;
}
int blk_set_volume_meta(const blk_volume_meta_t *meta) {
if (!meta) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
slot->vol_meta = *meta;
slot->vol_meta_dirty = 1;
return BLK_OK;
}
/* FABRIC-2.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
* the header/BAM/reloc-table regions. devblock_from_top counts down from
* the very last physical devblock of the canonical device (0 = last,
* 1 = second-to-last, ...); must be < the on-disk meta_fence_blocks or
* this refuses rather than silently reading/writing outside the
* reservation. Operates on the same "canonical device" first_disk_slot()
* already establishes for blk_get_volume_meta()/the relocation table.
* No FORTH word wraps this -- C-only, same discipline as
* vm_zuse_cert_install() itself; Zuse's cert is this zone's first tenant.
*/
int blk_meta_zone_read(uint32_t devblock_from_top, uint8_t buf[4096]) {
if (!buf) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot || !slot->dev) return BLK_ENODEV;
if (devblock_from_top >= slot->vol_meta.meta_fence_blocks) return BLK_EINVAL;
/* The fence index must also sit inside the physical device -- a corrupt
* volume whose meta_fence_blocks >= total_devblocks would otherwise pass
* the fence check and let (total_devblocks - 1 - devblock_from_top)
* underflow on the unsigned subtraction below, targeting a wild devblock. */
if ((uint64_t) devblock_from_top >= slot->vol_meta.total_devblocks) return BLK_EINVAL;
uint64_t devblock_idx = slot->vol_meta.total_devblocks - 1ULL - devblock_from_top;
uint32_t base1k = (uint32_t) devblock_idx * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_read(slot->dev, base1k + k, buf + k * 1024u) != BLKIO_OK)
return BLK_EIO;
}
return BLK_OK;
}
int blk_meta_zone_write(uint32_t devblock_from_top, const uint8_t buf[4096]) {
if (!buf) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot || !slot->dev) return BLK_ENODEV;
if (devblock_from_top >= slot->vol_meta.meta_fence_blocks) return BLK_EINVAL;
/* Same physical-bounds guard as blk_meta_zone_read(): prevents the
* unsigned underflow of (total_devblocks - 1 - devblock_from_top) on a
* corrupt fence >= device size. */
if ((uint64_t) devblock_from_top >= slot->vol_meta.total_devblocks) return BLK_EINVAL;
uint64_t devblock_idx = slot->vol_meta.total_devblocks - 1ULL - devblock_from_top;
uint32_t base1k = (uint32_t) devblock_idx * 4u;
for (uint32_t k = 0; k < 4; k++) {
if (blkio_write(slot->dev, base1k + k, buf + k * 1024u) != BLKIO_OK)
return BLK_EIO;
}
return BLK_OK;
}
int blk_is_valid(uint32_t block_num) {
if (!g.initialized) return 0;
block_num = resolve_lbn(block_num);
if (block_num < g.ram_user) return 1;
return lbn_to_slot(block_num) ? 1 : 0;
}
uint32_t blk_get_total_blocks(void) {
uint64_t n = g.total_user_lbn;
return (n > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t) n;
}
int blk_get_meta(uint32_t block_num, blk_meta_t *meta) {
if (!meta) return BLK_EINVAL;
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(block_num);
if (block_num < g.ram_user) {
memset(meta, 0, sizeof(*meta));
meta->magic = 0x424C4B5F5354524BULL;
return BLK_OK;
}
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
if (slot->raw_base) {
memset(meta, 0, sizeof(*meta));
meta->magic = 0x424C4B5F5354524BULL;
return BLK_OK;
}
uint32_t rel_pbn = lbn_to_slot_pbn(slot, block_num);
cache_slot_t *c = cache_load_devblock(slot, slot_pbn_to_devblock(rel_pbn));
if (!c) return BLK_EIO;
*meta = c->meta[slot_pbn_pack_offset(rel_pbn)];
return BLK_OK;
}
int blk_set_meta(uint32_t block_num, const blk_meta_t *meta) {
if (!meta) return BLK_EINVAL;
if (!g.initialized) return BLK_ENODEV;
block_num = resolve_lbn(block_num);
if (block_num < g.ram_user) return BLK_OK;
blk_dev_slot_t *slot = lbn_to_slot(block_num);
if (!slot) return BLK_ERANGE;
if (slot->raw_base) return BLK_OK;
uint32_t rel_pbn = lbn_to_slot_pbn(slot, block_num);
cache_slot_t *c = cache_load_devblock(slot, slot_pbn_to_devblock(rel_pbn));
if (!c) return BLK_EIO;
c->meta[slot_pbn_pack_offset(rel_pbn)] = *meta;
c->meta_dirty = 1;
return BLK_OK;
}
/* BMAPFMT field accessors -- FABRIC-2.md §H.12 step 14. Thin
* read-modify-write wrappers over blk_get_meta()/blk_set_meta() above,
* which already own caching/dirty-tracking -- these add no state of
* their own. */
int blk_owner_fp_get(uint32_t block_num, uint8_t out_fp[8]) {
blk_meta_t meta;
int rc;
if (!out_fp) return BLK_EINVAL;
rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
memcpy(out_fp, meta.owner_fp, sizeof(meta.owner_fp));
return BLK_OK;
}
int blk_owner_fp_set(uint32_t block_num, const uint8_t fp[8]) {
blk_meta_t meta;
int rc;
if (!fp) return BLK_EINVAL;
rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
memcpy(meta.owner_fp, fp, sizeof(meta.owner_fp));
return blk_set_meta(block_num, &meta);
}
int blk_acl_allow_get(uint32_t block_num, uint8_t *out_allow) {
blk_meta_t meta;
int rc;
if (!out_allow) return BLK_EINVAL;
rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
*out_allow = meta.acl_allow;
return BLK_OK;
}
int blk_acl_allow_set(uint32_t block_num, uint8_t allow) {
blk_meta_t meta;
int rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
meta.acl_allow = allow;
return blk_set_meta(block_num, &meta);
}
int blk_acl_ttl_get(uint32_t block_num, uint32_t *out_ttl) {
blk_meta_t meta;
int rc;
if (!out_ttl) return BLK_EINVAL;
rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
*out_ttl = meta.acl_ttl;
return BLK_OK;
}
int blk_acl_ttl_set(uint32_t block_num, uint32_t ttl) {
blk_meta_t meta;
int rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
meta.acl_ttl = ttl;
return blk_set_meta(block_num, &meta);
}
int blk_flags_get(uint32_t block_num, uint64_t *out_flags) {
blk_meta_t meta;
int rc;
if (!out_flags) return BLK_EINVAL;
rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
*out_flags = meta.flags;
return BLK_OK;
}
int blk_flags_set(uint32_t block_num, uint64_t flags) {
blk_meta_t meta;
int rc = blk_get_meta(block_num, &meta);
if (rc != BLK_OK) return rc;
meta.flags = flags;
return blk_set_meta(block_num, &meta);
}
/* FABRIC-2.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-2.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))
#endif
void blk_layer_attach_device(struct blkio_dev *dev) {
(void) dev;
blk_subsys_attach_device(dev);
}