Phase 8 v3: refuse a same-VM, cross-device raw block copy from within StarshipOS
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:
co-authored by
Claude Sonnet 5
parent
b7d9ed5425
commit
26c1117ccd
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
|
||||
@@ -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--) {
|
||||
|
||||
Reference in New Issue
Block a user