diff --git a/Makefile b/Makefile index b0e7586..bca3d87 100644 --- a/Makefile +++ b/Makefile @@ -437,7 +437,7 @@ ci-smoke: kernel @echo "" @echo "[1/3] Build verified: $(BUILD_DIR)/kernel.elf exists" @test -f $(BUILD_DIR)/kernel.elf || (echo "ERROR: Kernel not built" && exit 1) - @echo "[2/3] Running QEMU boot test (26 second timeout, shell session piped into the console)..." + @echo "[2/3] Running QEMU boot test (34 second timeout, shell session piped into the console)..." @# The SECOND `ghost` (after the run legs) gates qsh's first-match send_msg @# invariant (epic #175): qsh must still hold EXACTLY ONE IPC WRITE cap @# (->ghostd) after spawning children — a blanket spawn-channel mint (or any @@ -451,8 +451,14 @@ ci-smoke: kernel @# processed `exit` (bytes landing in the death->rebirth gap wait in the @# kernel RX ring for the reborn reader). Gated on the post-reborn log slice @# below. - @( printf 'help\nps\nfree\nuptime\ndate\nghost\nqrand\nls\ncat /docs/hello.txt\nrun /bin/hello\nrun /bin/args alpha quantumos\nrun /bin/libqtest\nrun /bin/consciousnessd\nrun /bin/qtop\nrun /bin/life\nghost\nimprint the cat sat on the mat\nimprint pure quantum wave dynamics\nimprint hello little world\nrecall the cxt sxt on thx mxt\nfieldtest\nwrite /data/note ramfs-works\nls /data\nrm /data/note\nsync\nexit\n'; sleep 15; printf 'ghost\n'; sleep 10 ) | \ - timeout 26s qemu-system-x86_64 -kernel $(BUILD_DIR)/kernel.elf32 \ + @# Then `ghost exit` + a FOURTH `ghost` (ADR-0023 Part-1 integration leg): + @# restart the TARGET (ghostd) under the LIVING reborn shell — the direction + @# a qsh exit never exercises. Without the dead-target unlink the shell's + @# stale lower-slot ghostd cap wins first-match and routes to the dead pid; + @# only unlink (Part 1) + the Pass-2 re-mint (Part 2) make the last answer + @# appear. Gated on the post-'GHOSTD: FIELD REBORN' slice below. + @( printf 'help\nps\nfree\nuptime\ndate\nghost\nqrand\nls\ncat /docs/hello.txt\nrun /bin/hello\nrun /bin/args alpha quantumos\nrun /bin/libqtest\nrun /bin/consciousnessd\nrun /bin/qtop\nrun /bin/life\nghost\nimprint the cat sat on the mat\nimprint pure quantum wave dynamics\nimprint hello little world\nrecall the cxt sxt on thx mxt\nfieldtest\nwrite /data/note ramfs-works\nls /data\nrm /data/note\nsync\nexit\n'; sleep 15; printf 'ghost\n'; sleep 4; printf 'ghost exit\n'; sleep 7; printf 'ghost\n'; sleep 6 ) | \ + timeout 34s qemu-system-x86_64 -kernel $(BUILD_DIR)/kernel.elf32 \ -append agentdemo \ -serial stdio -m 128M -display none -no-reboot 2>&1 | tee /tmp/qemu-boot.log || true @echo "" @@ -1253,6 +1259,34 @@ ci-smoke: kernel echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \ fi @echo "SUCCESS: reborn shell re-acquired its ghostd IPC pair (declarative re-mint, ADR-0023)" + @# Part-1 INTEGRATION gate (ADR-0023): `ghost exit` restarted ghostd under + @# the LIVING shell. The shell held a live qsh->ghostd cap across ghostd's + @# death; without the dead-target unlink that stale cap sits at a LOWER + @# first-fit slot than the Pass-2 re-mint and wins untargeted send_msg's + @# first-match, routing to the dead pid — no answer. Only unlink + re-mint + @# together produce a 'ghost R=' AFTER the 'GHOSTD: FIELD REBORN' banner. + @# (The self-test gates Part 1's MECHANISM; this leg gates its NECESSITY + @# in the live routing path.) Anchored the same way as the reborn-shell + @# gate: single UART, strictly ordered, the slice cannot see earlier + @# answers. Inherently non-vacuous: if `ghost exit` were dropped, the + @# banner never prints and the slice is empty. + @if ! grep -q "GHOSTD: exiting" /tmp/qemu-boot.log 2>/dev/null; then \ + echo "ERROR: 'ghost exit' never reached ghostd (GHOSTD: exiting missing)"; \ + echo "Boot log tail:"; tail -40 /tmp/qemu-boot.log 2>/dev/null || true; \ + echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \ + fi + @if ! grep -q "GHOSTD: FIELD REBORN" /tmp/qemu-boot.log 2>/dev/null; then \ + echo "ERROR: watchdog did not rebirth ghostd after 'ghost exit' (GHOSTD: FIELD REBORN missing)"; \ + echo "Boot log tail:"; tail -40 /tmp/qemu-boot.log 2>/dev/null || true; \ + echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \ + fi + @if ! awk '/GHOSTD: FIELD REBORN/{f=1} f' /tmp/qemu-boot.log 2>/dev/null | grep -q "qsh: ghost R="; then \ + echo "ERROR: living shell got no answer from REBORN ghostd — stale-cap unlink or re-mint broken (ADR-0023 Part 1)"; \ + echo "Post-FIELD-REBORN slice:"; \ + awk '/GHOSTD: FIELD REBORN/{f=1} f' /tmp/qemu-boot.log 2>/dev/null || true; \ + echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \ + fi + @echo "SUCCESS: living shell survived a ghostd rebirth (dead-target unlink + re-mint, ADR-0023 Part 1)" @# epic #73: the default boot attaches no rtl8139, so the NIC driver must @# report its honest absence and MUST NOT claim a NIC came up. @if ! grep -q "NET: no rtl8139" /tmp/qemu-boot.log 2>/dev/null; then \ diff --git a/docs/adr/0023-ipc-peer-rewire-on-rebirth.md b/docs/adr/0023-ipc-peer-rewire-on-rebirth.md index 5d3e499..892975c 100644 --- a/docs/adr/0023-ipc-peer-rewire-on-rebirth.md +++ b/docs/adr/0023-ipc-peer-rewire-on-rebirth.md @@ -164,8 +164,10 @@ first match. integration necessity (qsh's death REAPs its own outbound cap regardless of Part 1; the stale-first-match hazard only manifests when the TARGET restarts under a living sender). Part 1's mechanism is gated by the inverted - capability self-test; a ghostd-restart-under-living-qsh CI leg is the future - increment that would gate the integration. + capability self-test; the ghostd-restart-under-living-qsh leg (SHIPPED as + the follow-up increment: `GHOST_EXIT` + `ghost exit` + the + post-`GHOSTD: FIELD REBORN` slice gate) gates the integration — the living + shell's answer after ghostd's rebirth requires the unlink AND the re-mint. ## Evidence diff --git a/user/ghost.h b/user/ghost.h index e30b37d..5c60c52 100644 --- a/user/ghost.h +++ b/user/ghost.h @@ -37,6 +37,14 @@ #define GHOST_STATUS 3 /* report R, live count, lambda */ #define GHOST_SNAPSHOT 4 /* reply carries this node's 256 phase bytes + R_x (epic #97) */ #define GHOST_COUPLE 5 /* request carries the PEER's 256 phase bytes to fold in (epic #97) */ +/* GHOST_EXIT: terminate ghostd; the watchdog rebirths it (ADR-0023). + * Exit-is-a-feature, mirroring qsh: any holder of a ghostd IPC send-cap may + * ask (the same trust domain that can already flood every other op), no + * reply is sent, and abuse is bounded by ghostd's max_restarts budget. + * Exists so the operator (and the ci-smoke Part-1 integration leg) can + * restart the TARGET under a LIVING sender — the one direction a qsh + * `exit` rebirth never exercises. */ +#define GHOST_EXIT 6 /* Wide phase message (epic #97): 256 oscillators as one byte each — the * TOP byte of each phase "turn", so a difference is exact modular u8 diff --git a/user/ghostd.c b/user/ghostd.c index 9e3d840..b79a8fa 100644 --- a/user/ghostd.c +++ b/user/ghostd.c @@ -691,6 +691,15 @@ static void handle(const ghost_req_t *req, long sender) { rep.match = (int8_t)live_count; rep.r_q16 = field_order_param(); break; + case GHOST_EXIT: + /* Exit-is-a-feature (ADR-0023): terminate so the watchdog rebirths + * us — the imprinted field dies with this process (the reborn banner + * says so honestly) and every peer's IPC cap to this pid is unlinked + * by the kernel, then re-minted declaratively at our restart. No + * reply: the sender outlives us and its next query is the proof. */ + logline("GHOSTD: exiting — the watchdog will restart me"); + exit_(0); + break; /* unreachable */ default: rep.match = GHOST_NOMATCH; break; diff --git a/user/qsh.c b/user/qsh.c index 858623a..75c3e34 100644 --- a/user/qsh.c +++ b/user/qsh.c @@ -1276,6 +1276,32 @@ static void cmd_ghost(void) { out_bytes(b, o); } +/* `ghost ` — today only `ghost exit` (ADR-0023): ask ghostd to + * terminate so the watchdog rebirths it. Restarting the TARGET under this + * LIVING shell is the one rebirth direction a qsh `exit` never exercises: + * our stale ghostd cap must be unlinked at its death (Part 1) and the fresh + * pair re-minted at its restart (Part 2) for the NEXT `ghost` to answer — + * the ci-smoke Part-1 integration leg drives exactly that sequence. The op + * is fire-and-forget (ghostd sends no reply; it is dying). */ +static void cmd_ghost_arg(const char *sub) { + if (!is_cmd(sub, "exit")) { + out("qsh: ghost: usage: ghost [exit]\r\n"); + return; + } + ghost_req_t req; + req.op = GHOST_EXIT; + req.slot = 0; + req.pad[0] = req.pad[1] = 0; + for (int w = 0; w < GHOST_PW; w++) { + req.bits[w] = 0; + } + if (send_msg((const char *)&req, sizeof(req)) < 0) { + out("qsh: ghost exit send denied (EPERM)\r\n"); + return; + } + out("qsh: ghost exit sent — the watchdog will rebirth ghostd\r\n"); +} + /* Start an initrd program (SYS_SPAWN — the shell's spawn capability at * work) and poll its fate with SYS_WAITPID, heartbeating so the watchdog * never mistakes the wait for a hang. */ @@ -1425,6 +1451,8 @@ static void execute(const char *line) { cmd_qrand(); } else if (is_cmd(line, "qseed")) { cmd_qseed(); + } else if ((a = arg_of(line, "ghost")) != 0) { + cmd_ghost_arg(a); } else if (is_cmd(line, "ghost")) { cmd_ghost(); } else if (is_cmd(line, "clear")) {