Phase 8 v3: refuse a same-VM, cross-device raw block copy from within StarshipOS
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Two research passes confirmed the identity record itself (seed/pubkey/
cert) is already unreachable from any FORTH primitive -- only C-level
read_devblock/write_devblock touch it. But a device's ordinary user block
content CAN be copied between two attached devices today, using only
stock, unpinned words: <src> BLOCK <dst> BUFFER 1024 MOVE UPDATE
SAVE-BUFFERS, or the dedicated RELOCATE-BLOCK word (whose own doc comment
already admits "performs no policy validation of its own"). Checked
whether the existing per-block owner_fp/BLK-ACL-ALLOW@ metadata already
solves this -- it doesn't: owner_fp encodes who (a VM identity pubkey),
never where (physical device), and blk_get_buffer()/blk_update() never
consult acl_allow/acl_ttl at all -- those fields are completely inert.

Small, targeted fix, no rearchitecture:

- blk_subsys_relocate_block() (block_subsystem.c): same-device check.
  Its own documented purpose is wear-leveling (relocate on the SAME
  device) -- never stated as cross-device, and nothing enforced that
  until now.
- New public blk_lbn_device_handle() (block_subsystem.c/.h): the missing
  LBN-to-device direction (blk_get_device_range() already goes the other
  way). Opaque, stable, == comparable.
- MOVE (memory_words.c) and CMOVE/CMOVE> (string_words.c): refuse when
  both addresses are block-window addresses backed by two different
  devices -- the exact shape of the composed attack. A copy where either
  end is ordinary VM memory (the overwhelming common case: staging text
  from PAD, editing a block in place) is untouched.
- blk_vm_check_epoch()/blk_vm_slot_for_addr() exposed (block_words.h) so
  the two new call sites share the same window-slot invalidation contract
  rather than a second, divergent copy of it.

Verified live on all three architectures, not just boot-clean: same-
device MOVE/RELOCATE-BLOCK still succeed exactly as before; cross-device
MOVE/CMOVE/RELOCATE-BLOCK all refused. Zero UNKNOWN WORD, identical
dict_hash across all three (this change adds no FORTH-visible word, only
internal refusal conditions, as predicted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 04:44:22 -04:00
co-authored by Claude Sonnet 5
parent b7d9ed5425
commit 26c1117ccd
12 changed files with 27605 additions and 1 deletions
+12
View File
@@ -385,6 +385,18 @@ int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn);
*/
int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_t *out_count);
/* blk_lbn_device_handle - the missing direction from blk_get_device_range()
* above (that goes device -> LBN range; this goes LBN -> device). Returns
* an opaque, stable handle -- directly comparable with == to test "do
* these two LBNs belong to the same physical device" -- or NULL if lbn
* doesn't resolve to any attached device. Never dereference the returned
* pointer; it exists only for equality comparison (Phase 8 v3, 2026-09-23:
* added so MOVE/CMOVE/CMOVE> can refuse a same-VM, cross-device raw block
* copy through the block-window mechanism -- see those words' own callers
* in memory_words.c/string_words.c).
*/
const void *blk_lbn_device_handle(uint32_t lbn);
/* blk_get_device_free_blocks - free vs. total 1 KiB FORTH-block count for
* a specific already-attached device (same slot lookup as
* blk_get_device_range()). RAM-backed slots (no on-disk vol_meta) report
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16
View File
@@ -805,6 +805,18 @@ int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn) {
if (!blk_is_valid(home_lbn) || !blk_is_valid(target_lbn)) return BLK_ERANGE;
/* Same-device only (Phase 8 v3, 2026-09-23): this function's own purpose
* is relocating a block to a new LBN on the device it's being relocated
* to (wear-leveling) -- never documented as cross-device, and nothing
* enforced that until now. Without this check, RELOCATE-BLOCK is a
* ready-made, unpinned, silent cross-device raw block copy primitive --
* closing that is the whole point of this change. */
if (lbn_to_slot(resolve_lbn(home_lbn)) != lbn_to_slot(resolve_lbn(target_lbn))) {
log_message(LOG_WARN,
"blk: refused cross-device relocate LBN %u -> %u", home_lbn, target_lbn);
return BLK_EINVAL;
}
/* 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
@@ -1194,6 +1206,10 @@ int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_
return BLK_OK;
}
const void *blk_lbn_device_handle(uint32_t lbn) {
return (const void *) lbn_to_slot(resolve_lbn(lbn));
}
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);
+40 -1
View File
@@ -174,7 +174,7 @@ void empty_all_buffers(VM *vm) {
* the top of every function below that reads vm->blk_vm_lbn[]/
* vm->blk_vm_cbuf[] directly, not just blk_vm_find() -- blk_vm_flush_all()
* walks the same arrays without going through blk_vm_find() first. */
static void blk_vm_check_epoch(VM *vm) {
void blk_vm_check_epoch(VM *vm) {
uint64_t epoch = blk_subsys_epoch();
if (epoch == vm->blk_vm_epoch) return;
for (int i = 0; i < BLK_VM_SLOTS; i++) {
@@ -185,6 +185,45 @@ static void blk_vm_check_epoch(VM *vm) {
vm->blk_vm_epoch = epoch;
}
/* blk_vm_slot_for_addr ( addr -- slot-index|-1 ): does `addr` fall inside
* the block-window range at all? Pure address arithmetic, no VM state --
* the window's base/size are fixed compile-time constants (vm.h), so this
* doesn't need a VM* the way blk_vm_find() does (that also needs the
* per-VM blk_vm_lbn[]/cbuf[] arrays to check what's actually loaded).
* Exposed (Phase 8 v3, 2026-09-23) for MOVE/CMOVE/CMOVE>'s cross-device
* block-copy check -- they need "is this a block-window address" without
* needing to know or care whether anything's actually loaded there. */
int blk_vm_slot_for_addr(vaddr_t addr) {
if (addr < BLK_VM_WINDOW_BASE || addr >= BLK_VM_WINDOW_BASE + BLK_VM_WINDOW_SIZE)
return -1;
return (int) ((addr - BLK_VM_WINDOW_BASE) / BLOCK_SIZE);
}
/* blk_vm_is_cross_device_copy ( addr1 addr2 -- refuse? ): the actual
* Phase 8 v3 (2026-09-23) check MOVE/CMOVE/CMOVE> call before copying.
* True only when BOTH addresses are block-window addresses AND the LBNs
* currently loaded in those two slots resolve to two different physical
* devices -- the exact shape of `<src> BLOCK <dst> BUFFER ... MOVE`, a
* same-VM, cross-device raw block copy using only stock words. A copy
* where either (or both) address is ordinary VM memory -- the overwhelming
* common case, e.g. staging PAD text into a block, or reading a block out
* for display -- is never flagged: only both-window, both-different-device
* copies are refused. Callers must still do their own vm_addr_ok() bounds
* checks first; this only adds the device-boundary refusal on top. */
int blk_vm_is_cross_device_copy(VM *vm, vaddr_t addr1, vaddr_t addr2) {
int slot1 = blk_vm_slot_for_addr(addr1);
int slot2 = blk_vm_slot_for_addr(addr2);
if (slot1 < 0 || slot2 < 0) return 0;
blk_vm_check_epoch(vm);
uint32_t lbn1 = vm->blk_vm_lbn[slot1];
uint32_t lbn2 = vm->blk_vm_lbn[slot2];
if (lbn1 == 0 || lbn2 == 0) return 0; /* an empty/unassigned slot has nothing to protect */
const void *dev1 = blk_lbn_device_handle(lbn1);
const void *dev2 = blk_lbn_device_handle(lbn2);
if (!dev1 || !dev2) return 0; /* can't resolve -- not this check's job to refuse */
return dev1 != dev2;
}
/* Find the slot holding lbn; return slot index or -1 if not loaded. */
static int blk_vm_find(VM *vm, uint32_t lbn) {
blk_vm_check_epoch(vm);
+35
View File
@@ -158,4 +158,39 @@ void blk_vm_flush_all(VM * vm);
*/
void empty_all_buffers(VM * vm);
/**
* @brief Discards vm->blk_vm_lbn[]/cbuf[]/dirty[] if the device chain has
* changed since last validated (see this function's own doc comment in
* block_words.c for why). Exposed (Phase 8 v3, 2026-09-23) so callers
* outside this file that read vm->blk_vm_lbn[] directly (MOVE/CMOVE's
* cross-device block-copy check, memory_words.c/string_words.c) go through
* the same invalidation contract blk_vm_find() already uses internally,
* rather than a second, divergent copy of this logic.
* @param vm Pointer to the Forth virtual machine instance
*/
void blk_vm_check_epoch(VM *vm);
/**
* @brief Does `addr` fall inside the block I/O window? Pure address
* arithmetic against the window's fixed compile-time base/size -- no VM
* state needed. Exposed (Phase 8 v3, 2026-09-23) for MOVE/CMOVE/CMOVE>'s
* cross-device block-copy check.
* @param addr VM offset to test
* @return slot index (0..BLK_VM_SLOTS-1) if inside the window, -1 otherwise
*/
int blk_vm_slot_for_addr(vaddr_t addr);
/**
* @brief True if addr1/addr2 are both block-window addresses currently
* loaded from two different physical devices (Phase 8 v3, 2026-09-23) --
* the check MOVE/CMOVE/CMOVE> call before copying to refuse a same-VM,
* cross-device raw block copy. See this function's own doc comment in
* block_words.c for exactly what is and isn't flagged.
* @param vm VM performing the copy
* @param addr1 first VM offset (source or destination, order doesn't matter)
* @param addr2 second VM offset
* @return 1 if this copy should be refused, 0 otherwise
*/
int blk_vm_is_cross_device_copy(VM *vm, vaddr_t addr1, vaddr_t addr2);
#endif /* BLOCK_WORDS_H */
+7
View File
@@ -44,6 +44,7 @@
#include "../../include/word_registry.h"
#include "vm.h"
#include "../../include/log.h"
#include "include/block_words.h" /* blk_vm_is_cross_device_copy() -- Phase 8 v3 */
#include <string.h>
@@ -231,6 +232,12 @@ void memory_word_move(VM *vm) {
vm->error = 1;
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, addr1, addr2)) {
vm->error = 1;
return;
}
uint8_t *src = vm_ptr(vm, addr1);
uint8_t *dst = vm_ptr(vm, addr2);
memmove(dst, src, len);
+17
View File
@@ -46,6 +46,7 @@
#include "../../include/log.h"
#include "../../include/vm.h"
#include "../../include/vm_api.h"
#include "include/block_words.h" /* blk_vm_is_cross_device_copy() -- Phase 8 v3 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
@@ -626,6 +627,14 @@ static void string_word_cmove(VM *vm) {
(long) addr1, (long) addr2, (long) u);
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, src, dst)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE: refused cross-device block copy (src=%ld dst=%ld)",
(long) addr1, (long) addr2);
return;
}
/* Ascending copy (low->high) */
for (size_t i = 0; i < n; i++) {
@@ -665,6 +674,14 @@ static void string_word_cmove_greater(VM *vm) {
(long) addr1, (long) addr2, (long) u);
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, src, dst)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE>: refused cross-device block copy (src=%ld dst=%ld)",
(long) addr1, (long) addr2);
return;
}
/* Descending copy (high->low) */
for (size_t i = n; i > 0; i--) {