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
1526 lines
59 KiB
C
1526 lines
59 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 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);
|
||
}
|