boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit 89f8b2233d03e72ec1e61b7a4b40b853f4b2ea7c
parent 5073ee046922edc1734cd7c6abda83c25d743fef
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  5 May 2026 00:08:29 -0700

boot{0,1,2}: extract lib-pipeline DSL; add seed-kernel driver + accept

Refactor the inlined-heredoc run.sh blocks in boot0/1/2 onto a shared
DSL (pipeline_init / pipeline_input / stage / pipeline_export /
pipeline_run) with two backends:

  podman — accumulates stages into one /work/run.sh, runs once in the
           container (preserves the existing one-container-per-bootN
           cost; podman is still the default).
  seed   — runs each stage as one qemu boot of seed-kernel via the
           tier1-gate pattern (cpio /init + named inputs, dump tmpfs
           over UART, extract). aarch64 only.

Each bootN.sh is now host prep + linear `stage` calls + exports;
no transport-specific code in the bootN scripts. Outputs are
byte-identical between DRIVER=podman and DRIVER=seed for all of
boot0/1/2.

scripts/seed-accept.sh: end-to-end Tier-2 acceptance. Boots the
boot2-built scheme1 on seed-kernel with a .scm driver that writes
log lines, spawns the boot2 catm as child-prog (clone + execve),
waitids it, opens+reads the resulting file, exits 0. Exercises
all eight Tier-1 syscalls plus the three Tier-2 ones.

Diffstat:
Mdocs/OS-TODO.md | 51++++++++++++++++++++++++++++++++++++++++++++-------
Mscripts/boot0.sh | 108+++++++++++++++++++++++++++++++------------------------------------------------
Mscripts/boot1.sh | 131++++++++++++++++++++++++++++++++++++-------------------------------------------
Mscripts/boot2.sh | 158++++++++++++++++++++++++++++++++++---------------------------------------------
Ascripts/lib-pipeline.sh | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/seed-accept.sh | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 565 insertions(+), 234 deletions(-)

diff --git a/docs/OS-TODO.md b/docs/OS-TODO.md @@ -8,7 +8,11 @@ three Tier-2 syscalls, and supports both the host-side verification gates `scripts/tier1-gate.sh` and `scripts/tier2-gate.sh`. Verified against `boot0/catm`, `boot1/M1pp`, and `boot3/tcc0`; the canonical Tier-2 case (scheme1 driver spawns tcc0 to compile a `.c` into a -relocatable ELF object) round-trips end-to-end. +relocatable ELF object) round-trips end-to-end. The bootN scripts +themselves now run on seed-kernel via `DRIVER=seed scripts/bootN.sh +aarch64` for N∈{0,1,2}, producing byte-identical outputs to the +podman path; `scripts/seed-accept.sh` exercises the boot2-built +scheme1 spawning the boot2-built catm via the .scm prelude. ## Tier 1 @@ -105,12 +109,45 @@ relocatable ELF object) round-trips end-to-end. ## Things still worth doing (out of scope of the original list) -- **Multi-stage Tier 1 driving**: `make tcc-boot2 ARCH=aarch64` could be - taught to swap each podman invocation for `tier1-gate.sh`. The hooks - exist; it would just be a `seed-kernel/Makefile.gate` overlay. -- **Snapshot speed**: `mem_cpy(USER_POOL_SIZE = 768 MB)` is the dominant - cost of every clone (~30 s under TCG). A copy-on-write or only- - touched-pages strategy would help, but isn't needed for compliance. +- **Port boot3/4/5 to the seed driver.** boot0/1/2 already run under + `DRIVER=seed` on top of the shared [`scripts/lib-pipeline.sh`](../scripts/lib-pipeline.sh) + DSL (one qemu boot per stage; outputs byte-identical to the podman + path). boot3/4/5 follow a different shape: host-side prep stays on + the host (`stage1-flatten.sh`, `libc-flatten.sh`, the cc.scm bundle + catm) and produces both the input files and a `run.scm` driver; in + the VM, scheme1 evaluates `run.scm` to orchestrate the per-bootN + pipeline via `(spawn …)` / `(run …)` against the chain binaries + (tcc0, tcc1, …) staged as child-progs in the cpio. Each bootN is + one qemu boot that replaces today's one `podman run` of an inlined + `run.sh`. `scripts/seed-accept.sh` proves the scheme1 + child-prog + cycle round-trips end-to-end on seed-kernel, so the prerequisites + are in place; the work is generating the per-bootN `run.scm` and a + shared driver harness analogous to `lib-pipeline.sh`. +- **Pool-swap on execve instead of snapshot on clone.** With + `run.scm` driving boot3/4 inside the VM, every `(run "tccN" …)` + triggers `sys_clone`'s 768 MB `mem_cpy` (~30 s under TCG) followed + by a second 768 MB `mem_cpy` at child exit. The boot0/1/2 stages + move <100 KB of working RAM so this didn't matter for the existing + seed driver, but boot3 forks per tcc TU and the cost compounds. + + Cheap structural fix: don't snapshot at clone; allocate a second + physical pool of the same size and swap the L2 user entries to + point at it on `execve`. The prelude's rigid + `clone → execve → exit → waitid` shape means the child only reads + the parent's image between clone and execve (running prelude scheme + bytecode); `do_execve` already captures path/argv into a kernel + pool before any user-VA-reading kernel code runs, so by the time + we're about to call `load_elf` the user pool is no longer needed + for the parent. Swap L2 → tlbi vmalle1 → load_elf writes the new + image into the second pool; on child exit, swap L2 back. Cost per + fork drops from 1.5 GB of memcpy to ~3 KB of L2 writes plus a TLB + invalidate. `MAX_PROC_DEPTH = 1` means two pools is enough; a stack + of pools would generalise if the contract ever grew nested forks. + + Alternatives — copy-on-write or only-touched-pages tracking — work + but need per-page protection, a write-fault path, and a page + allocator the kernel doesn't currently have. The pool-swap fix + reuses the existing single-L2 single-allocation design. - **NULL-page hardening**: slot 0 is unmapped so a NULL deref faults to the kernel as a user sync; the kernel currently panics rather than delivering a SIGSEGV-equivalent. Acceptable per OS.md (default-action diff --git a/scripts/boot0.sh b/scripts/boot0.sh @@ -5,28 +5,16 @@ ## brings up: hex0 -> hex1 -> hex2 -> catm -> M0. Three of those (hex2, ## catm, M0) are the binaries every later stage depends on. ## -## ─── Inputs (sources, copied into staging) ──────────────────────────── -## vendor/seed/$ARCH/hex0-seed — hex-byte seed binary (target ELF) -## vendor/seed/$ARCH/hex0.hex0 — hex0 source for the hex0 assembler -## vendor/seed/$ARCH/hex1.hex0 — hex0 source for the hex1 assembler -## vendor/seed/$ARCH/hex2.hex1 — hex1 source for the hex2 assembler -## vendor/seed/$ARCH/catm.hex2 — hex2 source for catm (binary cat) -## vendor/seed/$ARCH/M0.hex2 — hex2 source for the M0 macro asm -## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment (catm input) -## -## ─── Inputs (binaries from prior stages) ────────────────────────────── -## none (this is stage 0). -## -## ─── Tools (in container) ───────────────────────────────────────────── -## busybox sh + cp + mkdir + chmod (scratch + busybox image only). +## ─── Inputs (sources) ───────────────────────────────────────────────── +## vendor/seed/$ARCH/{hex0-seed, hex0.hex0, hex1.hex0, hex2.hex1, +## catm.hex2, M0.hex2, ELF.hex2} ## ## ─── Outputs ────────────────────────────────────────────────────────── -## build/$ARCH/boot0/hex2 — hex2 assembler -## build/$ARCH/boot0/M0 — M0 macro assembler -## build/$ARCH/boot0/catm — binary concatenator (cat with -m) +## build/$ARCH/boot0/{hex2, catm, M0} ## ## Usage: scripts/boot0.sh <arch> -## <arch> ∈ {aarch64, amd64, riscv64} +## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default). +## DRIVER=seed currently supports aarch64 only (uses seed-kernel). set -eu @@ -44,64 +32,52 @@ esac ROOT=$(cd "$(dirname "$0")/.." && pwd) cd "$ROOT" -IMAGE=boot2-scratch:$ARCH +DRIVER=${DRIVER:-podman} SEED=vendor/seed/$ARCH OUT=build/$ARCH/boot0 STAGE=build/$ARCH/.boot0-stage -# ── ensure container image exists ───────────────────────────────────── -if ! podman image exists "$IMAGE"; then - echo "[boot0 $ARCH] building $IMAGE" - podman build --platform "$PLATFORM" -t "$IMAGE" \ - -f scripts/Containerfile.scratch scripts/ -fi +case "$DRIVER" in + podman) + IMAGE=boot2-scratch:$ARCH + if ! podman image exists "$IMAGE"; then + echo "[boot0 $ARCH] building $IMAGE" + podman build --platform "$PLATFORM" -t "$IMAGE" \ + -f scripts/Containerfile.scratch scripts/ + fi + export PLATFORM IMAGE ;; + seed) + [ "$ARCH" = "aarch64" ] || { echo "[boot0] DRIVER=seed: aarch64 only" >&2; exit 2; } + KERNEL_IMAGE=$ROOT/seed-kernel/build/Image + EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh + [ -f "$KERNEL_IMAGE" ] || { echo "[boot0] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; } + export KERNEL_IMAGE EXTRACT ;; + *) echo "[boot0] unknown DRIVER=$DRIVER" >&2; exit 2 ;; +esac -# ── reset staging, copy inputs explicitly ───────────────────────────── -rm -rf "$STAGE" -mkdir -p "$STAGE/in" "$STAGE/out" "$OUT" +. scripts/lib-pipeline.sh +pipeline_init "$STAGE" "$OUT" "$DRIVER" +# ─── inputs ─────────────────────────────────────────────────────────── for f in hex0-seed hex0.hex0 hex1.hex0 hex2.hex1 catm.hex2 M0.hex2 ELF.hex2; do [ -e "$SEED/$f" ] || { echo "[boot0 $ARCH] missing input: $SEED/$f" >&2; exit 1; } - cp "$SEED/$f" "$STAGE/in/$f" + pipeline_input "$f" "$SEED/$f" done -# ── materialize the container's run.sh into staging ─────────────────── -# Each boot stage writes the script the container will execute as a -# concrete file under $STAGE/in/run.sh, then `podman run` just sh's it. -# Easier to inspect/diff than a heredoc; matches how boot3/boot4 emit -# their host-generated run.sh. -cat > "$STAGE/in/run.sh" <<'RUN' -#!/bin/sh -set -eu -# The stage0 tools do one syscall per byte. Build everything in /tmp -# (RAM tmpfs from --tmpfs) and only cp the final binaries to /work/out. -chmod +x /work/in/hex0-seed -/work/in/hex0-seed /work/in/hex0.hex0 /tmp/hex0 -chmod +x /tmp/hex0 -/tmp/hex0 /work/in/hex1.hex0 /tmp/hex1 -chmod +x /tmp/hex1 -/tmp/hex1 /work/in/hex2.hex1 /tmp/hex2 -chmod +x /tmp/hex2 -/tmp/hex2 /work/in/catm.hex2 /tmp/catm -chmod +x /tmp/catm -/tmp/catm /tmp/M0.combined.hex2 /work/in/ELF.hex2 /work/in/M0.hex2 -/tmp/hex2 /tmp/M0.combined.hex2 /tmp/M0 -chmod +x /tmp/M0 -cp /tmp/hex2 /tmp/catm /tmp/M0 /work/out/ -RUN -chmod +x "$STAGE/in/run.sh" +# ─── pipeline ───────────────────────────────────────────────────────── +echo "[boot0 $ARCH/$DRIVER] hex0-seed -> hex0 -> hex1 -> hex2 -> catm -> M0" -# ── run the seed chain in the scratch container ─────────────────────── -echo "[boot0 $ARCH] hex0-seed -> hex0 -> hex1 -> hex2 -> catm -> M0" -podman run --rm -i --pull=never --platform "$PLATFORM" \ - --tmpfs /tmp:size=512M \ - -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \ - sh -eu /work/in/run.sh +stage hex0-seed hex0.hex0 hex0 -- hex0.hex0 -- hex0 +stage hex0 hex1.hex0 hex1 -- hex1.hex0 -- hex1 +stage hex1 hex2.hex1 hex2 -- hex2.hex1 -- hex2 +stage hex2 catm.hex2 catm -- catm.hex2 -- catm +stage catm M0.combined.hex2 ELF.hex2 M0.hex2 -- ELF.hex2 M0.hex2 -- M0.combined.hex2 +stage hex2 M0.combined.hex2 M0 -- M0.combined.hex2 -- M0 -# ── copy outputs to final destination ───────────────────────────────── -for f in hex2 catm M0; do - cp "$STAGE/out/$f" "$OUT/$f" - chmod 0700 "$OUT/$f" -done +pipeline_export hex2 +pipeline_export catm +pipeline_export M0 + +pipeline_run -echo "[boot0 $ARCH] OK -> $OUT/{hex2, catm, M0}" +echo "[boot0 $ARCH/$DRIVER] OK -> $OUT/{hex2, catm, M0}" diff --git a/scripts/boot1.sh b/scripts/boot1.sh @@ -5,25 +5,19 @@ ## hex2pp pair, built from their .P1 sources via the seed M0 + hex2 ## chain. catm is rebuilt from catm.P1pp in boot2. ## -## ─── Inputs (sources, copied into staging) ──────────────────────────── -## M1pp/M1pp.P1 — M1pp expander, P1 source -## hex2pp/hex2pp.P1 — hex2pp assembler/linker, P1 source -## P1/P1-$ARCH.M1 — pre-pruned per-arch P1 backend -## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment (catm input) +## ─── Inputs (sources) ───────────────────────────────────────────────── +## M1pp/M1pp.P1, hex2pp/hex2pp.P1 +## P1/P1-$ARCH.M1, vendor/seed/$ARCH/ELF.hex2 ## ## ─── Inputs (binaries from prior stages) ────────────────────────────── -## build/$ARCH/boot0/{hex2, M0, catm} — built by scripts/boot0.sh -## -## ─── Tools (in container) ───────────────────────────────────────────── -## busybox sh + cp + mkdir + chmod (scratch + busybox image only). -## Plus the boot0 binaries (M0, catm, hex2), staged in. +## build/$ARCH/boot0/{hex2, M0, catm} ## ## ─── Outputs ────────────────────────────────────────────────────────── -## build/$ARCH/boot1/M1pp — M1pp expander ELF -## build/$ARCH/boot1/hex2pp — hex2pp assembler/linker ELF +## build/$ARCH/boot1/{M1pp, hex2pp} ## ## Usage: scripts/boot1.sh <arch> -## <arch> ∈ {aarch64, amd64, riscv64} +## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default). +## DRIVER=seed currently supports aarch64 only. set -eu @@ -41,19 +35,11 @@ esac ROOT=$(cd "$(dirname "$0")/.." && pwd) cd "$ROOT" -IMAGE=boot2-scratch:$ARCH +DRIVER=${DRIVER:-podman} BOOT0=build/$ARCH/boot0 OUT=build/$ARCH/boot1 STAGE=build/$ARCH/.boot1-stage -# ── ensure container image exists ───────────────────────────────────── -if ! podman image exists "$IMAGE"; then - echo "[boot1 $ARCH] building $IMAGE" - podman build --platform "$PLATFORM" -t "$IMAGE" \ - -f scripts/Containerfile.scratch scripts/ -fi - -# ── prerequisite: boot0 binaries must exist ─────────────────────────── for bin in hex2 M0 catm; do [ -x "$BOOT0/$bin" ] || { echo "[boot1 $ARCH] missing prerequisite: $BOOT0/$bin (run scripts/boot0.sh $ARCH)" >&2 @@ -61,57 +47,60 @@ for bin in hex2 M0 catm; do } done -# ── reset staging, copy inputs explicitly ───────────────────────────── -rm -rf "$STAGE" -mkdir -p "$STAGE/in" "$STAGE/out" "$OUT" - -cp "$BOOT0/hex2" "$BOOT0/M0" "$BOOT0/catm" "$STAGE/in/" -cp M1pp/M1pp.P1 "$STAGE/in/M1pp.P1" -cp hex2pp/hex2pp.P1 "$STAGE/in/hex2pp.P1" -cp "P1/P1-$ARCH.M1" "$STAGE/in/P1.M1" -cp "vendor/seed/$ARCH/ELF.hex2" "$STAGE/in/ELF.hex2" - -# ── run the build pipeline ──────────────────────────────────────────── -# .P1 -> ELF via M0 + hex2 (seed), run twice — for M1pp and hex2pp: -# catm combined.M1 P1.M1 src (per-arch backend prepended) -# M0 combined.M1 -> prog.hex2 -# catm linked.hex2 ELF.hex2 prog.hex2 -# hex2 linked.hex2 -> ELF binary -# -# Stages everything through /tmp because the M0/hex2 seed tools do one -# syscall per byte; virtiofs round-trips would dominate. -cat > "$STAGE/in/run.sh" <<'RUN' -#!/bin/sh -set -eu -# Inlined (no function) so the container shell sees only sequential -# exec — kaem-friendly. +case "$DRIVER" in + podman) + IMAGE=boot2-scratch:$ARCH + if ! podman image exists "$IMAGE"; then + echo "[boot1 $ARCH] building $IMAGE" + podman build --platform "$PLATFORM" -t "$IMAGE" \ + -f scripts/Containerfile.scratch scripts/ + fi + export PLATFORM IMAGE ;; + seed) + [ "$ARCH" = "aarch64" ] || { echo "[boot1] DRIVER=seed: aarch64 only" >&2; exit 2; } + KERNEL_IMAGE=$ROOT/seed-kernel/build/Image + EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh + [ -f "$KERNEL_IMAGE" ] || { echo "[boot1] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; } + export KERNEL_IMAGE EXTRACT ;; + *) echo "[boot1] unknown DRIVER=$DRIVER" >&2; exit 2 ;; +esac + +. scripts/lib-pipeline.sh +pipeline_init "$STAGE" "$OUT" "$DRIVER" + +# ─── inputs ─────────────────────────────────────────────────────────── +pipeline_input hex2 "$BOOT0/hex2" +pipeline_input M0 "$BOOT0/M0" +pipeline_input catm "$BOOT0/catm" +pipeline_input P1.M1 "P1/P1-$ARCH.M1" +pipeline_input ELF.hex2 "vendor/seed/$ARCH/ELF.hex2" +pipeline_input M1pp.P1 "M1pp/M1pp.P1" +pipeline_input hex2pp.P1 "hex2pp/hex2pp.P1" + +# ─── pipeline ───────────────────────────────────────────────────────── +echo "[boot1 $ARCH/$DRIVER] M1pp.P1 + hex2pp.P1 -> M1pp + hex2pp" + +# .P1 -> ELF via M0 + hex2: +# catm P1.M1 + src -> combined.M1 +# M0 combined.M1 -> prog.hex2 +# catm ELF.hex2 + prog.hex2 -> linked.hex2 +# hex2 linked.hex2 -> ELF binary # M1pp.P1 -> M1pp -/work/in/catm /tmp/combined.M1 /work/in/P1.M1 /work/in/M1pp.P1 -/work/in/M0 /tmp/combined.M1 /tmp/prog.hex2 -/work/in/catm /tmp/linked.hex2 /work/in/ELF.hex2 /tmp/prog.hex2 -/work/in/hex2 /tmp/linked.hex2 /work/out/M1pp -chmod +x /work/out/M1pp +stage catm combined.M1 P1.M1 M1pp.P1 -- P1.M1 M1pp.P1 -- combined.M1 +stage M0 combined.M1 prog.hex2 -- combined.M1 -- prog.hex2 +stage catm linked.hex2 ELF.hex2 prog.hex2 -- ELF.hex2 prog.hex2 -- linked.hex2 +stage hex2 linked.hex2 M1pp -- linked.hex2 -- M1pp # hex2pp.P1 -> hex2pp -/work/in/catm /tmp/combined.M1 /work/in/P1.M1 /work/in/hex2pp.P1 -/work/in/M0 /tmp/combined.M1 /tmp/prog.hex2 -/work/in/catm /tmp/linked.hex2 /work/in/ELF.hex2 /tmp/prog.hex2 -/work/in/hex2 /tmp/linked.hex2 /work/out/hex2pp -chmod +x /work/out/hex2pp -RUN -chmod +x "$STAGE/in/run.sh" - -echo "[boot1 $ARCH] M1pp.P1 + hex2pp.P1 -> M1pp + hex2pp" -podman run --rm -i --pull=never --platform "$PLATFORM" \ - --tmpfs /tmp:size=512M \ - -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \ - sh -eu /work/in/run.sh - -# ── copy outputs to final destination ───────────────────────────────── -for f in M1pp hex2pp; do - cp "$STAGE/out/$f" "$OUT/$f" - chmod 0700 "$OUT/$f" -done +stage catm combined.M1 P1.M1 hex2pp.P1 -- P1.M1 hex2pp.P1 -- combined.M1 +stage M0 combined.M1 prog.hex2 -- combined.M1 -- prog.hex2 +stage catm linked.hex2 ELF.hex2 prog.hex2 -- ELF.hex2 prog.hex2 -- linked.hex2 +stage hex2 linked.hex2 hex2pp -- linked.hex2 -- hex2pp + +pipeline_export M1pp +pipeline_export hex2pp + +pipeline_run -echo "[boot1 $ARCH] OK -> $OUT/{M1pp, hex2pp}" +echo "[boot1 $ARCH/$DRIVER] OK -> $OUT/{M1pp, hex2pp}" diff --git a/scripts/boot2.sh b/scripts/boot2.sh @@ -5,34 +5,22 @@ ## via the freshly-built M1pp + hex2pp pipeline (replacing the seed ## boot0 catm so later stages run with zero boot0 dependencies); then ## builds the scheme1 interpreter from scheme1.P1pp using the new catm. -## End-to-end through M1pp + hex2pp (no seed M0/hex2 anywhere on the -## .P1pp pipeline). ## -## ─── Inputs (sources, copied into staging) ──────────────────────────── -## catm/catm.P1pp — catm, P1pp source -## scheme1/scheme1.P1pp — interpreter source -## P1/P1-$ARCH.M1pp — per-arch M1pp backend -## P1/P1.M1pp — arch-agnostic P1pp frontend -## P1/P1pp.P1pp — libp1pp standard library -## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment +## ─── Inputs (sources) ───────────────────────────────────────────────── +## catm/catm.P1pp, scheme1/scheme1.P1pp +## P1/P1-$ARCH.M1pp, P1/P1.M1pp, P1/P1pp.P1pp +## vendor/seed/$ARCH/ELF.hex2 ## ## ─── Inputs (binaries from prior stages) ────────────────────────────── -## build/$ARCH/boot0/catm — built by scripts/boot0.sh -## (used only to bootstrap the -## catm.P1pp build; replaced by -## the new catm afterwards) -## build/$ARCH/boot1/{M1pp, hex2pp} — built by scripts/boot1.sh -## -## ─── Tools (in container) ───────────────────────────────────────────── -## busybox sh + cp + mkdir + chmod (scratch + busybox image only). -## Plus the boot0 catm and boot1 M1pp + hex2pp, staged in. +## build/$ARCH/boot0/catm (only to bootstrap catm.P1pp build) +## build/$ARCH/boot1/{M1pp, hex2pp} ## ## ─── Outputs ────────────────────────────────────────────────────────── -## build/$ARCH/boot2/catm — catm ELF (rebuilt via M1pp+hex2pp) -## build/$ARCH/boot2/scheme1 — scheme1 interpreter ELF +## build/$ARCH/boot2/{catm, scheme1} ## ## Usage: scripts/boot2.sh <arch> -## <arch> ∈ {aarch64, amd64, riscv64} +## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default). +## DRIVER=seed currently supports aarch64 only. set -eu @@ -50,20 +38,12 @@ esac ROOT=$(cd "$(dirname "$0")/.." && pwd) cd "$ROOT" -IMAGE=boot2-scratch:$ARCH +DRIVER=${DRIVER:-podman} BOOT0=build/$ARCH/boot0 BOOT1=build/$ARCH/boot1 OUT=build/$ARCH/boot2 STAGE=build/$ARCH/.boot2-stage -# ── ensure container image exists ───────────────────────────────────── -if ! podman image exists "$IMAGE"; then - echo "[boot2 $ARCH] building $IMAGE" - podman build --platform "$PLATFORM" -t "$IMAGE" \ - -f scripts/Containerfile.scratch scripts/ -fi - -# ── prerequisite: prior-stage binaries must exist ───────────────────── [ -x "$BOOT0/catm" ] || { echo "[boot2 $ARCH] missing prerequisite: $BOOT0/catm (run scripts/boot0.sh $ARCH)" >&2 exit 1 @@ -75,66 +55,64 @@ for bin in M1pp hex2pp; do } done -# ── reset staging, copy inputs explicitly ───────────────────────────── -rm -rf "$STAGE" -mkdir -p "$STAGE/in" "$STAGE/out" "$OUT" - -cp "$BOOT0/catm" "$STAGE/in/catm" -cp "$BOOT1/M1pp" "$BOOT1/hex2pp" "$STAGE/in/" -cp catm/catm.P1pp "$STAGE/in/catm.P1pp" -cp scheme1/scheme1.P1pp "$STAGE/in/scheme1.P1pp" -cp "P1/P1-$ARCH.M1pp" "$STAGE/in/backend.M1pp" -cp P1/P1.M1pp "$STAGE/in/frontend.M1pp" -cp P1/P1pp.P1pp "$STAGE/in/libp1pp.P1pp" -cp "vendor/seed/$ARCH/ELF.hex2" "$STAGE/in/ELF.hex2" - -# ── run the build pipelines ─────────────────────────────────────────── -# Two .P1pp -> ELF pipelines, run back to back in a single container: -# -# catm.P1pp -> catm (boot0 catm bootstraps; produced binary then -# takes over for the rest of this script + boot3) -# catm combined.M1pp backend + frontend + libp1pp + catm.P1pp -# M1pp combined.M1pp -> expanded.hex2pp -# catm linked.hex2pp ELF.hex2 expanded.hex2pp -# hex2pp -B 0x600000 linked.hex2pp -> ELF binary -# -# scheme1.P1pp -> scheme1 (uses the just-built catm) -# (same shape, with /work/in/scheme1.P1pp in place of catm.P1pp) -cat > "$STAGE/in/run.sh" <<'RUN' -#!/bin/sh -set -eu -# Inlined (no function) so the container shell sees only sequential -# exec — kaem-friendly. +case "$DRIVER" in + podman) + IMAGE=boot2-scratch:$ARCH + if ! podman image exists "$IMAGE"; then + echo "[boot2 $ARCH] building $IMAGE" + podman build --platform "$PLATFORM" -t "$IMAGE" \ + -f scripts/Containerfile.scratch scripts/ + fi + export PLATFORM IMAGE ;; + seed) + [ "$ARCH" = "aarch64" ] || { echo "[boot2] DRIVER=seed: aarch64 only" >&2; exit 2; } + KERNEL_IMAGE=$ROOT/seed-kernel/build/Image + EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh + [ -f "$KERNEL_IMAGE" ] || { echo "[boot2] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; } + export KERNEL_IMAGE EXTRACT ;; + *) echo "[boot2] unknown DRIVER=$DRIVER" >&2; exit 2 ;; +esac + +. scripts/lib-pipeline.sh +pipeline_init "$STAGE" "$OUT" "$DRIVER" + +# ─── inputs ─────────────────────────────────────────────────────────── +pipeline_input catm0 "$BOOT0/catm" # bootstrap; replaced by output 'catm' +pipeline_input M1pp "$BOOT1/M1pp" +pipeline_input hex2pp "$BOOT1/hex2pp" +pipeline_input backend.M1pp "P1/P1-$ARCH.M1pp" +pipeline_input frontend.M1pp "P1/P1.M1pp" +pipeline_input libp1pp.P1pp "P1/P1pp.P1pp" +pipeline_input ELF.hex2 "vendor/seed/$ARCH/ELF.hex2" +pipeline_input catm.P1pp "catm/catm.P1pp" +pipeline_input scheme1.P1pp "scheme1/scheme1.P1pp" + +# ─── pipeline ───────────────────────────────────────────────────────── +echo "[boot2 $ARCH/$DRIVER] catm.P1pp -> catm; scheme1.P1pp -> scheme1" + +# .P1pp -> ELF: +# catm backend + frontend + libp1pp + src -> combined.M1pp +# M1pp combined.M1pp -> expanded.hex2pp +# catm ELF.hex2 + expanded.hex2pp -> linked.hex2pp +# hex2pp -B 0x600000 linked.hex2pp -> ELF binary # catm.P1pp -> catm (bootstrap with boot0 catm) -/work/in/catm /tmp/combined.M1pp \ - /work/in/backend.M1pp /work/in/frontend.M1pp \ - /work/in/libp1pp.P1pp /work/in/catm.P1pp -/work/in/M1pp /tmp/combined.M1pp /tmp/expanded.hex2pp -/work/in/catm /tmp/linked.hex2pp /work/in/ELF.hex2 /tmp/expanded.hex2pp -/work/in/hex2pp -B 0x600000 /tmp/linked.hex2pp /work/out/catm -chmod +x /work/out/catm - -# scheme1.P1pp -> scheme1 (uses the just-built catm) -/work/out/catm /tmp/combined.M1pp \ - /work/in/backend.M1pp /work/in/frontend.M1pp \ - /work/in/libp1pp.P1pp /work/in/scheme1.P1pp -/work/in/M1pp /tmp/combined.M1pp /tmp/expanded.hex2pp -/work/out/catm /tmp/linked.hex2pp /work/in/ELF.hex2 /tmp/expanded.hex2pp -/work/in/hex2pp -B 0x600000 /tmp/linked.hex2pp /work/out/scheme1 -RUN -chmod +x "$STAGE/in/run.sh" - -echo "[boot2 $ARCH] catm.P1pp -> catm; scheme1.P1pp -> scheme1" -podman run --rm -i --pull=never --platform "$PLATFORM" \ - --tmpfs /tmp:size=512M \ - -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \ - sh -eu /work/in/run.sh - -# ── copy outputs to final destination ───────────────────────────────── -for f in catm scheme1; do - cp "$STAGE/out/$f" "$OUT/$f" - chmod 0700 "$OUT/$f" -done +stage catm0 combined.M1pp backend.M1pp frontend.M1pp libp1pp.P1pp catm.P1pp \ + -- backend.M1pp frontend.M1pp libp1pp.P1pp catm.P1pp -- combined.M1pp +stage M1pp combined.M1pp expanded.hex2pp -- combined.M1pp -- expanded.hex2pp +stage catm0 linked.hex2pp ELF.hex2 expanded.hex2pp -- ELF.hex2 expanded.hex2pp -- linked.hex2pp +stage hex2pp -B 0x600000 linked.hex2pp catm -- linked.hex2pp -- catm + +# scheme1.P1pp -> scheme1 (uses just-built catm) +stage catm combined.M1pp backend.M1pp frontend.M1pp libp1pp.P1pp scheme1.P1pp \ + -- backend.M1pp frontend.M1pp libp1pp.P1pp scheme1.P1pp -- combined.M1pp +stage M1pp combined.M1pp expanded.hex2pp -- combined.M1pp -- expanded.hex2pp +stage catm linked.hex2pp ELF.hex2 expanded.hex2pp -- ELF.hex2 expanded.hex2pp -- linked.hex2pp +stage hex2pp -B 0x600000 linked.hex2pp scheme1 -- linked.hex2pp -- scheme1 + +pipeline_export catm +pipeline_export scheme1 + +pipeline_run -echo "[boot2 $ARCH] OK -> $OUT/{catm, scheme1}" +echo "[boot2 $ARCH/$DRIVER] OK -> $OUT/{catm, scheme1}" diff --git a/scripts/lib-pipeline.sh b/scripts/lib-pipeline.sh @@ -0,0 +1,189 @@ +# lib-pipeline.sh — driver-agnostic DSL for boot stage pipelines. +# +# A bootN.sh's "wiring" is a sequence of file→file program invocations +# in a flat namespace. This library exposes that as four primitives so +# the same wiring can run under different transports: +# +# podman — accumulate stages into one /work/run.sh, run once in a +# container against $IMAGE / $PLATFORM (env-set by caller). +# seed — run each stage as one qemu boot of seed-kernel via +# tier1-gate.sh's pattern (cpio /init + inputs, dump tmpfs +# over UART, extract). aarch64 only. +# +# DSL (source as `. scripts/lib-pipeline.sh`): +# +# pipeline_init <staging-dir> <out-dir> <driver> +# pipeline_input <name> <host-path> # may be called repeatedly +# stage <bin> <argv1> <argv2>... -- <input-names...> -- <output-names...> +# pipeline_export <name> # may be called repeatedly +# pipeline_run +# +# `stage` semantics: invoke `<bin>` with argv=[<bin>, <argv1>, ...]; the +# stage reads the listed input names and produces the listed output +# names. <bin> is also a name in the flat namespace — typically a +# pipeline_input, but may be the output of an earlier stage. +# +# Required env for podman driver: PLATFORM, IMAGE. +# Required env for seed driver: KERNEL_IMAGE, EXTRACT. + +P_DRIVER= +P_STAGE_DIR= +P_OUT_DIR= +P_SCRIPT= +P_IDX=0 +P_EXPORTS= + +pipeline_init() { + P_STAGE_DIR=$1; P_OUT_DIR=$2; P_DRIVER=$3 + rm -rf "$P_STAGE_DIR" + mkdir -p "$P_STAGE_DIR/in" "$P_OUT_DIR" + P_IDX=0 + P_EXPORTS= + case "$P_DRIVER" in + podman) + mkdir -p "$P_STAGE_DIR/out" + P_SCRIPT=$P_STAGE_DIR/run.sh + { + echo '#!/bin/sh' + echo 'set -eu' + # Stage everything in /tmp (RAM tmpfs) — the seed-stage tools + # do one syscall per byte, virtiofs round-trips would dominate. + echo 'cp /work/in/* /tmp/' + echo 'cd /tmp' + } > "$P_SCRIPT" + ;; + seed) + mkdir -p "$P_STAGE_DIR/work" + : "${KERNEL_IMAGE:?lib-pipeline:seed: KERNEL_IMAGE not set}" + : "${EXTRACT:?lib-pipeline:seed: EXTRACT not set}" + ;; + *) + echo "lib-pipeline: unknown driver '$P_DRIVER'" >&2; exit 2 ;; + esac +} + +pipeline_input() { + name=$1; src=$2 + cp "$src" "$P_STAGE_DIR/in/$name" + if [ "$P_DRIVER" = "seed" ]; then + cp "$src" "$P_STAGE_DIR/work/$name" + fi +} + +stage() { + bin=$1; shift + P_HEAD=""; P_IN=""; P_OUT=""; _s=head + while [ $# -gt 0 ]; do + if [ "$1" = "--" ]; then + case "$_s" in + head) _s=in ;; + in) _s=out ;; + *) echo "lib-pipeline: too many --" >&2; exit 2 ;; + esac + shift; continue + fi + case "$_s" in + head) P_HEAD="$P_HEAD $1" ;; + in) P_IN="$P_IN $1" ;; + out) P_OUT="$P_OUT $1" ;; + esac + shift + done + [ "$_s" = "out" ] || { echo "lib-pipeline: stage needs '<bin> argv... -- inputs... -- outputs...'" >&2; exit 2; } + P_IDX=$((P_IDX + 1)) + case "$P_DRIVER" in + podman) _stage_podman ;; + seed) _stage_seed ;; + esac +} + +_stage_podman() { + { + echo "# stage $P_IDX: $bin$P_HEAD" + echo "chmod +x ./$bin" + echo "./$bin$P_HEAD" + } >> "$P_SCRIPT" +} + +_stage_seed() { + cpio_dir=$P_STAGE_DIR/s$(printf '%02d' "$P_IDX") + rm -rf "$cpio_dir"; mkdir -p "$cpio_dir/cpio" + cp "$P_STAGE_DIR/work/$bin" "$cpio_dir/cpio/init" + chmod +x "$cpio_dir/cpio/init" + NAMES="init" + for inp in $P_IN; do + cp "$P_STAGE_DIR/work/$inp" "$cpio_dir/cpio/$inp" + NAMES="$NAMES +$inp" + done + INITRAMFS=$cpio_dir/initramfs.cpio + ( cd "$cpio_dir/cpio" && printf '%s\n' "$NAMES" | cpio -o -H newc 2>/dev/null ) > "$INITRAMFS" + + APPEND="$bin$P_HEAD dumpfs" + TRANSCRIPT=$cpio_dir/transcript.txt + echo "[lib-pipeline:seed] stage $P_IDX:$P_HEAD (bin=$bin)" >&2 + qemu-system-aarch64 \ + -machine virt -cpu cortex-a72 -m 2048M \ + -nographic -no-reboot \ + -kernel "$KERNEL_IMAGE" -initrd "$INITRAMFS" \ + -append "$APPEND" \ + > "$TRANSCRIPT" 2>&1 & + QPID=$! + ( sleep 240; kill -9 $QPID 2>/dev/null ) & + WATCHER=$! + wait $QPID 2>/dev/null || true + kill $WATCHER 2>/dev/null || true + + if ! grep -q '=== DUMP-END ===' "$TRANSCRIPT"; then + echo "[lib-pipeline:seed] FAIL stage $P_IDX (bin=$bin): no DUMP-END" >&2 + tail -40 "$TRANSCRIPT" >&2 + exit 3 + fi + + mkdir -p "$cpio_dir/dump" + "$EXTRACT" "$cpio_dir/dump" "$TRANSCRIPT" >/dev/null 2>&1 || true + + for o in $P_OUT; do + if [ ! -f "$cpio_dir/dump/$o" ]; then + echo "[lib-pipeline:seed] FAIL stage $P_IDX: missing output '$o'" >&2 + ls "$cpio_dir/dump" >&2 || true + exit 3 + fi + cp "$cpio_dir/dump/$o" "$P_STAGE_DIR/work/$o" + done +} + +pipeline_export() { + P_EXPORTS="$P_EXPORTS $1" +} + +pipeline_run() { + case "$P_DRIVER" in + podman) _run_podman ;; + seed) : ;; + esac + for n in $P_EXPORTS; do + case "$P_DRIVER" in + podman) cp "$P_STAGE_DIR/out/$n" "$P_OUT_DIR/$n" ;; + seed) cp "$P_STAGE_DIR/work/$n" "$P_OUT_DIR/$n" ;; + esac + chmod 0700 "$P_OUT_DIR/$n" + done +} + +_run_podman() { + : "${PLATFORM:?lib-pipeline:podman: PLATFORM not set}" + : "${IMAGE:?lib-pipeline:podman: IMAGE not set}" + if [ -n "$P_EXPORTS" ]; then + cmd="cp" + for n in $P_EXPORTS; do cmd="$cmd $n"; done + cmd="$cmd /work/out/" + echo "$cmd" >> "$P_SCRIPT" + fi + chmod +x "$P_SCRIPT" + SDIR=$(cd "$P_STAGE_DIR" && pwd) + podman run --rm -i --pull=never --platform "$PLATFORM" \ + --tmpfs /tmp:size=512M \ + -v "$SDIR:/work" -w /work "$IMAGE" \ + sh -eu /work/run.sh +} diff --git a/scripts/seed-accept.sh b/scripts/seed-accept.sh @@ -0,0 +1,162 @@ +#!/bin/sh +## seed-accept.sh — Tier-2 acceptance for boot0/1/2 on seed-kernel. +## +## Loads the boot2-built scheme1 as /init in the seed kernel, runs a +## .scm driver that: +## 1. Logs "hello" to stdout (sys_write fd=1 → UART). +## 2. Spawns child-prog (=catm from boot2) via clone+execve to +## concatenate two files A + B → C in the in-memory tmpfs. +## 3. waitids the child, reads C back, prints it to stdout. +## 4. exit_group(0). +## +## End-to-end exercise of every Tier-1 syscall (read/write/openat/close/ +## brk/exit_group) plus the three Tier-2 ones (clone/execve/waitid). +## +## Verifies the transcript contains the expected log lines and that +## sys_exit_or_resume_parent saw the child exit cleanly. +## +## Usage: scripts/seed-accept.sh + +set -eu + +ARCH=aarch64 +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +KERNEL=seed-kernel/build/Image +EXTRACT=seed-kernel/scripts/extract-dump.sh +SCHEME1=build/$ARCH/boot2/scheme1 +CATM=build/$ARCH/boot2/catm +PRELUDE=scheme1/prelude.scm + +[ -f "$KERNEL" ] || { echo "missing $KERNEL — run 'make' in seed-kernel/" >&2; exit 1; } +[ -x "$SCHEME1" ] || { echo "missing $SCHEME1 — run boot2 first" >&2; exit 1; } +[ -x "$CATM" ] || { echo "missing $CATM — run boot2 first" >&2; exit 1; } + +OUTDIR=$ROOT/build/$ARCH/seed-accept +rm -rf "$OUTDIR"; mkdir -p "$OUTDIR" + +STAGE=$(mktemp -d -t seed-accept.XXXXXX) +trap 'rm -rf "$STAGE"' EXIT + +# ─── driver.scm — the in-VM acceptance program ──────────────────────── +cat > "$STAGE/driver.scm" <<'SCM' +;; driver.scm — Tier-2 acceptance for seed-kernel. +(write-string stdout "scheme1: hello from acceptance driver\n") +(write-string stdout "scheme1: spawning child-prog (catm) C <- A + B\n") + +(let ((r (run "child-prog" "C" "A" "B"))) + (if (car r) + (begin + (write-string stdout "scheme1: child returned\n")) + (begin + (write-string stdout "scheme1: spawn FAILED\n") + (exit 1)))) + +(let ((rp (open-input "C"))) + (if (car rp) + (let* ((p (cdr rp)) + (rb (read-all p))) + (if (car rb) + (begin + (write-string stdout "scheme1: read C: [") + (write-bytes stdout (cdr rb)) + (write-string stdout "]\n")) + (write-string stdout "scheme1: read C FAILED\n")) + (close p)) + (write-string stdout "scheme1: open C FAILED\n"))) + +(write-string stdout "scheme1: ALL-OK\n") +(exit 0) +SCM + +# ─── Combine prelude + driver via host catm — this matches the chain's +# own boot-run-scheme1.sh wrapper, so the .scm shape is identical +# to what scheme1 expects everywhere. ────────────────────────────── +cat "$PRELUDE" "$STAGE/driver.scm" > "$STAGE/combined.scm" + +# ─── Two demo input files. catm reads them from the in-VM tmpfs. ────── +printf 'Hello, ' > "$STAGE/A" +printf 'seed-kernel!\n' > "$STAGE/B" + +# ─── Stage the cpio: /init=scheme1, /child-prog=catm, plus inputs. ──── +mkdir -p "$STAGE/cpio" +cp "$SCHEME1" "$STAGE/cpio/init"; chmod +x "$STAGE/cpio/init" +cp "$CATM" "$STAGE/cpio/child-prog"; chmod +x "$STAGE/cpio/child-prog" +cp "$STAGE/combined.scm" "$STAGE/cpio/combined.scm" +cp "$STAGE/A" "$STAGE/cpio/A" +cp "$STAGE/B" "$STAGE/cpio/B" + +NAMES='init +child-prog +combined.scm +A +B' + +INITRAMFS=$STAGE/initramfs.cpio +( cd "$STAGE/cpio" && printf '%s\n' "$NAMES" | cpio -o -H newc 2>/dev/null ) > "$INITRAMFS" + +TRANSCRIPT=$OUTDIR/transcript.txt +echo "[seed-accept] booting scheme1 + driver.scm on seed-kernel" +qemu-system-aarch64 \ + -machine virt -cpu cortex-a72 -m 2048M \ + -nographic -no-reboot \ + -kernel "$KERNEL" -initrd "$INITRAMFS" \ + -append "init combined.scm dumpfs" \ + > "$TRANSCRIPT" 2>&1 & +QPID=$! +( sleep 240; kill -9 $QPID 2>/dev/null ) & +WATCHER=$! +wait $QPID 2>/dev/null || true +kill $WATCHER 2>/dev/null || true + +if ! grep -q '=== DUMP-END ===' "$TRANSCRIPT"; then + echo "[seed-accept] FAIL: no DUMP-END in transcript" >&2 + tail -60 "$TRANSCRIPT" >&2 + exit 3 +fi + +# Extract files (we want C from the tmpfs). +"$EXTRACT" "$OUTDIR" "$TRANSCRIPT" >/dev/null 2>&1 || \ + "$EXTRACT" "$OUTDIR" "$TRANSCRIPT" >&2 + +# ─── Verify ─────────────────────────────────────────────────────────── +fail=0 +for needle in \ + 'scheme1: hello from acceptance driver' \ + 'scheme1: spawning child-prog' \ + 'scheme1: child returned' \ + 'scheme1: read C: \[Hello, seed-kernel!' \ + 'scheme1: ALL-OK' \ + 'exit_group(0)' +do + if ! grep -q "$needle" "$TRANSCRIPT"; then + echo "[seed-accept] MISSING in transcript: $needle" >&2 + fail=1 + fi +done + +if [ ! -f "$OUTDIR/C" ]; then + echo "[seed-accept] MISSING extracted file: $OUTDIR/C" >&2 + fail=1 +elif ! diff -q "$OUTDIR/C" - <<EOF >/dev/null +Hello, seed-kernel! +EOF +then + echo "[seed-accept] C differs from expected:" >&2 + od -c "$OUTDIR/C" | head -3 >&2 + fail=1 +fi + +if [ $fail -ne 0 ]; then + echo "[seed-accept] FAIL — see $TRANSCRIPT" >&2 + exit 4 +fi + +echo "" +echo "=== driver log (excerpt from transcript) ===" +grep '^scheme1:' "$TRANSCRIPT" || true +echo "===========================================" +echo "" +echo "[seed-accept] PASS — scheme1 + .scm + child-prog cycle complete" +echo "[seed-accept] artifacts in $OUTDIR/"