rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Edward <edward@localhost> | 2026-10-31 19:32:06 -0700 |
|---|---|---|
| commit | 50476f40140f0dfd179cf4fe8eaeb8def195c347 | |
| parent | 52913323cee4a3db45fc331519752f5b25db6693 | |
| tree | browse 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
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +5 -1 |
| M | README.md | +214 -0 |
| M | build.rs | +113 -1 |
| A | scripts/boot-v2-check.sh | +60 -0 |
| M | scripts/btrfs-check.sh | +8 -7 |
| M | scripts/exfat-check.sh | +21 -20 |
| M | scripts/ext2-check.sh | +22 -21 |
| M | scripts/gpu-check.sh | +4 -3 |
| A | scripts/qemu.sh | +25 -0 |
| M | scripts/run-headless.sh | +14 -13 |
| M | scripts/runner.sh | +9 -2 |
| M | scripts/screenshot.sh | +8 -7 |
| M | src/block/mod.rs | +10 -0 |
| M | src/block/partition.rs | +10 -1 |
| A | src/boot.rs | +106 -0 |
| M | src/fs/mod.rs | +60 -0 |
| M | src/lib.rs | +1 -0 |
| M | src/main.rs | +30 -0 |
| M | src/mm.rs | +12 -0 |
| M | src/syscall.rs | +1 -1 |
| M | src/syscall_ext.rs | +30 -0 |
| A | tests/boot_v2.rs | +135 -0 |
| M | tests/fat.rs | +15 -0 |
| M | tests/shell.rs | +1 -1 |
| A | user/inittest.c | +111 -0 |
| M | user/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)");