Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ""
Expand Down Expand Up @@ -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 \
Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0023-ipc-peer-rewire-on-rebirth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions user/ghost.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions user/ghostd.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions user/qsh.c
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,32 @@ static void cmd_ghost(void) {
out_bytes(b, o);
}

/* `ghost <sub>` — 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. */
Expand Down Expand Up @@ -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")) {
Expand Down
Loading