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
+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--) {