RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 19:32:06 -0700
commit50476f40140f0dfd179cf4fe8eaeb8def195c347
parent52913323cee4a3db45fc331519752f5b25db6693
treebrowse files at this commit
Stage 21: boot contract v2 (GPT root partition, /sbin/init as pid 1, exit status to QEMU), exFAT share at /mnt/share, msync, RUSTBELT_DISK0 + CARGO_TARGET_DIR; 0.21.0
MCargo.lock+1 -1
MCargo.toml+5 -1
MREADME.md+214 -0
Mbuild.rs+113 -1
Ascripts/boot-v2-check.sh+60 -0
Mscripts/btrfs-check.sh+8 -7
Mscripts/exfat-check.sh+21 -20
Mscripts/ext2-check.sh+22 -21
Mscripts/gpu-check.sh+4 -3
Ascripts/qemu.sh+25 -0
Mscripts/run-headless.sh+14 -13
Mscripts/runner.sh+9 -2
Mscripts/screenshot.sh+8 -7
Msrc/block/mod.rs+10 -0
Msrc/block/partition.rs+10 -1
Asrc/boot.rs+106 -0
Msrc/fs/mod.rs+60 -0
Msrc/lib.rs+1 -0
Msrc/main.rs+30 -0
Msrc/mm.rs+12 -0
Msrc/syscall.rs+1 -1
Msrc/syscall_ext.rs+30 -0
Atests/boot_v2.rs+135 -0
Mtests/fat.rs+15 -0
Mtests/shell.rs+1 -1
Auser/inittest.c+111 -0
Muser/nbtest.rs+1 -1

27 files changed, 1027 insertions, 80 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.20.0"+version = "0.21.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.20.0"+version = "0.21.0" edition = "2024"  [dependencies]@@ -83,6 +83,10 @@ harness = false  # Extra QEMU arguments used by `cargo run` (via `bootimage runner`). [package.metadata.bootimage]+# Stage 21: QEMU is started through scripts/qemu.sh, which makes the+# "target/..." paths below follow CARGO_TARGET_DIR (and gives the boot_v2+# test its extra disk).+run-command = ["scripts/qemu.sh", "-drive", "format=raw,file={}"] run-args = [     # Four CPU cores (smp.rs starts the other three).     "-smp", "4",
@@ -1011,6 +1011,8 @@ heap_allocation::simple_allocation...               [ok] | `tests/mremap.rs` | **mremap** (stage 19): `remaptest` twice with no leaks: grow in place, shrink, same size, `ENOMEM` without `MREMAP_MAYMOVE`, a move keeps the bytes and empties the old place, `MREMAP_FIXED`, the `EINVAL`/`EFAULT` cases, a moved file mapping still reads the file, copy-on-write after a move in a forked child, musl's `realloc` of big blocks | | `tests/static_pie.rs` (stage 20 part) | **plain static programs** (stage 20): `exechello` and `execthreads` (stock `musl-gcc -static`, ET_EXEC at `0x400000`) run, their heap and mmap memory stay out of the image, nothing leaks | | `tests/timerfd.rs` | **timerfd** (stage 20): `timerfdtest` (one-shot, interval counts, `EAGAIN`, `EINVAL`, poll/select/epoll wake-ups, `TFD_TIMER_ABSTIME`) twice; closing leaves no timer and no memory behind |+| `tests/boot_v2.rs` | **boot contract v2** (stage 21; only this test gets `target/rootfs-gpt.img`): the ext2 root partition is `/`, the exFAT share is `/mnt/share` (read-write), `/tmp` and drive B: unchanged; `/sbin/init` runs as pid 1 and its exit status comes back; the `/bin/init`, `/bin/sh` fallbacks; init's argv/env; the exit-code mapping; msync (inside inittest) |+| `tests/fat.rs` (stage 21 part) | **no root partition** (stage 21): the old layout, `/` the ramfs and no `/mnt/share` |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]` function and passes them to `rustbelt::test_runner` (in `src/lib.rs`), which@@ -1146,6 +1148,10 @@ user/{vdsotest,piehello,piethreads,dynhello,remaptest}.c  stage 19: C programs b                           (needs musl-tools; tests/vdso.rs, static_pie.rs and mremap.rs run them) user/{exechello,timerfdtest}.c  stage 20: plain `musl-gcc -static` programs (execthreads = piethreads.c                           built that way too); tests/static_pie.rs and tests/timerfd.rs run them+src/boot.rs               stage 21: boot contract v2: /sbin/init -> /bin/init -> /bin/sh as pid 1, its argv/env, exit status -> QEMU+user/inittest.c           stage 21: the test init on target/rootfs-gpt.img (tests/boot_v2.rs, scripts/boot-v2-check.sh)+scripts/qemu.sh           stage 21: QEMU wrapper for cargo run/test: honours CARGO_TARGET_DIR, adds rootfs-gpt.img for boot_v2+scripts/boot-v2-check.sh  stage 21: boots the headless kernel with the GPT root image, wants QEMU exit 47, fscks both partitions src/fs/lock.rs            stage 16: fcntl record locks (F_GETLK/F_SETLK/F_SETLKW), deadlock detection src/fpu.rs                stage 15.1: FPU/SSE/AVX on per CPU (CR0/CR4/XCR0), XSAVE or FXSAVE save/restore scripts/hvc0-session.txt  the commands run-headless.sh feeds into hvc0 (for `getty -1 /dev/hvc0`)@@ -1593,6 +1599,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 23 | `select` | n, *rd, *wr, *ex, *timeval | **stage 15**: fd bitmaps (n <= 1024) | | 24 | `sched_yield` | | | | 25 | `mremap` | addr, old_len, new_len, flags, new_addr | **stage 19**: grow (in place or, with `MREMAP_MAYMOVE` 1, moved), shrink, `MREMAP_FIXED` 2; mmap regions only; `MREMAP_DONTUNMAP` and old_len 0 are `EINVAL` |+| 26 | `msync` | addr, len, flags | **stage 21**: only checks its arguments (nothing to write back: no shared file mappings); `EINVAL` for an unaligned addr, unknown flags or `MS_SYNC`+`MS_ASYNC`, `ENOMEM` for an unmapped page | | 28 | `madvise` | addr, len, advice | **stage 16**: `MADV_DONTNEED` 4 / `MADV_FREE` 8 free private anonymous pages (they read back as zeros); other known hints return 0; unknown ones `EINVAL`, unmapped memory `ENOMEM` | | 32 | `dup` | fd | lowest free fd | | 33 | `dup2` | old, new | closes `new` first if open |@@ -5112,6 +5119,212 @@ Doesn't (yet):   ioctl, and the ~55 ms resolution above. - Still **no dynamic loader** (`PT_INTERP` gets `ENOEXEC`). +## Finding the root disk and starting init (stage 21)++Stage 21 is a small set of requests from Clarke's Foundry OS, the+operating system he builds on top of this kernel. Same rule as stage 20:+no new subsystems and no new dependencies. Four things:++1. The kernel can **find its root filesystem on a disk** and start the+   OS's own **init** program as process 1 ("boot contract v2").+2. An **exFAT share** partition is mounted at `/mnt/share` by itself.+3. **`msync`** (system call 26).+4. **`RUSTBELT_DISK0`**: build with somebody else's disk image as drive+   B:, and the scripts honour `CARGO_TARGET_DIR`.++### What "root" and "init" mean++On Linux, the kernel's last job at boot is to find the **root+filesystem** (the one that becomes `/`) and run one program from it:+**init**, which gets process id 1. Everything else (shells, services,+logins) is started by init or by its children. If init ever exits, the+system is over.++Until now Rustbelt did all of that itself: `/` was a ramfs inside the+kernel, and `main.rs` started our own shell (`sh /scripts/selftest.sh`+in a headless build). That's still exactly what happens on a normal+boot. Boot contract **v2** only switches on when a disk says it should.++### How the kernel finds the root++Every GPT partition has a **type GUID**: a 16-byte id that says what the+partition is for. The [Discoverable Partitions+Specification](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/)+reserves one for "the root filesystem of an x86-64 system":++```+4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709    Linux root (x86-64)+```++So at boot (`mount_disks` in `src/fs/mod.rs`):++- `block/mod.rs` now remembers the type of every partition it found.+- If a partition has the root type **and** holds an ext2 filesystem, it+  is mounted at `/` instead of the ramfs (`replace_root`). The kernel's+  own mounts stay: `/tmp` (ramfs), `/dev` and `/proc` are separate+  mounts, so they still work; the directories are made if the disk+  doesn't have them.+- The root partition **doesn't** get a `/mnt/diskN` name, so the other+  disks keep their numbers: the FAT32 data disk is still `/mnt/disk0`+  (drive B:).++GUIDs are stored "mixed-endian" on disk: the first three groups are+little-endian, so `4F68BCE3-E8CD-4DB1-...` is the bytes `e3 bc 68 4f cd+e8 b1 4d 96 e7 ...`. That's how it's written in `block/partition.rs`.++### How init starts (`src/boot.rs`)++With a root partition, `main.rs` doesn't start our shell. It tries, in+order:++```+/sbin/init   ->   /bin/init   ->   /bin/sh+```++The first one that starts becomes **pid 1** (it's the first process the+kernel makes). A missing file is skipped quietly; a file that exists but+can't run (not an ELF file, say) is logged and skipped too. If none of+the three works the kernel prints `[init] no init found` and QEMU exits+with "failed".++What init gets, by default:++| | |+|---|---|+| argv | `init` (or `sh` for the `/bin/sh` fallback) |+| argv in a headless build | `init selftest` (or `sh /scripts/selftest.sh`, like before) |+| environment | `PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin`, `HOME=/`, `TERM=linux` |+| environment in a headless build | the same plus `RUSTBELT_HEADLESS=1` |++Orphaned processes are adopted by pid 1 when it is called `init` (stage+16's rule), so init can reap them like on Linux.++### The knobs++bootloader 0.9 has **no kernel command line** (there's nowhere to type+`init=/bin/foo` like on Linux), so the knobs are set when the kernel is+**built**:++| Knob | What it does |+|------|--------------|+| `cargo build --features headless` | init gets `selftest` as argv[1] and `RUSTBELT_HEADLESS=1` |+| `RUSTBELT_INIT_ARGS="a b c"` (env var while building) | more arguments after those, split on spaces |+| `RUSTBELT_INIT_ENV="K=V K2=V2"` (env var while building) | more environment; replaces a default with the same name (so `RUSTBELT_INIT_ENV="PATH=/bin"` changes PATH) |+| `RUSTBELT_DISK0=/path/to/disk.img` (env var while building) | use that image as drive B: (see below) |++For example:++```bash+RUSTBELT_INIT_ARGS="--verbose" RUSTBELT_INIT_ENV="LANG=C.UTF-8" cargo build --features headless+```++### init's exit status becomes QEMU's++When pid 1 exits, the run is over (init isn't restarted). The kernel+hands init's status to QEMU through the `isa-debug-exit` device, which+makes QEMU exit with `value * 2 + 1`:++| init did | value written | QEMU's exit code |+|----------|---------------|------------------|+| `exit(0)` | `0x10` | 33 (the usual "success") |+| `exit(1)` | `0x11` | 35 (the usual "failure") |+| `exit(n)`, n from 0 to 111 | `0x10 + n` | **33 + 2n** |+| `exit(n)` with a bigger n, or killed by a signal | `0x11` | 35 |++So a script can get init's code back with `(code - 33) / 2`. (Codes+up to 111, because QEMU's exit code can't be more than 255.)++### The exFAT share++A partition with the **Microsoft basic data** type+(`EBD0A0A2-B9E5-4433-87C0-68B6B72699C7`, the one Windows uses for+FAT, exFAT and NTFS) that holds an **exFAT** filesystem (its boot sector+says `EXFAT   `) is mounted at **`/mnt/share`**. That's handy for moving+files between the host and the guest: make a partition, copy files in+with exfatprogs or a file manager, boot. If there's no such partition,+nothing happens and nothing is printed. Like the root, it doesn't use up+a `/mnt/diskN` number.++It's mounted **read-write**. The exFAT driver (stage 9) creates, writes,+appends, truncates, makes and removes directories and deletes files,+keeping the cluster bitmap, FAT chains and checksums right, and+`fsck.exfat` has found it clean after every write test since stage 9+(`scripts/exfat-check.sh`, and the stage 21 check below). What it+doesn't do: rename, file times, and the "volume dirty" flag (a crash in+the middle of a write could leave the volume needing an fsck). A TexFAT+volume with two FATs is still mounted read-only.++### msync++`msync(addr, len, flags)` asks the kernel to write a shared file mapping+back to its file. In Rustbelt there's nothing to write back: `mmap` of a+file with `MAP_SHARED` is refused (only private copies of files, and+shared anonymous memory, exist). So `msync` **only checks its+arguments**, like Linux does:++- `EINVAL` if `addr` isn't page-aligned, if `flags` has an unknown bit,+  or if it has both `MS_SYNC` and `MS_ASYNC`;+- `ENOMEM` if any page in the range isn't mapped (`mm.rs`'s `covers`);+- otherwise 0, with `len` rounded up to whole pages (len 0 is fine).++### RUSTBELT_DISK0 and CARGO_TARGET_DIR++Clarke builds his own drive B: image. With+`RUSTBELT_DISK0=/path/to/foundry.img` set while building, `build.rs`+**copies** that file to `disk.img` byte for byte instead of making our+FAT32 disk, and skips building the programs that only live on our disk+(sh, ls, cat and friends). Without it, nothing changes.++Cargo puts everything it builds in `target/`, unless `CARGO_TARGET_DIR`+says somewhere else. The disk images used to be written to the right+place but looked for in `target/`. Now `cargo run`/`cargo test` go+through `scripts/qemu.sh` (a tiny wrapper around QEMU set as+bootimage's `run-command` in `Cargo.toml`), which swaps `target/` for+`$CARGO_TARGET_DIR/`, and every script starts with+`T="${CARGO_TARGET_DIR:-target}"`.++### Testing it++`build.rs` makes one more image, `target/rootfs-gpt.img` (22 MiB): a GPT+disk with two partitions, made with the same host tools as before+(`sfdisk`, `mke2fs`, `mkfs.exfat`):++| Partition | Type | Contents |+|-----------|------|----------|+| 1 (12 MiB) | Linux root (x86-64) | ext2: `/sbin/init`, `/bin/init` and `/bin/sh` (all `user/inittest.c`), `/etc/rootfs-v2`, `/etc/init-status` = 7, and empty `tmp`, `dev`, `proc`, `mnt` |+| 2 (8 MiB) | Microsoft basic data | exFAT "SHARE" with `hello.txt` |++`inittest` looks at how it was started. As `/sbin/init` it checks that+it is pid 1, its argv and PATH, that `/` is the ext2 partition, that+`/tmp`, `/dev`, `/proc` and `/mnt/disk0` are there, reads+`/mnt/share/hello.txt` and writes a file next to it, and tries `msync`+the right and wrong ways; then it exits with the number in+`/etc/init-status` (7), or 1 if anything failed. As `/bin/init` it+exits 21, as `/bin/sh` 22, so a test can tell which one ran.++| Test | What it shows |+|------|---------------|+| `tests/boot_v2.rs` | (only this test kernel gets `rootfs-gpt.img`, from `scripts/qemu.sh`) `/` is the ext2 partition, `/mnt/share` is the exFAT one and writable, `/tmp` is still the ramfs, B: is still `/mnt/disk0`; `/sbin/init` runs as pid 1 and returns 7 (all of inittest's checks, msync included) with no leaks; delete `/sbin/init` and `/bin/init` runs, then `/bin/sh`, then `ENOENT`; the argv/env above; the exit-code table |+| `tests/fat.rs` (stage 21 part) | with no root partition: `/` is the ramfs, no `/mnt/share`, the disks are numbered as before |+| `scripts/boot-v2-check.sh` | the real headless kernel with a writable copy of the image: init runs, QEMU exits **47** (= `exit(7)`), and `e2fsck`/`fsck.exfat` find both partitions clean afterwards |++`scripts/run-headless.sh` doesn't attach the image, so it checks that a+normal boot is exactly what it was.++### What stage 21 does and doesn't do++Does: everything above.++Doesn't (yet):++- **No kernel command line**: knobs are fixed when the kernel is built.+- The root must be **ext2** (not exFAT, FAT or btrfs), and the first+  matching partition wins; no `root=` to pick another.+- **pid 1 isn't restarted** when it exits; the run ends.+- `msync` writes nothing back, because there are no shared file+  mappings to write.+- exFAT still has no rename, file times or dirty flag.+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,@@ -5169,6 +5382,7 @@ timers, user ids), and Unix sockets with `/proc/self` (stage 18). - [x] Unix sockets (stage 18): `AF_UNIX` stream/dgram/seqpacket with path, abstract and unnamed names, `socketpair`, `sendmsg`/`recvmsg` with `SCM_RIGHTS` (in-flight fds, `MSG_CTRUNC`, `MSG_CMSG_CLOEXEC`) and `SCM_CREDENTIALS`, `SO_PEERCRED`/`SO_PASSCRED`, real buffer limits with backpressure, poll/epoll with `POLLRDHUP`/`POLLHUP`, and `/proc/self/fd` + `/proc/self/exe` — [man 7 unix](https://man7.org/linux/man-pages/man7/unix.7.html), [man 3 cmsg](https://man7.org/linux/man-pages/man3/cmsg.3.html), [man 2 sendmsg](https://man7.org/linux/man-pages/man2/sendmsg.2.html), [man 5 proc](https://man7.org/linux/man-pages/man5/proc.5.html), [Wayland protocol: wire format](https://wayland.freedesktop.org/docs/html/ch04.html) - [x] The vDSO, static PIE and mremap (stage 19): `__vdso_clock_gettime`/`gettimeofday`/`time`/`getcpu` with a seqlocked vvar page and `AT_SYSINFO_EHDR`, a nanosecond kernel clock, ET_DYN programs without PT_INTERP at a random base (`AT_PHDR`/`AT_ENTRY`/`AT_BASE`, `PT_GNU_STACK`/`PT_TLS`/`PT_GNU_RELRO`), musl-gcc C programs in the build, `mremap` with `MREMAP_MAYMOVE`/`MREMAP_FIXED`, the threads flake fixed, Linux device numbers and `ttyname` — [man 7 vdso](https://man7.org/linux/man-pages/man7/vdso.7.html), [man 2 mremap](https://man7.org/linux/man-pages/man2/mremap.2.html) - [x] Plain static programs and timerfd (stage 20): the kernel moved to `0x200_0000_0000` so user space starts at 4 MiB and stock `musl-gcc -static` ET_EXEC binaries run unmodified; `timerfd_create`/`settime`/`gettime` on the existing timer table, readable with `read`/poll/select/epoll+- [x] Finding the root and starting init (stage 21): the GPT "Linux root (x86-64)" partition mounted at `/`, `/sbin/init` -> `/bin/init` -> `/bin/sh` as pid 1 with build-time argv/env knobs and its exit status passed to QEMU, an exFAT "Microsoft basic data" partition at `/mnt/share` (read-write), `msync`, `RUSTBELT_DISK0` and `CARGO_TARGET_DIR` everywhere — [Discoverable Partitions Specification](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/), [man 2 msync](https://man7.org/linux/man-pages/man2/msync.2.html) - [ ] The kernel/OS split and a musl port (next; waits for Tyler's go; see the missing-syscall list in "musl prerequisites") - [ ] A Hyprland-style tiling compositor on the framebuffer (later) 
@@ -60,6 +60,17 @@ //!    its size and CRC-32, which tests/btrfs.rs and scripts/btrfs-check.sh //!    compare with what Rustbelt reads. //!+//! 6. Stage 21: `target/rootfs-gpt.img` (24 MiB), for `cargo test --test+//!    boot_v2` only (scripts/qemu.sh attaches it to that test): a GPT disk+//!    laid out like boot contract v2 -- an ext2 partition of type "Linux+//!    root (x86-64)" holding user/inittest.c as /sbin/init, /bin/init and+//!    /bin/sh, and an exFAT partition of type "Microsoft basic data" (the+//!    share, /mnt/share).+//!+//! K1 (stage 21): with `RUSTBELT_DISK0=/path/to/disk.img` set, step 2 just+//! copies that file to `target/disk.img` (Foundry OS's image), and the+//! programs that moved to Foundry OS (`MOVED_PROGRAMS`) aren't built at all.+//! //! Needs dosfstools (mkfs.vfat), mtools (mmd, mcopy), fdisk (sfdisk), //! e2fsprogs (mke2fs), exfatprogs (mkfs.exfat) and btrfs-progs //! (mkfs.btrfs, btrfs-convert):@@ -86,6 +97,14 @@ const PROGRAMS: &[&str] = &[     "unixtest", // stage 18: Unix sockets, /proc/self ]; +/// Stage 21 (K1): the programs Foundry OS now builds itself. With+/// RUSTBELT_DISK0 set, they'd only go into the disk we no longer make, so+/// they're skipped.+const MOVED_PROGRAMS: &[&str] = &[+    "sh", "ls", "cat", "wc", "grep", "mkdir", "rm", "touch", "cp", "sleep", "ifconfig", "ping", "host", "fetch", "httpd",+    "getty", "tile", "random", "sysinfo",+];+ /// C programs in /bin, built with the host's C compiler and no libc at all: /// plain Linux binaries that check Rustbelt speaks the Linux ABI. const C_PROGRAMS: &[&str] = &["rawabi"]; // stage 15.1@@ -133,7 +152,13 @@ fn main() {     println!("cargo:rerun-if-changed=vdso");      build_vdso(&manifest, &out_dir);+    // K1: an OS image from outside (Foundry OS's `make image`) as disk 0.+    println!("cargo:rerun-if-env-changed=RUSTBELT_DISK0");+    let external_disk0 = env::var_os("RUSTBELT_DISK0").map(PathBuf::from);     for program in PROGRAMS {+        if external_disk0.is_some() && MOVED_PROGRAMS.contains(program) {+            continue; // K1: Foundry OS's image has its own+        }         build_program(&user_dir, &out_dir, program);     }     for program in C_PROGRAMS {@@ -142,10 +167,14 @@ fn main() {     for &(program, source, kind) in MUSL_PROGRAMS {         build_musl_program(&user_dir, &out_dir, program, source, kind);     }-    build_disk(&manifest, &out_dir);+    match &external_disk0 {+        Some(img) => use_external_disk(&manifest, img),+        None => build_disk(&manifest, &out_dir),+    }     build_ext2_disks(&manifest, &out_dir);     build_exfat_disks(&manifest, &out_dir);     build_btrfs_disks(&manifest, &out_dir);+    build_root_disk(&manifest, &user_dir, &out_dir); }  fn build_program(user_dir: &Path, out_dir: &Path, program: &str) {@@ -256,6 +285,16 @@ fn run(cmd: &mut Command, stdin: Option<&str>) {     assert!(status.success(), "{cmd:?} failed"); } +/// K1: copy an OS image built elsewhere to target/disk.img (instead of+/// building one), so `cargo run`/`cargo test` boot it as disk 0.+fn use_external_disk(manifest: &Path, img: &Path) {+    println!("cargo:rerun-if-changed={}", img.display());+    let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target"));+    fs::create_dir_all(&target_dir).unwrap();+    fs::copy(img, target_dir.join("disk.img"))+        .unwrap_or_else(|e| panic!("RUSTBELT_DISK0={}: {e}", img.display()));+}+ fn build_disk(manifest: &Path, out_dir: &Path) {     let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target"));     fs::create_dir_all(&target_dir).unwrap();@@ -562,3 +601,76 @@ fn build_btrfs_disks(manifest: &Path, out_dir: &Path) {     ];     write_manifest(&ctree, &extra, &target_dir.join("btrfs-convert.manifest")); }++// --- Boot contract v2 test disk (stage 21) -----------------------------------++/// target/rootfs-gpt.img: GPT, partition 1 = ext2 root (type "Linux root+/// (x86-64)"), partition 2 = exFAT share (type "Microsoft basic data").+/// Each file system is made as its own image first, then copied into place+/// (like build_disk).+fn build_root_disk(manifest: &Path, user_dir: &Path, out_dir: &Path) {+    const ROOT_SECTORS: u64 = 12 * 2048; // 12 MiB+    const SHARE_SECTORS: u64 = 8 * 2048; // 8 MiB+    let target_dir = env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| manifest.join("target"));++    // The init program, at the three places the kernel tries.+    let init = out_dir.join("inittest");+    let status = Command::new("musl-gcc")+        .args(["-O2", "-Wall", "-Wextra", "-static", "-Wl,--build-id=none"])+        .arg(user_dir.join("inittest.c"))+        .arg("-o")+        .arg(&init)+        .status()+        .expect("failed to run musl-gcc (install it: `sudo apt install musl-tools`)");+    assert!(status.success(), "building inittest failed");++    let tree = out_dir.join("rootfs-tree");+    let _ = fs::remove_dir_all(&tree);+    for dir in ["sbin", "bin", "etc", "tmp", "dev", "proc", "mnt/share"] {+        fs::create_dir_all(tree.join(dir)).unwrap();+    }+    for path in ["sbin/init", "bin/init", "bin/sh"] {+        fs::copy(&init, tree.join(path)).unwrap();+    }+    fs::write(tree.join("etc/rootfs-v2"), "This is the ext2 root partition of target/rootfs-gpt.img.\n").unwrap();+    // /sbin/init exits with this when all its checks pass, so a test can+    // see the status make it through to QEMU's exit code (33 + 2 * 7 = 47).+    fs::write(tree.join("etc/init-status"), "7\n").unwrap();+    let root = out_dir.join("rootfs-root.img");+    let _ = fs::remove_file(&root);+    run(+        Command::new(tool("mke2fs"))+            .args(["-q", "-F", "-t", "ext2", "-m", "0", "-b", "1024", "-L", "ROOT", "-E", "root_owner=0:0", "-d"])+            .arg(&tree)+            .arg(&root)+            .arg(format!("{}k", ROOT_SECTORS / 2)),+        None,+    );++    let share_tree = out_dir.join("rootfs-share");+    let _ = fs::remove_dir_all(&share_tree);+    fs::create_dir_all(&share_tree).unwrap();+    fs::write(share_tree.join("hello.txt"), "Hello from the exFAT share!\n").unwrap();+    let share = out_dir.join("rootfs-share.img");+    let _ = fs::remove_file(&share);+    fs::File::create(&share).unwrap().set_len(SHARE_SECTORS * 512).unwrap();+    run(Command::new(tool("mkfs.exfat")).args(["-q", "-L", "SHARE"]).arg(&share), None);+    exfat_populate::populate(&share, &share_tree);++    let disk = target_dir.join("rootfs-gpt.img");+    let share_start = PART_START + ROOT_SECTORS;+    let _ = fs::remove_file(&disk);+    fs::File::create(&disk).unwrap().set_len((share_start + SHARE_SECTORS + PART_START) * 512).unwrap();+    let layout = format!(+        "label: gpt\n\+         start={PART_START}, size={ROOT_SECTORS}, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709, name=\"root\"\n\+         start={share_start}, size={SHARE_SECTORS}, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=\"share\"\n"+    );+    run(Command::new(tool("sfdisk")).arg("--quiet").arg(&disk), Some(&layout));+    use std::io::{Seek, SeekFrom};+    let mut file = fs::OpenOptions::new().write(true).open(&disk).unwrap();+    for (img, start) in [(&root, PART_START), (&share, share_start)] {+        file.seek(SeekFrom::Start(start * 512)).unwrap();+        file.write_all(&fs::read(img).unwrap()).unwrap();+    }+}
@@ -0,0 +1,60 @@+#!/usr/bin/env bash+# Stage 21: boot contract v2, end to end. Boots the real (headless) kernel+# with the usual four data disks plus a writable copy of the GPT test disk+# (target/rootfs-gpt.img, made by build.rs), so main.rs finds the+# "Linux root (x86-64)" partition, mounts it at /, starts /sbin/init+# (user/inittest.c) as pid 1, and ends the run with init's exit status.+#+# inittest exits 7 when all its checks pass, and the kernel turns exit(n)+# into QEMU exit code 33 + 2n, so this script wants QEMU to exit with 47.+# Afterwards it runs e2fsck and fsck.exfat on the two partitions of the+# copy, which init wrote to.+#+# Usage: ./scripts/boot-v2-check.sh+set -euo pipefail+cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}"+export PATH="$HOME/.cargo/bin:$PATH:/usr/sbin:/sbin" # e2fsck, fsck.exfat++cargo build --features headless+touch $T/x86_64-rust_kernel/debug/rustbelt # (see scripts/runner.sh)+cargo bootimage --features headless+IMG=$T/x86_64-rust_kernel/debug/bootimage-rustbelt.bin++ROOT=$T/boot-v2-root.img+cp $T/rootfs-gpt.img "$ROOT"+LOG=$T/boot-v2-serial.log+set +e+timeout 60 qemu-system-x86_64 -smp 4 -cpu max \+    -drive format=raw,file="$IMG" \+    -drive file=$T/disk.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/ext2.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/exfat.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/btrfs.img,format=raw,if=virtio,snapshot=on \+    -drive file="$ROOT",format=raw,if=virtio \+    -device isa-debug-exit,iobase=0xf4,iosize=0x04 \+    -serial stdio -display none -no-reboot | tee "$LOG"+code=${PIPESTATUS[0]}+set -e++ok=1+for line in "mounted at / (root partition)" "mounted at /mnt/share" \+            "[inittest] started as /sbin/init" "[inittest] ok: I am pid 1" \+            'headless: argv[1] is "selftest"' "[inittest] 0 failed" \+            "[init] pid 1 (/sbin/init) exited: Exited(7)"; do+    grep -qF -- "$line" "$LOG" || { echo "[boot-v2] MISSING: $line"; ok=0; }+done+[ "$code" = 47 ] || { echo "[boot-v2] QEMU exited with $code, not 47 (= exit(7))"; ok=0; }++# The partitions (start and size in 512-byte sectors, as build.rs made them).+dd if="$ROOT" of=$T/boot-v2-p1.img bs=512 skip=2048 count=24576 status=none+dd if="$ROOT" of=$T/boot-v2-p2.img bs=512 skip=26624 count=16384 status=none+e2fsck -fn $T/boot-v2-p1.img >/dev/null || { echo "[boot-v2] e2fsck found errors on the root"; ok=0; }+fsck.exfat -n $T/boot-v2-p2.img >/dev/null || { echo "[boot-v2] fsck.exfat found errors on the share"; ok=0; }+rm -f $T/boot-v2-p1.img $T/boot-v2-p2.img++if [ $ok = 1 ]; then+    echo "[boot-v2] OK: init ran as pid 1, QEMU exited 47 (= exit 7), both partitions fsck-clean"+    exit 0+fi+exit 1
@@ -15,6 +15,7 @@ #        builds the images and the test kernel) set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir) export PATH="$HOME/.cargo/bin:$PATH:/usr/sbin:/sbin" for tool in btrfs qemu-system-x86_64 python3; do     command -v $tool >/dev/null || { echo "need $tool (apt install btrfs-progs qemu-system-x86)"; exit 1; }@@ -22,19 +23,19 @@ done status=0  for img in btrfs btrfs-lzo btrfs-zstd btrfs-convert btrfs-sha256; do-    echo "== btrfs check --readonly target/$img.img"-    btrfs check --readonly target/$img.img 2>&1 | grep -E "^(found|no error|ERROR)" | sed 's/^/  /' \+    echo "== btrfs check --readonly $T/$img.img"+    btrfs check --readonly $T/$img.img 2>&1 | grep -E "^(found|no error|ERROR)" | sed 's/^/  /' \         || true-    btrfs check --readonly target/$img.img >/dev/null 2>&1 || { echo "!! btrfs check found problems"; status=1; }+    btrfs check --readonly $T/$img.img >/dev/null 2>&1 || { echo "!! btrfs check found problems"; status=1; } done  echo "== run the tests/btrfs.rs kernel"-KERNEL=$(ls -t target/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-btrfs-*.bin 2>/dev/null | head -1 || true)+KERNEL=$(ls -t $T/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-btrfs-*.bin 2>/dev/null | head -1 || true) [ -n "$KERNEL" ] || { echo "no test kernel: run 'cargo test --test btrfs' first"; exit 1; }-LOG=target/btrfs-check.log+LOG=$T/btrfs-check.log drives=() for img in disk ext2 exfat btrfs ext2-4k ext2-rev0 exfat-512 btrfs-lzo btrfs-zstd btrfs-convert btrfs-sha256; do-    drives+=(-drive "file=target/$img.img,format=raw,if=virtio,snapshot=on")+    drives+=(-drive "file=$T/$img.img,format=raw,if=virtio,snapshot=on") done set +e timeout 120 qemu-system-x86_64 -smp 4 -drive format=raw,file="$KERNEL" -no-reboot "${drives[@]}" \@@ -45,7 +46,7 @@ echo "  $(grep -c '\[ok\]' $LOG) btrfs tests ok" [ $code = 33 ] || { echo "!! test kernel failed (exit $code), see $LOG"; exit 1; }  echo "== compare what Rustbelt read with the host's files (zlib.crc32)"-OUT=$(dirname "$(ls -td target/x86_64-rust_kernel/debug/build/rustbelt/*/out/btrfs-tree | head -1)")+OUT=$(dirname "$(ls -td $T/x86_64-rust_kernel/debug/build/rustbelt/*/out/btrfs-tree | head -1)") python3 - "$LOG" "$OUT" <<'PY' || status=1 import os, sys, zlib log, out = sys.argv[1:]
@@ -20,6 +20,7 @@ #        `cargo test --test exfat`, which build the images and the kernels) set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir) export PATH="$HOME/.cargo/bin:$PATH:/usr/sbin:/sbin" for tool in fsck.exfat dump.exfat qemu-system-x86_64; do     command -v $tool >/dev/null || { echo "need $tool (apt install exfatprogs qemu-system-x86)"; exit 1; }@@ -31,41 +32,41 @@ fsck() { # fsck <image> <title> } status=0 -for img in target/exfat.img target/exfat-512.img; do+for img in $T/exfat.img $T/exfat-512.img; do     fsck $img "pristine, as built" done -mkdir -p target/fsck-cp target/exfat.img target/fsck/selftest-exfat.img-for img in exfat exfat-512; do cp target/$img.img target/fsck/tests-$img.img; done+mkdir -p $T/fsck+cp $T/exfat.img $T/fsck/selftest-exfat.img+for img in exfat exfat-512; do cp $T/$img.img $T/fsck/tests-$img.img; done -echo "== boot the shell self-test on target/fsck/selftest-exfat.img"-EXFAT_IMG=target/fsck/selftest-exfat.img ./scripts/run-headless.sh >/dev/null && echo "self-test OK"+echo "== boot the shell self-test on $T/fsck/selftest-exfat.img"+EXFAT_IMG=$T/fsck/selftest-exfat.img ./scripts/run-headless.sh >/dev/null && echo "self-test OK" -echo "== run the tests/exfat.rs kernel on target/fsck/tests-exfat*.img"-KERNEL=$(ls -t target/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-exfat-*.bin 2>/dev/null | head -1 || true)+echo "== run the tests/exfat.rs kernel on $T/fsck/tests-exfat*.img"+KERNEL=$(ls -t $T/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-exfat-*.bin 2>/dev/null | head -1 || true) [ -n "$KERNEL" ] || { echo "no test kernel: run 'cargo test --test exfat' first"; exit 1; } set +e timeout 120 qemu-system-x86_64 -smp 4 -drive format=raw,file="$KERNEL" -no-reboot \-    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/ext2.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/fsck/tests-exfat.img,format=raw,if=virtio \-    -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/ext2-4k.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/ext2-rev0.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/fsck/tests-exfat-512.img,format=raw,if=virtio \+    -drive file=$T/disk.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/ext2.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/fsck/tests-exfat.img,format=raw,if=virtio \+    -drive file=$T/btrfs.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/ext2-4k.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/ext2-rev0.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/fsck/tests-exfat-512.img,format=raw,if=virtio \     -device isa-debug-exit,iobase=0xf4,iosize=0x04 \-    -serial stdio -display none > target/fsck/tests-exfat.log+    -serial stdio -display none > $T/fsck/tests-exfat.log code=$? set -e-grep -c "\[ok\]" target/fsck/tests-exfat.log | sed 's/$/ exFAT tests ok/'-[ $code = 33 ] || { echo "!! test kernel failed (exit $code), see target/fsck/tests-exfat.log"; exit 1; }+grep -c "\[ok\]" $T/fsck/tests-exfat.log | sed 's/$/ exFAT tests ok/'+[ $code = 33 ] || { echo "!! test kernel failed (exit $code), see $T/fsck/tests-exfat.log"; exit 1; } -for img in target/fsck/selftest-exfat.img target/fsck/tests-exfat.img target/fsck/tests-exfat-512.img; do+for img in $T/fsck/selftest-exfat.img $T/fsck/tests-exfat.img $T/fsck/tests-exfat-512.img; do     fsck "$img" "after Rustbelt wrote to it" done -IMG=target/fsck/selftest-exfat.img+IMG=$T/fsck/selftest-exfat.img echo "== dump.exfat: what is in / on the self-test copy" dump.exfat -s / $IMG 2>/dev/null | sed -n 's/^Path: /  /p' # (dump.exfat -s, a directory scan, rather than -d "/New Folder/Note.txt":
@@ -16,6 +16,7 @@ #        `cargo test --test ext2`, which build the images and the kernels) set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir) export PATH="$HOME/.cargo/bin:$PATH:/usr/sbin:/sbin" for tool in e2fsck debugfs qemu-system-x86_64; do     command -v $tool >/dev/null || { echo "need $tool (apt install e2fsprogs qemu-system-x86)"; exit 1; }@@ -27,44 +28,44 @@ fsck() { # fsck <image> <title> } status=0 -for img in target/ext2.img target/ext2-4k.img target/ext2-rev0.img; do+for img in $T/ext2.img $T/ext2-4k.img $T/ext2-rev0.img; do     fsck $img "pristine, as built" done -mkdir -p target/fsck-cp target/ext2.img target/fsck/selftest.img-for img in ext2 ext2-4k ext2-rev0; do cp target/$img.img target/fsck/tests-$img.img; done+mkdir -p $T/fsck+cp $T/ext2.img $T/fsck/selftest.img+for img in ext2 ext2-4k ext2-rev0; do cp $T/$img.img $T/fsck/tests-$img.img; done -echo "== boot the shell self-test on target/fsck/selftest.img"-EXT2_IMG=target/fsck/selftest.img ./scripts/run-headless.sh >/dev/null && echo "self-test OK"+echo "== boot the shell self-test on $T/fsck/selftest.img"+EXT2_IMG=$T/fsck/selftest.img ./scripts/run-headless.sh >/dev/null && echo "self-test OK" -echo "== run the tests/ext2.rs kernel on target/fsck/tests-*.img"-KERNEL=$(ls -t target/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-ext2-*.bin 2>/dev/null | head -1 || true)+echo "== run the tests/ext2.rs kernel on $T/fsck/tests-*.img"+KERNEL=$(ls -t $T/x86_64-rust_kernel/debug/build/rustbelt/*/out/bootimage-ext2-*.bin 2>/dev/null | head -1 || true) [ -n "$KERNEL" ] || { echo "no test kernel: run 'cargo test --test ext2' first"; exit 1; } set +e timeout 120 qemu-system-x86_64 -smp 4 -drive format=raw,file="$KERNEL" -no-reboot \-    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/fsck/tests-ext2.img,format=raw,if=virtio \-    -drive file=target/exfat.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/fsck/tests-ext2-4k.img,format=raw,if=virtio \-    -drive file=target/fsck/tests-ext2-rev0.img,format=raw,if=virtio \-    -drive file=target/exfat-512.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/disk.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/fsck/tests-ext2.img,format=raw,if=virtio \+    -drive file=$T/exfat.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/btrfs.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/fsck/tests-ext2-4k.img,format=raw,if=virtio \+    -drive file=$T/fsck/tests-ext2-rev0.img,format=raw,if=virtio \+    -drive file=$T/exfat-512.img,format=raw,if=virtio,snapshot=on \     -device isa-debug-exit,iobase=0xf4,iosize=0x04 \-    -serial stdio -display none > target/fsck/tests.log+    -serial stdio -display none > $T/fsck/tests.log code=$? set -e-grep -c "\[ok\]" target/fsck/tests.log | sed 's/$/ ext2 tests ok/'-[ $code = 33 ] || { echo "!! test kernel failed (exit $code), see target/fsck/tests.log"; exit 1; }+grep -c "\[ok\]" $T/fsck/tests.log | sed 's/$/ ext2 tests ok/'+[ $code = 33 ] || { echo "!! test kernel failed (exit $code), see $T/fsck/tests.log"; exit 1; } -for img in target/fsck/selftest.img target/fsck/tests-ext2*.img; do+for img in $T/fsck/selftest.img $T/fsck/tests-ext2*.img; do     fsck "$img" "after Rustbelt wrote to it" done  echo "== debugfs: ls -l / on the self-test copy"-debugfs -R "ls -l /" target/fsck/selftest.img 2>/dev/null+debugfs -R "ls -l /" $T/fsck/selftest.img 2>/dev/null echo "== debugfs: cat /made-here/note.txt"-debugfs -R "cat /made-here/note.txt" target/fsck/selftest.img 2>/dev/null+debugfs -R "cat /made-here/note.txt" $T/fsck/selftest.img 2>/dev/null  [ $status = 0 ] && echo "== all images are clean" || echo "== e2fsck found problems" exit $status
@@ -8,13 +8,14 @@ # Usage: ./scripts/gpu-check.sh set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir) -PPM=target/gpu-check.ppm+PPM=$T/gpu-check.ppm SESSION=$(mktemp /tmp/gpu-session.XXXXXX) echo tile > "$SESSION" rm -f "$PPM"-./scripts/screenshot.sh "$PPM" "$SESSION" "tile: 4 windows on 1024x768, console 61x46" > target/gpu-check.log 2>&1 \-    || { tail -20 target/gpu-check.log; echo "[gpu-check] screenshot session failed"; exit 1; }+./scripts/screenshot.sh "$PPM" "$SESSION" "tile: 4 windows on 1024x768, console 61x46" > $T/gpu-check.log 2>&1 \+    || { tail -20 $T/gpu-check.log; echo "[gpu-check] screenshot session failed"; exit 1; } rm -f "$SESSION"  python3 - "$PPM" <<'PY'
@@ -0,0 +1,25 @@+#!/usr/bin/env bash+# Stage 21: how `cargo run` and `cargo test` start QEMU (Cargo.toml's+# bootimage `run-command`). bootimage calls this with the boot image and+# Cargo.toml's run-args / test-args; we only change two things:+#+#  1. Those args say `target/disk.img` & co. If you build with+#     CARGO_TARGET_DIR=/somewhere, the images are in /somewhere instead,+#     so every "=target/" becomes "=$CARGO_TARGET_DIR/".+#  2. The boot_v2 test kernel (tests/boot_v2.rs) gets one more disk:+#     target/rootfs-gpt.img, the boot contract v2 image build.rs makes.+#+# `exec`: QEMU replaces this script, so its PID is the one bootimage+# started (and times out).+set -euo pipefail+T="${CARGO_TARGET_DIR:-target}"+args=()+boot_v2=0+for a in "$@"; do+    args+=("${a//=target\//=$T/}")+    case "$a" in *bootimage-boot_v2-*) boot_v2=1 ;; esac+done+if [ "$boot_v2" = 1 ]; then+    args+=(-drive "file=$T/rootfs-gpt.img,format=raw,if=virtio,snapshot=on")+fi+exec qemu-system-x86_64 "${args[@]}"
@@ -23,26 +23,27 @@ #            run-args); qemu64 has only FXSAVE, so this checks fpu.rs's fallback set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir)  cargo build --features "headless${1:+,$1}" # Force bootimage to repack the disk image (see scripts/runner.sh for why).-touch target/x86_64-rust_kernel/debug/rustbelt+touch $T/x86_64-rust_kernel/debug/rustbelt cargo bootimage --features "headless${1:+,$1}"-IMG=target/x86_64-rust_kernel/debug/bootimage-rustbelt.bin+IMG=$T/x86_64-rust_kernel/debug/bootimage-rustbelt.bin  if [ -n "${EXT2_IMG:-}" ]; then     EXT2_DRIVE="file=$EXT2_IMG,format=raw,if=virtio" else-    EXT2_DRIVE="file=target/ext2.img,format=raw,if=virtio,snapshot=on"+    EXT2_DRIVE="file=$T/ext2.img,format=raw,if=virtio,snapshot=on" fi  if [ -n "${EXFAT_IMG:-}" ]; then     EXFAT_DRIVE="file=$EXFAT_IMG,format=raw,if=virtio" else-    EXFAT_DRIVE="file=target/exfat.img,format=raw,if=virtio,snapshot=on"+    EXFAT_DRIVE="file=$T/exfat.img,format=raw,if=virtio,snapshot=on" fi -BTRFS_DRIVE="file=${BTRFS_IMG:-target/btrfs.img},format=raw,if=virtio,snapshot=on"+BTRFS_DRIVE="file=${BTRFS_IMG:-$T/btrfs.img},format=raw,if=virtio,snapshot=on"  # Network: QEMU's user-mode NAT (see Cargo.toml run-args). guestfwd runs a # tiny test web server for 10.0.2.100:80; hostfwd forwards a free host port@@ -51,7 +52,7 @@ BTRFS_DRIVE="file=${BTRFS_IMG:-target/btrfs.img},format=raw,if=virtio,snapshot=o HTTP_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])') NETDEV="user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat" NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80"-CURL_OUT=target/headless-curl.txt+CURL_OUT=$T/headless-curl.txt rm -f "$CURL_OUT" ( for _ in $(seq 1 80); do      # retry until httpd is up (~1 s apart)       sleep 1@@ -63,17 +64,17 @@ CURL_PID=$! # virtio-rng, and virtio-console whose hvc0 port reads the commands in # scripts/hvc0-session.txt and writes into target/headless-hvc0.txt (the # self-test runs `getty -1 /dev/hvc0`, a shell on that port).-HVC_OUT=target/headless-hvc0.txt+HVC_OUT=$T/headless-hvc0.txt rm -f "$HVC_OUT" VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci     -device virtio-serial-pci -device virtconsole,chardev=hvc     -chardev "file,id=hvc,path=$HVC_OUT,input-path=scripts/hvc0-session.txt") -LOG=target/headless-serial.log+LOG=$T/headless-serial.log set +e timeout 120 qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \     -drive format=raw,file="$IMG" \-    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/disk.img,format=raw,if=virtio,snapshot=on \     -drive "$EXT2_DRIVE" \     -drive "$EXFAT_DRIVE" \     -drive "$BTRFS_DRIVE" \@@ -88,7 +89,7 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(-        "Rustbelt 0.20.0 (stage 20: gcc -static and timerfd) x86_64" # ver+        "Rustbelt 0.21.0 (stage 21: root partition and init) 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@@ -159,7 +160,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "[nbtest] all checks passed"         "[fputest] all checks passed"                  # stage 15.1: musl prerequisites         "[pathtest] all checks passed"-        "rawabi: uname: Rustbelt 0.20.0"               # a no-libc gcc binary+        "rawabi: uname: Rustbelt 0.21.0"               # a no-libc gcc binary         "rawabi: all checks passed"         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"@@ -184,7 +185,7 @@ if [ -z "${1:-}" ] && [ "$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.20.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.21.0" "$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@@ -196,7 +197,7 @@ if [ -z "${1:-}" ] && [ "$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.20.0 (stage 20" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.21.0 (stage 21" \                 "$ 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
@@ -6,6 +6,13 @@ # (e.g. --features headless and back), cargo re-uses an older kernel binary # with an *old* timestamp, so bootimage thinks its cached disk image is still # up to date and boots the wrong kernel. Touching the ELF forces a rebuild.+#+# Stage 21: it also runs from the repository root, so the relative paths in+# Cargo.toml (scripts/qemu.sh, tests/hvc0-input.txt...) work wherever you+# typed `cargo run`. set -euo pipefail-touch "$1"-exec bootimage runner "$@"+kernel=$(realpath "$1")+shift+touch "$kernel"+cd "$(dirname "$0")/.."+exec bootimage runner "$kernel" "$@"
@@ -13,12 +13,13 @@ # Exits 1 if any expected text doesn't show up. set -euo pipefail cd "$(dirname "$0")/.."+T="${CARGO_TARGET_DIR:-target}" # stage 21: the build output (cargo's target dir)  cargo build # Force bootimage to repack the disk image (see scripts/runner.sh for why).-touch target/x86_64-rust_kernel/debug/rustbelt+touch $T/x86_64-rust_kernel/debug/rustbelt cargo bootimage-IMG=target/x86_64-rust_kernel/debug/bootimage-rustbelt.bin+IMG=$T/x86_64-rust_kernel/debug/bootimage-rustbelt.bin OUT="${1:-$PWD/boot.png}" SESSION="${2:-}" shift $(( $# > 2 ? 2 : $# ))@@ -45,7 +46,7 @@ fputest | grep passed pathtest | grep passed rawabi | grep -v "ok   " TXT-    set -- "Rustbelt 0.20.0 (stage 20: gcc -static and timerfd) x86_64" "tile: 4 windows on 1024x768, console 61x46" \+    set -- "Rustbelt 0.21.0 (stage 21: root partition and init) x86_64" "tile: 4 windows on 1024x768, console 61x46" \         "^C" "(interrupted: SIGINT)" "threads: 4 threads summed 1..=40000000 = 800000020000000" \         "echoserver: epoll: 3 clients, 9 echoes" "[threads] all checks passed" \         "[fputest] all checks passed" "[pathtest] all checks passed" \@@ -55,10 +56,10 @@ SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)  qemu-system-x86_64 -smp 4 -cpu max -drive format=raw,file="$IMG" \-    -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/ext2.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/exfat.img,format=raw,if=virtio,snapshot=on \-    -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/disk.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/ext2.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/exfat.img,format=raw,if=virtio,snapshot=on \+    -drive file=$T/btrfs.img,format=raw,if=virtio,snapshot=on \     -netdev "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh" \     -device virtio-net-pci,netdev=net0 \     -vga none -device virtio-vga -device virtio-rng-pci \
@@ -80,6 +80,15 @@ pub fn devices() -> Vec<(String, Arc<dyn BlockDevice>)> {     DEVICES.lock().clone() } +/// Stage 21: the partition type of each registered partition ("vdb1" ->+/// "Linux root (x86-64)"), so fs::mount_disks can find the root.+static PART_TYPES: Mutex<Vec<(String, &'static str)>> = Mutex::new(Vec::new());++/// The partition-table type of device `name`, if it is a partition.+pub fn partition_type(name: &str) -> Option<&'static str> {+    PART_TYPES.lock().iter().find(|(n, _)| n == name).map(|(_, t)| *t)+}+ pub fn get(name: &str) -> Option<Arc<dyn BlockDevice>> {     DEVICES.lock().iter().find(|(n, _)| n == name).map(|(_, d)| d.clone()) }@@ -114,6 +123,7 @@ pub fn init(pci_devices: &[crate::pci::PciDevice]) -> Vec<String> {                                 if p.name.is_empty() { String::new() } else { format!(" name \"{}\"", p.name) }                             ));                             register(&pname, Arc::new(partition::Partition::new(disk.clone(), p.start, p.sectors)));+                            PART_TYPES.lock().push((pname, p.type_name));                         }                     }                     Err(e) => log.push(format!("{}: no partition table ({:?})", name, e)),
@@ -124,14 +124,23 @@ fn mbr_type_name(t: u8) -> &'static str {     } } +/// Stage 21: the type name of a GPT "Linux root (x86-64)" partition (the+/// GUID from the Discoverable Partitions Specification). fs::mount_disks+/// mounts the first one holding ext2 at "/".+pub const ROOT_TYPE: &str = "Linux root (x86-64)";+/// ...and of a "Microsoft basic data" one (an exFAT one becomes /mnt/share).+pub const BASIC_DATA_TYPE: &str = "Microsoft basic data (FAT/exFAT/NTFS)";+ /// GUIDs are stored with their first three fields little-endian, so the /// bytes look scrambled compared to the usual text form. fn gpt_type_name(guid: &[u8]) -> &'static str {     const BASIC_DATA: [u8; 16] = [0xa2, 0xa0, 0xd0, 0xeb, 0xe5, 0xb9, 0x33, 0x44, 0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7]; // EBD0A0A2-B9E5-4433-87C0-68B6B72699C7     const LINUX_FS: [u8; 16] = [0xaf, 0x3d, 0xc6, 0x0f, 0x83, 0x84, 0x72, 0x47, 0x8e, 0x79, 0x3d, 0x69, 0xd8, 0x47, 0x7d, 0xe4]; // 0FC63DAF-8483-4772-8E79-3D69D8477DE4     const EFI_SYSTEM: [u8; 16] = [0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b]; // C12A7328-F81F-11D2-BA4B-00A0C93EC93B+    const ROOT_X86_64: [u8; 16] = [0xe3, 0xbc, 0x68, 0x4f, 0xcd, 0xe8, 0xb1, 0x4d, 0x96, 0xe7, 0xfb, 0xca, 0xf9, 0x84, 0xb7, 0x09]; // 4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709     match guid {-        g if g == BASIC_DATA => "Microsoft basic data (FAT/exFAT/NTFS)",+        g if g == BASIC_DATA => BASIC_DATA_TYPE,+        g if g == ROOT_X86_64 => ROOT_TYPE,         g if g == LINUX_FS => "Linux filesystem",         g if g == EFI_SYSTEM => "EFI system (FAT)",         _ => "unknown",
@@ -0,0 +1,106 @@+//! Stage 21: boot contract v2 -- which program becomes pid 1.+//!+//! If `fs::mount_disks` found a GPT "Linux root (x86-64)" partition with+//! ext2 and mounted it at "/" ([`fs::root_on_disk`]), the kernel starts the+//! first of [`INIT_PATHS`] that runs, the same order Linux tries, as pid 1:+//!+//! | program      | argv                                   |+//! |--------------|----------------------------------------|+//! | /sbin/init   | `init` (+ `selftest` when headless)    |+//! | /bin/init    | `init` (+ `selftest` when headless)    |+//! | /bin/sh      | `sh` (+ `/scripts/selftest.sh` when headless) |+//!+//! The environment is [`V2_ENV`] (+ `RUSTBELT_HEADLESS=1` when headless).+//! There's no boot loader command line (bootloader 0.9 has none), so the+//! knobs are build time, like the `headless` feature itself:+//!   * `cargo build --features headless`: the headless argv/env above;+//!   * `RUSTBELT_INIT_ARGS="a b"`: extra arguments after those;+//!   * `RUSTBELT_INIT_ENV="K=V K2=V2"`: extra environment variables (one+//!     with a name already there replaces it, e.g. `PATH=...`).+//! (Both are read with `option_env!`, so changing them rebuilds the kernel.)+//!+//! The kernel does not restart pid 1. When it exits, its status ends the+//! QEMU run through isa-debug-exit: exit(n) writes 0x10 + n, so QEMU exits+//! with 33 + 2n (33 = exit 0 = the tests' "pass", 35 = exit 1, ...). A+//! status above 111 (it wouldn't fit in QEMU's 8-bit exit code) or death+//! by a signal writes 0x11 (35).+//!+//! Without a root partition nothing here is used: main.rs runs the old+//! shell supervisor (boot contract v1) exactly as before.++use crate::errno::Errno;+use crate::fs;+use crate::process::{self, ExitStatus, Process, SpawnArgs};+use alloc::{string::String, sync::Arc, vec::Vec};++/// Tried in this order; the first that starts becomes pid 1.+pub const INIT_PATHS: [&str; 3] = ["/sbin/init", "/bin/init", "/bin/sh"];++/// pid 1's environment under contract v2.+pub const V2_ENV: [&str; 3] = ["PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"];++/// argv and environment for `path` (one of [`INIT_PATHS`]).+pub fn init_args(path: &str, headless: bool) -> SpawnArgs {+    let sh = path.ends_with("/sh");+    let mut args = SpawnArgs::kernel(if sh { "sh" } else { "init" });+    if headless {+        args.args.push(String::from(if sh { "/scripts/selftest.sh" } else { "selftest" }));+    }+    args.args.extend(option_env!("RUSTBELT_INIT_ARGS").unwrap_or("").split_whitespace().map(String::from));+    args.env = V2_ENV.iter().map(|s| String::from(*s)).collect();+    if headless {+        args.env.push(String::from("RUSTBELT_HEADLESS=1"));+    }+    for var in option_env!("RUSTBELT_INIT_ENV").unwrap_or("").split_whitespace() {+        let key = var.split('=').next().unwrap_or(var);+        args.env.retain(|e| e.split('=').next() != Some(key));+        args.env.push(String::from(var));+    }+    args+}++/// Start pid 1: the first of [`INIT_PATHS`] that starts. Returns its path+/// and process, or the last error if none did. `log` gets a line for every+/// one that was tried and failed.+pub fn start_init(headless: bool, log: &mut Vec<String>) -> Result<(&'static str, Arc<Process>), Errno> {+    let mut last = Errno::ENOENT;+    for path in INIT_PATHS {+        match process::spawn_path(path, init_args(path, headless)) {+            Ok(p) => return Ok((path, p)),+            Err(e) => {+                if e != Errno::ENOENT {+                    log.push(alloc::format!("{}: can't start it: {:?}", path, e));+                }+                last = e;+            }+        }+    }+    Err(last)+}++/// What to write to isa-debug-exit for pid 1's exit status (see above).+pub fn qemu_exit_value(status: ExitStatus) -> u32 {+    match status {+        ExitStatus::Exited(n) if (0..=111).contains(&n) => 0x10 + n as u32,+        _ => 0x11,+    }+}++/// End the QEMU run with pid 1's exit status.+pub fn exit_qemu_with(status: ExitStatus) {+    use x86_64::instructions::port::Port;+    unsafe { Port::new(0xf4).write(qemu_exit_value(status)) };+}++/// Run contract v2's pid 1 and wait for it. Returns its status (Err: no+/// init could be started).+pub fn run_init(headless: bool, log: &mut Vec<String>) -> Result<(&'static str, ExitStatus), Errno> {+    let (path, p) = start_init(headless, log)?;+    let status = process::wait(&p);+    Ok((path, status))+}++/// Is contract v2 in use (a root partition was mounted at "/")?+pub fn v2() -> bool {+    fs::root_on_disk()+}
@@ -631,11 +631,45 @@ pub fn init() { /// anything else is tried as FAT (fatfs checks the boot sector itself). /// A disk whose driver refuses it (say, a btrfs with sha256 checksums) /// gets a log line saying why, and no /mnt/diskN.+///+/// Stage 21 (boot contract v2) adds two special partitions, found by their+/// GPT type (block::partition_type) and *not* numbered as /mnt/diskN:+///   * the first "Linux root (x86-64)" partition holding ext2 is mounted+///     at "/" in place of the ramfs (see [`root_on_disk`]);+///   * the first "Microsoft basic data" partition holding exFAT is+///     mounted at /mnt/share (read-write).+/// Without them, everything is exactly as before. pub fn mount_disks() -> Vec<String> {+    use crate::block::{partition, partition_type};     let mut log = Vec::new();     let devices = crate::block::devices();+    let mut special: Vec<&str> = Vec::new(); // partitions that get no /mnt/diskN+    let root = devices.iter().find(|(name, dev)| partition_type(name) == Some(partition::ROOT_TYPE) && ext2::probe(&**dev));+    if let Some((name, dev)) = root {+        special.push(name);+        match ext2::Ext2Fs::mount(dev.clone()) {+            Ok(fs) => {+                log.push(format!("{}: {} mounted at / (root partition)", name, fs.describe()));+                replace_root(fs);+                // Mount points for the kernel's own mounts, if the image+                // doesn't have them (lookups work without them; `ls /` and+                // creating /mnt/diskN need them).+                for dir in ["/tmp", "/dev", "/proc", "/mnt"] {+                    let _ = create(dir, InodeKind::Directory);+                }+            }+            Err(e) => log.push(format!("{}: root partition, but can't mount its ext2: {:?}", name, e)),+        }+    }+    let share = devices.iter().find(|(name, dev)| partition_type(name) == Some(partition::BASIC_DATA_TYPE) && exfat::probe(&**dev));+    if let Some((name, _)) = share {+        special.push(name);+    }     let mut n = 0;     for (name, dev) in &devices {+        if special.contains(&name.as_str()) {+            continue;+        }         // Skip a whole disk if it has partitions ("vda" when "vda1" exists).         let has_partitions = devices.iter().any(|(other, _)| other.len() > name.len() && other.starts_with(name.as_str()));         if has_partitions {@@ -694,5 +728,31 @@ pub fn mount_disks() -> Vec<String> {         }         n += 1;     }+    if let Some((name, dev)) = share {+        let _ = create("/mnt/share", InodeKind::Directory); // mount point (may exist)+        match exfat::ExfatFs::mount(dev.clone()).map_err(|e| format!("{:?}", e)).and_then(|fs| {+            let d = fs.describe();+            mount("/mnt/share", fs).map(|_| d).map_err(|e| format!("{:?}", e))+        }) {+            Ok(d) => log.push(format!("{}: {} mounted at /mnt/share", name, d)),+            Err(e) => log.push(format!("{}: exFAT share, but can't mount it: {}", name, e)),+        }+    }     log }++/// Did [`mount_disks`] mount a root partition at "/" (stage 21)? Then the+/// kernel starts /sbin/init (boot contract v2) instead of the old shell.+pub fn root_on_disk() -> bool {+    ROOT_ON_DISK.load(core::sync::atomic::Ordering::Relaxed)+}+static ROOT_ON_DISK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);++/// Put `fs` at "/" in place of the boot ramfs. The other mounts (/tmp,+/// /dev, /proc) stay: the mount table finds them by path, not through "/".+fn replace_root(fs: Arc<dyn FileSystem>) {+    if let Some(m) = MOUNTS.lock().iter_mut().find(|m| m.path == "/") {+        m.fs = fs;+    }+    ROOT_ON_DISK.store(true, core::sync::atomic::Ordering::Relaxed);+}
@@ -24,6 +24,7 @@ use core::panic::PanicInfo; pub mod acpi; // ACPI tables: just enough to list the CPUs (MADT) pub mod allocator; // heap: maps heap pages + the #[global_allocator] pub mod apic; // local APIC: per-CPU timer, inter-processor interrupts+pub mod boot; // stage 21: boot contract v2 -- pid 1 from the root partition pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions pub mod dev; // /dev: null, random, hvc0, fb0, tty, ptmx, pts/N pub mod elf; // ELF64 loader for user programs
@@ -138,6 +138,12 @@ fn shell_supervisor() {     #[cfg(feature = "demos")]     run_demo_programs(); +    // Stage 21, boot contract v2: a root partition is mounted at "/", so+    // pid 1 is its /sbin/init (or /bin/init, or /bin/sh), see boot.rs.+    if rustbelt::boot::v2() {+        run_init_v2();+        return;+    }     if fs::lookup(SHELL).is_err() {         kprintln!("[kernel] no {} -- was QEMU started with the disk image?", SHELL);         #[cfg(feature = "headless")]@@ -195,6 +201,30 @@ fn shell_supervisor() {     } } +/// Boot contract v2 (stage 21): start pid 1 from the root partition, wait+/// for it, and end the QEMU run with its exit status (33 + 2 * status).+/// pid 1 is not restarted: that's init's job now.+fn run_init_v2() {+    if rustbelt::virtio::console::present() {+        rustbelt::tty::hvc();+    }+    let mut log = alloc::vec::Vec::new();+    let result = rustbelt::boot::run_init(cfg!(feature = "headless"), &mut log);+    for line in &log {+        kprintln!("[init] {}", line);+    }+    match result {+        Ok((path, status)) => {+            kprintln!("[init] pid 1 ({}) exited: {:?}; ending the run", path, status);+            rustbelt::boot::exit_qemu_with(status);+        }+        Err(e) => {+            kprintln!("[init] no init found ({:?}): tried {:?}", e, rustbelt::boot::INIT_PATHS);+            rustbelt::exit_qemu(rustbelt::QemuExitCode::Failed);+        }+    }+}+ /// `demos` feature: the stage 5/6 user programs, one after the other. #[cfg(feature = "demos")] fn run_demo_programs() {
@@ -271,6 +271,18 @@ impl Mm {      /// mprotect: new permissions for `start..end`, which must be fully     /// covered by regions (else ENOMEM, like Linux).+    /// Is every page of `start..end` part of some region? (msync, stage 21.)+    pub fn covers(&self, start: u64, end: u64) -> bool {+        let mut covered = start;+        for v in self.vmas.iter().filter(|v| v.end > start && v.start < end) {+            if v.start > covered {+                return false; // a hole+            }+            covered = v.end;+        }+        covered >= end+    }+     pub fn protect(&mut self, start: u64, end: u64, prot: u64) -> Result<(), Errno> {         let mut covered = start;         for v in self.vmas.iter().filter(|v| v.end > start && v.start < end) {
@@ -1439,7 +1439,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         host.as_str(),                  // nodename (hostname; sethostname changes it)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 20: gcc -static and timerfd", // version+        "stage 21: root partition and init", // version         "x86_64",                       // machine         domain.as_str(),                // domainname     ];
@@ -38,6 +38,7 @@ //! | 223-226 | timer_settime/gettime/getoverrun/delete |                         | //! | 283 | timerfd_create       | stage 20: a timer you read() (timer.rs)            | //! | 286/287 | timerfd_settime/gettime | arm / read it, like timer_settime       |+//! | 26  | msync                | stage 21: checks its arguments, then nothing to do | //! | 102/104/107/108 | getuid/getgid/geteuid/getegid | the stored ids           | //! | 105/106 | setuid/setgid    | change ids (root: any; others: their own)          | //! | 113/114 | setreuid/setregid |                                                   |@@ -127,6 +128,8 @@ pub const SYS_MREMAP: u64 = 25; pub const SYS_TIMERFD_CREATE: u64 = 283; pub const SYS_TIMERFD_SETTIME: u64 = 286; pub const SYS_TIMERFD_GETTIME: u64 = 287;+// Stage 21.+pub const SYS_MSYNC: u64 = 26;  /// linkat: link what a symlink points to, not the symlink. pub const AT_SYMLINK_FOLLOW: u64 = 0x400;@@ -162,6 +165,7 @@ pub fn dispatch(nr: u64, a: [u64; 6]) -> Option<Result<u64, Errno>> {         SYS_TIME => sys_time(a[0]),         SYS_GETCPU => sys_getcpu(a[0], a[1]),         SYS_MREMAP => sys_mremap(a[0], a[1], a[2], a[3], a[4]),+        SYS_MSYNC => sys_msync(a[0], a[1], a[2]),         // 6. Sockets.         SYS_GETSOCKOPT => sys_getsockopt(a[0], a[1], a[2], a[3], a[4]),         SYS_GETSOCKNAME => with_socket(a[0], |s| Ok(s.local())).and_then(|addr| write_sockaddr(a[1], a[2], addr).map(|_| 0)),@@ -262,6 +266,7 @@ pub fn name(nr: u64) -> Option<&'static str> {         SYS_TIME => "time",         SYS_GETCPU => "getcpu",         SYS_MREMAP => "mremap",+        SYS_MSYNC => "msync",         SYS_FCHOWNAT => "fchownat",         SYS_LINKAT => "linkat",         SYS_SYMLINKAT => "symlinkat",@@ -549,6 +554,31 @@ fn sys_getcpu(cpu: u64, node: u64) -> Result<u64, Errno> { /// moves it to `new_addr`. MREMAP_DONTUNMAP (4) and old_len 0 (a second /// copy of a shared mapping) aren't supported: EINVAL. The real work is /// Mm::remap in mm.rs.+/// msync(addr, len, flags), stage 21: "write my MAP_SHARED file mapping+/// back to the file". Rustbelt has nothing to write back: mmap refuses+/// MAP_SHARED for files (an error, not silent loss), private mappings never+/// go back to the file, and shared anonymous memory has no file. So after+/// the same checks Linux makes, there is nothing left to do:+///   * EINVAL: addr not page aligned, unknown flags, or both MS_ASYNC and+///     MS_SYNC;+///   * ENOMEM: part of addr..addr+len isn't mapped.+fn sys_msync(addr: u64, len: u64, flags: u64) -> Result<u64, Errno> {+    const MS_ASYNC: u64 = 1;+    const MS_INVALIDATE: u64 = 2;+    const MS_SYNC: u64 = 4;+    if addr % 4096 != 0 || flags & !(MS_ASYNC | MS_INVALIDATE | MS_SYNC) != 0 || flags & (MS_ASYNC | MS_SYNC) == (MS_ASYNC | MS_SYNC) {+        return Err(Errno::EINVAL);+    }+    let end = addr.checked_add(len).ok_or(Errno::ENOMEM)?.checked_next_multiple_of(4096).ok_or(Errno::ENOMEM)?;+    if len == 0 {+        return Ok(0);+    }+    if !current()?.mm().covers(addr, end) {+        return Err(Errno::ENOMEM);+    }+    Ok(0)+}+ fn sys_mremap(old: u64, old_len: u64, new_len: u64, flags: u64, new_addr: u64) -> Result<u64, Errno> {     const MAYMOVE: u64 = 1;     const FIXED: u64 = 2;
@@ -0,0 +1,135 @@+//! Stage 21: boot contract v2. scripts/qemu.sh gives this test kernel one+//! more disk, target/rootfs-gpt.img (build.rs): a GPT disk with an ext2+//! partition of type "Linux root (x86-64)" and an exFAT "Microsoft basic+//! data" partition. Its /sbin/init, /bin/init and /bin/sh are all+//! user/inittest.c, which exits 7 (from /etc/init-status) when every check+//! passes as /sbin/init, 21 as /bin/init and 22 as /bin/sh.+//!+//! (The other half, "no root partition = the old layout", is checked by+//! tests/fat.rs and by scripts/run-headless.sh; main.rs's use of all this,+//! with QEMU's exit code, by scripts/boot-v2-check.sh.)+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use alloc::vec::Vec;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::boot;+use rustbelt::errno::Errno;+use rustbelt::fs::{self, InodeKind};+use rustbelt::process::{self, ExitStatus};+use rustbelt::{memory, thread};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rustbelt::init();+    rustbelt::init_memory(boot_info);+    rustbelt::init_smp();+    fs::init();+    rustbelt::init_storage();+    rustbelt::timer::start(); // (its kernel stack isn't a leak)+    test_main();+    rustbelt::hlt_loop();+}++fn mount_type(path: &str) -> Option<&'static str> {+    fs::mounts().into_iter().find(|(p, _)| p == path).map(|(_, t)| t)+}++/// The first process this kernel starts: /sbin/init as pid 1. inittest+/// checks pid 1, argv, PATH, the mounts, /mnt/share and msync itself, and+/// exits 7 only if all of that held.+#[test_case]+fn init_runs_as_pid_1_and_its_status_comes_back() {+    let mut log = Vec::new();+    let before = memory::frames_in_use();+    let (path, status) = boot::run_init(false, &mut log).expect("no init started");+    assert_eq!(path, "/sbin/init");+    assert_eq!(status, ExitStatus::Exited(7), "inittest's checks failed (see its [inittest] lines)");+    assert!(log.is_empty(), "{:?}", log);+    // What init wrote to the share is really there.+    assert_eq!(fs::read_all("/mnt/share/from-init.txt").unwrap(), b"written by init\n");+    thread::sleep_ticks(3);+    assert_eq!(process::count(), 0);+    assert_eq!(memory::frames_in_use(), before, "running init leaked frames");+}++#[test_case]+fn the_root_partition_is_slash_and_the_rest_is_as_before() {+    assert!(boot::v2());+    assert_eq!(mount_type("/"), Some("ext2"));+    assert!(fs::read_all("/etc/rootfs-v2").unwrap().starts_with(b"This is the ext2 root"));+    // The kernel's own mounts are still there...+    assert_eq!(mount_type("/tmp"), Some("ramfs"));+    assert!(mount_type("/dev").is_some() && mount_type("/proc").is_some());+    // ...the FAT32 data disk is still /mnt/disk0 (B:), and the other disks+    // keep their numbers: the root and the share don't take one.+    assert_eq!(mount_type("/mnt/disk0"), Some("fat32"));+    assert_eq!(mount_type("/mnt/disk1"), Some("ext2"));+    assert_eq!(mount_type("/mnt/disk2"), Some("exfat"));+    assert!(mount_type("/mnt/disk9").is_some());+    assert!(mount_type("/mnt/disk10").is_none());+    assert!(fs::lookup("/mnt/disk0/bin").is_ok());+    // The exFAT partition is the share, mounted read-write.+    assert_eq!(mount_type("/mnt/share"), Some("exfat"));+    assert_eq!(fs::read_all("/mnt/share/hello.txt").unwrap(), b"Hello from the exFAT share!\n");+    let f = fs::create("/mnt/share/kernel.txt", InodeKind::File).unwrap();+    f.write_at(0, b"rw").unwrap();+    assert_eq!(fs::read_all("/mnt/share/kernel.txt").unwrap(), b"rw");+    fs::remove("/mnt/share/kernel.txt", false).unwrap();+}++#[test_case]+fn init_gets_the_documented_argv_and_env() {+    let s = |v: &[&str]| v.iter().map(|x| String::from(*x)).collect::<Vec<_>>();+    let a = boot::init_args("/sbin/init", false);+    assert_eq!(a.args, s(&["init"]));+    assert_eq!(a.env, s(&["PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"]));+    let a = boot::init_args("/bin/init", true);+    assert_eq!(a.args, s(&["init", "selftest"]));+    assert!(a.env.iter().any(|e| e == "RUSTBELT_HEADLESS=1"));+    let a = boot::init_args("/bin/sh", true);+    assert_eq!(a.args, s(&["sh", "/scripts/selftest.sh"]));+}++/// Take away /sbin/init, then /bin/init, then /bin/sh (the disk is+/// snapshot=on, so the file stays): the next one in line starts each time.+#[test_case]+fn the_fallback_chain() {+    let mut log = Vec::new();+    fs::remove("/sbin/init", false).unwrap();+    assert_eq!(boot::run_init(false, &mut log), Ok(("/bin/init", ExitStatus::Exited(21))));+    fs::remove("/bin/init", false).unwrap();+    assert_eq!(boot::run_init(false, &mut log), Ok(("/bin/sh", ExitStatus::Exited(22))));+    fs::remove("/bin/sh", false).unwrap();+    assert_eq!(boot::run_init(false, &mut log), Err(Errno::ENOENT));+    // Something that isn't a program is skipped (and logged), like Linux.+    fs::create("/sbin/init", InodeKind::File).unwrap().write_at(0, b"not an ELF file").unwrap();+    assert_eq!(boot::run_init(false, &mut log), Err(Errno::ENOENT));+    assert_eq!(log.len(), 1, "{:?}", log);+    assert!(log[0].starts_with("/sbin/init: can't start it"), "{:?}", log);+}++/// pid 1's status -> the value for isa-debug-exit (QEMU exits with+/// 2 * value + 1): exit(n) -> 33 + 2n.+#[test_case]+fn exit_status_to_qemu() {+    assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(0)), 0x10); // 33, as before+    assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(1)), 0x11); // 35, as before+    assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(7)), 0x17); // 47+    assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(111)), 0x7f); // 255+    assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(112)), 0x11);+    assert_eq!(boot::qemu_exit_value(ExitStatus::Killed(9, "SIGKILL")), 0x11);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -131,6 +131,21 @@ fn block_io_adapter_handles_unaligned_access() {     assert!(io.seek(SeekFrom::End(1)).is_err()); // past the end of the disk } +/// Stage 21: with no "Linux root (x86-64)" partition on any disk (none of+/// the usual test disks has one), boot contract v1 stays exactly as it+/// was: "/" is the ramfs, no /mnt/share, and the disks are /mnt/disk0...+#[test_case]+fn no_root_partition_keeps_the_old_layout() {+    let mounts = fs::mounts();+    let mount_type = |path: &str| mounts.iter().find(|(p, _)| p == path).map(|(_, t)| *t);+    assert!(!rustbelt::boot::v2());+    assert_eq!(mount_type("/"), Some("ramfs"));+    assert!(fs::read_all("/hello.txt").is_ok());+    assert_eq!(mount_type("/mnt/share"), None);+    assert_eq!(mount_type("/mnt/disk0"), Some("fat32"));+    assert_eq!(mount_type("/mnt/disk2"), Some("exfat")); // the whole-disk exFAT is not a share+}+ #[panic_handler] fn panic(info: &PanicInfo) -> ! {     rustbelt::test_panic_handler(info)
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.20.0",+        "Rustbelt 0.21.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -0,0 +1,111 @@+/* Stage 21: the "init" on the boot-contract-v2 test image (build.rs puts+ * the same file at /sbin/init, /bin/init and /bin/sh of the root+ * partition in target/rootfs-gpt.img).+ *+ * It looks at AT_EXECFN (the path the kernel started it from):+ *   /sbin/init -> checks what the contract promises (below), then exits+ *                 with the number in /etc/init-status (0 if missing), or+ *                 1 if a check failed;+ *   /bin/init  -> exits 21;   /bin/sh -> exits 22+ * so the tests can tell which one of the fallback chain ran.+ *+ * The checks: pid 1, argv[0] "init", PATH, "/" is the ext2 root, the+ * kernel's mounts are still there, /mnt/share (the exFAT partition) is+ * readable and writable, and msync's argument checks.+ * Also runs on a normal Linux host (where it isn't pid 1).+ */+#include <errno.h>+#include <fcntl.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/auxv.h>+#include <sys/mman.h>+#include <sys/stat.h>+#include <unistd.h>++static int failed;++static void check(int ok, const char *what) {+    printf("[inittest] %s: %s\n", ok ? "ok" : "FAILED", what);+    if (!ok)+        failed++;+}++static int read_file(const char *path, char *buf, size_t len) {+    int fd = open(path, O_RDONLY);+    if (fd < 0)+        return -1;+    int n = read(fd, buf, len - 1);+    close(fd);+    if (n >= 0)+        buf[n] = 0;+    return n;+}++static int is_dir(const char *path) {+    struct stat st;+    return stat(path, &st) == 0 && S_ISDIR(st.st_mode);+}++static void contract(int argc, char **argv) {+    const char *path = getenv("PATH");+    const char *headless = getenv("RUSTBELT_HEADLESS");+    printf("[inittest] pid %d, argc %d, argv[0] %s, argv[1] %s, PATH=%s, RUSTBELT_HEADLESS=%s\n", getpid(), argc,+           argv[0], argc > 1 ? argv[1] : "-", path ? path : "-", headless ? headless : "-");+    check(getpid() == 1, "I am pid 1");+    check(strcmp(argv[0], "init") == 0, "argv[0] is \"init\"");+    check(!headless || (argc > 1 && strcmp(argv[1], "selftest") == 0), "headless: argv[1] is \"selftest\"");+    check(path && strcmp(path, "/sbin:/bin:/usr/bin:/mnt/disk0/bin") == 0, "PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin");++    char buf[256];+    check(read_file("/etc/rootfs-v2", buf, sizeof buf) > 0 && strstr(buf, "ext2 root"), "/ is the ext2 root partition");+    check(is_dir("/tmp") && is_dir("/dev") && is_dir("/proc") && is_dir("/mnt/disk0"), "/tmp /dev /proc /mnt/disk0 are there");+    int fd = open("/tmp/inittest.tmp", O_CREAT | O_WRONLY | O_TRUNC, 0644);+    check(fd >= 0 && write(fd, "x", 1) == 1, "/tmp is writable");+    close(fd);+    unlink("/tmp/inittest.tmp");++    /* The exFAT share. */+    check(read_file("/mnt/share/hello.txt", buf, sizeof buf) > 0 && strcmp(buf, "Hello from the exFAT share!\n") == 0,+          "/mnt/share/hello.txt reads back");+    fd = open("/mnt/share/from-init.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);+    check(fd >= 0 && write(fd, "written by init\n", 16) == 16, "/mnt/share is writable");+    close(fd);+    check(read_file("/mnt/share/from-init.txt", buf, sizeof buf) == 16 && strcmp(buf, "written by init\n") == 0,+          "...and the new file reads back");++    /* msync (26): Rustbelt has nothing to write back, but checks its+     * arguments like Linux. */+    long pg = 4096;+    char *anon = mmap(0, 2 * pg, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);+    check(anon != MAP_FAILED, "mmap MAP_SHARED|MAP_ANONYMOUS");+    anon[0] = 1;+    check(msync(anon, 2 * pg, MS_SYNC) == 0, "msync(MS_SYNC) = 0");+    check(msync(anon, 1, MS_ASYNC | MS_INVALIDATE) == 0, "msync(MS_ASYNC|MS_INVALIDATE), partial page = 0");+    check(msync(anon, 0, MS_SYNC) == 0, "msync(len 0) = 0");+    check(msync(anon + 1, pg, MS_SYNC) == -1 && errno == EINVAL, "msync(unaligned) = EINVAL");+    check(msync(anon, pg, 8) == -1 && errno == EINVAL, "msync(bad flags) = EINVAL");+    check(msync(anon, pg, MS_SYNC | MS_ASYNC) == -1 && errno == EINVAL, "msync(MS_SYNC|MS_ASYNC) = EINVAL");+    munmap(anon + pg, pg);+    check(msync(anon, 2 * pg, MS_SYNC) == -1 && errno == ENOMEM, "msync over an unmapped page = ENOMEM");+    munmap(anon, pg);+    check(msync(anon, pg, MS_SYNC) == -1 && errno == ENOMEM, "msync(unmapped) = ENOMEM");+    char *priv = mmap(0, pg, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+    check(priv != MAP_FAILED && msync(priv, pg, MS_ASYNC) == 0, "msync(private mapping) = 0");+}++int main(int argc, char **argv) {+    const char *execfn = (const char *)getauxval(AT_EXECFN);+    printf("[inittest] started as %s\n", execfn ? execfn : "?");+    if (execfn && strcmp(execfn, "/bin/init") == 0)+        return 21;+    if (execfn && strcmp(execfn, "/bin/sh") == 0)+        return 22;+    contract(argc, argv);+    char buf[16];+    int status = read_file("/etc/init-status", buf, sizeof buf) > 0 ? atoi(buf) : 0;+    printf("[inittest] %d failed\n", failed);+    fflush(stdout);+    return failed ? 1 : status;+}
@@ -137,7 +137,7 @@ fn main() -> i64 {     c.check(n > 0 && cwd[0] == b'/', "getcwd gives an absolute path");     let mut ub = [0u8; 6 * 65];     let u = uname(&mut ub);-    c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 20"), "uname: Rustbelt, stage 20");+    c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 21"), "uname: Rustbelt, stage 21");      let (secs, _) = clock_gettime(0);     c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");