Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2729c03
bootloader: refuse a packed-relocation kernel, and start the stack on…
Japabu Sep 26, 2026
efd689b
Link everything that boots with rust-lld, and freeze toyos-ld
Japabu Sep 26, 2026
b01e23d
issues: what the linker switch found, and the TinyCC track is superseded
Japabu Sep 26, 2026
8d7750d
tests: a failed C link says every linker error on its first line
Japabu Sep 26, 2026
a5adb88
bootloader: the boot map holds the loader's own image, wherever firmw…
Japabu Sep 26, 2026
0e8be55
loader: the executable's TLS module ends at the thread pointer at its…
Japabu Sep 26, 2026
7400a0b
issues: a LAN test's socket path outgrows sun_path under a five-digit…
Japabu Sep 26, 2026
e348f48
bootloader: one tree links one image
Japabu Sep 26, 2026
09182b0
Merge remote-tracking branch 'origin/main' into wt/toyos-lld
Japabu Sep 26, 2026
f270301
rust: x86_64-unknown-toyos names rust-lld
Japabu Sep 26, 2026
bb38e4f
toolchain: the layout test knows a toolchain needs its linker
Japabu Sep 26, 2026
a028e6f
CLAUDE.md: self-hosting, upstreaming paused, the packaging mirror, an…
Japabu Sep 26, 2026
b92b545
Merge origin/main (#524, the ARM64 port) into wt/toyos-lld
Japabu Sep 26, 2026
1a83651
toyos-elf: the kernel's stack placement and its refused relocation fo…
Japabu Sep 26, 2026
1556dbb
bootloader, tests: remove a no-op strip and a second copy of the targ…
Japabu Sep 26, 2026
392c5a1
toyos-ld: refuse an executable with TLS, and delete the libc note's a…
Japabu Sep 26, 2026
39b0add
issues: the calloc defect exits on one C allocator; the kernel's stac…
Japabu Sep 26, 2026
e4317d3
toolchain: rust_lld is public again, for the C tests' link
Japabu Sep 27, 2026
d4c1b08
issues: swap_netd's loaded recurrence, and this branch's duplicate SU…
Japabu Sep 27, 2026
845b2ab
issues: rewrap the swap_netd recurrence
Japabu Sep 27, 2026
a7f423b
toolchain: the hosted rustc's build archives its target C with LLVM's ar
Japabu Sep 27, 2026
f3bd16d
issues: quiesce_wakes_on_the_last_park's recurrence in this branch's …
Japabu Sep 27, 2026
62dc9c4
issues: a ROOT image rebuilt at its used blocks can run out by one
Japabu Sep 27, 2026
a55d62c
issues: the ROOT sizing issue claims no mechanism it did not measure
Japabu Sep 27, 2026
59bd29f
issues: blockd_survives_its_death reds at a rate, on #524's toyos-ld …
Japabu Sep 27, 2026
65e389a
issues: the blockd rate belongs under filesystem/, beside blockd's ot…
Japabu Sep 27, 2026
5776c1b
image: trim ROOT to its used blocks rather than reformat it there
Japabu Sep 27, 2026
806d746
image: the actual trim_root fix and its boundary test
Japabu Sep 27, 2026
7b07271
Merge origin/main (1ce71831: #527 logging, #534 nightly) into the rus…
Japabu Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not

## Dependencies

Only **Rust** and **QEMU** (for development). The rules: no binary outside those two — a macOS binary is a hard no, and "only for tests" does not soften it; only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; no Python; third-party crates are used as published, and a fork exists only to carry a change being upstreamed and goes when upstream has it. The north star is **self-hosting**: nothing — build, test, or verification — rests on a host binary. Ask of anything new: could this ever run inside ToyOS?
Only **Rust** and **QEMU** (for development). The rules: no binary outside those two — a macOS binary is a hard no, and "only for tests" does not soften it; only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; no Python; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull requests are sent for now: ToyOS needs more attention and more contributors before upstream projects take it seriously, and upstreams tend to refuse AI-first projects and their contributions. A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork. The north star is **self-hosting**: nothing — build, test, or verification — rests on a host binary. Ask of anything new: could this ever run inside ToyOS? Self-hosting means ToyOS rebuilds itself on ToyOS and reproduces the host's bytes; a bootstrap from source with no binary seed is out of scope.

Vendor firmware a device verifies by its maker's signature may be shipped: pinned by version and hash, redistributable unmodified, recorded in `NOTICE`, and loaded only by that device's own driver through its IOMMU domain; it never executes on the CPU.

The bar is not yet the tree. The standing failures are declared rather than removed — Python via `rust/x`, `cc` for every host link, four macOS FAT tools. `NOTICE` names every committed third-party file with its hash, upstream and licence; an image carrying `DOOM1.WAD` may not be sold.

- **toyos-ld** — custom linker for bootloader, kernel and all userland. Its output is reproducible, and the container types say so: anything iterated into the output is a `BTreeMap`/`BTreeSet`; a container asked only for membership stays hashed.
- **toyos-ld** — frozen: everything links with rust-lld, and toyos-ld stays only as the linker inside ToyOS until lld runs there, then goes.
- **toyos-cc** — minimal C compiler; exists to bootstrap tinycc and compile doomgeneric, not to grow. A layout or linkage construct it does not implement is refused by name — dropping one silently is a miscompilation.
- **rust/** — Rust compiler/std fork with ToyOS platform support (submodule). Auto-bootstraps; kept current with upstream. Its rules: `src/forkcheck.rs`'s module header.

Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 17 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ sha2 = "0.10"

[dev-dependencies]
toyos-cc = { path = "toyos-cc" }
toyos-ld = { path = "toyos-ld" }
# `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel
# checks every user pointer against and not against a second copy of it.
toyos-userbound = { path = "toyos-userbound" }
Expand All @@ -178,7 +177,7 @@ toyos-i219 = { path = "toyos-i219" }
#
# `check` because that is where the type lives, and it lives there because
# `src/build.rs`'s artifact gate asks the *shipping* kernel to carry none of the
# check instruments' literals — `toyos-ld` keeps a string constant no code
# check instruments' literals — a linker may keep a string constant no code
# reaches, so an unconditional `Display` puts the report's prefix in every
# image. This is a dev-dependency of a host binary and `kernel/` is excluded
# from this workspace with its own lockfile, so nothing here reaches the kernel's
Expand All @@ -201,18 +200,14 @@ tar = "0.4"
[profile.release]
opt-level = 2

# The two host tools every build runs are built `--release` by
# `src/toolchain.rs`, and `[profile.release]` above would otherwise drop them
# from the default `opt-level = 3` to 2 now that they are members here rather
# than workspace roots of their own. The linker runs once per guest binary and
# the compiler once per translation unit: neither pays for the harness's
# compile-time tradeoff.
# The host tool every build runs is built `--release` by `src/toolchain.rs`,
# and `[profile.release]` above would otherwise drop it from the default
# `opt-level = 3` to 2 now that it is a member here rather than a workspace root
# of its own. The compiler runs once per translation unit and does not pay for
# the harness's compile-time tradeoff.
[profile.release.package.toyos-cc]
opt-level = 3

[profile.release.package.toyos-ld]
opt-level = 3

# The test harness is a compute-bound host program, not a debug build of the
# thing under test: it decodes a 2.07 M-pixel screendump per poll and formats a
# 300 MB disk image per boot. Nothing here is cross-compiled — the kernel,
Expand Down Expand Up @@ -271,9 +266,20 @@ opt-level = 3
inherits = "dev"
opt-level = 2
debug = true
# DWARF stays out of the image: symbols name a frame, and `debug = true` is kept
# for the codegen it decides.
strip = "debuginfo"
debug-assertions = true
overflow-checks = true

# Cargo does not carry the profile's `strip` onto a package with an override of
# its own, and these two have one in `dev`.
[profile.toyos.package.toyos-cc]
strip = "debuginfo"

[profile.toyos.package.toyos-ld]
strip = "debuginfo"

[[test]]
name = "toyos-build"
path = "tests/toyos.rs"
Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ your machine in milliseconds, with no VM and no hardware. Several forbid
`unsafe` outright. The kernel calls them; it does not contain them. That is
what makes an OS this size possible to change with confidence.

The north star is **building ToyOS from within ToyOS**: its own compiler, its
own linker, on its own hardware, with no external toolchain anywhere in the
chain.
The north star is **building ToyOS from within ToyOS**: the system rebuilds
itself on its own hardware and reproduces, byte for byte, what the host built.

## Milestones

Expand All @@ -66,15 +65,14 @@ chain.

| | |
|---|---|
| ✅ | **Our own linker** — the UEFI bootloader as PE32+, the kernel and all userland as position-independent ELF |
| ✅ | **One linker, `rust-lld`** — the UEFI bootloader as PE32+, the kernel and all userland as position-independent ELF |
| ✅ | **Our own C compiler** — preprocessor through codegen, Cranelift instead of LLVM |
| ✅ | **A real Rust target** with a real `std`: threads, `dlopen`, unwinding, symbolized backtraces |
| ✅ | The Rust ecosystem, mostly unmodified — crates.io crates compile and run as published |
| ✅ | `rustc` itself built for ToyOS and shipped inside the image |
| 🔨 | Compiling a Rust program *inside* ToyOS |
| ⬜ | `cargo` inside ToyOS |
| ⬜ | Building ToyOS from within ToyOS |
| ⬜ | No LLVM anywhere in the chain, bootstrap included |

### The kernel

Expand Down Expand Up @@ -163,11 +161,11 @@ the `/system/bin/doom` in the desktop image. It also takes cases from TinyCC's o
`tests2` corpus all the way to running ToyOS processes, comparing each one's
output against TinyCC's expectations.

**Our own linker.** `toyos-ld` links everything that runs on ToyOS, plus
everything that runs before it: the UEFI bootloader as PE32+, the kernel and
every userland program as position-independent ELF. No LLVM linker touches
anything that boots. The largest thing it lays out is the Rust compiler itself,
built for ToyOS, in a single shared object.
**LLVM's linker.** `rust-lld`, the one the Rust toolchain carries, links
everything that runs on ToyOS, plus everything that runs before it: the UEFI
bootloader as PE32+, the kernel and every userland program as
position-independent ELF. `toyos-ld`, the linker this project wrote, is frozen:
it is what `toyos-cc` links through inside ToyOS until LLD runs there.

**A real Rust target.** `x86_64-unknown-toyos` lives in ToyOS's fork of the
compiler, with a prebuilt `std` in the sysroot. One `rustc` invocation turns an
Expand Down Expand Up @@ -232,7 +230,8 @@ script whose whole job is to find a Python to run `bootstrap.py` with — so a
clean clone needs one, and so does every toolchain change.

Nothing in the OS goes near either. `bootloader/`, `kernel/` and `userland/`
all link with `toyos-ld`, and no image contains a C toolchain or a Python. On
all link with the toolchain's `rust-lld`, and no image contains a C toolchain
or a Python. On
macOS both arrive with the Xcode Command Line Tools; on Debian and Ubuntu they
are `build-essential` and `python3`.

Expand Down
9 changes: 6 additions & 3 deletions bootloader/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ rustflags = [
# on x86-64 is the AVX2 backend, which this soft-float target has no
# registers for.
"--cfg=curve25519_dalek_backend=\"serial\"",
# `/Brepro` stamps the image with a hash of itself rather than the clock,
# and `/DEBUG:NONE` writes no PDB, whose GUID the image would otherwise
# carry and whose contents name the target directory: without both, two
# builds of one tree are two images.
"-Clink-arg=/Brepro",
"-Clink-arg=/DEBUG:NONE",
]

[target.x86_64-unknown-uefi]
linker = "toyos-ld"
86 changes: 44 additions & 42 deletions bootloader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use core::mem;

use alloc::vec;
use alloc::alloc::Layout;
use toyos_elf::section::{SectionTable, SHT_RELA};
use toyos_elf::section::SectionTable;
use toyos_elf::{RelaTable, RelocKind};
use uefi::{
prelude::*,
Expand Down Expand Up @@ -309,14 +309,6 @@ const WATCHDOG_CODE: u64 = 0x0001_0000;
/// Kernel virtual base: all physical memory is mapped here in the kernel's address space.
const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000;

/// `SHT_REL`, the relocation form whose addend lives in the destination word.
///
/// Named here rather than taken from `toyos-elf`, which names only the section
/// types it consumes and consumes no `SHT_REL`: nothing in this tree emits one,
/// and an image that carried them would otherwise start with every one of them
/// silently unapplied.
const SHT_REL: u32 = 9;

/// `[offset, offset + len)` of the file, or `None` when that is not wholly
/// inside it.
///
Expand Down Expand Up @@ -362,18 +354,10 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel {
println!("Kernel stack size: {}", stack_size);
// `vaddr_max` is the largest `p_vaddr + p_memsz` over the `PT_LOAD`
// segments, and the image is laid out at its own vaddrs — so it is what the
// kernel's memory has to cover before the stack is added to it. The stack
// starts on the next page: where the image ends is the linker's choice, and
// both ABIs want the stack pointer 16-byte aligned, which AArch64's
// `SCTLR_EL1.SA` enforces on every access through it.
const PAGE: u64 = 4096;
let mem_size = layout
.vaddr_max
.checked_add(PAGE - 1)
.map(|end| end & !(PAGE - 1))
.and_then(|stack_base| stack_base.checked_add(stack_size as u64))
.and_then(|n| usize::try_from(n).ok())
// kernel's memory has to cover before the stack is added after it.
let placed = toyos_elf::StackedImage::place(layout.vaddr_max, stack_size as u64)
.expect("kernel.elf: image plus stack does not fit an allocation");
let mem_size = usize::try_from(placed.size).expect("kernel.elf: image plus stack does not fit an allocation");

println!("Kernel memory size: {}", mem_size);

Expand All @@ -390,13 +374,11 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel {
process_mem[vstart..vstart + src.len()].copy_from_slice(src);
}

assert!(
!sections.iter().any(|section| section.kind == SHT_REL),
"kernel.elf: SHT_REL is not supported"
);
let rela_sections =
sections.rela_sections().unwrap_or_else(|form| panic!("kernel.elf: {form} is not supported"));

let mut reloc_count = 0u64;
for section in sections.iter().filter(|section| section.kind == SHT_RELA) {
for section in rela_sections {
let table = file_range(kernel_elf_bytes, section.offset, section.size)
.expect("kernel.elf: SHT_RELA section is past the end of the file");
for rela in RelaTable::new(table, arch::ELF_MACHINE).iter() {
Expand Down Expand Up @@ -427,8 +409,8 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel {
// the 8-byte write lands fully inside `process_mem`'s
// allocation. `write_unaligned`, not `write`: an
// `r_offset` from the file is not guaranteed 8-byte
// aligned by anything checked here, only by toyos-ld
// always emitting `R_X86_64_RELATIVE` against aligned
// aligned by anything checked here, only by the
// linker emitting `R_X86_64_RELATIVE` against aligned
// slots — a fact this reader has no way to verify.
process_mem
.as_mut_ptr()
Expand All @@ -447,7 +429,7 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel {
LoadedKernel {
memory: process_mem,
entry_offset: layout.entry as usize,
stack_offset: mem_size - stack_size,
stack_offset: placed.stack as usize,
stack_size,
}
}
Expand Down Expand Up @@ -643,24 +625,42 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
//
// Said before it is applied: a machine this refuses leaves the refusal in
// `loader.log`, which is the artifact a machine with no console has.
// Where firmware loaded this image, which is where the x86-64 switch to
// the boot map runs from: the map holds it wherever that is.
let loader = {
let bs = system_table.boot_services();
let image = bs
.open_protocol_exclusive::<LoadedImage>(bs.image_handle())
.expect("firmware answers LoadedImage for the image it started");
let (base, size) = image.info();
(base as u64, size)
};
// Firmware's write-back memory, for an architecture whose boot map types
// pages by it. Before the exit, while the map can still be asked for; the
// attributes a descriptor carries do not change across it.
let write_back = write_back_memory(&system_table);
let planned =
Plan::new(gop.as_ref().map(|g| (g.framebuffer, g.framebuffer_size)), arch::typing(&write_back));
let planned = Plan::new(
gop.as_ref().map(|g| (g.framebuffer, g.framebuffer_size)),
loader,
arch::typing(&write_back),
);
match &planned {
Ok(plan) => match plan.scanout() {
Some((at, len)) => println!(
"Scanout: {at:#x}+{len:#x} mapped as the scanout in 2 MiB pages at identity and at \
PHYS_OFFSET, in {} page directories",
plan.directories().len()
),
None => println!("Scanout: this machine has none"),
},
Err(why) => println!("Scanout: NO BOOT MAP HOLDS IT, {why}"),
Ok(plan) => {
match plan.scanout() {
Some((at, len)) => println!(
"Scanout: {at:#x}+{len:#x} mapped as the scanout in 2 MiB pages at identity and at \
PHYS_OFFSET, in {} page directories",
plan.directories().len()
),
None => println!("Scanout: this machine has none"),
}
let (at, len) = plan.loader();
println!("Loader image: {:#x}+{:#x}, mapped at identity as {at:#x}+{len:#x}", loader.0, loader.1);
}
Err(why) => println!("Boot map: NO MAP HOLDS THIS MACHINE, {why}"),
}
let plan = planned.unwrap_or_else(|why| panic!("the boot map cannot hold the scanout: {why}"));
let plan = planned
.unwrap_or_else(|why| panic!("the boot map cannot hold the scanout and the loader: {why}"));

// SAFETY: `pt_mem` is the `MAX_PAGES * 4096`-byte, 4096-aligned, zeroed
// allocation above, and a `Plan` never names more pages than that.
Expand Down Expand Up @@ -789,8 +789,10 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v

let image = (kernel_phys, kernel_args.kernel_memory_size);
// SAFETY: `kernel_args.boot_pml4_addr` is the table built above,
// identity-mapping low memory (so the code and stack a switch to it runs
// from stay mapped across it) and mapping the same range at `PHYS_OFFSET`;
// identity-mapping low memory and this loader's own image (so the code and
// stack a switch to it runs from stay mapped across it; the stack
// `kernel_args` lives on was proved inside the low map before the exit) and
// mapping the same at `PHYS_OFFSET`;
// the assert before the exit proved the whole kernel image is inside that
// range, and `image` is that image, relocated, with its entry at
// `entry_offset`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ crate's output", and it is the only place the answer should be derived —
`<primary>/target` via `primary_checkout()`, degenerating to `<root>/target`
where there are no worktrees. Two sites build the path themselves and would have
to go through it: `src/build.rs`'s `stage_artifact` and `src/pr.rs`'s merge-file
directory. Two more are target-directory computations in disguise:
`toolchain::toyos_ld_binary` and `toyos_cc_binary`.
directory. One more is a target-directory computation in disguise:
`toolchain::toyos_cc_binary`.

It also needs an absolute `build.target-dir` in each worktree's gitignored
`.cargo/config.toml`, because agents type `cargo test` by hand. Measured: such a
Expand Down
8 changes: 8 additions & 0 deletions issues/build/parallel-tests-red-under-other-suites.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,11 @@ mechanism for it. `src/redlist.rs` carries the sighting.
parallel run — a loaded full fast tier in which `i8042_undecoded_bytes`'
first mute line names nothing and its second names the sequence, or the
retirement's clause narrowed to the conditions under which it holds.

- **`swap_netd`, again on its recorded signature.** The rust-lld branch's fast
tier at `e4317d3f` (PR #532), on a dev host whose load averages read 39.8,
31.1 and 37.5 as it started, from other worktrees: `the stream's redial was
turned away 64 time(s), its ceiling of 64, and gave up`, with init's words on
netd `["accepted"]` ending in `None`; the one red of 400 besides
`lan_mdns_answer`'s `SUN_LEN`, and `ALONE swap_netd: GREEN` in 10 s. The
branch touches neither netd, swap nor init. Not investigated here.
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ on the sibling arm.

**Exit**: a cause for the empty uart on a boot that rebooted as designed, or
the marker waited for where the boot's reboot cannot race it.

Again in PR #532's fast tier at `a7f423b5`, 399 passed and this one of two reds:
`QEMU died before ===READY===` (exit status 0), the console through
`stop: 7 of 7 userland thread(s) stopped ... in 1 ms of a 2010 ms budget`,
`Rebooting.` and `shutdown: /log did not answer in 2000ms`, the uart
`nothing at all`; `ALONE ... GREEN`. That branch touches no quiesce path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
status: open
kind: defect
opened: 2026-09-26
---

# The ToyOS-hosted rustc names a linker ToyOS does not have

`x86_64-unknown-toyos` names `rust-lld`, and the rustc `src/toolchain.rs`
builds for a ToyOS host carries that target spec into the guest, where no
`rust-lld` exists: LLD runs on ToyOS only once clang and libc++ do. The linker
that does run there is the frozen `/system/bin/toyos-ld`, which that rustc
reaches only when told `-C linker=toyos-ld`. No image ships the hosted rustc
today — `src/build.rs` refuses `hosted-rustc = true` until its licences are
read — so nothing links through it yet.

Exit: the hosted rustc links a program inside ToyOS through a linker the image
carries, and a guest test compiles and runs one.
Loading
Loading