Compare commits
8
Commits
v2.1.0
...
6302dcb50e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6302dcb50e | ||
|
|
26c1117ccd | ||
|
|
b7d9ed5425 | ||
|
|
81049da268 | ||
|
|
089ab2160e | ||
|
|
2406158668 | ||
|
|
2008c4596a | ||
|
|
7306872848 |
+20
-31
@@ -1,37 +1,26 @@
|
||||
# FABRIC-3.5.md — the Tripod/kernel reshuffle
|
||||
|
||||
**Status: REOPENED 2026-09-19, by direct instruction ("reopen 3.5 and write it up as a gap
|
||||
analysis section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set. The design
|
||||
phase remains closed; §XXXI adds findings, not new design.**
|
||||
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** Per §XXVI.5's own close
|
||||
condition ("this document closes when the tag exists"). Produced the full design ruling for
|
||||
the Tripod/kernel reshuffle: Hermes moves from a FORTH VM into the kernel as kernel-Hermes; the
|
||||
Tripod becomes Hera/Artemis/Hestia; every design question this document opened is ruled (§XXX.7
|
||||
/ §XLI's punch list). Execution against that design is recorded in `FABRIC-3.6.md`, not here —
|
||||
all five phases (0–5) closed there, ending in the `v2.1.0` tag this header names. **Nothing is
|
||||
carried forward and no successor is created** — this document is not part of the
|
||||
`FABRIC-0 → -1 → -2 → -3` chain (§XXVI.5), so closing it hands nothing to a successor.
|
||||
**`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare metal boot);
|
||||
nothing here ever superseded it.
|
||||
|
||||
> **Prior status, kept rather than overwritten: DESIGN PHASE CLOSED as of 2026-09-19, by
|
||||
> direct instruction ("close the document for now"). Not yet archival.** That close stood for
|
||||
> the duration of the sweep and its substance is unchanged — every design question was and
|
||||
> remains ruled. The reopen is recorded rather than the close deleted, per this series' own
|
||||
> rule against silently rewriting a prior state.
|
||||
|
||||
**What is closed, and what is deliberately not.** Every design question this document opened is
|
||||
ruled — see §XXX.7. **No code has been written and no code is authorized.** The execution
|
||||
sequence (§XXVI.6, as amended by §XXX.7) is untouched: the surgical strip, the build, the
|
||||
Isabelle/HOL pass, the documentation sweep, the SBOM, the merge to `master`, and the `v2.1.0`
|
||||
tag all remain ahead.
|
||||
|
||||
**This is therefore a narrower close than §XXVI.5 specified**, and the difference is stated
|
||||
rather than glossed. §XXVI.5 ruled that this document closes *when the tag exists*, in
|
||||
`FABRIC-2.md`'s CLOSED/ARCHIVAL form, naming the tag it closed at. **The tag does not exist**,
|
||||
so claiming that close would be a label running ahead of the real state — the exact error
|
||||
`FABRIC-3.md` §I.2 corrected when it rolled `LITHOS_VERSION` back, and the same standard §XXIX
|
||||
applied to the LTS question. **The archival close specified by §XXVI.5 still stands and still
|
||||
happens at `v2.1.0`.** This header is the provisional one the instruction's own "for now" asks
|
||||
for.
|
||||
|
||||
**Nothing is carried forward and no successor is created.** As §XXVI.5 establishes, this
|
||||
document is not part of the `FABRIC-0 → -1 → -2 → -3` chain, so closing it hands nothing to a
|
||||
successor. **`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare
|
||||
metal boot); nothing here supersedes it. The live artifact from this document is its punch list
|
||||
(§XXVI.6 / §XXX.7) — that, not this header, is what the build works from.
|
||||
|
||||
**Open by design, not by omission:** items 10–17 of §XXVI.6 are execution, not decisions.
|
||||
> **Prior status headers, kept rather than overwritten, per this series' own rule against
|
||||
> silently rewriting a prior state:**
|
||||
>
|
||||
> **REOPENED 2026-09-19**, by direct instruction ("reopen 3.5 and write it up as a gap analysis
|
||||
> section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set.
|
||||
>
|
||||
> **DESIGN PHASE CLOSED as of 2026-09-19**, by direct instruction ("close the document for
|
||||
> now"). Not yet archival at that point — the execution sequence (surgical strip, build,
|
||||
> Isabelle pass, doc sweep, SBOM, merge, tag) was still entirely ahead. That gap is now closed:
|
||||
> `FABRIC-3.6.md`'s Phases 0–5 are the execution this header once described as pending.
|
||||
Three things recorded here are explicitly *outside* this reshuffle and still need their own
|
||||
authorization — the `.claude/CLAUDE.md`/`MANIFEST.md` documentation reconciliation (§XXVI.1),
|
||||
the `src/*.c.bak` hygiene question (§XXII.5), and the stray `refs/heads/v2.0.1` branch and
|
||||
|
||||
+41
-4
@@ -1,6 +1,24 @@
|
||||
# FABRIC-3.6.md — the Tripod/kernel reshuffle: execution log
|
||||
|
||||
> ## START HERE — session handoff
|
||||
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** All five phases (0–5) closed —
|
||||
Category A strip, Hestia relocation/birth, kernel-Hermes build (allocator, arena, message
|
||||
types), Stage-cutover of every FORTH-owned message type, the Category B strip (Hermes VM +
|
||||
`messaging.4th` removed), the Isabelle pass, the documentation sweep, `make sbom`, the version
|
||||
bump, and the merge to `master` + tag. **This document is closed; the reshuffle it tracked is
|
||||
done, not merely planned.** Per §XXVI.5 (cited here, ruled in `FABRIC-3.5.md`, also closed at
|
||||
this same tag): closing this document hands nothing to a successor and does not touch
|
||||
`FABRIC-3.md`, which remains open and authoritative for its own topic (bare metal boot).
|
||||
|
||||
**The `START HERE` section immediately below is kept as historical record of how this
|
||||
execution began — it describes a session about to start work, not the current state.** Do not
|
||||
follow its "first action: task 0.0" instruction; every task it points at is closed. If future
|
||||
work touches this fleet again (a Phase 8 PKI elevation entrypoint, further hardware bring-up,
|
||||
etc.), it gets its own new document, not a reopening of this one — matching the discipline
|
||||
`FABRIC-3.5.md`'s own close just followed.
|
||||
|
||||
---
|
||||
|
||||
> ## START HERE — session handoff (historical — reshuffle complete, see status above)
|
||||
>
|
||||
> **If you have just been told "go build it", read this section, then `FABRIC-3.5.md`'s §XLI,
|
||||
> then start at task 0.0 below. Do not re-derive the design — it is settled.**
|
||||
@@ -1345,7 +1363,7 @@ intermediate states.
|
||||
build its own entrypoint rather than resuming `SEND-ELEVATE-REQUEST`. No further action
|
||||
this phase.
|
||||
|
||||
## Phase 5 — Close-out
|
||||
## Phase 5 — Close-out — **COMPLETE, tag `v2.1.0` cut 2026-09-22**
|
||||
|
||||
**Note on commit granularity:** tasks 5.1–5.4 landed as one commit (`a4ad14a`), a deliberate
|
||||
deviation from the per-task-commit discipline this document has otherwise used since Phase 0.
|
||||
@@ -1511,8 +1529,27 @@ opposite of what per-task commits are for. Tasks 5.5–5.7 return to one-commit-
|
||||
closed `2026-09-18T21:28:26Z` — 20 seconds later, before any of this reshuffle's actual
|
||||
work existed on the branch. Already closed, already not merged, blocks nothing. Nothing
|
||||
to resolve beyond recording what it is.
|
||||
- [ ] **5.6** — Merge to `master`; tag `v2.1.0`.
|
||||
- [ ] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
|
||||
- [x] **5.6** — Merge to `master`; tag `v2.1.0`.
|
||||
2026-09-22 · **Closed, explicit approval obtained first (this is a shared-branch
|
||||
operation on the sole production line — the plan authorizing it is not a standing
|
||||
grant, per `.claude/CLAUDE.md`'s own scope rule).** `master` was a strict ancestor of
|
||||
this branch (`git merge-base --is-ancestor origin/master HEAD` — true, 0 commits unique
|
||||
to `master`, 85 unique to this branch), so this was a clean fast-forward, not a merge
|
||||
commit: `git push origin HEAD:refs/heads/master` (`e56974e..8edbd3c`), never checking out
|
||||
`master` locally, so the uncommitted `FABRIC-3.md` WIP on this branch's working tree was
|
||||
never touched. Tagged `v2.1.0` (annotated, `8edbd3c`) and pushed. `refs/heads/v2.0.1`
|
||||
(task 5.5's finding — fully merged, safe to delete) also deleted this same pass, explicit
|
||||
approval obtained first.
|
||||
- [x] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
|
||||
2026-09-22 · **Closed.** `FABRIC-3.5.md`'s provisional "design phase closed, not yet
|
||||
archival" header replaced with the real CLOSED/ARCHIVAL form, naming `v2.1.0`, per its
|
||||
own §XXVI.5 spec — prior status headers kept underneath, not deleted, matching this
|
||||
series' own no-silent-rewrite rule. This document (`FABRIC-3.6.md`) closed the same way:
|
||||
a CLOSED/ARCHIVAL status banner added above the `START HERE` section, which is kept as
|
||||
historical record of how the session began rather than removed. Neither closure triggers
|
||||
the `FABRIC-0 → -1 → -2 → -3` carry-forward chain (both documents are standalone topic
|
||||
documents, not part of it) and neither touches `FABRIC-3.md`, which stays open for its
|
||||
own topic. **Phase 5, and the Tripod/kernel reshuffle itself, are complete.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint
|
||||
|
||||
**Status: OPEN — design only, no code written or authorized.**
|
||||
|
||||
**CORRECTION (2026-09-22, before any code was written against this document): §2's central
|
||||
claim — that the old `SEND-ELEVATE-REQUEST` passed a raw cross-VM address into `ELEVATE-GRANT`'s
|
||||
`waddr` — is wrong.** Found while starting Part A's implementation: reading the actual deleted
|
||||
source (`git show 3e201c8^:capsules/common/messaging.4th`, block 5040) shows
|
||||
`SEND-ELEVATE-REQUEST` copied the target word's **name as literal character bytes** (via
|
||||
`ELEVATE-REQ-APPEND`'s `CMOVE`) into a scratch buffer, building the text `S" <name-text>" <pk0>
|
||||
<pk1> <pk2> <pk3> ELEVATE-GRANT`, and sent *that whole string* to Hera. When Hera's own
|
||||
interpreter runs `S" <name-text>"`, it allocates a fresh string **in Hera's own memory** and
|
||||
pushes Hera's own valid address — no numeric cross-VM address ever appears anywhere in this
|
||||
flow. §2 was written from `ELEVATE-GRANT`'s signature alone, assuming the caller forwarded a raw
|
||||
address, without first reading how the caller actually built its message. It didn't.
|
||||
|
||||
**What is still real, much narrower than originally claimed:** if a future caller ever spliced
|
||||
*attacker-influenced* text into the name field without checking for an embedded `"` character,
|
||||
that could break out of the `S" ... "` literal early and inject arbitrary FORTH source, executed
|
||||
with Hera's privilege. That's an input-validation discipline question for whoever writes the new
|
||||
caller (validate: no embedded `"`, or just always use a compile-time-fixed literal name, never a
|
||||
runtime-supplied one) — not an architectural cross-VM-memory defect requiring the buffer/message
|
||||
redesign §3 originally called for. **§3's proposed mechanism (Hera-side fixed receive buffer,
|
||||
kernel-constructed integer-literal-only command) is not needed** — the original text-copy
|
||||
design was already safe against the bug as actually diagnosed. Kept below, struck through, for
|
||||
traceability, per this series' own rule against silently rewriting a prior state.
|
||||
|
||||
Successor to
|
||||
`FABRIC-3.5.md`/`FABRIC-3.6.md` (both CLOSED/ARCHIVAL at `v2.1.0`) for exactly one topic: the
|
||||
Ed25519-challenge-response elevation entrypoint that `.claude/CLAUDE.md`'s ACL section names as
|
||||
Phase 8, the last open item in the word-level ACL system. This is a **new document**, not a
|
||||
reopening of `FABRIC-3.6.md` — that document's own close header says future fleet work gets its
|
||||
own document, and this is that.
|
||||
|
||||
**Provenance.** Written 2026-09-22, immediately after `FABRIC-3.6.md`'s close, from a design
|
||||
conversation with Captain Bob about a security concern he raised directly: how to rebuild the
|
||||
elevation entrypoint that Phase 4's Category B strip left dangling, without reopening a hole.
|
||||
The design below was proposed, and Captain Bob asked for it in writing here rather than left
|
||||
only in session memory.
|
||||
|
||||
---
|
||||
|
||||
## 1. What's dangling, and why
|
||||
|
||||
`FABRIC-3.6.md` Phase 4 (Category B strip, 2026-09-22) deleted `capsules/common/messaging.4th`
|
||||
after every FORTH-owned message type had been cut over to kernel-Hermes. One casualty was
|
||||
collateral, not intended: `SEND-ELEVATE-REQUEST` (`messaging.4th` block 5040) was the only
|
||||
caller of both `KH-ELEVATE-SEND` (`src/starkernel/repl.c`) and, transitively, `ELEVATE-GRANT`
|
||||
(`capsules/zuse-eligibility.4th`, blocks 4021–4022). All three still exist in the tree.
|
||||
`ELEVATE-GRANT` is still loaded at boot (`capsules/init.4th:18`). Nothing can call it any more.
|
||||
|
||||
Captain Bob's decision at the time (`FABRIC-3.6.md`'s own Phase 4 entry): leave it unreachable,
|
||||
don't patch a caller back in as part of that strip. Phase 8 builds its own entrypoint instead of
|
||||
resuming this one. **This document is that entrypoint's design.**
|
||||
|
||||
<details>
|
||||
<summary>Original §2/§3 (WRONG — see the correction at the top of this document; kept for
|
||||
traceability, not current design)</summary>
|
||||
|
||||
### 2. The security hole in the old mechanism — found before any code was written
|
||||
|
||||
`ELEVATE-GRANT`'s signature, unchanged since it was written:
|
||||
|
||||
```
|
||||
ELEVATE-GRANT ( waddr wu pk0 pk1 pk2 pk3 -- )
|
||||
```
|
||||
|
||||
`waddr`/`wu` are an address/length pair meant to point at the string naming the word to elevate.
|
||||
`pk0`–`pk3` are the caller's Ed25519 pubkey, packed 8 bytes per cell (`ELEVATE-PUBKEY-UNPACK`,
|
||||
`mama_forth_words.c`).
|
||||
|
||||
**The old `SEND-ELEVATE-REQUEST` computed `waddr` in the *sending* VM's own address space, but
|
||||
`ELEVATE-GRANT` always executes on Hera** (`ELEVATE-GRANT always runs on Hera` — `repl.c`'s own
|
||||
comment on `KH-ELEVATE-SEND`, still there). A word's name string lives in the sending VM's
|
||||
memory. `ELEVATE-GRANT` dereferences `waddr` in Hera's memory. Those are not the same address
|
||||
space by construction — `vaddr_t` is per-VM.
|
||||
|
||||
**Consequence:** whoever controls `waddr` controls what bytes `NAME>XT` reads and resolves as a
|
||||
word name, in Hera's dictionary, not the caller's. This is not "the string might be malformed" —
|
||||
it is a primitive for making Hera's own `ELEVATE-GRANT` grant `ACL-ALLOW!`/`ACL-TTL!` on
|
||||
*whatever dictionary entry the attacker's chosen `waddr` happens to land on*, regardless of what
|
||||
word name the caller claims to be requesting elevation for. A caller who can influence `waddr`
|
||||
at all — not forge a signature, not defeat `zuse_eligibility_is_member()`, just choose a number
|
||||
— has a privilege-escalation primitive against the fleet governor.
|
||||
|
||||
This was never exploited (the entrypoint has had zero live callers since the file that called it
|
||||
was deleted), and is reported here as a design defect found by inspection, not a live incident.
|
||||
|
||||
### 3. The fix: never cross an address, only ever cross bytes
|
||||
|
||||
This project already solved the general version of this problem once, this same session
|
||||
(`FABRIC-3.6.md` tasks 3.8/3.9, the payload-aliasing fix): a kernel-Hermes message's payload
|
||||
must be **copied into the message's own storage**, never a pointer into the sender's memory that
|
||||
might be reused or freed before the receiver drains it. `SkHermesMessage.payload_buf`
|
||||
(`include/starkernel/vm/kernel_hermes.h:160`, `SK_HERMES_CHUNK_MAX_PAYLOAD` = 1024 bytes) is
|
||||
exactly that fix, already built, already proven on all three architectures.
|
||||
|
||||
**The elevation entrypoint's hole is the same defect one level up: an address crossing a
|
||||
boundary it isn't valid on the other side of.** The fix generalizes directly:
|
||||
|
||||
1. **Never send `waddr`/`wu` across the kernel-Hermes boundary.** Send the pubkey (32 bytes,
|
||||
already the right shape for `payload_buf`) and the target word's **name, as literal bytes**,
|
||||
copied inline into the message payload — not an address, the actual characters. This is
|
||||
already how `CONSOLE-CMD-EVENT`'s payload works (a command string's bytes, not a pointer to
|
||||
one), so this isn't a new pattern, it's applying the existing one to the one caller that
|
||||
still passed a raw address.
|
||||
|
||||
2. **On receipt, kernel-Hermes's C drain-checkpoint copies those name bytes into a small,
|
||||
fixed, kernel-owned buffer that already lives in Hera's own VM memory** — a receive-side
|
||||
mirror of the existing send-side pattern (`g_kh_elevate_buf`, `repl.c:445`, is the
|
||||
already-built precedent for "a static buffer this mechanism owns"; this needs its Hera-side
|
||||
counterpart). The buffer's address is a compile-time constant, known to the kernel, never
|
||||
computed from anything the caller supplied.
|
||||
|
||||
3. **The FORTH command handed to `vm_interpret()` on Hera references only that fixed buffer's
|
||||
address and length as plain integer literals.** Both are always kernel-controlled. Neither is
|
||||
ever derived from caller input. `ELEVATE-GRANT` itself does not change — same signature, same
|
||||
`zuse_eligibility_is_member()` check, same `ACL-ALLOW!`/`ACL-TTL!` grant. Policy logic stays
|
||||
in FORTH, per `ACL.4th`'s own rule (no new C primitive for policy) — this fix is entirely
|
||||
about how bytes get from one VM to another, not about who is allowed to grant what.
|
||||
|
||||
**Why this closes the hole structurally, not by validation:** there is no string to sanitize and
|
||||
no address to bounds-check, because the interpreted command never contains anything an attacker
|
||||
touched except opaque data bytes that get copied, never dereferenced as a pointer, by the
|
||||
receiving side. The class of bug (cross-address-space pointer confusion) becomes impossible by
|
||||
construction, the same way `payload_buf` made use-after-free impossible by construction rather
|
||||
than by careful lifetime tracking.
|
||||
|
||||
</details>
|
||||
|
||||
## 2 (corrected). What the old mechanism actually did, and the one real gap in it
|
||||
|
||||
Re-read from the actual deleted source (`git show 3e201c8^:capsules/common/messaging.4th`,
|
||||
blocks 5039–5040): `SEND-ELEVATE-REQUEST ( pk3 pk2 pk1 pk0 waddr wu -- )` used `waddr`/`wu` only
|
||||
to `CMOVE` the target word's **name bytes**, as text, into a scratch buffer
|
||||
(`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`) it owned — building the literal string `S"
|
||||
<name-text>" <pk0> <pk1> <pk2> <pk3> ELEVATE-GRANT` entirely in the *sending* VM's own memory.
|
||||
Only that finished string — not `waddr` itself — went to `KH-ELEVATE-SEND` and across to Hera.
|
||||
When Hera's interpreter runs `S" <name-text>"`, Hera's own `S"` allocates a fresh string **in
|
||||
Hera's own memory** and pushes Hera's own valid address. `waddr`/`wu` never cross the VM
|
||||
boundary as numbers at any point — only as copied character content. There is no cross-VM
|
||||
pointer dereference anywhere in this flow.
|
||||
|
||||
**The one real, much narrower gap:** the name text is spliced into `S" ... "` with no check for
|
||||
an embedded `"` character. If a future caller ever passed attacker-influenced text as the name
|
||||
(none ever did — the word had zero live callers), a `"` in the name would close the string
|
||||
literal early and let the rest of the name execute as raw FORTH source, with Hera's privilege.
|
||||
This is a caller-discipline / input-validation question, not an architectural defect: either
|
||||
always use a compile-time-fixed name literal at the call site (no runtime input, no risk at
|
||||
all), or validate for an embedded `"` before building the command if a name ever does need to
|
||||
come from something less trusted than the call site's own source code.
|
||||
|
||||
**Net effect on Phase 8 v1's scope:** Part A, as originally conceived in §3 above, is not
|
||||
needed. If a `SEND-ELEVATE-REQUEST` replacement is ever built, it can follow the original
|
||||
text-copy design as-is, with the one-line `"`-check added if and only if the name is ever
|
||||
runtime-supplied rather than a fixed literal. No kernel-Hermes/`repl.c` changes required. Part B
|
||||
(`capsules/zuse.4th`, gating `ZUSE-ELIGIBILITY-ADD`) stands on its own, independently verified,
|
||||
unaffected by this correction.
|
||||
|
||||
## 4. What Phase 8 actually needs to build
|
||||
|
||||
Corrected per §2's re-read above. Concretely, when Phase 8 next picks this up:
|
||||
|
||||
- If a caller into `ELEVATE-GRANT` is ever needed again, rebuild it close to the original
|
||||
`SEND-ELEVATE-REQUEST` shape (`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`/text-copy into `S" ...
|
||||
"`) — it was already safe. Add the one-line embedded-`"` check only if the name is ever
|
||||
runtime-supplied rather than a call-site literal. No `kernel_hermes.c`/`kernel_hermes.h`/
|
||||
`repl.c` changes needed for this.
|
||||
- `ELEVATE-GRANT` unchanged either way.
|
||||
- **Part B is done** (`capsules/zuse.4th`, committed and three-arch verified this session,
|
||||
2026-09-22) — `ZUSE-ELIGIBILITY-ADD` denied by default, granted only inside `ACL-ZUSE-BOOT`'s
|
||||
authenticated branch. This closes the actual "grant yourself eligibility with no real drive at
|
||||
all" path — a real, independently-confirmed gap, unaffected by this correction.
|
||||
- **Defending against a cloned drive — settled, 2026-09-23: not going to happen, by design.**
|
||||
Today, WIREBIND/MINT trust whatever identity is stored on an attached thumbdrive with no
|
||||
challenge at all (confirmed by grep: no `ed25519_sign`/`ed25519_verify` call anywhere in
|
||||
`capsule_wirebind.c` or `capsule_mint.c`), and the private key seed itself is stored in
|
||||
plaintext on the drive, read in the same devblock as the pubkey/cert. **This is accepted, not
|
||||
a gap.** Captain Bob, directly: *"nothing like a pin or a password or secret code or any
|
||||
bullshit... Everybody has secrets. There's only the drive."* Physical possession of the drive
|
||||
is the entire, deliberate credential model — a byte-for-byte clone being equivalent to the
|
||||
real drive is the accepted design, not a defect to close. A PIN/passphrase second factor was
|
||||
built, live-tested on all three architectures, and fully reverted before commit
|
||||
(`/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`, now marked rejected; memory
|
||||
`feedback_no_knowledge_factor_identity`) — **do not revisit a knowledge-factor approach here.**
|
||||
Any future work in this space needs a fundamentally different mechanism (not something typed
|
||||
and known) or stays an accepted limitation.
|
||||
- **Phase 8 v3, 2026-09-23 — done, a distinct and narrower concern from the item above.** The
|
||||
"accepted limitation" above is about a drive image copied *outside* StarshipOS entirely (e.g.
|
||||
imaged on an external computer) — that's still accepted, unchanged by this item. Captain Bob
|
||||
separately asked to close a narrower, different threat: **cloning a device's block content
|
||||
from *within* StarshipOS's own console**, using its own stock, unpinned words
|
||||
(`<src> BLOCK <dst> BUFFER 1024 MOVE`/`RELOCATE-BLOCK`). That's now closed — `MOVE`/`CMOVE`/
|
||||
`CMOVE>`/`RELOCATE-BLOCK` all refuse a same-VM, cross-device copy, verified live on all three
|
||||
architectures. See `/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`'s "Phase 8 v3"
|
||||
section for the full design and verification record. The identity record itself
|
||||
(seed/pubkey/cert) was already unreachable from FORTH before this — this closes the one real
|
||||
gap the research found: ordinary block content, not the identity record.
|
||||
|
||||
## 5. What this document is not
|
||||
|
||||
Not a reopening of `FABRIC-3.6.md`, not a change to anything currently built, not an
|
||||
authorization to write code. Per this series' own convention: design here, execution gets its
|
||||
own document when the work actually starts, the same relationship `FABRIC-3.5.md` had to
|
||||
`FABRIC-3.6.md`.
|
||||
+10
-1
@@ -597,6 +597,7 @@ KERNEL_OBJS := \
|
||||
.PHONY: qemu qemu-esp qemu-gdb
|
||||
.PHONY: thumbdrive iso-usb
|
||||
.PHONY: info help
|
||||
.PHONY: FORCE
|
||||
|
||||
# ==============================================================================
|
||||
# MAIN TARGETS
|
||||
@@ -627,7 +628,15 @@ clean-kernel:
|
||||
# BUILD RULES
|
||||
# ==============================================================================
|
||||
|
||||
include/version.h:
|
||||
FORCE:
|
||||
|
||||
# FORCE prerequisite (matches the hosted Makefile's own pattern, Makefile:613/626):
|
||||
# this file is also written by the hosted Makefile with different, incompatible
|
||||
# content (no LITHOS_VERSION/LITHOS_VERSION_STR) -- without FORCE, a bare build
|
||||
# here after a hosted `make` run silently reuses that stale/wrong-flavored file
|
||||
# and fails with "LITHOS_VERSION_STR undeclared" deep in kernel_main.c, instead
|
||||
# of regenerating its own correct version.
|
||||
include/version.h: FORCE
|
||||
@mkdir -p include
|
||||
@BUILD_TS=$$(date -Iseconds 2>/dev/null || echo "unknown"); \
|
||||
printf '#ifndef STARFORTH_VERSION_H\n#define STARFORTH_VERSION_H\n\n' > $@; \
|
||||
|
||||
@@ -12,6 +12,8 @@ Block 4016
|
||||
( >BODY-then-store, so a pinned CONSTANT isn't tamper-proof. )
|
||||
( Read with ZUSE-PUBKEY@ / ZUSE-CERT-INSTALLED? -- both C )
|
||||
( primitives, read-only; the seed has no FORTH access at all. )
|
||||
( ZUSE-ELIGIBILITY-ADD denied by default -- see block 4017. )
|
||||
0 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
|
||||
|
||||
Block 4017
|
||||
( ACL-ZUSE-BOOT ( -- ) re-invokable: capsule_zuse_boot.c )
|
||||
@@ -23,6 +25,8 @@ Block 4017
|
||||
: ACL-ZUSE-BOOT ( -- )
|
||||
ZUSE-CERT-INSTALLED? IF
|
||||
ZUSE-AUTHENTICATE
|
||||
1 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
|
||||
['] ZUSE-ELIGIBILITY-ADD ACL-PIN
|
||||
LOG-INFO" zuse: activated"
|
||||
ELSE
|
||||
LOG-INFO" zuse: NOT activated -- no cert installed"
|
||||
|
||||
@@ -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
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
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