foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Grok Bot <grokbot@localhost> | 2026-10-31 19:16:00 -0700 |
|---|---|---|
| commit | 7acb8de4eb9f4927355be39372d2d866b446b9fe | |
| parent | ea587e9ad0f757440fd55aa357a23a6fc100b983 | |
| tree | browse files at this commit | |
Bump kernel pin to Rustbelt 0.20.0 (5291332) Rustbelt 0.20.0 (stage 20: gcc -static and timerfd): user space now starts at 0x40_0000, so plain static ET_EXEC programs run at 0x400000. - config.mk: KERNEL_REV 5291332, KERNEL_VERSION 0.20.0, new KERNEL_BANNER; new fixtures exechello, execthreads (piethreads.c as ET_EXEC) and timerfdtest; /bin now has 47 programs. - build-fixtures.sh: the same stock recipes as the kernel's build.rs now (no -Ttext-segment, crt1.o, --no-relax or _DYNAMIC tricks); FIXTURE_MUSL entries can name their source file (name:kind:source). - run-headless.sh: banner, version and /bin count come from config.mk via the Makefile instead of being hard-coded. - docs/bumping-the-kernel.md: how to bump the pin, and a history table. make test, make test-reference and make compare pass (47 programs, frames 1049 -> 1049, all 76+12 expected lines).
| M | Makefile | +7 -2 |
| M | README.md | +3 -3 |
| M | config.mk | +13 -6 |
| M | docs/boot-contract.md | +7 -5 |
| A | docs/bumping-the-kernel.md | +75 -0 |
| M | docs/kernel-requests.md | +1 -1 |
| M | docs/plan.md | +2 -2 |
| M | scripts/build-fixtures.sh | +13 -13 |
| M | scripts/run-headless.sh | +22 -7 |
9 files changed, 143 insertions, 39 deletions
@@ -64,12 +64,17 @@ image: staging run: image kernel scripts/run-qemu.sh $(IMAGE) $(KBOOT) $(KTARGET) $(OUT) +# What the expected-line checks need to know about the pinned kernel.+TEST_ENV := KERNEL_BANNER="$(KERNEL_BANNER)" KERNEL_VERSION=$(KERNEL_VERSION)+ test: image kernel- scripts/run-headless.sh $(IMAGE) $(KBOOT_HL) $(KTARGET) $(OUT)+ $(TEST_ENV) BIN_COUNT=$(words $(BIN_ORDER)) \+ scripts/run-headless.sh $(IMAGE) $(KBOOT_HL) $(KTARGET) $(OUT) # The baseline: the kernel's own disk.img through the very same checks. test-reference: kernel- scripts/run-headless.sh $(KTARGET)/disk.img $(KBOOT_HL) $(KTARGET) $(OUT)/reference+ $(TEST_ENV) BIN_COUNT=$(words $(BIN_ORDER)) \+ scripts/run-headless.sh $(KTARGET)/disk.img $(KBOOT_HL) $(KTARGET) $(OUT)/reference compare: image kernel scripts/compare-image.sh $(IMAGE) $(KTARGET)/disk.img $(OUT)
@@ -69,10 +69,10 @@ contain. QEMU gets our image as its first virtio disk, the kernel mounts it at `/mnt/disk0` (the shell calls it drive `B:`) and runs `/mnt/disk0/bin/sh`. We build against **one exact kernel commit** (`KERNEL_REV` in `config.mk`,-now `a55b41d` = Rustbelt 0.19.0). The build exports that commit from the+now `5291332` = Rustbelt 0.20.0). The build exports that commit from the kernel repo's git store with `git archive` and compiles it in `out/`, so the-kernel repo itself is never changed. To move to a newer kernel, change-`KERNEL_REV`, run `make test`, and commit.+kernel repo itself is never changed. To move to a newer kernel, follow+[docs/bumping-the-kernel.md](docs/bumping-the-kernel.md). ## Where this came from
@@ -7,8 +7,12 @@ # into out/kernel-src and build it there, with its own target dir. Bump # KERNEL_REV deliberately (and re-run `make test`) when the kernel moves on. KERNEL_DIR ?= /workspace/rust-kernel-KERNEL_REV ?= a55b41d-KERNEL_VERSION ?= 0.19.0+KERNEL_REV ?= 5291332+KERNEL_VERSION ?= 0.20.0+# The first line the kernel prints (also `ver` and uname's "version"). The+# self-test checks for it, so it changes with every pin bump; see+# docs/bumping-the-kernel.md.+KERNEL_BANNER ?= Rustbelt 0.20.0 (stage 20: gcc -static and timerfd) # ---- The image ------------------------------------------------------------- ARCH ?= x86_64@@ -32,9 +36,11 @@ FIXTURE_RUST := hello evil spin demo child overflow forktest cowtest \ mmaptest sigtest threads echoserver ptytest nbtest \ fputest pathtest libctest coretest unixtest FIXTURE_C := rawabi-# name:kind, kind = pie (musl-gcc -static-pie), exec (static ET_EXEC at the-# user base) or dyn (dynamic; Rustbelt must refuse it).-FIXTURE_MUSL := vdsotest:exec piehello:pie piethreads:pie dynhello:dyn remaptest:pie+# name:kind[:source], kind = pie (musl-gcc -static-pie), exec (plain+# musl-gcc -static, an ET_EXEC at 0x400000; works since 0.20.0) or dyn+# (dynamic; Rustbelt must refuse it). source defaults to name.+FIXTURE_MUSL := vdsotest:exec piehello:pie piethreads:pie dynhello:dyn remaptest:pie \+ exechello:exec execthreads:exec:piethreads timerfdtest:exec # The order programs are copied into /bin: the same order as the kernel's # build.rs (PROGRAMS, then C_PROGRAMS, then MUSL_PROGRAMS), so `ls` lists@@ -44,7 +50,8 @@ BIN_ORDER := sh ls cat wc grep mkdir rm touch cp sleep \ ifconfig ping host fetch httpd random getty tile \ forktest cowtest mmaptest sigtest threads echoserver ptytest nbtest \ fputest pathtest libctest coretest unixtest \- rawabi vdsotest piehello piethreads dynhello remaptest+ rawabi vdsotest piehello piethreads dynhello remaptest \+ exechello execthreads timerfdtest # ---- Toolchain pins (downloaded into cache/, checked by SHA256) ----------- MUSL_VERSION ?= 1.2.5
@@ -6,7 +6,7 @@ image was built, only that it follows the contract. ## Contract v1: "today, written down" (in use now) -Pinned kernel: Rustbelt 0.19.0, commit a55b41d. No kernel change needed.+Pinned kernel: Rustbelt 0.20.0, commit 5291332 (was 0.19.0, a55b41d). No kernel change needed. 1. **Where.** The OS image is the **first virtio-blk disk** QEMU is given. The kernel mounts it at `/mnt/disk0` (drive `B:` in rbsh).@@ -17,10 +17,12 @@ Pinned kernel: Rustbelt 0.19.0, commit a55b41d. No kernel change needed. - **`/bin/sh`**: the program the kernel starts. In Foundry OS this is **rbsh** (crate `rust/rbsh`, binary `rbsh`), copied under the name `sh`. - `/scripts/selftest.sh`: the self-test, for headless kernels.- - Programs live in `/bin`. Each is a **static** ELF: either `ET_EXEC`- linked at or above `0x80_0040_0000`, or (since 0.19.0) a- **static-PIE** (`ET_DYN` without `PT_INTERP`). Dynamic programs are- refused.+ - Programs live in `/bin`. Each is a **static** ELF: either a+ **static-PIE** (`ET_DYN` without `PT_INTERP`, since 0.19.0; what+ Foundry OS builds) or an `ET_EXEC` linked anywhere in user space,+ which since 0.20.0 starts at `0x40_0000`, so a plain `gcc -static`+ program at the usual `0x400000` runs (until 0.19.0 it had to be at+ or above `0x80_0040_0000`). Dynamic programs are refused. 4. **What the kernel provides.** `/` is a ramfs the kernel builds itself (with `/tmp`, `/dev`, `/proc`, `/mnt`). It runs `/mnt/disk0/bin/sh` with `PATH=/bin:/mnt/disk0/bin HOME=/ TERM=linux`, and restarts it when
@@ -0,0 +1,75 @@+# Bumping the kernel pin++Foundry OS never builds "whatever the kernel repo has checked out". It+builds **one exact commit**, `KERNEL_REV` in `config.mk`. Moving to a newer+Rustbelt is a deliberate step with its own commit, so that if something+breaks you know exactly which kernel change did it (`git bisect` works+across our history too).++The kernel repo is only ever *read*: `git archive` copies the pinned+commit out of its git store into `out/kernel-src`, and the build happens+there, with its own target dir (`out/kernel-target`).++## The steps++1. **See what changed in the kernel** (read-only commands):++ ```sh+ cd /workspace/rust-kernel+ git log --oneline OLD..NEW+ git diff --stat OLD..NEW+ git diff OLD..NEW -- build.rs scripts/run-headless.sh disk/ user/+ ```++ The files that matter to us:++ | Kernel file | What to update here |+ |---|---|+ | `Cargo.toml` (`version`), `src/` banner | `KERNEL_VERSION`, `KERNEL_BANNER` in `config.mk` |+ | `build.rs`: `PROGRAMS`, `C_PROGRAMS`, `MUSL_PROGRAMS` | `FIXTURE_RUST`, `FIXTURE_C`, `FIXTURE_MUSL`, `BIN_ORDER` in `config.mk` |+ | `build.rs`: the compile commands | `scripts/build-fixtures.sh` (use the *same* flags) |+ | `scripts/run-headless.sh`: the `expect` list | `scripts/run-headless.sh` here (version strings come from `config.mk`) |+ | `disk/` (readme, notes, `scripts/selftest.sh`) | `rootfs/` (our copy; keep Foundry's own section at the end) |+ | `scripts/http-reply.sh`, `scripts/hvc0-session.txt` | our copies in `scripts/` |+ | `user/link.ld` | `rust/link.ld` (where our Rust programs are linked) |++2. **Edit `config.mk`**: `KERNEL_REV`, `KERNEL_VERSION`, `KERNEL_BANNER`,+ and the fixture lists if the kernel added or renamed test programs.+ The banner is the kernel's first line without the trailing ` x86_64`;+ `ver` in the shell prints it too.++3. **Run all three checks**:++ ```sh+ make test # our image on the new kernel+ make test-reference # the kernel's own disk.img, same checks: the baseline+ make compare # our image vs the kernel's disk.img, file by file+ ```++ If `make test` fails but `make test-reference` passes, the problem is on+ our side. If both fail the same way, it is the kernel (or a flaky test:+ run it again before you blame anyone; see "Known flaky checks" below).++4. **Update the docs** that name the version (`grep -rn 0.19 README.md docs/`),+ then commit everything as **one** commit: `Bump kernel pin to Rustbelt X.Y.Z`.++## Where the version-dependent bits live++- `config.mk`: `KERNEL_REV`, `KERNEL_VERSION`, `KERNEL_BANNER`, the fixture lists.+- `scripts/run-headless.sh` gets `KERNEL_BANNER`, `KERNEL_VERSION` and+ `BIN_COUNT` (the `dir bin | wc -l` result) from the Makefile, so the+ expected lines don't need hand edits for a plain version bump.++## History of bumps++| Date | From | To | Notes |+|---|---|---|---|+| 2026-10-31 | `a55b41d` 0.19.0 | `5291332` 0.20.0 | User space now starts at 0x40_0000, so plain `gcc -static` / `musl-gcc -static` ET_EXEC programs run: the fixture build lost its `-Ttext-segment`/crt1/`_DYNAMIC` tricks. New fixtures `exechello`, `execthreads` (piethreads.c as ET_EXEC), `timerfdtest`: /bin has 47 programs. timerfd (283/286/287) works. |++## Known flaky checks++- `[fputest] 40 async signals with clobbering handlers` (kernel fixture):+ seen once at 0.20.0 as "39 async signals". Ordinary signals are not+ queued, so two arriving before the first is delivered count as one. It+ is the kernel's test, not ours; re-run and report it to the kernel side+ if it repeats.
@@ -101,7 +101,7 @@ Follow-ups, in order (each optional): | What | How | Goes away when | |---|---|---|-| the kernel itself | `git archive a55b41d` → `out/kernel-src`, built in `out/kernel-target` | never (that's the point), but bumps are deliberate |+| the kernel itself | `git archive $(KERNEL_REV)` (now 5291332) → `out/kernel-src`, built in `out/kernel-target` | never (that's the point), but bumps are deliberate | | the 25 ABI fixtures in `/bin` | compiled from `out/kernel-src/user` | the self-test stops running them (they move to the kernel's own tests) or contract v2 | | the C:/D:/E: test disks | `ext2.img`, `exfat.img`, `btrfs.img` from the kernel build | stays: they are kernel-owned fixtures | | the expected self-test lines | copied into `scripts/run-headless.sh` (same 76 + 12 checks) | — (ours now; kernel-version lines must be updated on a bump) |
@@ -10,7 +10,7 @@ | 4 | Root file system | Later: an **ext2 root**, so `A:` = `/` is the OS disk. The FAT32 disk stays as a **data disk** (`B:`). Plus an **exFAT data partition** for sharing files with Windows/macOS. M1 keeps contract v1 (today's layout). | | 5 | Name clashes | The Rust tools stay the default. busybox/sbase applets go under **`/usr/lib/busybox`**, off the default `PATH` (call them by full path, or add the directory to `PATH` yourself). | | 6 | Tarballs | **Not in git.** A git-ignored download cache (`cache/`), each file pinned by SHA256 (`toolchain/musl/musl.sha256` + `fetch.sh`; `ports/<name>/<name>.sha256`). |-| 7 | Which kernel | A **pinned commit**: `KERNEL_REV=a55b41d` (Rustbelt 0.19.0) in `config.mk`, bumped deliberately. |+| 7 | Which kernel | A **pinned commit**: `KERNEL_REV` in `config.mk` (now `5291332`, Rustbelt 0.20.0; was `a55b41d`, 0.19.0), bumped deliberately: see [bumping-the-kernel.md](bumping-the-kernel.md). | | 8 | Box packages | Small build packages are OK (`make`, `bzip2`, `musl-tools`, `autoconf`). | ## Disk layout later (contract v2, not built yet)@@ -35,7 +35,7 @@ Inside the ext2 root: | | Goal | Status | |---|---|---|-| **M1** | Foundry OS builds the same image; the kernel's self-test passes on it | **done**: `make test` passes; `make compare` shows the same 44 programs and identical non-program files |+| **M1** | Foundry OS builds the same image; the kernel's self-test passes on it | **done**: `make test` passes; `make compare` shows the same 44 programs (47 since the 0.20.0 pin) and identical non-program files | | M1b | `rbrt` becomes a real library crate; rbsh's parser tests run on the host | next | | M2 | musl hello + probes in `/bin`, built with plain `musl-gcc -static-pie` | the toolchain pieces exist (`make musl`, `c/hello-musl.c`) | | M3 | dash as `/bin/sh`, ext2 root (contract v2), Rust `/sbin/init` | needs kernel K5 |
@@ -19,26 +19,26 @@ for p in $RUST; do -C relocation-model=static -C code-model=large \ -C "link-arg=-T$SRC/link.ld" -o "$OUT/$p" "$SRC/$p.rs" done-# No libc at all: raw Linux syscalls, an ordinary static ET_EXEC.+# No libc at all: raw Linux syscalls, an ordinary static ET_EXEC at the+# usual 0x400000 (user space since Rustbelt 0.20.0). for p in $C; do ${CC:-cc} -O2 -static -nostdlib -ffreestanding -fno-stack-protector \- -fpie -no-pie -Wall -Wextra \- -Wl,-Ttext-segment=0x8000400000 -Wl,--build-id=none -z noexecstack \+ -no-pie -Wall -Wextra \+ -Wl,--build-id=none -z noexecstack \ -o "$OUT/$p" "$SRC/$p.c" done-# musl (Debian's musl-tools). Rustbelt 0.19.0 loads static-PIE, so "pie"-# is plain `musl-gcc -static-pie` with no tricks; "exec" is the old-# ET_EXEC-at-the-user-base recipe (kept as a fixture for the loader).-MUSL_LIB=/usr/lib/x86_64-linux-musl+# musl (Debian's musl-tools), always the stock recipes: "pie" is+# `musl-gcc -static-pie`, "exec" is plain `musl-gcc -static` (an ET_EXEC at+# 0x400000; Rustbelt runs that since 0.20.0), "dyn" must be refused.+# Entries are name:kind[:source] (source file in user/, default = name). for entry in $MUSL; do- p=${entry%%:*} kind=${entry#*:}+ IFS=: read -r p kind src <<< "$entry"+ src=$SRC/${src:-$p}.c common=(-O2 -Wall -Wextra -pthread -Wl,--build-id=none) case $kind in- pie) musl-gcc "${common[@]}" -static-pie "$SRC/$p.c" -o "$OUT/$p" ;;- dyn) musl-gcc "${common[@]}" -fPIE -pie "$SRC/$p.c" -o "$OUT/$p" ;;- exec) musl-gcc "${common[@]}" -static -no-pie -nostartfiles \- -Wl,-Ttext-segment=0x8000400000 -Wl,--defsym=_DYNAMIC=0x8000400000 -Wl,--no-relax \- $MUSL_LIB/crt1.o $MUSL_LIB/crti.o "$SRC/$p.c" $MUSL_LIB/crtn.o -o "$OUT/$p" ;;+ pie) musl-gcc "${common[@]}" -static-pie "$src" -o "$OUT/$p" ;;+ dyn) musl-gcc "${common[@]}" -fPIE -pie "$src" -o "$OUT/$p" ;;+ exec) musl-gcc "${common[@]}" -static "$src" -o "$OUT/$p" ;; *) echo "unknown musl kind $kind for $p"; exit 1 ;; esac done
@@ -11,13 +11,21 @@ # KERNEL_TARGET out/kernel-target (has the kernel-owned test disks # ext2.img, exfat.img, btrfs.img: drives C:, D:, E:) # OUT_DIR where logs go (out/)-# Env: QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s).+# Env (the Makefile sets these from config.mk; they change with the kernel pin):+# KERNEL_BANNER the kernel's first line, e.g. "Rustbelt 0.20.0 (stage 20: ...)"+# KERNEL_VERSION e.g. 0.20.0+# BIN_COUNT how many programs the image has in /bin (`dir bin | wc -l`)+# EXTRA_EXPECT optional file: more lines the log must contain, one per+# line (Foundry OS's own checks; # starts a comment)+# QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s). # # Only the QEMU this script starts is ever killed: QEMU writes its PID to # OUT_DIR/qemu-headless.pid and the exit trap kills exactly that PID. set -euo pipefail cd "$(dirname "$0")/.." # repo root: QEMU's guestfwd runs scripts/http-reply.sh IMAGE=$1 IMG=$2 KT=$3 OUT=$4+: "${KERNEL_BANNER:?set KERNEL_BANNER (see config.mk)}" "${KERNEL_VERSION:?}" "${BIN_COUNT:?}"+STAGE=${KERNEL_BANNER#*(}; STAGE=${STAGE%%:*} # "stage 20" mkdir -p "$OUT" for f in "$IMAGE" "$IMG" "$KT/ext2.img" "$KT/exfat.img" "$KT/btrfs.img"; do [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; }@@ -78,7 +86,7 @@ set -e # Lines the shell self-test must have printed (same list as the kernel repo at the pinned commit). if [ "$code" = 33 ]; then expect=(- "Rustbelt 0.19.0 (stage 19: vDSO and static PIE) x86_64" # ver+ "$KERNEL_BANNER x86_64" # ver "[smp] 4 CPUs online" # smp.rs started 3 more cores "[fpu] " # XSAVE (-cpu max) or FXSAVE (qemu64) "B: /mnt/disk0 fat32" # drives@@ -149,21 +157,28 @@ if [ "$code" = 33 ]; then "[nbtest] all checks passed" "[fputest] all checks passed" # stage 15.1: musl prerequisites "[pathtest] all checks passed"- "rawabi: uname: Rustbelt 0.19.0" # a no-libc gcc binary+ "rawabi: uname: Rustbelt $KERNEL_VERSION" # a no-libc gcc binary "rawabi: all checks passed" "sh: nosuchprogram: command not found" "cd: there is no drive Q:" "A:/tmp $ pwd" )+ if [ -n "${EXTRA_EXPECT:-}" ]; then+ while IFS= read -r line; do+ case $line in ''|'#'*) continue ;; esac+ line=${line//@KERNEL_VERSION@/$KERNEL_VERSION}+ expect+=("$line")+ done < "$EXTRA_EXPECT"+ fi missing=0 for line in "${expect[@]}"; do grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; } done- # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 44 (the programs),+ # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> $BIN_COUNT (the programs), # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3, # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.- for n in 1 44 3 4 20000; do+ for n in 1 "$BIN_COUNT" 3 4 20000; do grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; } done # No memory leaks: once the shell and all ~60 programs it ran have@@ -174,7 +189,7 @@ if [ "$code" = 33 ]; then echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1 fi # The other direction: the host fetched a page from httpd in the guest.- if grep -qF "Hello from Rustbelt! Rustbelt 0.19.0" "$CURL_OUT" 2>/dev/null; then+ if grep -qF "Hello from Rustbelt! Rustbelt $KERNEL_VERSION" "$CURL_OUT" 2>/dev/null; then echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")" else echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1@@ -186,7 +201,7 @@ if [ "$code" = 33 ]; then # drop the '\r's before comparing whole lines. tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT" # The shell on hvc0 ran the host's commands and answered over hvc0.- for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.19.0 (stage 19" \+ for line in "Rustbelt getty: shell on this terminal" "Rustbelt $KERNEL_VERSION ($STAGE" \ "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; } done