blk_subsys_detach_device() (block_subsystem.c) walks the device chain, refuses removal of anything but the current tail (a mid-chain removal would corrupt every later slot's start_lbn -- this architecture's own doc already argues USB stays last specifically to avoid that), unlinks, shrinks total_user_lbn, closes and frees the slot. Discards rather than flushes dirty state -- the device is physically gone by the time this runs (PORTSC disconnect only). Trigger wiring mirrors the attach path: bot_msc_attached (set only once attach actually succeeds) gates a new bot_msc_detach_pending flag set at PORTSC disconnect (not Disable Slot completion, which is conditionally skipped and would miss concurrent connect/disconnect pairs), consumed in sk_repl_idle(). Advisor flagged the real hazard ahead of time: block_words.c's VM block window (blk_vm_lbn[]/blk_vm_cbuf[]) can go stale across a detach then a same-LBN re-attach, and suggested a pointer-identity re-check in blk_vm_load() as a minimal fix. That fix was implemented, then directly falsified by its own designed-for-this test: attach a blank device, read a block (populating the cache), detach, re-attach a device with distinct content at the identical LBN, read again -- served stale content from the first device. Root cause, confirmed live: glibc's allocator hands free(slot) straight back to the very next same-size calloc(), so the "fresh" and stale pointers were bitwise identical despite being two different devices. Fixed properly with a monotonic blk_subsys_epoch() counter (bumped on every attach/detach) checked by a new blk_vm_check_epoch() helper at the one choke point (blk_vm_find(), plus blk_vm_flush_all() which reads the same arrays directly) that covers every path touching the window cache -- unfooled by address reuse. Verified live with a new disk/usb-thumbdrive-test2.img fixture (distinct content from the existing blank test image): attach A, read (cache hit populated), detach, re-attach B at the same LBN, read again -- correctly ran a fresh device read and returned B's real content, not A's stale cached zeros. The failing pointer-comparison attempt's own capture log kept as evidence, not deleted. All three architectures re-verified clean. FABRIC-2.md Section X 2h marked complete -- enumeration through hot-detach all live and verified; only WRITE(10) (2g's own still-open item) remains unimplemented in the driver, not blocking anything here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
289 lines
11 KiB
C
289 lines
11 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 — 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 - Layer 2: Mapping & Business Logic (v2)
|
||
---------------------------------------------------------
|
||
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)
|
||
|
||
Each non-RAM device is attached via blk_subsys_attach_device() (formatted disk)
|
||
or blk_subsys_add_raw_device() (volatile RAM buffer). Devices are appended to
|
||
a chain; each gets a contiguous slot in the unified logical BAM (g.bam[]).
|
||
Physical BAMs (on-disk) are synced to/from their logical BAM slot at attach/flush.
|
||
|
||
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.
|
||
*/
|
||
#ifndef STARFORTH_BLOCK_SUBSYSTEM_H
|
||
#define STARFORTH_BLOCK_SUBSYSTEM_H
|
||
|
||
#include <stdint.h>
|
||
#include <stddef.h>
|
||
#include "vm.h"
|
||
#include "blkio.h" /* ensure struct blkio_dev is fully visible */
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* Core configuration constants */
|
||
#define BLK_FORTH_SIZE 1024u /* Forth block size */
|
||
#define BLK_RAM_BLOCKS 2080u /* Physical RAM blocks (user-visible LBN 0..2047) */
|
||
#define BLK_DISK_START 2080u /* Device-backed Forth blocks start at LBN 2080 */
|
||
#define BLK_DEVICE_SECTOR 4096u /* Physical “devblock” size (4×1 KiB blkio units) */
|
||
#define BLK_PACK_RATIO 3u /* 3× 1 KiB data per 4 KiB devblock (plus 1 KiB metadata) */
|
||
#define BLK_META_TOTAL 1024u /* Last 1 KiB in a 4 KiB devblock is metadata */
|
||
#define BLK_META_PER_BLOCK 341u /* 341×3 ~= 1023, padded to 1024 */
|
||
|
||
/* Forth-friendly reserved ranges */
|
||
#ifndef BLK_FORTH_SYS_RESERVED
|
||
#define BLK_FORTH_SYS_RESERVED 32u /* RAM physical blocks 0..31 hidden; user LBN 0 = PBN 32 */
|
||
#endif
|
||
#ifndef BLK_DISK_SYS_RESERVED
|
||
#define BLK_DISK_SYS_RESERVED 32u /* First 32 blocks of each disk reserved (byte-aligned BAM offset) */
|
||
#endif
|
||
|
||
/* =========================
|
||
* On-disk volume header v2
|
||
* =========================
|
||
* Serialized into device devblock 0 (4 KiB = 4×1 KiB blkio blocks 0..3).
|
||
* BAM lives in external devblocks [bam_start .. bam_start+bam_devblocks-1] (each 4 KiB).
|
||
* All *_devblocks indices are in 4 KiB units.
|
||
*/
|
||
typedef struct {
|
||
/* Identification & versioning */
|
||
uint32_t magic; /* 0x53544652 "STFR" */
|
||
uint32_t version; /* 2 */
|
||
|
||
/* Administrative info */
|
||
uint32_t total_volumes;
|
||
uint32_t flags;
|
||
char label[64];
|
||
|
||
/* Physical device geometry (4 KiB devblocks) */
|
||
uint64_t total_devblocks; /* count of 4 KiB devblocks (blkio_info.total_blocks / 4) */
|
||
|
||
/* BAM placement (external 1-bit bitmap region, stored in 4 KiB pages) */
|
||
uint32_t bam_start; /* usually 1 */
|
||
uint32_t bam_devblocks; /* number of 4 KiB pages used by BAM */
|
||
uint32_t devblock_base; /* first payload devblock = bam_start + bam_devblocks */
|
||
|
||
/* Capacity modeling (Forth 1 KiB blocks tracked/usable) */
|
||
uint64_t tracked_blocks; /* 32768 * bam_devblocks (bits per 4 KiB page) */
|
||
uint64_t total_blocks; /* min(tracked, 3 * (total_devblocks - 1 - bam_devblocks)) */
|
||
uint64_t free_blocks;
|
||
|
||
/* Allocation hints */
|
||
uint64_t first_free; /* next free Forth block (>= 1024) */
|
||
uint64_t last_allocated;
|
||
|
||
/* Reserved low ranges */
|
||
uint32_t reserved_disk_lo; /* e.g., 32 blocks reserved at 1024.. */
|
||
uint32_t reserved_ram_lo; /* e.g., 33 blocks reserved at 0..32 */
|
||
|
||
/* Timestamps (optional) */
|
||
uint64_t created_time;
|
||
uint64_t mounted_time;
|
||
|
||
/* Optional integrity (unused yet) */
|
||
uint64_t hdr_crc;
|
||
|
||
/* Padding to keep header ≤ 4096 bytes */
|
||
uint8_t _pad[4096 - (
|
||
4 + 4 + /* magic, version */
|
||
4 + 4 + 64 + /* total_volumes, flags, label */
|
||
8 + /* total_devblocks */
|
||
4 + 4 + 4 + /* bam_start, bam_devblocks, devblock_base */
|
||
8 + 8 + 8 + /* tracked_blocks, total_blocks, free_blocks */
|
||
8 + 8 + /* first_free, last_allocated */
|
||
4 + 4 + /* reserved ranges */
|
||
8 + 8 + /* timestamps */
|
||
8 /* hdr_crc */
|
||
)];
|
||
} blk_volume_meta_t;
|
||
|
||
/* Per-1 KiB block metadata (packed into top 1 KiB region of each 4 KiB sector). */
|
||
typedef struct {
|
||
/* Core integrity (16 bytes) */
|
||
uint64_t checksum; /* CRC64 of block data */
|
||
uint64_t magic; /* 0x424C4B5F5354524BULL "BLK_STRK" */
|
||
|
||
/* Timestamps (16 bytes) */
|
||
uint64_t created_time; /* Unix timestamp (creation) */
|
||
uint64_t modified_time; /* Unix timestamp (last write) */
|
||
|
||
/* Block status (16 bytes) */
|
||
uint64_t flags; /* Status flags */
|
||
uint64_t write_count; /* Number of writes (wear leveling) */
|
||
|
||
/* Content identification (32 bytes) */
|
||
uint64_t content_type; /* 0=empty, 1=source, 2=data, ... */
|
||
uint64_t encoding; /* 0=ASCII, 1=UTF-8, 2=binary, ... */
|
||
uint64_t content_length; /* Actual data length (≤ 1024) */
|
||
uint64_t reserved1; /* Alignment/future use */
|
||
|
||
/* Cryptographic (64 bytes) */
|
||
uint64_t entropy[4]; /* 256-bit entropy/random seed */
|
||
uint64_t hash[4]; /* SHA-256 (optional) */
|
||
|
||
/* Security & ownership (40 bytes) */
|
||
uint64_t owner_id; /* User/process ID */
|
||
uint64_t permissions; /* rwx-style permissions */
|
||
uint64_t acl_block; /* Block number containing ACL (0=none) */
|
||
uint64_t signature[2]; /* 128-bit signature */
|
||
|
||
/* Link/chain support (32 bytes) */
|
||
uint64_t prev_block; /* Previous in chain (0=none) */
|
||
uint64_t next_block; /* Next in chain (0=none) */
|
||
uint64_t parent_block; /* Parent/index (0=none) */
|
||
uint64_t chain_length; /* Total blocks in chain */
|
||
|
||
/* Application-specific (120 bytes) */
|
||
uint64_t app_data[15]; /* 15×64-bit app-defined fields */
|
||
|
||
/* Padding to reach 341-byte slice */
|
||
uint8_t padding[5];
|
||
} blk_meta_t;
|
||
|
||
/* Error codes */
|
||
enum {
|
||
BLK_OK = 0,
|
||
BLK_EINVAL = -1,
|
||
BLK_ERANGE = -2,
|
||
BLK_EIO = -3,
|
||
BLK_ENODEV = -4,
|
||
BLK_ERESERVED = -5,
|
||
BLK_EDIRTY = -6,
|
||
BLK_ENOMEM = -7
|
||
};
|
||
|
||
/* Per-block BAM entry — one per user block in each device slot */
|
||
typedef struct {
|
||
uint8_t allocated; /* 0=free, 1=in use */
|
||
uint8_t dirty; /* 1=UPDATE called; content needs flush */
|
||
} blk_bam_entry_t;
|
||
|
||
/* Low-level disk container format state. This is distinct from — and known
|
||
* nothing about — any higher-level content classification (e.g. Artemis's
|
||
* own BLANK/LithosAnanke/Unrecognized marker check). A slot stays
|
||
* PROVISIONAL, and all writes to it are refused, until its owner explicitly
|
||
* calls blk_subsys_confirm_format() after deciding the disk is safe to
|
||
* touch. This is what makes "halt, disk preserved" actually true instead
|
||
* of being overwritten by this layer's own opportunistic reformat before
|
||
* the owner ever gets a look. */
|
||
enum { BLK_FMT_FORMATTED = 0, BLK_FMT_PROVISIONAL = 1 };
|
||
|
||
/* ===== Public API ===== */
|
||
int blk_subsys_init(VM *vm, uint8_t *ram_base, size_t ram_size);
|
||
|
||
int blk_subsys_attach_device(struct blkio_dev *dev);
|
||
|
||
/* Milestone 2h hot-detach. Refuses (BLK_EINVAL) unless dev's slot is the
|
||
* current chain tail -- see this function's own doc comment in
|
||
* block_subsystem.c for why. Discards any dirty cache/BAM/vol_meta state
|
||
* rather than attempting to flush it (the device is already physically
|
||
* gone by the time this is called). Returns BLK_ENODEV if dev isn't
|
||
* attached, BLK_EINVAL if dev is NULL or not the chain tail.
|
||
*/
|
||
int blk_subsys_detach_device(struct blkio_dev *dev);
|
||
|
||
/* Monotonic counter, bumped on every attach/detach (Milestone 2h). A raw
|
||
* pointer comparison against a blk_get_buffer() result cannot reliably
|
||
* detect a same-address device swap (glibc's allocator can hand back the
|
||
* exact address just free()'d by a detach to the very next attach's
|
||
* calloc()) -- callers that cache a blk_get_buffer() result across calls
|
||
* (block_words.c's VM block window) must instead compare this epoch
|
||
* against the value they last observed, invalidating their whole cache on
|
||
* any change rather than trusting a stored pointer's identity.
|
||
*/
|
||
uint64_t blk_subsys_epoch(void);
|
||
|
||
int blk_subsys_shutdown(void);
|
||
|
||
uint8_t *blk_get_buffer(uint32_t block_num, int writable);
|
||
|
||
uint8_t *blk_get_empty_buffer(uint32_t block_num);
|
||
|
||
int blk_flush(uint32_t block_num);
|
||
|
||
int blk_update(uint32_t block_num);
|
||
|
||
int blk_get_volume_meta(blk_volume_meta_t *meta);
|
||
|
||
int blk_set_volume_meta(const blk_volume_meta_t *meta);
|
||
|
||
int blk_is_valid(uint32_t block_num);
|
||
|
||
uint32_t blk_get_total_blocks(void);
|
||
|
||
int blk_get_meta(uint32_t block_num, blk_meta_t *meta);
|
||
|
||
int blk_set_meta(uint32_t block_num, const blk_meta_t *meta);
|
||
|
||
int blk_is_allocated(uint32_t block_num);
|
||
|
||
int blk_mark_allocated(uint32_t block_num);
|
||
|
||
int blk_mark_free(uint32_t block_num);
|
||
|
||
int blk_allocate(uint32_t * block_num);
|
||
|
||
int blk_subsys_add_raw_device(uint8_t *buf, uint32_t nblocks);
|
||
|
||
/* Commit the low-level format (write header + BAM) for the disk slot that
|
||
* owns lbn. No-op (returns BLK_OK) if already FORMATTED. Must be called
|
||
* by the disk's owner before any write to that slot will succeed. */
|
||
int blk_subsys_confirm_format(uint32_t lbn);
|
||
|
||
#ifdef __cplusplus
|
||
} /* extern "C" */
|
||
#endif
|
||
|
||
#endif /* STARFORTH_BLOCK_SUBSYSTEM_H */ |