Corrected meta_fence_blocks units from "Forth 1 KiB blocks" to 4 KiB devblocks (matching bam_devblocks/reloc_devblocks) before anything depended on the original meaning -- a clean fix, not a migration. This let the fence fold directly into compute_totals_from_B()'s existing payload4k formula (total_devblocks - 1 - B - R - F) instead of a separate user_blocks subtraction: total_blocks/user_blocks/free_blocks all shrink correctly for free, in both the fresh-format and reload paths, from one formula change. New blk_meta_zone_read()/blk_meta_zone_write() -- raw, unpacked 4 KiB devblock I/O, same shape as the header/BAM/reloc-table regions, addressed by devblock_from_top counting down from the device's last physical devblock. Refuses rather than clamps if the index exceeds the on-disk meta_fence_blocks. C-only, no FORTH word wraps either -- same discipline as vm_zuse_cert_install(), which will be this zone's first real tenant. Verified independently at every step, never trusting the kernel's own report: capacity math cross-checked against a from-scratch Python recomputation of the same formula (exact match); accessor correctness via a temporary probe (written/run/captured/reverted) that wrote a known pattern and read it back, then independently confirmed via a raw read of the disk image at the exact expected physical byte offset. Full 3-arch acceptance boot against the real, untouched disk/artemis.img, probe code fully reverted -- clean, conservation intact. Still open: wiring vm_zuse_cert_install() to actually persist through these accessors, and the MINT word itself. Documented in FABRIC-3.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
1222 lines
46 KiB
C
1222 lines
46 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-2.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-2.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;
|
||
uint64_t reserved = (uint64_t) BLK_DISK_SYS_RESERVED;
|
||
if (reserved > m->total_blocks) reserved = m->total_blocks;
|
||
m->first_free = BLK_DISK_SYS_RESERVED + reserved;
|
||
m->last_allocated = BLK_DISK_SYS_RESERVED + reserved - 1;
|
||
}
|
||
|
||
/* ===== 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);
|
||
|
||
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-2.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-2.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();
|
||
if (c->meta[pack].magic == 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;
|
||
}
|
||
|
||
/*
|
||
* 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;
|
||
|
||
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;
|
||
|
||
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;
|
||
}
|
||
|
||
/* ===== 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);
|
||
}
|