foundry
Foundry OS: the operating system built on the Rustbelt kernel
20637 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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | # Foundry OS: decisions and plan
## Tyler's answers to the proposal (2026-10-31)
| # | Question | Decision |
|---|---|---|
| 1 | Name, place, history | The OS is **Foundry OS**, in `/workspace/foundry` (local git only: no remote, no push). The kernel's `user/` and `disk/` histories were kept with `git subtree split`, done in a scratch clone at a55b41d (`/workspace/foundry-scratch/rk`), never in `/workspace/rust-kernel`. |
| 2 | What stays in the kernel | The ABI test programs stay as kernel fixtures: `hello evil spin demo child overflow forktest cowtest mmaptest sigtest threads echoserver ptytest nbtest fputest pathtest libctest coretest unixtest rawabi` and the musl ones `vdsotest piehello piethreads dynhello remaptest`. The shell, the tools and the net/sys programs moved here. |
| 3 | Which shell | The Rust shell stays the interactive login shell, renamed **rbsh**, keeping its DOS commands (`run`/`start`, `dir`/`type`/`cls`...) and drive letters. **dash** (later busybox `ash`) becomes `/bin/sh` for POSIX scripts once the root is ext2. Under contract v1 rbsh is still installed as `/bin/sh`, because that is the path the kernel runs. |
| 4 | Root file system | An **ext2 root**, so `A:` = `/` is the OS disk. The FAT32 disk stays as a **data disk** (`B:`). Plus an **exFAT share** (`C:`, `/mnt/share`) for files that go to Windows/macOS. Built in M3 (contract v2). |
| 5 | Name clashes | The Rust tools stay the default. busybox/sbase applets go under **`/usr/lib/busybox`**, off the default `PATH` (call them by full path, or add the directory to `PATH` yourself). |
| 6 | Tarballs | **Not in git.** A git-ignored download cache (`cache/`), each file pinned by SHA256 (`toolchain/musl/musl.sha256` + `fetch.sh`; `ports/<name>/<name>.sha256`). |
| 7 | Which kernel | A **pinned commit**: `KERNEL_REV` in `config.mk` (now `50476f4`, Rustbelt 0.21.0; before that `5291332`, 0.20.0), bumped deliberately: see [bumping-the-kernel.md](bumping-the-kernel.md). |
| 8 | Box packages | Small build packages are OK (`make`, `bzip2`, `musl-tools`, `autoconf`). |
## Disk layout (contract v2, built in M3)
The details and the reasons are in [boot-contract.md](boot-contract.md).
One disk, three partitions:
```
foundry.img (GPT, 90 MiB)
├── 1 ext2 "Linux root (x86-64)" → / (A:) the OS, from root/
├── 2 FAT32 "Microsoft basic data" → /mnt/disk0 (B:) data disk + the kernel's ABI test programs, from rootfs/
└── 3 exFAT "Microsoft basic data" → /mnt/share (C:) the share, from share/
```
Inside the ext2 root:
```
/sbin/init Foundry OS init (rust/init)
/bin rbsh, dash (also /bin/sh), the Rust tools (ls cat grep ...), our C programs
/etc passwd group hosts resolv.conf motd (root/etc/)
/scripts the self-test init runs headless
/usr/lib/busybox busybox + applet symlinks, off the default PATH (M5)
```
## Milestones
| | Goal | Status |
|---|---|---|
| **M1** | Foundry OS builds the same image; the kernel's self-test passes on it | **done**: `make test` passes; `make compare` shows the same 44 programs (47 since the 0.20.0 pin) and identical non-program files |
| M1b | `rbrt` becomes a real library crate; rbsh's parser tests run on the host (`make unit`); Foundry OS branding in rbsh and getty | done |
| **M2** | musl hello + probes in `/bin`, built static-PIE with `toolchain/foundry-cc` | **done**: `hello-musl` and `fprobe` (40 checks incl. timerfd) in `/bin`, checked by `make test` |
| M2b | dash built from ports/ as `/bin/dash`, with script checks in `make test` | **done** |
| **M3** | dash as `/bin/sh`, ext2 root (contract v2), Rust `/sbin/init`, exFAT share | **done** on Rustbelt 0.21.0: `make test` (v2) passes, plus host fsck of all three file systems; `make test-v1` still passes |
| M4 | sbase tools | needs testing per tool (docs/syscall-gaps.md) |
| M5 | busybox in `/usr/lib/busybox` | `bzip2` for the tarball |
| M6 | User management: passwords, `login`, users and groups, real file owners and modes ([below](#m6-user-management-approved-not-started)) | approved; needs kernel K13 |
| M7 | A bootable Raspberry Pi 3 build ([below](#m7-raspberry-pi-3-bootable-build-placeholder)) | placeholder; Edward's arm64/Raspberry Pi port is in progress on the kernel side |
## M6: user management (approved, not started)
**In plain words.** Today everything on Foundry OS runs as **root**, the
all-powerful user (user id, *uid*, 0): any program can read or delete any
file. On a real multi-user system each person has an **account**: a name,
a number (uid), a **group** (gid), a home folder and a password. The
kernel then checks, on every file access, whether *this* uid may read,
write or run *that* file, using the file's owner and its **mode bits**
(`rwxr-x---`: what the owner, the group and everyone else may do). M6
makes Foundry OS work like that: you log in as `tyler`, your shell runs
as uid 1000 in `/home/tyler`, and you need root's password (`su`) for
system changes.
Words used below:
- **`/etc/passwd`**: one line per account (`tyler:x:1000:1000:Tyler:/home/tyler:/bin/rbsh`:
name, password placeholder, uid, gid, full name, home, shell). Everyone
may read it, because programs like `ls -l` turn uids into names with it.
- **`/etc/group`**: the groups and who is in them (`wheel:x:10:tyler`).
- **`/etc/shadow`**: the password **hashes**, readable by root only. A
hash is a one-way scramble of the password: easy to compute from the
password, practically impossible to turn back into it. `login` hashes
what you type and compares. We use **SHA-512 crypt** (`$6$...` in the
file, done by musl's `crypt()`), with a random *salt* per password so
two equal passwords look different. **yescrypt** (`$y$`), which is slower
to brute-force, comes later.
- **setuid bit**: a mode bit (`rws` instead of `rwx`) that makes a program
run as the file's *owner* rather than as whoever started it. That is how
`su` and `passwd`, owned by root, can read or change `/etc/shadow` when
`tyler` runs them. They are therefore the most security-sensitive
programs on the system and must stay small.
- **getty → login → shell**: `getty` opens a terminal and asks for a name,
`login` checks the password, switches to the user's uid/gid/groups,
goes to the home folder and starts the user's shell (rbsh).
**Size: L.** The OS-side programs are small, and busybox brings most of
them. But every piece depends on the kernel really enforcing ids and
permissions (K13, not assumed to work), and on the image build getting
owners and modes exactly right. The security pieces (first-boot setup,
no default password, setuid programs) need care and tests, not just code.
### Scope
1. **Account files**: `/etc/passwd`, `/etc/group` (root, wheel, tyler's
group, ...), and `/etc/shadow` with **mode 0600 root:root** (or 0640
root:shadow if a `shadow` group is needed for a non-root helper).
Hashes are SHA-512 crypt through musl; yescrypt later.
2. **Login chain**: init starts `getty` on each terminal (instead of rbsh
directly), `getty` runs `login`, `login` runs **rbsh as the user in
their home dir** with `HOME`, `USER`, `LOGNAME`, `SHELL` and `PATH` set.
A wrong password gives "Login incorrect" and a short delay.
3. **Becoming root**: **`su`** now (busybox `su`, setuid root). **`sudo`**
or a small Rust **`doas`** later.
4. **Account tools**: `passwd`, `adduser`/`deluser`, `addgroup`, `chmod`,
`chown`: **busybox first**, Rust replacements (in `rust/`) later.
5. **First-boot setup (no default password, ever)**: the image ships with
root's password field **locked** (`!` or `*` in `/etc/shadow`: no
password can match) and no user account. On the first interactive boot
init sees `/etc/foundry/setup-needed` and runs a setup program on the
console *before* any login prompt: it asks for a new root password
(twice), and a user name + password, creates the user and their home
folder, then deletes the marker.
*Why this matters:* a Raspberry Pi is usually on a home network, often
reachable by other machines (and, through port forwarding or an
attacker on the same Wi-Fi, by the internet). A built-in password like
`root`/`root` is the first thing every scanning bot tries: the Mirai
botnet took over hundreds of thousands of devices with exactly such
factory passwords. Anything that ever listens on the network (`httpd`
today, a future ssh) is only as safe as the weakest password. So the
image must not contain *any* password that works, not even "just for
testing".
6. **Test credentials for headless `make test`, without shipping them**:
- The image in `out/foundry.img` stays locked (setup-needed).
- `make test` makes its own copy (it already boots `out/test-v2.img`)
and writes test accounts **into that copy only**, from the host,
with e2fsprogs (`debugfs -w`): a `/etc/shadow` with hashes of
throwaway passwords generated for this run (`openssl passwd -6`,
random each time, kept in `out/`), and the setup marker removed.
- The guest test script gets the passwords on its standard input from
the host-side feed (the same way `scripts/hvc0-session.txt` already
feeds the hvc0 console), never from a file in the image.
- A check in `make test` fails if the release image has any shadow
entry that is not locked.
7. **Owners and modes set by the image build** (`mke2fs -d` copies modes
but takes owners from `root_owner`; per-file fixes go through
`debugfs -w` `set_inode_field` or an ext2 build step that reads a
small table like `root/permissions.txt`):
| path | owner | mode |
|---|---|---|
| `/etc/shadow` | root:root | 0600 (or root:shadow 0640) |
| `/etc/passwd`, `/etc/group` | root:root | 0644 |
| `/bin/*`, `/sbin/*` | root:root | 0755 |
| `/bin/su`, `/bin/passwd` (busybox links: the busybox binary they point to, or a separate setuid busybox with only these applets) | root:root | **4755** (setuid) |
| `/root` | root:root | 0700 |
| `/home/tyler` (test image) | tyler:tyler | 0700 |
| `/tmp` | root:root | 1777 (sticky: you can only delete your own files) |
| `/etc/foundry/setup-needed` | root:root | 0600 |
FAT32 and exFAT (B:, C:) have no owners at all; what they *show* is
up to the kernel's mount options (K13 (j)).
### Done when (checks `make test` can run)
On the test copy of the image, logged in through `getty → login` on hvc0
with the throwaway credentials:
1. Host side, before boot: `debugfs` shows `/etc/shadow` mode 0600 uid 0,
`/bin/su` mode 4755 uid 0, `/home/tyler` uid 1000; the **release** image
has only locked shadow entries and the setup marker present.
2. A wrong password prints `Login incorrect`; the right one gives a
prompt, and `id` prints `uid=1000(tyler) gid=1000(tyler) groups=...`,
`pwd` prints `/home/tyler`.
3. As tyler: `cat /etc/shadow` fails with `Permission denied`; writing to
`/etc/passwd`, `/bin/ls` and `/root` fails; creating a file in
`/home/tyler` works and `ls -l` shows it as `tyler tyler`, with the
mode umask 022 gives (`-rw-r--r--`); a file chmodded 0600 by tyler is
unreadable by a second test user.
4. `su` with root's test password gives uid 0 (`id -u` = 0); `su` with a
wrong password fails; `exit` returns to tyler.
5. As root: `adduser`/`passwd` create a third user; the new `/etc/shadow`
line starts with `$6$`; that user can log in on the console; `deluser`
removes them and their login fails.
6. `chown` by tyler on a file of tyler's fails with `Operation not
permitted`; by root it works.
7. First boot, simulated: booting the release image copy headless with the
setup answers fed on the console leads to the setup program, then to a
working login; booting again does not run setup again.
### Kernel prerequisites
All in [kernel-requests.md](kernel-requests.md), **K13** (open). Nothing
there is assumed to work until Edward's audit says so and our checks
pass: earlier probes showed ids and modes being *stored* but not
*enforced*.
## M7: Raspberry Pi 3 bootable build (placeholder)
Edward's arm64 / Raspberry Pi port of Rustbelt is in progress on the
kernel side. Once it boots, this milestone makes Foundry OS build an SD
card image for a Pi 3: `aarch64` builds of our Rust and C programs (a
second target for `foundry-cc` and Cargo), the Pi's boot partition
(firmware + kernel), and the same ext2 root / data / share layout. M6
comes first on purpose: a Pi on a network must not boot with a default
password. Details once the kernel's boot contract for the Pi is known.
## Long-term track: a self-hosted Rust toolchain (after M5, not started)
**The goal:** write a Rust program *on* Foundry OS, compile it there, and
run it there, without the box. Today every program is compiled on the box
(Linux) and copied into the image.
Words used below:
- **Compiler, toolchain.** A compiler turns source code (`main.rs`,
`hello.c`) into a program the CPU can run. A *toolchain* is the compiler
plus its helpers: the linker (glues the pieces into one file), the
standard library, and for Rust, `cargo` (the build tool).
- **Cross-compiling.** Compiling on one system for another one. That is
what we do now: the box's compilers make programs for Foundry OS.
- **Self-hosted.** A system that can build its own programs (in the end,
its own compiler) on itself. Linux is self-hosted; Foundry OS isn't yet.
- **Bootstrap.** Getting the first compiler onto a system that has none:
you cross-compile a compiler on another machine, copy it over, and from
then on it can build things locally, even newer versions of itself.
- **Dynamic linker (`ld.so`).** Most Linux programs don't carry their
libraries inside them. They name shared libraries (`.so` files), and a
small program, the dynamic linker, loads those into memory when the
program starts. Rustbelt has none today: it refuses such programs
(`PT_INTERP`), so everything we run is *static* (all in one file).
**Hard parts, honestly:**
- **Size.** rustc is big: the compiler itself (most of it is LLVM, a
huge C++ library) is about 150-200 MB, and the standard library for one
target another 100-300 MB. Our whole root partition is 32 MiB today.
- **Memory.** LLVM is hungry: a hello world needs a few hundred MB of RAM,
a real crate 1 GB or more. Our QEMU runs use QEMU's default of 128 MiB.
- **QEMU speed.** Our tests run QEMU without KVM, i.e. by emulating
every instruction in software ("TCG"), roughly 5-20x slower than the
box. With KVM (hardware virtualization) it's near native. Then add the
kernel's own speed: Rustbelt's FAT driver re-walks the path on every
read, for example, and nobody has measured page-fault or disk speed.
- **64 file descriptors per process** (`MAX_FDS` in the kernel). cargo,
rustc and the linker can open more than that at once (many `.rlib`
files, jobserver pipes, threads).
- **No `root=` option**, and only ext2 roots. A toolchain partition can't
be chosen at boot. It has to sit on the one ext2 root (so the root must
grow to a few GB) or be mounted elsewhere, e.g. as another ext2 disk
that appears at `/mnt/diskN`.
### Stage 1: a C compiler that runs on Foundry OS
*In plain words:* first prove that "compile on the target" works at all,
with a small compiler. **tcc** (Tiny C Compiler, ~300 KB, compiles and
links in one program, needs no separate assembler or linker) is the
easiest first step. Then a real one: **gcc** (with binutils' `as` and
`ld`) or **clang/lld**, built static against our musl.
- *Size:* **M** for tcc (one port, like dash, plus its own small libc
headers path); **L** for gcc/clang (big builds; gcc needs `as`, `ld`,
`cc1` and lots of processes; clang is one big LLVM program, a good
rehearsal for rustc).
- *Done when:* `make test` runs `tcc -static hello.c -o /tmp/hello &&
/tmp/hello` on Foundry OS and prints the line; later the same with
`gcc`, and on-target gcc builds `c/fprobe.c` and it passes 40/40.
- *OS-side:* ports/tcc, ports/binutils + ports/gcc (or clang), the musl
headers and `crt*.o` installed into the root (`/usr/include`,
`/usr/lib`), a bigger root partition.
- *Kernel (light, maybe nothing):* whatever the ports trip over, found
the usual way (fprobe-style checks, docs/syscall-gaps.md).
### Stage 2: what rustc and cargo need from the system
*In plain words:* rustc and cargo are ordinary Linux programs, but big
and demanding ones. Before porting them, make sure the system can carry
them: room on disk, enough memory, fast enough files and memory mapping,
lots of threads and child processes, and a way to get libraries
(*crates*).
| Need | Why | Kernel or OS |
|---|---|---|
| **Either** a dynamic linker **or** a fully static rustc | Official rustc builds load `librustc_driver-*.so` at run time. Option A: Rustbelt loads `PT_INTERP` programs and we ship musl's `ld.so` (musl's `libc.so` is its own dynamic linker). Option B: build rustc statically (no `dylib` driver, `crt-static`, LLVM linked in). B needs no kernel work but a custom rustc build. | A: **kernel** (load the interpreter, pass `AT_BASE`/aux vector) + OS (ship `ld.so`). B: OS only |
| RAM: 1-2 GB | LLVM | **kernel**: confirm/raise how much RAM Rustbelt can use; OS: `-m 2G` in our QEMU runs |
| Disk: a few GB of ext2 | toolchain + `target/` dirs | OS (bigger root or a second ext2 disk); **kernel**: ext2 performance and large-file/large-dir behaviour |
| `mmap` of big files, `mremap` | rustc mmaps `.rlib`/metadata files; musl's `realloc` tries `mremap` (25, still ENOSYS, K10) | **kernel** (file-backed `mmap` speed, `mremap`) |
| More than 64 fds | rustc + linker + jobserver | **kernel** (raise `MAX_FDS`) |
| Threads, jobserver pipes | rustc uses threads; cargo hands out build slots through a pipe (`jobserver`) | mostly there (K-tests pass); check `poll` on pipes under load |
| Spawning many processes | cargo starts rustc, rustc starts the linker (`cc`/`ld`); Rust's `Command` uses musl `posix_spawn`, i.e. `clone(CLONE_VM \| CLONE_VFORK)` | **kernel** (check that clone flag set; today's table lists fork-like `SIGCHLD [+ CLONE_VFORK]` and threads) |
| `/proc/self/exe` and more `/proc` | rustc finds its sysroot through `current_exe()`; cargo reads `/proc/cpuinfo`-style info for job counts | `/proc/self/exe` exists; **kernel** for the rest (K7) |
| Crates | `cargo build` downloads from crates.io over **HTTPS** | Option A: TLS (an OS-side port, e.g. rustls in a tool or BearSSL) + DNS, both on our network stack. Option B (simpler first): an **offline vendored registry** (`cargo vendor` on the box, copied in, `--offline`). OS only |
- *Size:* **XL**. Each row is small to medium, but together they are a
lot, and some (RAM, fds, the dynamic linker) are design changes in the
kernel.
- *Done when:* a stress program (C or Rust, cross-built) on Foundry OS
maps a 200 MB file, runs 16 threads, opens 200 fds, spawns 500
children through `posix_spawn`, and the frame count is flat afterwards.
Plus `free` shows ≥ 1.5 GB, and `df` shows ≥ 3 GB on the toolchain disk.
- *Kernel requests for Edward (kept light):* RAM limit, `MAX_FDS`,
`mremap`, `CLONE_VM|CLONE_VFORK`, and (only if we choose option A) the
`PT_INTERP` loader. Everything else is OS-side.
### Stage 3: rustc and cargo on Foundry OS
*In plain words:* cross-compile rustc and cargo on the box for
`x86_64-unknown-linux-musl` (the same Linux ABI Foundry OS speaks), copy
them onto a Foundry OS disk, and use them there. Our own `no_std`
programs (rbsh, the tools) need only `core`/`alloc` and our `rbrt`, so
they are a good first real test.
Cheaper wins on the way, each its own milestone:
1. `rustc --version` runs on Foundry OS (proves loading, size, memory).
2. `rustc hello.rs` (no cargo) builds a static hello that runs on Foundry
OS. Needs rustc to find a linker: our on-target gcc/`ld` from stage 1,
or rustc's bundled `rust-lld`.
3. `cargo build --offline` of a one-file crate.
4. **End:** `cargo build --offline --release` of `rust/rbsh` (our shell)
**on Foundry OS**, and the resulting `rbsh` runs the v2 self-test.
- *Size:* **XL**. The cross-build of rustc itself is long (hours on the
box, many GB of disk), and every failure on target is slow to debug
under TCG.
- *Done when:* step 4 passes in a `make test-selfhost` (allowed to be
slow, outside the normal `make test`).
- *OS-side:* the cross-build recipe (`ports/rust`, pinned like musl),
the toolchain disk, the vendored registry, `make test-selfhost`.
- *Kernel:* nothing new beyond stage 2, plus whatever stage 3 finds.
|