RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

foundry / docs / boot-contract.md
9539 bytes · raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# The boot contract: what the kernel expects from the Foundry OS image

The Rustbelt kernel and Foundry OS live in different repos. This page is the
whole agreement between them. The kernel never needs to know *how* the
image was built, only that it follows the contract.

## Contract v2: a real root file system (in use since M3)

Pinned kernel: Rustbelt 0.21.1, commit 6bb2b25 (stage 21 added v2, which
was kernel request K5; 0.21.1 added the clean shutdown, K12). `make image` builds out/foundry.img with
scripts/mkimage-v2.sh; `make test` checks it.

### What the kernel does (Rustbelt 0.21.1)

1. **Root.** It mounts the **first GPT partition of type "Linux root
   (x86-64)"** (`4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709`, the freedesktop
   *Discoverable Partitions* type) that holds **ext2** at **`/`**. Only
   ext2, and there is no `root=` option to pick another one.
2. **Share.** It mounts the first **"Microsoft basic data"** partition
   holding **exFAT** at **`/mnt/share`**, read-write. Limits: no rename,
   no timestamps, no volume-dirty flag. A TexFAT volume (two FATs) is
   mounted read-only, so our share has one FAT (mkfs.exfat's default).
3. **Other disks/partitions** get `/mnt/disk0`, `/mnt/disk1`, ... in device
   order (the root and the share don't use a number). It also provides a
   ramfs at `/tmp`, devfs at `/dev`, proc at `/proc`.
4. **pid 1.** It runs the first of `/sbin/init`, `/bin/init`, `/bin/sh`
   that starts, with the console as its terminal:
   - argv `["init"]` (`["sh"]` for the sh fallback); environment
     `PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin HOME=/ TERM=linux`.
   - Headless kernel build: argv `["init", "selftest"]` (sh:
     `["sh", "/scripts/selftest.sh"]`) plus `RUSTBELT_HEADLESS=1`.
   - `RUSTBELT_INIT_ARGS` / `RUSTBELT_INIT_ENV` at kernel *build* time add
     arguments and variables. There are no run-time boot options.
   - Orphaned processes are handed to pid 1 **only if it is named
     `init`**. pid 1 is never restarted: when it exits with code n
     (0-111), QEMU exits with **33 + 2n** (33 = pass); anything else gives 35.
   - **Shutdown (0.21.1+).** Before QEMU exits, the kernel syncs and
     unmounts every file system, newest mount first (`/` last), and logs
     one `[shutdown] unmounted PATH (TYPE)` line for each. So B: (FAT32)
     is left with its "dirty bit" clear, like a disk that was safely
     ejected.
5. **No root partition** → the kernel behaves exactly as under v1.

### What Foundry OS puts on the disk

One disk, GPT, three partitions (sizes in config.mk):

| # | GPT type             | file system | size   | mounted at   | rbsh drive | from      |
|---|----------------------|-------------|--------|--------------|------------|-----------|
| 1 | Linux root (x86-64)  | ext2 (1 KiB blocks), label `foundry-root` | 32 MiB | `/`          | **A:** | `root/`   |
| 2 | Microsoft basic data | FAT32, label `FOUNDRY` | 40 MiB | `/mnt/disk0` | **B:** | `rootfs/` |
| 3 | Microsoft basic data | exFAT (1 FAT), label `SHARE` | 16 MiB | `/mnt/share` | **C:** | `share/`  |

**Why one disk and not three?** A real computer installs an OS on one
disk with several partitions, and the kernel looks for the root and the
share by partition type, so one image file is enough: one file to copy,
one `-drive` for QEMU, and the letters can't get mixed up by the order of
disks. (Several disks would also work; nothing in the kernel needs one.)
`make test` attaches *only* this disk. The kernel's own ext2/exFAT/btrfs
test disks are checked by `make test-v1` and `make test-reference`.

**A: (root, ext2)** holds the system:

- `/sbin/init`: Foundry OS init (rust/init). It is named `init`, so the
  kernel hands it orphans. It starts **rbsh** as the login shell on the
  console and on `/dev/hvc0` (when QEMU has a virtio console), starts a
  new one when a shell exits (it gives up on a terminal when the shell
  dies within a second of starting 5 times in a row, or can't be started
  at all), and reaps every orphan it is given.
  Headless (`init selftest` or `RUSTBELT_HEADLESS=1`) it runs
  `rbsh /scripts/selftest.sh` on the console, waits up to 5 s for all
  leftover processes, prints `[init] memory: N frames in use before the
  self-test, M after ...` (the kernel does not print a frame report under
  v2), and exits with the shell's status.
- `/bin`: **rbsh** (`/bin/rbsh`), **dash** (`/bin/dash`, and `/bin/sh`, a
  symlink to it), the Rust tools (`ls`, `cat`, `cp`, ...: first on PATH),
  our C programs (`hello-musl`, `fprobe`, `orphan`).
- `/etc` (passwd, group, hosts, resolv.conf, motd), `/scripts` (the
  self-test), and empty `/usr/bin`, `/tmp`, `/root`, `/home/tyler`,
  `/mnt/disk0`, `/mnt/share`.

**B: (data, FAT32)** holds `readme.txt`, `notes/` and, in `/bin`, the
kernel's **ABI test programs** (`forktest`, `rawabi`, `piehello`, ... 28
of them, built from the pinned kernel by `make fixtures`). They are last
on PATH (`/mnt/disk0/bin`), so the self-test runs them by name and they
never hide one of our tools.

**C: (share, exFAT)** holds `README.TXT`. scripts/exfat.py writes it
there, because mkfs.exfat can only create an empty volume.

### Drive letters and the share, for beginners

Linux has one tree of folders starting at `/`. Each disk (or partition)
is *mounted* on a folder in that tree: its files appear under that
folder. rbsh adds DOS-style **drive letters** on top, as shortcuts:

- **A:** is `/`, the root: the whole system. `A:/bin/ls` is `/bin/ls`.
- **B:** is `/mnt/disk0`, the data disk (FAT32, the format of old USB sticks).
- **C:** is `/mnt/share`, the share.
- More disks would be D:, E:, ... (`drives` lists them).

`cd C:` takes you to the share; `ls B:/bin` lists the test programs.
Other programs (dash, C programs) don't know the letters and use the
real paths.

**The share** is a place for files that go between Foundry OS and other
computers. It uses **exFAT**, the format of SD cards and USB sticks,
which Windows, macOS and Linux all read and write, so the partition can
be copied out of the image and opened elsewhere. In Foundry OS you can
create, write and delete files there (not rename them yet). `make test`
writes `from-foundry.txt` to it and then reads it back *on the host*,
without the kernel, to prove another system can see it.

### How `make test` checks this contract

- the banner; the kernel's mount lines (`mounted at / (root partition)`,
  `mounted at /mnt/share`); init's start line and its mount list;
- `drives` shows `A:  /  ext2`, `B:  /mnt/disk0  fat32`, `C:  /mnt/share  exfat`;
- rbsh writes to A: and C:; dash (root/scripts/v2-test.sh) checks the
  environment, PATH order, `/bin/sh` → dash, and writes, reads back and
  deletes files on A:, B: and C:;
- the kernel's stages (processes, threads, network, virtio...) as under v1;
- `orphan` (c/orphan.c): a grandchild left behind is adopted by pid 1
  and reaped; init reports how many orphans it reaped (must be ≥ 1);
- frames flat: init's memory line shows the same number before and after;
- QEMU exit 33 (init exited 0), after the kernel's `[shutdown] unmounted ...` lines;
- afterwards, on the host (scripts/check-v2-image.sh): `e2fsck -fn` on A:,
  `fsck.fat -n` on B: (its FAT dirty bit must be clear), `fsck.exfat -n`
  on C:, a single-FAT check, and
  scripts/exfat.py reads `from-foundry.txt` back and checks deleted files
  are gone.

## Contract v1: the old layout (still supported: `make test-v1`)

Used until M3; Rustbelt 0.21.x still falls back to it when a disk has no
root partition. `make image-v1` builds it (out/foundry-v1.img) and
`make test-v1` / `make compare` check it.

1. **Where.** The OS image is the **first virtio-blk disk** QEMU is given.
   The kernel mounts it at `/mnt/disk0` (drive `B:` in rbsh).
2. **Format.** A **GPT** partition table with one partition starting at
   sector 2048 (1 MiB), type "Microsoft basic data", formatted **FAT32**,
   volume label `TYLERDISK` (kept from the kernel for now). 64 MiB total.
3. **Contents.**
   - **`/bin/sh`**: the program the kernel starts. In Foundry OS this is
     **rbsh** (crate `rust/rbsh`, binary `rbsh`), copied under the name `sh`.
   - `/scripts/selftest.sh`: the self-test, for headless kernels.
   - Programs live in `/bin`. Each is a **static** ELF: either a
     **static-PIE** (`ET_DYN` without `PT_INTERP`, since 0.19.0; what
     Foundry OS builds) or an `ET_EXEC` linked anywhere in user space,
     which since 0.20.0 starts at `0x40_0000`, so a plain `gcc -static`
     program at the usual `0x400000` runs (until 0.19.0 it had to be at
     or above `0x80_0040_0000`). Dynamic programs are refused.
4. **What the kernel provides.** `/` is a ramfs the kernel builds itself
   (with `/tmp`, `/dev`, `/proc`, `/mnt`). It runs `/mnt/disk0/bin/sh`
   with `PATH=/bin:/mnt/disk0/bin HOME=/ TERM=linux`, and restarts it when
   it exits. A headless kernel (`--features headless`) instead runs
   `sh /mnt/disk0/scripts/selftest.sh` once and powers QEMU off with the
   shell's status: QEMU exit code **33 = pass**, 35 = fail.
5. **Other disks belong to the kernel.** The ext2, exFAT and btrfs test
   disks are built by the kernel's build into `out/kernel-target`. Our run
   scripts attach them after our disk, so `C:`, `D:`, `E:` keep their letters.

Under v1 the image must also carry the kernel's ABI test programs
(`forktest`, `rawabi`, ...), because the self-test runs them. They are
built from the pinned kernel source (`make fixtures`); see
[kernel-requests.md]kernel-requests.md for how that dependency goes away.