From 8b83e0a72dfc3692d11d56dd8e319a00e024a4b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 18:14:54 +0000 Subject: [PATCH] feat(kernel): sk_v4_mint_zuse -- the real Zuse minted onto a second stick, read back before the marker names her (#92, 4.5) [skip ci] Asks for a stick, shows its size, writes nothing until YES; then gpt_write_one, the partition's first devblocks zeroed, a fresh Zuse minted, read back, and only then the marker's real key. Any failure before the marker: 'failed -- nothing changed'. The gate remembers the last stick it turned away as a spare, offered first by the next mint. Builds on three ISAs; not yet called. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2 --- ...-10-29-stick-5-proxy-zuse-and-real-zuse.md | 2 +- kernel/Makefile | 4 +- kernel/include/starkernel/v4/sk_v4_identity.h | 11 ++ kernel/include/starkernel/v4/sk_v4_mint.h | 16 ++ kernel/src/v4/sk_v4_identity.c | 34 ++++- kernel/src/v4/sk_v4_mint.c | 143 ++++++++++++++++++ 6 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 kernel/include/starkernel/v4/sk_v4_mint.h create mode 100644 kernel/src/v4/sk_v4_mint.c diff --git a/docs/v4.0.0/plans/2026-10-29-stick-5-proxy-zuse-and-real-zuse.md b/docs/v4.0.0/plans/2026-10-29-stick-5-proxy-zuse-and-real-zuse.md index 15ffce70..3c5965e9 100644 --- a/docs/v4.0.0/plans/2026-10-29-stick-5-proxy-zuse-and-real-zuse.md +++ b/docs/v4.0.0/plans/2026-10-29-stick-5-proxy-zuse-and-real-zuse.md @@ -355,7 +355,7 @@ Tasks 1-3 accepted and merged (#159, master 594d2f93); #155 merged with them. - [x] 4.2 run `mint_zuse` on amd64 before any kernel change: FAIL, "UNKNOWN WORD: 'MINT-ZUSE'" (logs/20261108-175928); the prompt check tightened to need the minted line first - [x] 4.3 `gpt_write_one()` in `kernel/src/gpt.c` (one partition, protective MBR, both headers): builds clean on three ISAs; checked on the host (a throwaway harness, not in the tree) -- 600 MiB and 32 MiB blank images and the pair's used Zuse stick each give one identity partition, `gpt_read` reads it back, `sfdisk --verify` clean. Not yet called. - [x] 4.4 `sk_v4_identity_offer_wait()` / `sk_v4_identity_recheck_offered()`: a stick offered, not attached, while a mint waits (`g_mint_waiting`, `g_offered` in `sk_v4_identity.c`; any stick not this system's and readable -- blank, foreign or a bad table; ends on a stick, a key, or the system stick leaving). Builds on three ISAs; not yet called. -- [ ] 4.5 `sk_v4_mint.c`: the flow -- plug-in line, size line, `YES`, GPT, zeroed, minted, read back, marker +- [x] 4.5 `sk_v4_mint.c`: the flow -- plug-in line, size line, `YES`, GPT, zeroed, minted, read back, marker. Builds on three ISAs; not yet called. The gate gained a **spare**: the last stick turned away as not this system's (or handed back by a refused mint) is offered first by the next mint (for `mint_zuse_refuse`: "a second `MINT-ZUSE` is offered again"); and accessors `sk_v4_identity_stick_in/offered_present/release_offered/marker/set_marker`. - [ ] 4.6 `MINT-ZUSE` in `sk_v4_words[]` (request 2): word_count 317 -> 318 - [ ] 4.7 the real Zuse stick recognised and attached after the system stick; her session - [ ] 4.8 `mint_zuse` passes on amd64; then the other tests: `mint_zuse_refuse`, `mint_pull_system`, `zuse_stick_first`, `mint_small`, the pair's stick offered diff --git a/kernel/Makefile b/kernel/Makefile index 6ce5b28d..f43feb87 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -728,8 +728,8 @@ V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard V4_ROM_C := $(BUILD_DIR)/v4_rom_64.c V4_MAP_C := $(BUILD_DIR)/v4_map_64.c -LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c -KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c +LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c +KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c $(KERNEL_SRC)/v4/sk_v4_marker.c $(KERNEL_SRC)/v4/sk_v4_grow.c $(KERNEL_SRC)/v4/sk_v4_mint.c # v4/system/boot.c is the boot the hosted v4 system runs too: the ROM # assembles the system, then the capsules from this kernel's own capsule # directory, then the prompt. diff --git a/kernel/include/starkernel/v4/sk_v4_identity.h b/kernel/include/starkernel/v4/sk_v4_identity.h index 979b9e6f..c8b7486d 100644 --- a/kernel/include/starkernel/v4/sk_v4_identity.h +++ b/kernel/include/starkernel/v4/sk_v4_identity.h @@ -9,6 +9,7 @@ #include "uefi.h" /* BootDisk */ #include "blkio.h" +#include "starkernel/v4/sk_v4_marker.h" /* After POST and the block chain: every xHCI controller brought up, and the * heartbeat started, as the v3 path does before its headless wait. `boot_disk` @@ -38,6 +39,16 @@ void sk_v4_identity_check_now(void); * leaves: 0. */ int sk_v4_identity_offer_wait(blkio_dev_t **raw, uint64_t *bytes); +/* For the mint: the system stick's Artemis is attached; the offered stick is + * still plugged in; the offered stick handed back unused (a mint refused or + * stopped -- the next mint offers it again); the genesis marker as read, and + * as the mint has just written it. */ +int sk_v4_identity_stick_in(void); +int sk_v4_identity_offered_present(void); +void sk_v4_identity_release_offered(void); +const sk_v4_marker_t *sk_v4_identity_marker(void); +void sk_v4_identity_set_marker(const sk_v4_marker_t *m); + /* The arrival check run again on the offered stick, now that the mint has * written its GPT and identity (it is then recognised as the real Zuse's). */ void sk_v4_identity_recheck_offered(void); diff --git a/kernel/include/starkernel/v4/sk_v4_mint.h b/kernel/include/starkernel/v4/sk_v4_mint.h new file mode 100644 index 00000000..f1ea7828 --- /dev/null +++ b/kernel/include/starkernel/v4/sk_v4_mint.h @@ -0,0 +1,16 @@ +/* sk_v4_mint.h -- MINT-ZUSE: the real Zuse minted onto a second stick + * (docs/v4.0.0/STICK.md 6a; plan 2026-10-29-stick-5, Task 4). + * + * Built only with STARFORTH_V4=1 (Kconfig.kernel). + */ +#ifndef STARKERNEL_V4_SK_V4_MINT_H +#define STARKERNEL_V4_SK_V4_MINT_H + +/* The whole of STICK.md 6a's minting: asks for a stick, shows its size, writes + * nothing until YES, then a GPT and a fresh Zuse identity on it, read back, + * and only then the real key in the genesis marker. 0 when she is minted; + * -1 otherwise, with its line said and the marker as it was. Run by the + * MINT-ZUSE word, and (Task 5) the fixed screen's R. */ +int sk_v4_mint_zuse(void); + +#endif /* STARKERNEL_V4_SK_V4_MINT_H */ diff --git a/kernel/src/v4/sk_v4_identity.c b/kernel/src/v4/sk_v4_identity.c index 64be80e2..fffefc06 100644 --- a/kernel/src/v4/sk_v4_identity.c +++ b/kernel/src/v4/sk_v4_identity.c @@ -100,6 +100,9 @@ static uint64_t g_last_beat; * not refused; it is remembered here until the mint is done with it */ static int g_mint_waiting; static sk_v4_drive_t *g_offered; +/* the last stick turned away as not this system's (or handed back by a mint + * that was refused): still plugged in, so a mint offers it first */ +static sk_v4_drive_t *g_spare; static const BootDisk *g_boot_disk; /* the loader's: the disk it booted from */ static int g_stick_in; /* a stick's Artemis partition is attached */ @@ -297,6 +300,7 @@ static int sk_v4_drive_arrived(sk_v4_drive_t *d) } if (gpt_rc != GPT_OK || !sk_v4_is_this_stick(nparts)) { console_println("USB: a drive that is not this system's stick -- not used"); + if (gpt_rc != GPT_EIO) g_spare = d; return 1; } if (sk_v4_grow_stick(d, &nparts) != 0) { @@ -370,6 +374,7 @@ static void sk_v4_drive_left(sk_v4_drive_t *d) blkio_dev_t *dev = d->used; if (d == g_offered) g_offered = (sk_v4_drive_t *)0; /* the offered stick, pulled */ + if (d == g_spare) g_spare = (sk_v4_drive_t *)0; if (dev) { d->used = (blkio_dev_t *)0; @@ -457,7 +462,8 @@ int sk_v4_identity_offer_wait(blkio_dev_t **raw, uint64_t *bytes) { int got = 0; - g_offered = (sk_v4_drive_t *)0; + g_offered = g_spare; /* one already plugged in, turned away */ + g_spare = (sk_v4_drive_t *)0; g_mint_waiting = 1; for (;;) { sk_v4_identity_beat(); @@ -474,6 +480,32 @@ int sk_v4_identity_offer_wait(blkio_dev_t **raw, uint64_t *bytes) return got; } +int sk_v4_identity_stick_in(void) +{ + return g_stick_in; +} + +int sk_v4_identity_offered_present(void) +{ + return g_offered != (sk_v4_drive_t *)0; +} + +void sk_v4_identity_release_offered(void) +{ + g_spare = g_offered; + g_offered = (sk_v4_drive_t *)0; +} + +const sk_v4_marker_t *sk_v4_identity_marker(void) +{ + return &g_marker; +} + +void sk_v4_identity_set_marker(const sk_v4_marker_t *m) +{ + g_marker = *m; +} + void sk_v4_identity_recheck_offered(void) { sk_v4_drive_t *d = g_offered; diff --git a/kernel/src/v4/sk_v4_mint.c b/kernel/src/v4/sk_v4_mint.c new file mode 100644 index 00000000..0dab36a5 --- /dev/null +++ b/kernel/src/v4/sk_v4_mint.c @@ -0,0 +1,143 @@ +/* sk_v4_mint.c -- MINT-ZUSE: the real Zuse minted onto a second stick + * (docs/v4.0.0/STICK.md 6a; plan 2026-10-29-stick-5, Task 4). + * + * The stick is asked for, offered (sk_v4_identity_offer_wait), its size + * shown, and nothing is written until YES is typed. Then a GPT with one + * StarshipOS identity partition (gpt_write_one), the partition's first + * devblocks zeroed (a used stick's old bytes must not read as an identity), a + * fresh Zuse minted into it, read back -- and only then the real key written + * into the genesis marker on the system stick's Artemis. Any failure before + * the marker leaves the marker as it was: the old Zuse stick, if there is + * one, still works. + */ +#include "starkernel/v4/sk_v4_mint.h" +#include "starkernel/v4/sk_v4_identity.h" +#include "starkernel/v4/sk_v4_marker.h" +#include "starkernel/capsule_zuse_boot.h" +#include "starkernel/homeblocks_sig.h" +#include "starkernel/kbd_input.h" +#include "starkernel/console.h" +#include "starkernel/arch.h" +#include "starkernel/gpt.h" +#include + +#define MINT_MIN_BYTES (64ULL * 1024ULL * 1024ULL) /* a stick under 64 MiB is refused */ +#define MINT_IDENT_SECTORS (500ULL * 2048ULL) /* the identity partition: 500 MiB */ +#define MINT_ZERO_FBLOCKS 32u /* its first 8 devblocks, zeroed */ + +/* "MINT-ZUSE: a stick of MiB -- all of it is erased" */ +static void say_size(uint64_t bytes) +{ + static char line[80]; + char num[24]; + uint64_t mib = bytes / (1024ULL * 1024ULL); + int n = 0, i = 0; + + do { num[n++] = (char)('0' + (int)(mib % 10u)); mib /= 10u; } while (mib && n < 23); + memcpy(line, "MINT-ZUSE: a stick of ", 22); + i = 22; + while (n) line[i++] = num[--n]; + memcpy(line + i, " MiB -- all of it is erased", 28); /* with its NUL */ + console_println(line); +} + +/* A line typed at the keyboard or the serial line, echoed, while the beat + * runs. 1 when Enter ended it; 0 when the system stick or the offered stick + * left first. */ +static int read_answer(char *buf, int max) +{ + int len = 0; + + for (;;) { + int c = sk_input_getc(); + if (c < 0) { + if (!sk_v4_identity_stick_in() || !sk_v4_identity_offered_present()) return 0; + sk_v4_identity_beat(); + arch_halt(); + continue; + } + if (c == '\r' || c == '\n') { console_putc('\n'); buf[len] = '\0'; return 1; } + if ((c == 8 || c == 127) && len > 0) { len--; console_puts("\b \b"); continue; } + if (c >= 32 && c < 127 && len < max - 1) { buf[len++] = (char)c; console_putc((char)c); } + } +} + +static int failed(void) +{ + console_println("MINT-ZUSE: failed -- nothing changed"); + sk_v4_identity_release_offered(); + return -1; +} + +int sk_v4_mint_zuse(void) +{ + static blkio_dev_t part; + static gpt_part_dev_t part_state; + static gpt_part_t parts[4]; + static uint8_t zero[BLKIO_FORTH_BLOCK_SIZE]; + blkio_dev_t *raw; + uint64_t bytes, sectors; + uint8_t guid[16], seed[32], pub[32], rseed[32], rpub[32]; + homeblocks_sig_t sig; + homeblocks_sig_result_t sig_rc; + sk_v4_marker_t m; + unsigned n, i; + int pi, rc; + char answer[8]; + + console_println("MINT-ZUSE: plug in the stick for the real Zuse"); + if (!sk_v4_identity_offer_wait(&raw, &bytes)) { + console_println("MINT-ZUSE: stopped -- nothing written"); + return -1; + } + if (bytes < MINT_MIN_BYTES) { + console_println("MINT-ZUSE: the stick is under 64 MiB"); + sk_v4_identity_release_offered(); + return -1; + } + say_size(bytes); + console_println("MINT-ZUSE: type YES to write it"); + if (!read_answer(answer, (int)sizeof answer) || strcmp(answer, "YES") != 0) { + console_println("MINT-ZUSE: stopped -- nothing written"); + if (sk_v4_identity_offered_present()) sk_v4_identity_release_offered(); + return -1; + } + + /* the stick: one identity partition, 500 MiB or the stick less 2 MiB */ + sectors = bytes / 512u; + sectors = sectors - 4096u < MINT_IDENT_SECTORS ? sectors - 4096u : MINT_IDENT_SECTORS; + rc = gpt_write_one(raw, gpt_type_starship_identity, sectors, guid); + if (rc == GPT_BAD) { + console_println("MINT-ZUSE: the stick's sectors are not 512 bytes"); + sk_v4_identity_release_offered(); + return -1; + } + if (rc != GPT_OK || gpt_read(raw, parts, 4u, &n) != GPT_OK) return failed(); + pi = gpt_find(parts, n, gpt_type_starship_identity); + if (pi < 0 || gpt_part_open(&part, &part_state, raw, &parts[pi]) != BLKIO_OK) return failed(); + + /* nothing of the stick's old bytes may read as an identity */ + for (i = 0; i < MINT_ZERO_FBLOCKS; i++) + if (blkio_write(&part, i, zero) != BLKIO_OK) return failed(); + + /* Zuse, minted, then read back from the stick */ + if (capsule_zuse_boot_genesis_mint(&part, seed, pub) != 0) return failed(); + sig_rc = homeblocks_sig_check(&part, HOMEBLOCKS_SIG_START_FBLOCK, &sig); + if (capsule_zuse_boot_check_key(&part, sig_rc, &sig, pub, rseed, rpub) != ZUSE_DRIVE_HERS || + memcmp(rseed, seed, 32) != 0) + return failed(); + + /* only now the marker: the proxy kept, the real Zuse named */ + m = *sk_v4_identity_marker(); + if (!m.present) return failed(); + memcpy(m.real, pub, 32); + m.has_real = 1; + if (sk_v4_marker_write(&m) != 0) return failed(); + sk_v4_identity_set_marker(&m); + memset(seed, 0, sizeof seed); + memset(rseed, 0, sizeof rseed); + + console_println("Zuse: real Zuse minted onto the second stick"); + sk_v4_identity_recheck_offered(); + return 0; +}