Architecture
Why the tree looks the way it does. Read when changing structure, not every session.
Goal
Re-implement the OpenBSD kernel in Rust, file by file, preserving OpenBSD’s structure, names and
semantics, as a standalone #![no_std] kernel for amd64 and arm64, booted by Limine, run in QEMU.
The C tree under reference/openbsd-src/sys is the specification.
Four crates: hal, kernel, abi and the LZ tree bsd (decision 20, 2026-10-16)
sys/ holds four Cargo packages (docs/ZERO_UNSAFE.md section 0, the one diagram): sys/hal/,
the hardware and the runtime, the only crate where unsafe may live; sys/kernel/, every
decision of the operating system, #![forbid(unsafe_code)] at its root; sys/abi/, OpenBSD’s
userland interface as the loadable module abi_openbsd.ko, forbid, translation only; and
bsd at sys/ itself (OpenBSD’s kernel image is /bsd), the LZ tree with its subsystems as
modules (kern, uvm, dev, ddb, sys, machine, arch), replaced subsystem by subsystem
and deleted at N9. Dependencies go one way: bsd -> kernel -> hal, abi -> hal and abi ->
KernelApi; hal depends on nothing of ours but libkern.
The cycles OpenBSD resolves at link time (trap → uvm_fault → pmap → tsleep → sched) were the
reason bsd was one crate until 2026-10-16 (v1: a module could become a crate only if it used
nothing from crate::{kern, uvm, arch, machine}). Under v2 an upward call is never a
dependency: it is a trait kernel or hal declares and bsd implements and registers in
hal’s registry at boot, one row of bridges.toml each (section 0.3). sys/lib/libkern and
sys/lib/libz stay leaves (OpenBSD builds both as libraries too; libz holds what the kernel
compiles of zlib: crc32, adler32, deflate, inflate; zlib licence).
No src/ directory ([lib] path = "lib.rs"), so C and Rust paths differ only by extension.
The runtime API (N2.3)
What kernel builds on, all safe to call, every unsafe inside hal with its proof
(docs/ZERO_UNSAFE.md decisions 30, 32-36): hal::once::OnceStatic<T> (a global set once at
boot), hal::mutex::Mutexed<T> and hal::rwlock::Rwlocked<T> (locks that own their data; the
write guard sleeps with rwsleep and runs a closure unlocked), hal::percpu::PerCpu<T>,
hal::refcnt::Refcnt, hal::intr::splraise (a guard that restores only a level it raised),
hal::cpu (the CPU’s identity), hal::kmbuf::KmBuf (pageable kernel memory it owns),
hal::pool::TypedPool<T> with PoolArc<T> (one pool(9) per type, so vmstat -m and
systat pool name it), hal::malloc::MBox<T> (a T in a malloc(9) block of the C’s type),
hal::user::UserPtr<T> with abi_pod! (typed user copies), hal::sleep (wakeup) and
hal::cons (the console primitive). What still lives in bsd is reached through
hal::bridges, a registry of safe traits bsd fills at boot (sys/kern/hal_bridges.rs,
called by stand::start_kernel before main) and checked on every answer
(sys/hal/trust.rs). At the N3 close, 21: IntrLevel, CpuId, Sleep, UserCopy,
ModulePlatform (the boot entry), TlbCpus, KernelLock, SysInfo, VmProc, VmSysctl and
pipe’s PipeGlue, UioMove, PipeFiles (paid by N4); BufCacheStats, VnodeOps (N5);
NetLock (N6); Console, Random (N7); PoolBackend, Malloc, Printf (N9). N3 paid
PageMemory, PhysPages’s row, KmMaps, KmPages and FaultMap with the kernel modules that
implement them. Each is a row of bridges.toml; lz check counts them.
Path and name mapping
| OpenBSD | Here |
|---|---|
sys/kern/subr_prf.c |
sys/kern/subr_prf.rs |
sys/sys/proc.h (struct proc) |
sys/sys/proc.rs (pub struct Proc) |
sys/kern/kern_fork.c (fork1()) |
sys/kern/kern_fork.rs (impl Proc { pub fn fork1 } or pub fn fork1) |
sys/arch/amd64/amd64/pmap.c |
sys/arch/amd64/amd64/pmap.rs |
sys/arch/amd64/include/pte.h |
sys/arch/amd64/include/pte.rs |
sys/lib/libkern/strlcpy.c |
sys/lib/libkern/strlcpy.rs |
sys/net/if.h, sys/netinet/in.h (Rust keywords) |
sys/net/if_.rs, sys/netinet/in_.rs (docs/C_TO_RUST.md) |
sys/arch/*/stand/, boot(8), efiboot |
sys/stand/ (Limine glue) until M14, when the user decided (2026-10-03) to port boot(8)/efiboot and sys/lib/libsa/; Limine is scaffolding until boot(8) boots the same kernel in QEMU |
sys/conf/, config(8), Makefiles, newvers.sh |
Cargo features and tools/xtask; ioconf.c is sys/arch/<arch>/conf/ioconf.rs, by hand (M7b); the vers.c that newvers.sh generates is sys/conf/vers.rs, fed by sys/build.rs (“The system’s identity”, below); not ported |
Types live where the header is; functions live where the .c is. Rust allows inherent
impl blocks in any module of the defining crate, which is exactly the header/implementation split.
Zones in a file (M15)
Every .rs under sys/ and tools/ (not init/) is split into zones by comment lines of their
own: /* <LICENSES> */, /* <CODE> */, /* <TESTS> */, each with its closing marker, in that
order (.claude/rules/rust-kernel.md has the layout, tools/xtask/src/layout.rs the validator
that cargo xtask ports check runs). The reason is reading cost: a ported file is mostly licence
text on top and tests at the bottom, and an agent that wants the code reads
sed -n '/<CODE>/,/<\/CODE>/p' file.rs. The tests are inline in the same file, however long:
the former <name>/tests.rs files (324 of them) became #[cfg(test)] mod tests { .. } in the
TESTS zone, so the module path of every test (crate::x::tests::name) is unchanged and one file
holds a module and its tests. Every LICENSES zone starts with the author’s ISC block (the
user’s decision of 2026-10-09): before the original notices of a port, or alone in the ports whose
C file has no licence text (or is generated, license = "none" in lineage.toml), the
[[extra]] files and the files outside lineage.toml. One exception is written down: the test
helpers other modules share (mod testutil;, fn foo_reset() under #[cfg(test)])
stay in CODE, since they are code for other files’ tests. init/ (a stand-in) is outside.
The machine contract
sys/machine/<header>.rs holds the traits standing in for <machine/*.h> and cpufunc.h, one
module per OpenBSD header (param.rs, cpu.rs with boot(9), delay(9) and, since M5, curcpu() with the
cpu_info accessors the clock code needs, cons.rs for
consinit(), bus.rs for bus_space(9), db_machdep.rs for what ddb needs; later pmap.rs,
intr.rs, …; autoconf.rs is what ioconf.c and the machine’s autoconf.c give
subr_autoconf.c; signal.rs is <machine/signal.h> plus sendsig, sys_sigreturn and the
signal trampoline), all re-exported from sys/machine/mod.rs, which also re-exports
consinit(), bus.rs for bus_space(9) and (M7b) bus_dma(9), db_machdep.rs for what ddb
needs; later pmap.rs, intr.rs, …; autoconf.rs is what ioconf.c and the machine’s
autoconf.c give subr_autoconf.c; pci_machdep.rs (M7b) is <machine/pci_machdep.h>;
conf.rs (M8) is the device switch each arch’s conf.c fills; isa_machdep.rs (M8) is
<machine/isa_machdep.h>; disklabel.rs (M8) is <machine/disklabel.h> plus the machine’s
disksubr.c, readdisklabel and writedisklabel; acpi_machdep.rs (M13) is the machine half of
<dev/acpi/acpivar.h>, each arch’s acpi_machdep.c: acpi_map, the register maps, the SCI, the
global lock, pwr_action, ci_acpi_proc_id, cpu_suspended, and the ACPI_PRT/ACPI_SECTWO
constants that stand for acpi.c’s #ifdef __amd64__/__arm64__ walks; arm64 answers as a
machine without ACPI until M14), all re-exported from sys/machine/mod.rs, which also re-exports
crate::arch::current::Machine and asserts at compile time that it implements every trait. Generic
code names only crate::machine. bus.rs also carries the C names as free functions
(bus_space_read_1(t, h, o), bus_dmamap_load(t, map, ...)), so a driver reads like its
original; the tag and handle types are the architecture’s (X86BusSpace/BusSpaceHandle on
amd64, &'static BusSpace on arm64), and so are the DMA tag, map and segment types (each arch’s
include/bus.rs; the tag is a table of functions, as in C). MI code reads a map’s public
members by their C names (dm_nsegs, dm_segs(), ds_addr, …), which every arch must
define (bus_dma_public_members checks it at compile time).
param.rs, vmparam.rs and exec.rs (the traits MachineParam, VmParam, MachineExec) live
in sys/hal/machine/ since N2.2, and sys/machine/mod.rs re-exports them under their old names.
pmap.rs is an adapter over hal::pmap since N3.1 (docs/ZERO_UNSAFE.md 0.10): the Pmap
trait’s methods call the safe API of hal, whose two backends (sys/hal/arch/amd64/pmap/,
sys/hal/arch/arm64/pmap/) replaced the LZ pmaps, and the host double’s pmap is hal’s host
backend (sys/hal/arch/host/pmap.rs). The adapter has no unsafe block: it holds the
LegacyLend of the kernel map’s token (kernel::vm::km), and its three legacy unsafe fn
signatures (pmap_kenter_pa, pmap_kremove and arm64’s pmap_kenter_cache) stay until
their last caller moves. The page provider bootstrap takes as a value (decision 39) is the
kernel’s since N3.3 (kernel::vm::page, sys/kernel/vm/page.rs); bsd’s BsdPages went
with sys/uvm/.
Since N3, memory is the kernel’s (docs/ZERO_UNSAFE.md 0.10): sys/kernel/vm/ holds the page
array and its free ranges, the owners (anon, amap, object, aobj, pager, vnode), the fault
handler, the map and the vmspace, the vm system calls (Native entries of the module) and the
rest of uvm’s 26 files, all #![forbid(unsafe_code)]. Their locks are hal::rwlock’s
SharedRwlock with Guarded cells: a cell opens only for a witness of its own lock, and the
fault carries its witnesses through its loop; the lock order (map, amap, object, anon) stays
the code’s, as in the C, each acquisition saying where it stands. What they reach of the hardware is hal’s
safe API: hal::pmap (the kernel pmap through the KernelMapper token, held by
kernel::vm::km), MappedBytes for the pager’s windows, ForeignWindow for uvm_io’s
copies, the intrusive lists and trees of the page system and the map. What they reach of bsd
is a bridge of bridges.toml (VmProc and VmSysctl until N4’s Proc, VnodeOps until N5).
Constants travel the same way as functions: MachineParam (M1) carries <machine/param.h> and
the alignment rules of <machine/_types.h> as associated consts, each arch defines them in
arch/<arch>/include/{param,_types}.rs, and sys/sys/param.rs re-exports them, so generic code
imports PAGE_SIZE from sys::param exactly as C includes <sys/param.h>.
Three implementors:
sys/arch/amd64:cfg(all(target_os = "none", target_arch = "x86_64"))sys/arch/arm64:cfg(all(target_os = "none", target_arch = "aarch64"))sys/arch/host:cfg(not(target_os = "none")), a std-backed double. It makescargo testwork on macOS and proves mechanically that the contract is complete (a missing method fails to compile for the host too). It must not grow logic.
Boot flow
Limine (UEFI, both archs) → _start in sys/stand/mod.rs (protocol structs in
sys/stand/limine.rs) → machine::BootInfo (bootloader-neutral: memory map, HHDM offset, kernel
load addresses, DTB/RSDP pointers, the UEFI system table and memory map (efiboot’s
openbsd,uefi-* properties), command line; it lives in sys/machine/bootinfo.rs so the
machine traits can name it) → boothowto from the command line (BootInfo::boothowto, the
boot(8) letters -a -c -d -s as arm64’s initarm parses them) →
machine::Machine::early_init(&BootInfo) (OpenBSD’s init_x86_64 / initarm as far as they are
ported: the message buffer, consinit(), and db_enter() for boot -d) → the bsd: booted on
banner → kern::init_main::main (OpenBSD’s main() in the C’s order; every step whose subsystem
is not here yet reports itself with unported!). Under feature qemu, main ends with the success
exit where proc0 would go to sleep.
A panic anywhere (panic! is kern::subr_prf::panic through the crate’s panic handler) prints
panic: <message> through db_printf, a frame-pointer stack trace (db_stack_dump →
machine::DbMachdep::db_stack_trace_print, addresses only; cargo xtask symbolize --arch A names
them offline from the ELF symbol table) and reaches reboot → machine::Cpu::boot, which, cold,
halts; under feature qemu the “press any key” wait is the failure exit (status 35).
Leaving the machine goes through machine::Exit: under feature qemu, amd64 uses the
isa-debug-exit device and arm64 the semihosting SYS_EXIT call, both making QEMU exit with
status 33 for success and 35 for failure, which xtask smoke checks. Without the feature the CPU
halts.
At entry Limine (protocol base revision 6) guarantees: 64-bit mode, MMU on, kernel mapped at
0xffffffff80000000, a higher-half direct map of physical memory (HHDM), a memory map, a stack of
at least 64 KiB, interrupts masked, secondary CPUs parked. Limine’s page tables live in
bootloader-reclaimable memory, so the kernel owns its own pmap and trap vectors before reclaiming
(M3/M4).
Why Limine: it is the only option with an identical boot contract on amd64 and aarch64, which keeps
the arch split focused on real kernel work (traps, pmap, interrupts). OpenBSD also keeps the
bootloader separate from the kernel, so this is faithful in spirit. bootloader (crate) is
x86_64-only; QEMU -kernel raw loading on virt would mean two unrelated early-boot paths.
The protocol is specified in https://github.com/limine-bootloader/limine-protocol (PROTOCOL.md,
include/limine.h); sys/stand/limine.rs implements the subset the kernel asks for, at base
revision 6, with no crate in between (see “Dependencies”).
Toolchain and targets
Stable Rust, pinned in rust-toolchain.toml.
x86_64-unknown-none: kernel code model, no SSE/AVX, no red zone.aarch64-unknown-none-softfloat: no NEON/FP. The hardfloat variant lets the compiler useqregisters inmemcpy; a kernel that does not save FP state on traps must not touch them. This is the Rust equivalent of OpenBSD’s-mgeneral-regs-only.
No nightly: asm!/global_asm!, #[panic_handler], #[global_allocator], #[unsafe(no_mangle)]
and #[unsafe(link_section)] are stable; core and alloc ship precompiled for both targets.
extern "x86-interrupt" is unstable, so interrupt stubs are assembly, like OpenBSD’s vector.S.
custom_test_frameworks is unstable, so in-QEMU tests are serial smoke tests driven by xtask.
Linking
sys/arch/<arch>/conf/kernel.ld (arm64: base 0xffffffff80000000 with physical addresses
from 0 and the entry at _start’s offset, from M14; amd64: OpenBSD’s layout from
M14, KERNTEXTOFF with physical addresses from 0x1000000, “Boot loaders”),
PHDRS text/rodata/data, Limine request sections kept, .eh_frame/.note discarded.
M16d: both scripts add OpenBSD’s openbsd_randomize header (PT_OPENBSD_RANDOMIZE) over
.openbsd.randomdata, page-aligned inside the rodata segment, which boot(8)’s loadfile fills
with its seed (dev/rnd.c’s entropy_pool0/rs_buf0, __guard_local); Limine fills nothing
there, so a Limine boot starts the generator from a zero seed and says so (random_start’s
“warning: no entropy supplied by boot loader”, the boot glue’s unported: line). arm64’s
script provides etext too.
sys/build.rs passes it with cargo:rustc-link-arg-bins only when target_os = "none".
Per-target rustflags in .cargo/config.toml: relocation-model=static (non-PIE higher-half kernel)
and force-frame-pointers=yes (backtraces in panic). No [build] target, so host builds stay
the default and cargo test just works.
abi_openbsd.ko (N2.4, sys/abi/ko, just build-abi-<arch>) is linked as efiboot is:
RUSTFLAGS="-C relocation-model=pie" (replacing the kernel’s static), its own target
directory (target/abi), CARGO_PROFILE_DEV_STRIP=debuginfo (boot(8) reads the whole file
into efiboot’s 1 MiB heap), and sys/abi/ko/abi.ld: a static PIE at 0 with -pie
--no-dynamic-linker -z notext -Bsymbolic --pack-dyn-relocs=none -z norelro, three
page-aligned PT_LOADs (text R X; rodata R with the module header first at the ELF entry,
the GOT and .data.rel.ro, relocated by the loader before it is mapped; data R W) and no
import (section “abi_openbsd.ko”).
abi_openbsd.ko (N2.4)
The ABI crate is a loadable module (decision 22, docs/ZERO_UNSAFE.md 0.1). The pieces:
- The table.
sys/abi/sysent.rs: oneNative(fn) | Legacyentry per number of<sys/syscall.h>(hal::sys::syscall), every oneLegacyat N2.4 but pipe and pipe2,Nativesince N2.6 (throughKernelApi::pipe2, which runs the samesysent[]row and callskernel::pipe, so theLegacypath gives the same answer);abi_initregisters the dispatcher throughKernelApi. The binary issys/abi/ko(packageabi_openbsd): the headerhal::api::AbiModuleHeader(magicEMIABIKO,KernelApiversion and size, entry point) as a#[used]static thatabi.ldputs first in the read-only segment and names as the ELF entry. It links no kernel code and nohalstate; its panic handler ishal::module::rt(featuremodule-rt), which parks the CPU. - The vtable.
hal::api::KernelApi,#[repr(C)], version and size first (ApiPrefix), Rust-ABIfnpointers (one toolchain builds both sides): the handler’s registration, theLegacypath,copyin/copyout.bsdfills it (sys/kern/abi_module.rs); the module receives it in every call and keeps no state. - The dispatch. Both arch traps call
kern::abi_module::syscallwhere they chose thesysent[]entry; it callshal::syscall::dispatch, which calls the registered handler (the module’s, throughkernel::api’s trampoline) or theLegacyfallback (bsd’ssysent[]andmi_syscall), both registered once at boot. Errnos crosshalasi32. - The hand-over. Limine: a
module_path:oflimine.conf, found by name inBootInfo::modules. boot(8) (both efiboots,sys/stand/boot/boot.rs):/abi_openbsd.kofrom the kernel’s device, read straight onto the page after the kernel’s image inside the room the program reserved (BootMd::abi_module_room) withmarks[MARK_END]raised over it, so amd64’s move and arm64’s cache clean carry it andesymkeeps it out of free memory; amd64 passesBOOTARG_ABIMODULE(BiosAbiModule, type 0x100), arm64/chosen’semibsd,abi-module(two big-endian 64-bit cells);getbootinfoturns either intoBootInfo::abi_module, andsys/stand/mod.rsabi_module_handoverkeeps the record only when oneKernelAndModulesregion of the memory map covers it (else one line, and the kernel boots without the module). The open descriptor acrossloadfileholds about 8 KiB of libsa’s 1 MiB heap, bounded by the file system’s block size, not by the file. The boot glue registers theLegacyfallback beforemain. - The loader.
hal::module(elf.rs,load.rs), called frommainjust beforeinit(8)may exec: parses the ELF64 in safe code (a static PIE of the running machine, page-aligned W^X loads,RELATIVErelocations only), copies it into wired memory, applies the relocations for the range it will run at, checks the header, maps each page with its segment’s protection (text R X, rodata R, data R W) and calls the entry point. The hand-over comes throughModulePlatform(imageonly, the bridge ofbridges.toml), the writable copy’s memory fromkernel::vm::km::module_memory()(aModuleMemorypassed toloadas a value, N3.2), and the mapping ishal’s own (hal::pmap::module_window,module_enter). Threeunsafethere, each resting on that platform premise (load.rs’s module doc). The module runs from a 16 MiB window outside every uvm map on both machines, linked once bypmap_bootstrap: on amd64 in the kernel’s L4 slot after the image, sincekernel_map’s directory entries carry NX as OpenBSD’s do (sys/hal/arch/amd64/pmap/ window.rs); on arm64 after the boot loader’s last L2 entry of the image’s gigabyte, with PXN onkernel_map’s tables (sys/hal/arch/arm64/pmap/window.rs; docs/ZERO_UNSAFE.md decision 37). - The boot line.
abi_openbsd.ko: 331 syscalls, 17 native(2 until N3.5), orabi_openbsd.ko: not loaded, <why>; every system call takes the legacy path(decision 29: a medium without the file still boots).just smoke-abiandsmoke-efiboot(both archs) check the first;smoke-abirefuses the second and runs a pipeline through the module (abi-pipe-43). - The media.
cargo xtask image, efiboot-disk, base80.tgz (./abi_openbsd.ko) and install80.img and cd80.iso carry the file the build made (xtask::boot::default_abi_module); the name is one ISO 9660 keeps, so libsa’s cd9660 opens it (decision 38). So/abi_openbsd.kois a new file of an installed system’s/, from the base set, outside OpenBSD’s set lists; its boot(8) reads it there. - Since the review of N2.4a. The vtable’s type is
hal::api::KernelApi(withApiPrefixandAbiModuleHeader), soabidepends onhalalone and the.kolinks no kernel code;kernel::api::tablefills it.hal::module::loadtakes&'static KernelApi, checks its prefix, runs once, and releases the writable copy (ModulePlatform::release) before the entry point runs, so no writable alias of the module is left but the direct map. boot(8) reads the file straight into the room after the kernel, never into its heap.
Cargo features ↔ option(4)
| Feature | OpenBSD | Effect |
|---|---|---|
alloc |
— | extern crate alloc and the GlobalAlloc over malloc(9); default since M3 |
diagnostic |
option DIAGNOSTIC |
kassert! active |
debug |
option DEBUG |
kdassert! active |
kmemstats |
option KMEMSTATS |
malloc(9) statistics and per-type limits |
pool_debug |
option POOL_DEBUG |
pool_debug = 1 (poisoning, once subr_poison.c is here) |
ffs |
option FFS |
the fast file system (sys/ufs) and its vfsconflist[] entry; default |
ffs2 |
option FFS2 |
FFS2 (UFS2 dinodes, the 64 KB super-block) in ffs; default |
qemu |
— | QEMU-only exits (isa-debug-exit, semihosting), the boot self-tests, the TSC under TCG and the uptime went backwards check |
inet6 |
option INET6 |
IPv6: the #ifdef INET6 sites outside sys/netinet6 and inet6domain in domains[]; default, as in GENERIC. sys/netinet6 itself (and the IPv6 tables and usrreqs it names, route6_cache, tcp6_usrreqs, …) always compiles, like a library nothing reaches without the option, so the tree builds both ways |
multiprocessor |
option MULTIPROCESSOR |
a default since decision 40 (the user, 2026-10-17): the MULTIPROCESSOR kernel is the only one built (every recipe, image, smoke, abi_openbsd.ko and host test; hal, kernel and abi have no such option, their code is MULTIPROCESSOR only), and bsd’s lib.rs refuses a build without it. Until then it was off by default and smoke-up booted a kernel built without it; now smoke-up boots this one with -smp 1. The uniprocessor branches still in bsd’s inherited and adapted modules are unbuilt and go when N4 redesigns those files. Since M11e every just smoke recipe boots it, since 2026-10-07 with -smp 2 but for the smp4 group (“Parallel smokes”). M11a: MAXCPUS 255/256, the kernel lock and the spinning mutex (kern_lock.c), the Limine MP request and the application processors’ start (see “Deviations”); just smoke-mp boots it with -smp 4. M11d: NET_TASKQ 8 softnet queues, softnet_percpu keeps one per CPU; just smoke-net-mp runs both VMs of the two-VM smokes on it |
ntfs |
option NTFS |
the read-only NTFS file system (sys/ntfs) and its vfsconflist[] entry; default, but compiled only where the architecture’s GENERIC has it (amd64): see below |
fuse |
option FUSE |
FUSE (sys/miscfs/fuse), its vfsconflist[] entry, cdevsw[] 92 (/dev/fuse0) and fuseattach in pdevinit[]; default, as in GENERIC |
viocon |
amd64 GENERIC’s commented-out #viocon* at virtio? |
viocon(4) (M16d) in neither GENERIC: the feature stands for uncommenting the line, so amd64’s ioconf.rs gets the viocon* at virtio? entry and cdevsw[] 94 its entry points (NVIOCON 1, 0 without the feature as config(8) counts it: ENXIO); arm64’s GENERIC has no such line, so nothing changes there. dev/pv/viocon.rs compiles on every build, so just clippy and the host tests cover it either way; off by default |
More appear as they are needed (small_kernel, …), one per option(4).
An option that only some architectures’ GENERIC sets (M10d: option NTFS, in
arch/amd64/conf/GENERIC alone) cannot be a per-target cargo feature, so sys/build.rs plays
config(8)’s part: its ARCH_OPTIONS table names the feature, a cfg and the architectures,
and it emits the cfg (option_ntfs) when the feature is on and the target is one of them, or
a host build (so the host tests cover the code). The code is gated on the cfg, not on the
feature: the arm64 kernel has no NTFS, as OpenBSD’s arm64 GENERIC has none, and generic code
still never names an architecture.
A machine-independent driver that files.<arch> lists only for some architectures and whose
C uses those machines’ headers directly (M12: dev/fdt/pciecam.c, written against struct
machine_pci_chipset, struct bus_space, struct machine_intr_handle) is gated the same
way: sys/build.rs’s ARCH_MACHINE table emits cfg machine_pci_chipset for arm64
bare-metal builds (never for host, whose double has none of these headers), the driver’s
module is #[cfg(machine_pci_chipset)], and it reaches the machine items through
sys/machine/pci_chipset.rs, which re-exports them from crate::arch::current under the
same cfg. Generic code still never names an architecture; the alternative, a dozen contract
methods with fake amd64 and host implementations, would make the driver unlike its C and the
fakes untestable anyway. The price: such a driver has no host tests, like arch code.
M16e adds the x86 counterpart, cfg machine_x86 (amd64 only) with sys/machine/x86.rs, for
dev/acpi/acpidmar.c (the VT-d and AMD-Vi IOMMUs), which builds its own struct pic and
struct bus_dma_tag over the _bus_dma* functions, walks page tables through the direct map
with pmap_flush_cache, reads bios_memmap and builds an MSI pci_intr_handle_t. There the
driver’s items carry the cfg one by one, so the header part of the file (registers, table
entries, source ids, the device scope parser) still compiles on every target and keeps its
host tests. dev/acpi/acpicpu_x86.c (acpicpu(4), M16e) is written against it the same way: it
sets the machine’s cpu_idle_cycle_fcn and cpu_suspend_cycle_fcn, links itself into
struct cpu_info (ci_acpicpudev, ci_mwait) and idles with hlt, inb or
monitor/mwait; its _CST/_PSS parsing and its choice of an idle state are plain
functions with host tests. M16a’s ISA floppy stack is the third user: isadma(4)’s maps ask
for BUS_DMA_24BIT (its attachment carries the cfg, the rest of isadma.c is generic), and
fdc(4), fd(4) and fdlink.h are gated as whole modules, since nearly every item names
<machine/ioctl_fd.h>’s struct fd_type or the RTC’s NVRAM diskette byte; their register
headers (fdreg.h, nec765reg.h) compile everywhere with their host tests.
Dependencies
| Crate | Where | Why it is not OpenBSD code |
|---|---|---|
| (none for Limine) | sys/stand/limine.rs |
the limine crate was dropped: 0.6+ needs nightly (ptr_metadata), 0.5 is stable but frozen at base revision 3, which Limine has already tried to drop once. The protocol is about twenty #[repr(C)] structs; they are written from PROTOCOL.md |
bitflags |
sys/ |
typed flag sets for #define groups; a macro, no runtime |
| (none for lists and trees) | sys/sys/queue.rs, sys/sys/tree.rs |
intrusive-collections was dropped at M1: the OpenBSD macros are short, their semantics are the project’s to keep, and a crate’s policy changes would bind us as the limine crate’s did |
libsa, boot, efi |
sys/lib/libsa, sys/stand/boot, sys/stand/efi |
not crates.io crates: OpenBSD code ported here (M14), split into crates as OpenBSD builds libsa as a library and compiles sys/stand/boot into every boot program; the boot loaders (sys/arch/amd64/stand/efiboot, package efiboot-amd64) depend on them, the kernel does not. libsa dev-depends on bsd for the host tests that pin its header layouts to the kernel’s |
proptest |
dev-only | property tests for libkern |
serde, toml |
tools/xtask |
tracker parsing |
fatfs |
tools/xtask |
writes the FAT boot image; a host tool, not kernel code |
Not allowed: crates that replace OpenBSD code (x86_64, aarch64-cpu, spin, uart_16550,
fdt, linked_list_allocator, buddy_system_allocator). Porting that code is the project.
The userland build (M8)
The userland is OpenBSD’s own C, cross-compiled unmodified (the user’s M8 decision), not ported.
cargo xtask userland --arch A (tools/xtask/src/userland.rs, just userland) builds, into
target/userland/<arch>/: the /usr/include sysroot as include/Makefile installs it
(FILES, DIRS, LFILES/MFILES links, the kernel headers of LDIRS, <machine/*>, and of
the RDIRS only lib/libutil’s headers and lib/librpcsvc’s rpcgen output, which libc’s YP
code includes); lib/csu; libc.a (988 objects on amd64, 989 on arm64), libutil.a, libm.a
(259 objects on both; the programs’ -lm), libkvm.a (kvm_getprocs, kvm_getfiles, …
over sysctl(2) when no kernel image is named, the way ps, fstat and vmstat use it) and
libcompiler_rt.a; and
sbin/init, bin/ksh, bin/cat, bin/echo, bin/ls, usr.bin/uname, sbin/mount,
sbin/mount_ffs, libexec/getty, usr.bin/login, libexec/login_passwd, the network tools
sbin/ifconfig, sbin/ping (with its ping6 link, setuid root), sbin/route, sbin/pfctl and
sbin/ipsecctl, the diagnostic tools bin/ps, bin/df, usr.bin/fstat (and its fuser
link) and usr.bin/vmstat, the disk tools sbin/umount, sbin/newfs (with its mount_mfs link),
sbin/fsck, sbin/fsck_ffs, sbin/disklabel and sbin/fdisk (M10a), the quota tools
sbin/quotacheck, usr.sbin/quotaon (and quotaoff), usr.sbin/edquota, usr.sbin/repquota
and usr.bin/quota (over librpcsvc.a, whose sources rpcgen makes from its .x files) with
usr.bin/su, bin/mkdir and bin/chmod (with its chgrp and /sbin/chown links; M10b), and a few more as static PIE executables, the form
OpenBSD’s cc -static gives /bin and /sbin (rcrt0.o relocates the program itself; no
PT_INTERP).
A git worktree’s first build starts from the main checkout’s (the user’s decision of
2026-10-09: the OpenBSD sources and the toolchain are pinned, so compiling them again in every
agent’s worktree is wasted time). When target/userland/<arch> (or, for comp,
target/comp) is missing in a worktree and the main checkout has it, userland/seed.rs
copies it with cp -Rcp: an APFS clone, instant and free of space until a file changes, that
keeps the modification times the up-to-date checks compare. The build writes absolute paths
into its records, so the copy’s rule stamps (*.cmd), dependency files (*.d), archive member
lists, include manifests and absolute symbolic links are rebased from the main checkout’s path
to the worktree’s, except paths into reference/, which a worktree shares with the main
checkout. The build then runs as usual and redoes only what differs. Measured on 2026-10-09:
a fresh worktree’s cargo xtask userland --arch amd64 took 23 s, every OpenBSD object (0
rebuilt), only the three tools/ programs (checked out after the main checkout’s objects)
recompiled. A worktree whose own changes touch the userland rebuilds what they touch; a copy
that cannot be made or rebased is removed and the build starts from nothing, as before.
Nothing is listed by hand. tools/xtask/src/bsdmake.rs evaluates the subset of make(1) the
Makefiles use (assignments, lazy expansion, the :L :M :N :R :S :old=new modifiers, .if,
.for, .include, .PATH, explicit rules) and fails on anything else. SRCS, OBJS, .PATH
and CFLAGS come from it; explicit rules are run through /bin/sh as make would, so the
system-call stubs are made exactly as lib/libc/sys/Makefile.inc makes them (GENERATE.*
piped into FINISH.*), and so are the generated hash helpers and the rpcsvc headers.
share/mk is not in the reference clone: sys.mk and bsd.own.mk are stood in for by
predefined variables (CFLAGS?= -O2 -pipe ${DEBUG}, COMPILE.c, YP=yes, STATIC=-static,
…), bsd.prog.mk/bsd.lib.mk by their variable effects (../Makefile.inc, COPTS) and the
implicit .c.o/.S.o rules; CDIAGFLAGS (warnings only) is empty. Apple clang’s OpenBSD
target supplies the rest of OpenBSD’s defaults by itself: PIE, -fstack-protector-strong,
IBT (-fcf-protection=branch) on amd64, BTI and return-address signing on arm64, emulated TLS.
Workarounds, each printed by the build (flags only; no source is edited):
-fret-clean(amd64 libc) is an OpenBSD-local clang option Apple clang rejects; it is dropped.rpcgen,makefs,pwd_mkdbandyaccare built for the Mac with-D'pledge(p,e)=0'(macOS has nopledge(2))..ysources (sbin/pfctl/parse.y,sbin/ipsecctl/parse.y) followbsd.sys.mk’s.y.crule:${YACC.y} parse.y(YACC.yis${YACC} -d ${YFLAGS}), thenmv y.tab.c parse.c, run in the program’s object directory.YACCis OpenBSD’s ownusr.bin/yacc, built for the Mac the first time a.yis met (host/bin/yacc, named by its absolute path, so never macOS’s bison-based/usr/bin/yacc). Its only shim is a force-includedreallocarray(3), which macOS’s libc lacks.usr.bin/uname,usr.bin/id,usr.bin/login,usr.bin/fstat,usr.bin/vmstat,libexec/gettyandlibexec/login_passwdare linked-static(their Makefiles are dynamic, as/usr/binand/usr/libexecare on OpenBSD), as the install media’s crunched programs are: the ramdisk’s programs all stay static;ld.soand the shared libraries (M14, “Shared libraries and ld.so”) serve whatcclinks.- M10e’s
usr.sbin/portmapandusr.bin/showmountare linked-staticfor the same reason, and so aresbin/mountdandsbin/nfsd, whose Makefiles end withLDSTATIC=(OpenBSD ships them dynamic). No source or other flag differs. The ramdisk gets what they need from a base install: the_portmapuser and group (28:28, frometc/),/etc/rpc(the portmapper, nfs, mountd and rquotad lines ofetc/rpc) and/var/db(mountd’smountdtab); no/etc/exports. - A program whose Makefile sets
BINOWN,BINGRPorBINMODE(login_passwd: root:auth, setuid 4555, in/usr/libexec/auth, wherelib/libc/gen/auth_subr.c’s_PATH_AUTHPROGlooks for BSD Auth styles) gets them in the image (below). kshis built by its own Makefile (-lcurses, no-DSMALL) since M14c, as OpenBSD’s base set has it: it setsKSH_VERSION, whichrc.subrwants. There is no terminfo database yet (share/termtypesneeds tic(1)):setuptermfails quietly, and only emacs mode’sclear-screenneeds it. Until M14c it was the install media’s-DSMALLbuild; that one is nowdistrib/special/ksh, built for the miniroot only (below).- macOS file systems ignore case: libc’s
_exit.ostub andstdlib/_Exit.oare built in separate directories (both are archive members). libcompiler_rt.ais built fromgnu/lib/libcompiler_rtovergnu/llvm/compiler-rt(in the clone since 2026-10-03, Apache-2.0 WITH LLVM-exception) and linked as-lcompiler_rt -lc -lcompiler_rt, as OpenBSD’s clang driver does; arm64 needs its quad-float helpers (__multf3). The stand-inbsd.own.mksetsBUILD_CLANG=yes, as the real one does on amd64 and arm64.
M9+ adds LibreSSL (lib/libcrypto, lib/libssl, lib/libtls), lib/libcurses (ncurses)
and lib/libedit (LIBRARIES in userland.rs, the code in userland/libraries.rs), for
usr.bin/ftp and usr.bin/nc (static, like login). Nothing new is listed by hand either:
- Generated sources are made by running the Makefiles’ own rules for
BUILDFIRST, whichbsd.lib.mkmakes before any object, each after the sources a rule of its own makes (make_target, make’s recursion). On amd64 that runs libcrypto’s perlasm: each${f}.Srule ofarch/amd64/Makefile.inc(a two-variable.for dir f in ${SSLASM}, whichbsdmake.rssupports) runs/usr/bin/perl ./asm/${f}.pl openbsd, the Mac’s perl, into the object directory; arm64 has only its.Ssources.objects.plandobj_dat.plmakeobj_mac.handobj_dat.hthe same way. libcurses’s rules run itsMK*.sh/MK*.awkscripts with the Mac’ssh,awk(the one-true-awk OpenBSD has) andsort, and buildmake_keysandmake_hashwith${HOSTCC}(the Mac’s clang, whichsys.mk’sHOSTCCnames) and run them; libedit’s run itsmakelistscript. - Headers:
include/Makefile’sRDIRSentries for these libraries are installed by running each library’s ownincludesrule, with aninstall(1)stand-in in$PATHthat records what it is asked to install (and acmp(1)that always says “different”); xtask then copies the recorded files. So<openssl/*.h>gets libcrypto’s generatedobj_mac.h, and libcurses’scurses.hbecomes<ncurses.h>, as their Makefiles say. - LibreSSL’s Makefiles add
-Werror. Where they do,-Wno-pointer-signis added (WERROR_DEFAULTS): OpenBSD’s clang does not warn about mixingchar *andunsigned char *by default, Apple clang does, and libcrypto mixes them. share/mk’sbsd.subdir.mk(recursion intoSUBDIR, themandirectories) is stood in for by nothing.
Every OpenBSD file compiled or included is classified by licence into licences.txt;
LibreSSL’s two licences are named OpenSSL and SSLeay there (accepted by the user on
2026-10-03), not counted as BSD-4-Clause.
The ramdisk image
The ffs image the kernel boots from (target/userland/<arch>/ramdisk.ffs, the rd(4) root) is
made by OpenBSD’s own makefs(8), built for the Mac from usr.sbin/makefs (in the clone since
2026-10-03), not by a file system writer of our own (decided by the user on 2026-10-03). The
on-disk format is then OpenBSD’s by construction. makefs runs as OpenBSD’s distrib/ runs it
for its ramdisks (-t ffs -o disklabel=rdroot,minfree=0,density=4096). The rdroot entry
is read by OpenBSD’s own getdiskbyname (lib/libc/gen/disklabel.c, built in) from a disktab
xtask writes (target/userland/<arch>/host/disktab, OpenBSD’s /etc/disktab is not in the
clone): one track of one cylinder spanning the image, partition a FFS at offset 0 with
4096/512 blocks/fragments. makefs’s own rdroot=1 label is not used: it leaves d_nsectors
0, which checkdisklabel rejects. The result is FFS1 in a and the label in sector 1; the
size is twice the contents in whole MiB (at least 2 MiB), the timestamps fixed (-T).
Its tree is root/ plus /etc, /dev and the directories below.
M10c adds /root/images (images.rs): a FAT12 and an ISO 9660 image made by the same makefs
(-t msdos, -t cd9660 -o rockridge) and a UDF image made by macOS’s own hdiutil makehybrid
-udf (OpenBSD has no UDF writer; nothing is installed), for vnd(4) to attach in smoke-fs.
Disk nodes follow MAKEDEV: UNITMULT 64 minors per unit (MAXPARTITIONSUNIT).
M10d adds /root/images/ntfs.img on amd64 only (ntfs is only in amd64’s kernel): a 4 MiB
NTFS 3.1 volume made by a generator of our own, tools/xtask/src/ntfsgen.rs (ISC, written
from the public description of the format, no code of ntfs-3g or any other implementation;
cargo xtask ntfs-image OUT [--check] makes it alone). Why our own: OpenBSD has no NTFS
writer, and the usual one, ntfs-3g’s mkntfs, does not build on macOS (decided by the user,
2026-10-04). It holds the 16 system files ($MFT .. $Extend, a full $UpCase, a $LogFile of
0xff bytes, which readers take as empty and clean), a root that is a large index (one INDX
block), a resident m10d-ntfs.txt and a non-resident m10d-ntfs-big.txt. So that a
generator bug shared with our kernel cannot pass unseen, every image is mounted read-only by
an independent reader, macOS’s own NTFS driver (ntfs.fs, an FSKit module on macOS 26:
diskutil mount readOnly over an hdiutil attach -nomount raw device, no root), which must
list exactly the two files and read their bytes back (docs/SETUP.md, “NTFS check”).
/etc is our own minimal set (OpenBSD’s etc/ is not in the clone), text in ramdisk.rs:
motd, shells, fstab (/dev/rd0a / ffs rw 1 1, which mount -uw / needs), ttys (a
getty std.9600 on tty00, console off), gettytab, login.conf (a default class with
auth=passwd, a daemon class), group, master.passwd (root, daemon, nobody; only root
has a password, emibsd, docs/SETUP.md) and rc, a minimal script that runs mount -uw /,
creates utmp, wtmp, lastlog and failedlogin and prints rc: multi-user. pwd.db,
spwd.db and passwd are made by OpenBSD’s own pwd_mkdb(8) (-p -d <staging>/etc), built
for the Mac like makefs from usr.sbin/pwd_mkdb (in the clone since 2026-10-03), over OpenBSD’s
own db(3) (lib/libc/db, hash and btree) and pw_scan (lib/libutil/passwd.c), not macOS’s
dbopen, so the databases have OpenBSD’s format by construction. The hash is OpenBSD’s
bcrypt.c with blowfish.c, in a small helper that replaces arc4random_buf with a fixed
salt before including the unmodified source, so the image is reproducible. For the network
clients (M9+): resolv.conf (nameserver 10.0.2.3, QEMU’s user-network DNS), hosts
(localhost, and emibsd-host for 10.0.2.2, QEMU’s alias of the Mac, where
smoke --https-server runs test servers), and /etc/ssl: cert.pem, LibreSSL’s CA bundle
(lib/libcrypto/cert.pem, as its distribution target installs it, 0444), and
emibsd-test-ca.pem, the certificate of the test CA userland/testca.rs makes once with the
Mac’s openssl (docs/SETUP.md, “The test CA”). With LibreSSL, ftp and nc the image is about
22 MiB, so the boot image (boot.rs, IMAGE_SECTORS) is 128 MiB. The directories are
/home, /mnt, /root (0700), /tmp and /var/tmp (1777), /var/{log,mail,run}. /dev has
console, tty, mem, kmem, null, zero, klog, tty00 (the console on both
architectures: com0 on amd64, and on arm64 pluart0 takes com’s slot, major 8, in
pluartcnattach), rd0{a,b,c} (block 17), rrd0{a,b,c} (47), sd{0,1}{a..p} (block 4) and rsd{0,1}{a..p}
(character 13; minor unit * 16 + partition, 0640 root:operator, as MAKEDEV’s dodisk;
the image has sd0 and sd1), fd/0..63 and
stdin/stdout/stderr; the majors and minors, with their conf.c lines, are in
DEVICES’s comment.
makefs is written for OpenBSD only; the Mac build takes host shims, all in
tools/xtask/src/userland/ramdisk.rs and none in the sources: a force-included header
(daddr_t is 64-bit, st_*tim, OpenBSD’s MAXBSIZE, no-op pledge/unveil,
srandom_deterministic as srandom), OpenBSD headers macOS lacks taken from the clone
(ufs/, msdosfs/, sys/disklabel.h, machine/disklabel.h, sys/uuid.h with uuid_t
renamed), a sys/endian.h over <libkern/OSByteOrder.h>, scan_scaled from
lib/libutil/fmt_scaled.c, cgetent pointed at $EMIBSD_DISKTAB, and an lstat wrapper for device nodes: macOS lets only root
mknod and OpenBSD’s makefs has no mtree spec, so a staging file holding one
emibsd-makefs-device c <major> <minor> <mode> line is reported to makefs as that device (with
OpenBSD’s makedev() encoding). The same wrapper makes every file root:wheel and applies a
table ($EMIBSD_OWNERS, mode uid gid path; paths relative to $EMIBSD_STAGING) for the
exceptions (login_passwd setuid, spwd.db root:_shadow, master.passwd 0600, /tmp
sticky, /dev modes); makefs would otherwise take owner and group from the host files, the
building user’s. pwd_mkdb’s shims (passwd.rs) are likewise a force-included header
(__BSD_VISIBLE, OpenBSD’s <pwd.h> before macOS’s, __dead, libc’s DEF_WEAK/PROTO_*
macros as nothing, explicit_bzero, a check that the host is little-endian as the db(3) files
are made in host order), OpenBSD’s <pwd.h>, <util.h>, <mpool.h> and hidden/db.h from the
clone in a directory searched first, and a getgrnam("_shadow") that answers with the building
user’s group (macOS has no such group, and pwd_mkdb insists on one).
EmiBSD’s own test programs (M10f) are the one thing in the userland build that is not OpenBSD’s
C. OWN_PROGRAMS in userland.rs lists directories of this repository (tools/<name>/, paths
relative to the workspace root), each with an OpenBSD-style Makefile (PROG, BINDIR,
LDSTATIC= -static, NOMAN, .include <bsd.prog.mk>) and an ISC-licensed source naming the
EmiBSD authors. They are built after PROGRAMS by the same build_prog, with the same
clang, sysroot, libc and static-PIE link, and installed stripped under root/ where BINDIR
says; only make_for differs, taking the Makefile’s directory from the workspace instead of
the reference tree. The licence report lists reference-tree files only, so they do not appear
in it. First user: tools/sr6create (/usr/sbin/sr6create), which creates a softraid(4) RAID 6
volume through BIOCCREATERAID; OpenBSD’s bioctl(8) refuses -c 6 (“unsupported RAID level”)
although softraid_raid6.c is in the kernel, and the userland is compiled unmodified, so
the test program does what bioctl’s bio_createraid() does for a non-crypto level, for level 6.
Second user (M10d): tools/fusehello (/usr/sbin/fusehello), a read-only FUSE file system
with a fixed tree (/hello.txt, /sub/deep.txt) over OpenBSD’s own libfuse (fuse_main with
getattr, readdir, open, read and statfs), for smoke-fuse: OpenBSD has no FUSE file
system of its own in base, only the library. Its Makefile adds -I${DESTDIR}/usr/include/fuse
and -lfuse, what libfuse’s fuse.pc gives its users.
M10d adds lib/libfuse to LIBRARIES. Its includes rule makes /usr/include/fuse with
install -d before installing its headers there; the install(1) stand-in records a -d dir
line for that, and xtask makes the directory. Its sources include the kernel’s
<sys/fusebuf.h> from the sysroot. ext2fs’s newfs_ext2fs(8), fsck_ext2fs(8) and
mount_ext2fs(8) and mount_ntfs(8) are in PROGRAMS; mount_ntfs’s Makefile sets NOPROG=
unless MACHINE is alpha, amd64 or i386, and build_prog then builds nothing, as
bsd.prog.mk does, so arm64’s ramdisk has no mount_ntfs. The ramdisk has /dev/fuse0
(character 92, minor 0, 0600, as MAKEDEV makes it), the one node libfuse opens.
smoke-ext2fs checks the guest’s ext2 file system twice: with OpenBSD’s fsck_ext2fs(8) in
the guest and with e2fsprogs on the Mac (cargo xtask e2fsck, tools/xtask/src/e2fs.rs;
docs/SETUP.md, “e2fsprogs”), an independent implementation, so a bug shared by our kernel and
OpenBSD’s tools cannot pass unseen. xtask finds partition a as readdoslabel does (the MBR’s
0xA6 partition, its label in sector 1) and hands e2fsck and debugfs image?offset=BYTES.
The comp set (M14)
The compiler is OpenBSD’s: clang and lld from gnu/llvm (LLVM 22.1.6, Apache-2.0 WITH
LLVM-exception, compiled unmodified, not ported; the user’s decision of 2026-10-04), built by
OpenBSD’s own build glue (gnu/usr.bin/clang, gnu/lib/libcxx, gnu/lib/libcxxabi,
gnu/lib/libclang_rt, lib/librthread) through bsdmake.rs, with the userland’s toolchain.
cargo xtask comp --arch A [--jobs N] (tools/xtask/src/userland/comp.rs, just comp)
writes target/comp/:
host/: the build toolsgnu/usr.bin/clang/Makefilebuilds and runs during the build (llvm-min-tblgen,llvm-tblgen,clang-tblgen, withlibLLVMSupport,libLLVMTableGenandlibclangSupport), built for macOS from the same Makefiles, once for both architectures. The target trees reach them where the generation rules look (${.OBJDIR}/../../../llvm-tblgen/llvm-tblgen) through symbolic links.<arch>/sysroot:userland’s sysroot (copied;just userlandmust have run), plus/usr/include/c++/v1by libc++’s and libc++abi’s ownincludesrules (with OpenBSD’s__config_site),libpthread.a,libc++abi.a(with the libunwind sources OpenBSD builds into it) andlibc++.a.<arch>/obj:gnu/usr.bin/clang’sSUBDIRfor thatMACHINE(Makefile.arch: the X86 or AArch64 backend, plus AMDGPU, as OpenBSD builds): everyinclude/*directory’s generation rules (tblgen,llvm-config.h, the.deffiles), then the libraries, all compiled in one parallel run (libLLVM.ais all of LLVM, aslibLLVM/Makefileincludes everylibLLVM*/Makefile), then the programs, linked withld.lldas OpenBSD’sc++ -staticwould (static PIE:rcrt0.o,-lc++ -lc++abi -lpthread -lm,-lcompiler_rt -lc -lcompiler_rt).<arch>/root: the staging root. The programs at their Makefiles’BINDIRwith theirLINKS(clangascc,c++,cpp,clang++,clang-cpp;ld.lldasld;arasranlib;llvm-objcopyasstrip; …), stripped; clang’s resource headers (/usr/lib/clang/22/include, byinclude/clang/intrin’s owninstallrule);libclang_rt.profile.aandlibclang_rt.ubsan_minimal.ain/usr/lib/clang/22/lib; and the sysroot’s/usr/includeand/usr/lib, whatccneeds to compile and link there.comp.ffsis that root as an ffs image (OpenBSD’s makefs asuserlandbuilds it; owners and modes as the Makefiles say), the diskjust smoke-ccmounts.<arch>/licences.txt: the licence report, as foruserland.
Incremental: objects are remade only when a source, a header or the command changed (the
.d files and .cmd stamps of userland), archives only when an object is newer, links
only when an input is; installed headers keep their mtime when unchanged. just comp is not
part of ci: a first build compiles about 2,850 C++ files per
architecture (measured: 30 minutes for arm64 with 5 jobs, while other builds kept the Mac at
a load average near 30), plus about 200 files of build tools for macOS, once; a run with
nothing changed takes 15 to 20 seconds (most of it checking the objects’ .d files and the
licence report). Its job count defaults to about half the Mac’s cores (COMP_JOBS):
other builds share the machine.
What of OpenBSD’s comp set (distrib/sets/lists/comp/{mi,md.<arch>,clang.<arch>}) and of
the base set’s compiler is built: clang (and links), ld.lld (ld), clang-scan-deps,
llvm-config, llvm-objcopy (strip), llvm-objdump, llvm-readobj (llvm-readelf),
llvm-symbolizer (llvm-addr2line), llvm-profdata, llvm-cov, ar (ranlib), libc++,
libc++abi, libpthread, libclang_rt.*.a and the headers. Not built: lldb and lldb-server
(BUILD_LLDB=no: the debugger needs ptrace(2) work and a host libLLVM for
lldb-tblgen; the exit criterion is cc hello.c); the profiled *_p.a libraries (none are
built anywhere here); /usr/include/llvm (libLLVM’s includes), binutils’ as, gdb,
ldscripts (md.amd64; GPL, not in the clone); the mi list’s other libraries and tools
that userland builds or not, as its lists say.
Workarounds, each printed by the build (flags only; no source is edited):
-fno-ret-protector(gnu/usr.bin/clang/Makefile.inc) is OpenBSD-local; dropped (UNSUPPORTED_FLAGS).- macOS’s file system ignores case, OpenBSD’s does not. An
-Idirectory holding a header whose name differs from a libc or libc++ header only in case (llvm/Support/Errno.h,Locale.h,llvm/BinaryFormat/ELF.h) becomes-iquote:<errno.h>would find it. Where two-Idirectories of one compile hold such a pair (JITLink’sx86.hand the X86 backend’sX86.hinlibLLVM), the headerCASE_COLLISIONSnames is hidden behind a directory of symbolic links to the rest; an unnamed collision stops the build. - The build tools are built against macOS with OpenBSD’s
include/llvm/Config/config.h:-DHAVE_MACH_MACH_H=1(itsENABLE_CRASH_OVERRIDEScode on macOS needs<mach/mach.h>) and a force-included header mappingpthread_set_name_np/pthread_get_name_npto macOS’spthread_setname_np/pthread_getname_np(HOST_FLAGS,HOST_COMPAT_H). bsdmake.rs: a whole-line comment ends at its newline even after a backslash, as in OpenBSD’s make (libclangASTMatchers/Makefilecomments out a continued line); the<bsd.lib.mk>stand-in includes<bsd.own.mk>first, as the real one does (the glue’sMakefile.incsetsCXX=clang++unlessCOMPILER_VERSIONis clang). A target compile whoseCCorCXXis not the cross compiler stops the build.
Deviations: the programs are static PIE executables, like the rest of the userland’s;
OpenBSD links them dynamically and ships libLLVM as a shared
library (NOLIBSTATIC), here libLLVM.a is linked into each program.
just smoke-cc (not in smokes: it needs just comp) boots the ramdisk kernel with
comp.ffs as sd0, mounts it on /mnt, runs /mnt/usr/bin/clang --version (OpenBSD
clang version 22.1.6, Target: amd64-unknown-openbsd8.0 or aarch64-unknown-openbsd8.0),
compiles a hello world with cc --sysroot=/mnt -static and runs it. On the way the kernel
reports one gap it works around: unported: amap_copy: chunking. Then the plain dynamic link
of an installed system: chroot /mnt /usr/bin/cc -o /tmp/d /tmp/h.c (a dynamic PIE linked
against libc.so.M.m with -dynamic-linker /usr/libexec/ld.so), run in the chroot, and
ldd /mnt/tmp/d from the ramdisk, whose own ld.so and libc.so serve it.
Shared libraries and ld.so (M14)
A plain cc hello.c on OpenBSD makes a dynamic PIE, so the system needs the run-time
link-editor and the shared libc. userland builds them (tools/xtask/src/userland/shlib.rs)
after the static libraries, as OpenBSD’s Makefiles say:
libc.so.104.0,libutil.so.22.0,libm.so.10.1,libpthread.so.28.1(lib/librthread, also built static, ascompdoes):bsd.lib.mk’s${FULLSHLIBNAME}. Every object again as a.soobject (.c.soand.S.so:${PICFLAG} -DPIC,-DSOLIB; libc’s system-call stubs by its own${SASM}rules), linked-shared -soname lib${LIB}.so.M.mwith the Makefile’sLDADD(libc:-nostdlib -lcompiler_rt -Wl,-zinitfirst,-znow; libpthread:-Wl,-znodelete) and itsVERSION_SCRIPT(libc’sSymbols.mapmade by its rule from theSymbols.listfiles). The version isshlib_version’s. Installed in the sysroot and the staging root at/usr/lib, root:bin 444.libcompiler_rtstays static, as on OpenBSD./usr/libexec/ld.so: its sources, libc’s string functions through itsVPATH, thedl_<syscall>.ostubs of its.forloop, linked by its rule (-e _dl_start, itsSymbols.mapandld.script,--shared -Bsymbolic --no-undefined) and checked by itsCHECK_LDSO(onlyR_X86_64_RELATIVE/R_AARCH64_RELATIVEdynamic relocations; read withllvm-objdump --dynamic-relocwhere the Makefile runsreadelf). root:bin 444.ldconfig(8),ldd(1)andchroot(8)joinPROGRAMS, static like the rest of the ramdisk. No ramdisk program is dynamic: OpenBSD’s install media are static too; dynamic programs are whatccmakes.compcopies/usr/libexecof the sysroot into its root with/usr/lib, socomp.ffscarriesld.soand the shared libraries.
Deviations, each written in shlib.rs: the host’s LLD 17 is given two defaults OpenBSD’s lld
has built in (--undefined-version: libc’s Symbols.list names symbols that only other
architectures define; --ignore-function-address-equality); ld.so is built with
STACK_PROTECTOR= (its __guard_local and __stack_smash_handler stubs), the branch of
the architectures OpenBSD’s clang has no retguard for, because the host’s clang has no
retguard and emits the stack protector on amd64 and arm64 too; objects are linked in
Makefile order, not shuffled (sort -R), and no relink kits (/usr/share/relink,
ld.so.a) are made; test-ld.so is not built or run on the Mac (as in OpenBSD’s cross
builds), smoke-cc is that test. The kernel side is uvm_mmap.c’s file half
(uvm_mmapfile over uvn_attach; device mappings other than /dev/zero report the
unported udv_attach). __thrsleep/__thrwakeup (kern_synch.c) are still sys_nosys:
libc’s FILE locks and libpthread’s _rthread_dl_lock call them only when two threads
contend.
The install media (M14c)
OpenBSD’s installer is run unmodified: distrib/miniroot/install.sub, the arch’s install.md,
dot.profile, over a bsd.rd and signed sets that this build makes. Commands (all in
tools/xtask/src/install.rs; recipes in the justfile, “the install media”):
cargo xtask miniroot|sets|install-media --arch A,just install-media-<arch>: the miniroot,bsd.rdand the sets intarget/install/<arch>. Needsjust userlandandjust comp.just smoke-install-<arch>(cargo xtask install,--check-onlyrepeats only its check boot): the install run; not insmokes(a few minutes, below).just smoke-install-boot-<arch>(cargo xtask install-boot): the disk the install made, booted through the loader installboot(8) put on it; not insmokeseither (it needs the disksmoke-install-<arch>made).
bsd.rd. OpenBSD’s RAMDISK_CD kernel is a smaller GENERIC with option MINIROOTSIZE
and rdsetroot(8) patches the file system into it. Here the kernel is our GENERIC MP kernel
with cargo feature miniroot (sys/dev/rd.rs): rd_root_image[ROOTBYTES] and rd_root_size
are compiled in under the C names, ROOTBYTES is $EMIBSD_MINIROOTSIZE sectors (the
justfile’s miniroot_sectors, 65536 = 32 MiB, the miniroot is padded to it so one kernel
serves every run), and rdattach roots on rd0a itself. cargo xtask rdsetroot
(rdsetroot.rs) is usr.sbin/rdsetroot over our ELF; bsd.rd is that kernel with its debug
information stripped. Deviation: the Limine module ramdisk.ffs path stays and wins. The
kernel is built in target/bsdrd (in smoke-build only).
The miniroot (userland/miniroot.rs) is built from the arch’s list
(distrib/amd64/ramdisk_cd/list, distrib/arm64/ramdisk/list) with list2sh.awk’s meaning
(COPY SCRIPT LINK SYMLINK MKDIR REMOVE SPECIAL TZ TERMCAP), the directories of
mtree.conf, and install.sub, install.md, dot.profile, group, master.passwd
(pwd_mkdb), protocols, services, /dev/MAKEDEV. Deviations: no crunchgen instbin
(each name is the static program userland built, from the normal Makefiles; distrib/special’s
-DSMALL -Oz variants are not used, except init, which must be -DDEFAULT_STATE=single_user
without DEBUGSHELL/SECURE, more, and doas: the media’s own is a root-only doas -u user
command with no doas.conf, which install.sub’s unpriv runs ftp and signify through);
/dev from the smoke ramdisk’s node table plus diskmap (MAKEDEV ramdisk makes it;
opendev(3) opens a disk by its DUID through it, and install.sub names every partition by
DUID); usr/mdec/mbr a 512-byte
stub; no firmware, termcap or Raspberry Pi files; /etc/signify/openbsd-80-base.pub is a test
key (userland/signify.rs: OpenBSD’s signify built for macOS with shims, key pair made once
in target/install/test-signify), never OpenBSD’s. The entries not satisfied go to
miniroot.txt.
The sets (userland/sets.rs): base80.tgz, comp80.tgz, bsd (our MP kernel as built, debug
info kept; only bsd.rd is stripped), bsd.mp (the same kernel: OpenBSD ships a
uniprocessor bsd beside it, and install.sub insists on bsd.mp when hw.ncpufound is
above 1, installs it as /bsd and keeps bsd as /bsd.sp), bsd.rd, INSTALL.<arch> (host
m4 on distrib/notes/INSTALL), SHA256 and SHA256.sig. A set is its list
(distrib/sets/lists) applied to the tree this build has: userland’s and comp’s roots,
etc/mtree/4.4BSD.dist’s directories, the etc distribution of etc/Makefile (as a table,
with var/sysmerge/etc.tgz that install.sub extracts after base), zoneinfo from OpenBSD’s
zic built for macOS, usr/mdec (efiboot; BOOTIA32.EFI is a zero sector because installboot
copies it; biosboot is a placeholder i386 ELF whose one segment is a sector of text saying
so, because installboot loads the BIOS boot record (md_loadboot) before it looks at the
disk and never writes it on a GPT disk with an EFI system partition, while the BIOS boot
programs are not built (stand/biosboot’s Makefile wants GNU as); BOOTX64.EFI and
BOOTAA64.EFI come from target/efiboot/<arch>, and the set build fails without them). Archives are
written by xtask (ustar with OpenBSD’s owner names and ids, hard links) and the host’s gzip.
What the lists name and the build lacks is printed and written to MISSING-<set>.txt with a
reason: perl, cvs, texinfo and GNU as, gdb, ld.bfd and the binutils tools are GPL and not
in the clone (the user’s decision of 2026-10-05), and man pages, terminfo, locale, usr/share/misc,
firmware, httpd/nsd/unbound files, the BIOS boot programs and the programs and libraries
userland does not build are not built yet. Built files in no list are EXTRA-<set>.txt
(our test programs). Every program is static, as in the rest of the userland.
The install run (install.rs): a fresh 3 GiB disk is sd0 (the install media’s own disk
is sd1; on arm64 virt the virtio-mmio slots are probed top-down, so the disk QEMU is
given last is sd0 there too). bsd.rd is booted by our efiboot (BOOTX64.EFI,
BOOTAA64.EFI) from a disk laid out as OpenBSD’s miniroot80.img (efiboot-disk: the
OpenBSD partition’s FFS holding /bsd, which is bsd.rd, and the EFI system partition; the
run answers boot> ). Its ramdisk holds
/auto_install.conf (autoinstall(8): .profile starts the install after five seconds when
that file exists, the way OpenBSD does when no DHCP server names a response file; static
10.0.2.15, so dhcpleased is not needed), the sets come over HTTP from this machine
(10.0.2.2:PORT, the server of diff-openbsd), the disk is set up by the arch’s
md_prep_fdisk: on amd64 a GPT with an EFI system partition (fdisk -gy, answer G, so
installboot(8) uses its EFI path and copies BOOTX64.EFI), on arm64 an MBR whose FAT
partition (fdisk -iy -b 32768@32768:C, answer whole) installboot -p formats and
installboot fills with BOOTAA64.EFI and startup.nsh; both are then partitioned by
disklabel -T (a template: / 2400M first, so it is a, and 64M of swap), newfs, the
sets (-all bsd bsd.mp base* comp*) verified by signify. The run ends at CONGRATULATIONS!
and the installer’s reboot (boot(9)’s vfs_shutdown: “syncing disks… done”); a second
boot of the plain bsd.rd makes the disk’s nodes (MAKEDEV sd0), prints its label, mounts
it read-only and lists /bsd, /bsd.sp, /usr/bin/cc, /etc/rc, /usr/libexec/ld.so,
amd64’s /etc/boot.conf (arm64’s installer writes none: its console is already the serial
one), the EFI system partition (efi/BOOT/BOOTX64.EFI or bootaa64.efi, and the copy in
efi/openbsd/) and runs fsck_ffs -n (five phases, clean).
install-boot copies the installed disk into a fresh VM’s boot image: the firmware (OVMF,
EDK2 AArch64) starts \EFI\BOOT\BOOTX64.EFI (BOOTAA64.EFI) from its ESP, efiboot reads
amd64’s boot.conf the installer wrote (stty com0 115200, set tty com0), loads /bsd
and boots it; the kernel finds its root by
boot(8)’s DUID, /etc/rc from the base set runs (fsck, pf, the network, rc.firsttime) to
login:, root logs in and cc hello.c && ./a.out prints hello from cc 42 (the source is
written with ksh’s print -r: base has no printf(1) yet).
On an ACPI arm64 machine (just smoke-install-arm64-acpi, smoke-install-boot-arm64-acpi:
both runs with --acpi, QEMU virt,acpi=on, the disks and vio0 on PCI) the same media install
and boot; there the media’s disk is probed first and is sd0, so the answers name the fresh
disk sd1 (install.rs, target_sd, checked against the kernel’s sd1: 3072MB line), with
the run’s disks in target/smoke/smoke-install-acpi and its logs in target/install/arm64/acpi.
Status (2026-10-05): both architectures pass both (smoke-install-amd64 about 3 minutes with
the media already made, the installer itself 2.5; arm64 about 4.5, the installer 4;
smoke-install-boot-<arch> under a minute each). What the installed system lacks shows in
its /etc/rc run: the programs userland does not build (sort, head, cut, mktemp,
find, install, printf, swapctl, ttyflags, kvm_mkdb, dev_mkdb, savecore,
ssh-keygen, openssl, mail, …), so the rc.d daemons (syslogd, pflogd, ntpd,
smtpd, sndiod, cron) say (failed): rc.subr accepts base’s /bin/ksh, the full build
(install-boot fails on wrong shell); /dev/random (random was not a driver yet; rnd(4) and /dev/random came with M16d);
/dev/ttyC* (wscons is not ported); installboot cannot add its UEFI boot entry (/dev/efi,
efi(4), is not ported), so the firmware boots the ESP’s fallback \EFI\BOOT\BOOTX64.EFI
(BOOTAA64.EFI). On arm64 the installed kernel has rd0 with no image; it once took the
root disk’s DUID from a stale buffer and fsck failed (sys/dev/rd.rs, deviations: an
empty rd0 reads no label now).
The release images (M16g)
OpenBSD ships its installer on two more images, which distrib/ makes from the install
media: install80.img, a disk image for a USB stick with bsd.rd, efiboot and the sets on it
(distrib/amd64/iso/Makefile, distrib/arm64/iso/Makefile), and amd64’s boot-only
cd80.iso (distrib/amd64/ramdisk_cd/Makefile). xtask makes both from what
install-media made (tools/xtask/src/distrib.rs; recipes install-img-<arch>,
cd-iso-amd64), into target/install/<arch>:
- install80.img is laid out as the Makefile lays it out, sector for sector (checked against
the snapshot’s own image,
target/openbsd/<arch>/install80.img, and in the module’s tests): the MBRfdisk -i -bwrites (amd64: an EFI system partition of 960 sectors at 64, the OpenBSD partition from 1024 toFSSIZE=1638400, active, in an image ofFSSIZE+960sectors; arm64: a FAT partition of type0x0Cat 32768, 16384 sectors, active, then the OpenBSD partition to 1433600), with fdisk’s CHS values; the labeldisklabel -wAT-writes with/ *on the image’s vnd(4) (vnd device, 100 sectors a track,athe OpenBSD partition,cthe disk,ithe boot partition as MSDOS); the boot partition asnewfs -t msdosmakes it (amd64 FAT12, 8 sectors a cluster,efi/boot/BOOTX64.EFIandBOOTIA32.EFI; arm64 FAT16 labelledBOOT, one sector a cluster,efi/boot/bootaa64.efi,startup.nsh,config.txt, an emptyoverlays/), by OpenBSD’s makefs with the matching options; andaan FFS1 (newfs -O 1 -m 0 -o space -i 524288: 16 KB blocks, 2 KB fragments) by makefs, holding8.0/<arch>/with the sets, the kernels,INSTALL.<arch>andSHA256but noSHA256.sig(the Makefile’sXXX no SHA256.sig), and the install kernel as/bsdand its link/bsd.rd(amd64, with/etc/boot.confsayingset image /8.0/amd64/bsd.rd; arm64’s are links to8.0/arm64/bsd.rd). - cd80.iso holds
8.0/amd64/bsd.rd,etc/boot.conf(the sameset image) and8.0/amd64/eficdboot, the 350 KB FAT image (makefs -t msdos -o create_size=350K) with efiboot, named by the El Torito catalogue8.0/amd64/boot.catalog. OpenBSD makes it with mkhybrid, GNU code outside the clone (gnu/usr.sbin/mkhybrid), and no host tool is installed for it:tools/xtask/src/iso9660.rswrites the ISO 9660 image asmkhybrid -a -R -T -L -l -d -D -Ndoes (Rock Ridge withSP, andERthrough aCE, which the kernel requires;TRANS.TBLin every directory; upper-case names, directories keeping their dots, as libsa’spnmatchneeds for/8.0/amd64/bsd.rd).cd-iso --rd-kernelmakes the miniroot andbsd.rdfirst, asinstall-mediadoes, so the CD needs no comp build andsmoke-buildmakes it.
Deviations: what the build does not make is not on the images (printed when they are made):
the BIOS boot programs (the MBR’s code, biosboot, boot, cdbr, cdboot; stand/ wants
GNU as), so the CD has no BIOS El Torito entry and its EFI image is the default entry; the
game, man and X sets and BUILDINFO; arm64’s Raspberry Pi firmware and U-Boot
(packages). The label’s d_uid is fixed per arch (I80AMD64, I80ARM64); makefs makes its
own cylinder groups (newfs’s -c FSSIZE has no makefs option). The CD’s descriptors name
EmiBSD and its author where mkhybrid’s command line names OpenBSD and its release engineer,
as the kernel’s ostype does. makefs needs two workarounds (EXT-223 and EXT-224 in LZ’s
external bugs log).
The smokes. just smoke-install80-<arch> (cargo xtask install80, part of ci-full,
not smokes: two minutes on amd64, four on arm64) boots install80.img as a USB stick
(boot.rs, BootMedium::UsbStick: a qemu-xhci of its own and usb-storage with
bootindex=0, QEMU’s writes to an overlay) beside a fresh 2304 MB disk, small enough that
disklabel’s automatic layout is one / and swap (alloc_small). The firmware boots efiboot
from the stick (on amd64 the run types set tty com0 and boot at its prompt, as one does on
a serial console; the kernel comes from boot.conf’s set image), and OpenBSD’s installer is
answered over the console as a person answers it (Vm::respond_in_turn: a question asked
twice gets its answers in turn, and one no answer covers fails the run at once): no response
file, no network, no HTTP server; the sets come from disk, sd1, partition a,
8.0/<arch>, and the installer goes on without SHA256.sig. After CONGRATULATIONS! the
installed disk boots to login: and compiles a program (install-boot’s run,
install.rs::boot_installed). The fresh disk is sd0 and the stick sd1 (umass attaches
after the virtio disk); the run checks both attach lines. At efiboot’s prompt each line is
typed a character at a time, a character again when its echo does not come back
(distrib.rs, type_echoed): after set tty com0 a boot typed at once under the load of
four smokes once reached efiboot as b alone, and efiboot waited for the rest for ever.
cargo xtask install80 --image FILE puts another image on the stick: with OpenBSD’s own
install80.img (the diff-openbsd snapshot’s) the same answers install OpenBSD on both archs
(2026-10-09), its installer asking the same questions in the same order, with the same
automatic layout, but for one: EmiBSD’s amd64 installer also asks An EFI/GPT disk may not
boot. Proceed?, because efi(4) is not ported, so efi0 at bios0 is not in the dmesg and
install.md does not set MDEFI (M14’s known gap, as installboot’s /dev/efi). just smoke-cd80 (cargo xtask
cd80, part of smoke, about a minute) boots cd80.iso as q35’s CD-ROM (ide-cd on AHCI’s
first port): OVMF loads the El Torito EFI image, efiboot reads etc/boot.conf from the CD
(cd0a:) and loads bsd.rd, the kernel attaches the drive (cd0 at scsibus1, ahci’s ATAPI)
and the installer asks its first question; a shell then mounts the CD with mount_cd9660(8)
and lists it by its Rock Ridge names.
Boot loaders (M14)
OpenBSD’s boot programs are ported as OpenBSD builds them (the user’s decision of 2026-10-03, M14; track A1 did amd64’s efiboot): three crates and one binary.
sys/lib/libsa(packagelibsa): the standalone library, as efiboot’sMakefile.commonbuilds it (__INTERNAL_LIBSA_CREAD:open/read… decompress, the plain ones areoopen…). A leaf likelibkern: it depends onlibkernandlibzonly, as the C takesmem*,strlcpyandinflatefrom${S}/lib/libkernand${S}/lib/libzthrough.PATH. The kernel headers libsa includes (<ufs/ffs/fs.h>,<sys/disklabel.h>,<sys/exec_elf.h>…) are ported in the kernel crate, tied to kernel types; libsa cannot link the kernel, sosys/lib/libsa/hdr/declares the parts it reads again, one file per header, and the TESTS zone ofhdr/mod.rschecks their layouts against the kernel’s (a dev-dependency onbsd). The module foralloc.cissa_alloc(allocis Rust’s crate). Host tests run the FFS1, FFS2 and ISO 9660 readers on images OpenBSD’s makefs made (testdata/gen_fixtures.py), andloadfileon a hand-made ELF.sys/stand/boot(packageboot): boot(8)’s machine-independent files (boot.c,cmd.c,vars.c,bootarg.c), which OpenBSD compiles into each boot program. The command state is passed to the commands (fn(&mut CmdState) -> i32).sys/stand/efi(packageefi): the UEFI headers ofsys/stand/efi/includeas#[repr(C)]types, the specification’s names kept, function pointersextern "efiapi".sys/arch/amd64/stand/efiboot(packageefiboot-amd64, binarybootx64): efiboot’s files; the files ofsys/arch/amd64/stand/libsait compiles (disk.h,libsa.h,mdrandom.c) and<machine/biosvar.h>(sys/arch/amd64/include/biosvar.rs, written self-contained so the kernel can include it too) are modules by#[path].- What a library takes from the program at link time in C (libsa’s
file_system[],devsw[],constab[],devopen(),_rtt(),LOADADDR; boot(8)’smachdep(),devboot(),run_loadfile(), themachinetable…) is one table offnpointers and slices the program registers at its entry (libsa::stand::SaConf,boot::boot::BootMd, efiboot’sconf.rs); the C options that select those routines (MDRANDOM,FWRANDOM,HIBERNATE,BOOT_STTY…) are itsOptions (docs/C_TO_RUST.md). SOFTRAID=yes(bootx64’s Makefile) is the cargo featuresoftraidof efiboot, not ported yet: its libsa files aretodoinports.toml, and efiboot says so where the C opens or probes softraid volumes.IDLE_POWEROFF, which the option also defines, is compiled with the feature.- The banner names the system as the kernel does (“The system’s identity”):
>> EmiBSD/amd64 BOOTX64 3.71, where the C printsOpenBSD.
Building BOOTX64.EFI (just efiboot-amd64, part of just build). Deviations, each for a
reason:
- The UEFI Rust targets are not installed (and the toolchain is not changed): efiboot is
built for
x86_64-unknown-none, linked position-independent at 0 bysys/arch/amd64/stand/efiboot/ldscript.amd64, and made a PE32+ image the way OpenBSD’s arm64 efiboot makes its own: a hand-written PE header (.peheader, first in the image, instart_amd64.Sin front of amd64’s_start), one.textand one.datasection (rodata, data, GOT, bss as file bytes, then.dynamicand.rela.dyn), andllvm-objcopy -O binary(cargo xtask efiboot; llvm-objcopy is the toolchain’sllvm-tools). OpenBSD’s amd64 Makefile uses GNU objcopy’sefi-app-x86_64output target, which llvm-objcopy does not have. self_relocapplies the image’sR_X86_64_RELATIVErelocations at entry, as in C. The code is compiled with-C relocation-model=pie(theRUSTFLAGSof the recipe, which replace.cargo/config.toml’srelocation-model=staticfor this build only): withpic, calls between crates go through the GOT, whichself_relocitself would call before it is relocated. The build has its own target directory (target/efiboot), so switching the flags never rebuilds the kernel.build.rspasses the script,-Bsymbolic,--pack-dyn-relocs=none(efiboot’sLDFLAGS) and-z norelro(one.data).just smoke-efibootboots it under OVMF from a disk laid out as OpenBSD installs one (cargo xtask efiboot-disk: an MBR with the OpenBSD partition, its disklabel and an FFS holding/bsd, made by OpenBSD’s makefs; and the EFI system partition holdingEFI/BOOT/BOOTX64.EFI), types atboot>and stops once efiboot has loaded the kernel and boots the kernel tologin:(next section).
arm64’s efiboot, BOOTAA64.EFI (M14 track A3; just efiboot-arm64, part of just build,
output target/efiboot/arm64/BOOTAA64.EFI, which installboot copies from /usr/mdec):
sys/arch/arm64/stand/efiboot(packageefiboot-arm64, binarybootaa64): every file of OpenBSD’s directory, on the same three crates and registration tables as amd64’s. OpenBSD’s arm64 Makefile already makes the PE image with its.peheaderinstart.Sandobjcopy -O binary, which is what is done here, foraarch64-unknown-none-softfloat.dt_blob.S(dtc’s output ofacpi.dts, the template efiacpi fills from the ACPI tables when the firmware has no device tree) is built by aconst fnindt_blob.rs; a reference-backed test checks its bytes againstdt_blob.S. efipxe uses libsa’s network stack (netif,ether,arp,netudp,tftp, ported for it), which a program registers throughSaConf::netif_drivers(andSaConf::getsecs).- Link flags: the aarch64
nonetarget is not static-pie, sobuild.rsadds-pie --no-dynamic-linker -z notextto amd64’s (the precompiledcore/allockeep absolute addresses in read-only data, which becomeR_AARCH64_RELATIVErelocations thatself_relocapplies). softraid_arm64.cis not ported (efiboot’s featuresoftraid, as amd64’s); itssrdevsw entries say so. Which device tree the kernel gets is the C’s choice: the firmware’s (EDK2 onvirt,acpi=off, what the smokes run) and, only without one or aftermachine acpi, efiacpi’s. The efiacpi path is covered by host tests on synthetic tables; the arm64 kernel has no acpi(4) to use the tables it points at.
The kernel’s boot(8) entry (amd64, M14 track A2)
The same kernel ELF boots from Limine and from efiboot (the user’s decision: Limine retires only once boot(8) boots the kernel in QEMU).
- Link: amd64’s
kernel.ldis OpenBSD’s layout (conf/ld.script): linked atKERNTEXTOFF(0xffffffff81000000), physical addresses from0x1000000(AT()), so efiboot’spaddr & 0xfffffffmove puts the image wherelocore0.Sexpects it; the ELF entry islocore0.S’s 32-bitstart. Limine ignores the physical addresses and enters_startthrough its entry point request (sys/stand/mod.rs)..gotis kept in.data: nothing may followend, wherelocore0.Sputs its tables. arm64’skernel.ldhas its own layout (next section). sys/arch/amd64/amd64/locore0.S(global_asm!, AT&T,const/symplaceholders forassym.h): saves boot(8)’s arguments, copies thebootarglist intobootinfo[], probes the CPU, builds the bootstrap page tables (the kernel atKERNBASE, the first 4 GB of the direct map atPDIR_SLOT_DIRECT, the recursive slot), enters long mode and callsbootarg_maininsys/stand/bootarg.rson a 64 KiB.bssboot stack, where the C callsinit_x86_64andmain.- The glue:
sys/stand/bootarg.rsbesidelimine.rs, both ending instand::start_kernel(boothowto, DUID,early_init, the banner,main). The facts come from a machine trait method,Cpu::getbootinfo(amd64’smachdep.cgetbootinfowith the boot(8) half ofinit_x86_64; arm64 and host return an error until track A3), sostandnames no arch module.BootInfogainedhowtoandduid, boot(8)’s ownboothowtoandBOOTARG_BOOTDUID. - Processors: under Limine its MP request; after boot(8) the MADT’s enabled LAPIC entries
(
cpu.rsmp_madt_cpus, standing for acpimadt0, which is not ported) and the C’s start:map_trampscopiesmptramp.StoMP_TRAMPOLINE,pmap_prealloc_lowmem_ptpsmaps the low 2 MB in the kernel pmap,mp_cpu_startsends INIT and two STARTUP IPIs. - Known limits: no Meltdown/SEV probe (
pg_g_kern0), nopmap_direct_rand, the direct map stops at 4 GB (pmap_bootstrap’s extension is not ported; more memory is reported and left out),dkcsum.cis not ported (the root is found by the DUID, not bybootdev).
The kernel’s boot(8) entry (arm64, M14 track A3)
The same arm64 kernel ELF boots from Limine and from arm64’s efiboot.
- Link:
conf/kernel.ldkeeps the image at0xffffffff80000000(Limine’s higher half, whicharm64/pmap.rsadopts) and gives it physical addresses from 0 (AT()): efiboot’sLOADADDRkeeps the low 39 bits of an address and adds its 64 MB block’s (efi_loadaddr), which puts the image at the block’s start. The ELF entry is__start_phys, the offset oflocore0.S’s_startin the image, becausee_entrygoes throughLOADADDRtoo;_startis first in.text(.text.locore0), so efiboot’s cache clean from the entry to the end of the symbols covers the whole image.locore0.Szeroes__bss_start.._end, andlocore.S’sesymstarts atend. Limine ignores the physical addresses and enters the boot glue’s_startthrough its entry point request (that function is not#[no_mangle]: the symbol_startis OpenBSD’s). sys/arch/arm64/arm64/locore0.S(with the boot half oflocore.S:drop_to_el1,get_virt_delta,start_mmu, the page tables,initstack): efiboot enters with the MMU on and an identity map,x0its end of the symbols,x2the device tree._startdrops to EL1, turns the MMU off and builds what the kernel otherwise gets from Limine: a four-levelTTBR1_EL1(T1SZ16) with the 64 MB block at the link address by 2 MB blocks and a direct map at0xffff000000000000(Limine’s offset) by 1 GB blocks, and an identity map of the block inTTBR0_EL1;MAIR_EL1in this kernel’s layout (include/pte.rs); then it jumps high, zeroes the BSS and callsbootarg_mainon a 64 KiB stack with thearm64_bootparams(include/bootconfig.rs).Cpu::getbootinfo(machdep.rs’sgetbootinfo, the/chosenhalf ofinitarm) readsbootargs,openbsd,boothowto,openbsd,bootduid,openbsd,bootmac, the softraid boot volume and key,openbsd,uefi-mmap-*,openbsd,uefi-system-tableandopenbsd,dma-constraint, and the memory asinitarmloads it (the EFI map’s conventional and boot services memory less/reserved-memory’sno-mapranges and the 64 MB block, whose part after the symbols is usable). It maps those gigabytes in the direct map before handing them over; the device tree and the EFI map are read in place (loader data, never given to uvm).- Processors: under Limine its MP request; after boot(8) the
/cpusnodes and psci(4)’spsci_cpu_on(dev/fdt/psci.rs,hvcon QEMU) atlocore.S’scpu_hatch_secondarywith thecpu_infoas context: it brings the MMU up on the identity map and the kernel’sTTBR1_EL1(cpu.rs’sAP_TTBR1, cleaned to memory first) and enterscpu_hatch_entryon the processor’s kernel stack. - ACPI (M14, the user’s decision of 2026-10-08: the install media must boot an ACPI
machine too): on
virt,acpi=onEDK2 hands efiboot ACPI tables and no device tree, and efiboot’sefiacpibuilds one from them (the GIC and its MSI frame, the generic timer, PSCI, the CPUs from the MADT, the SPCR’s UART as/serial, left disabled, and anopenbsd,acpi-5.0node whoseregis the RSDP). The kernel attaches what that tree names as onacpi=off, then acpi0 at the ACPI node (arm64/acpi_machdep.rs’sacpi_fdtattachment, asfiles.arm64has it):acpimcfgmaps the ECAM window (pci_mcfg_init, amachine::pci_machdepmethod that amd64 implements too),acpiiortreads the IORT,acpipciattaches thePNP0A08host bridge with bus spaces that translate its_CRSwindows and MSI through the GICv2m frame (the requester ID mapped by the IORT), andpluartattaches at acpi0 (ARMH0011) and is the console when the SPCR names its address. Interrupts that ACPI describes go to the controller with phandle 1, which efiacpi gives the GIC. The PCI virtio disk then holds the root, found by its DUID.just smoke-acpiboots this way; the extents acpipci fills (subr_extent.rs) are not handed to the PCI bus yet (pcivar.rshas no extent members), so a BAR the firmware left unassigned would not be placed (EDK2 assigns them all).
Deviations from OpenBSD (deliberate)
- One kernel is both
bsdandbsd.rd(M8). OpenBSD builds GENERIC (config bsd swap generic,swapgeneric.c) and RAMDISK (config bsd root on rd0a swap on rd0b, withrd(4)and its image) and boot(8) loads one. Heresys/conf/swapgeneric.rsholds the generic values, and when Limine hands overramdisk.ffsthe boot glue (sys/stand) installs it inrd(4)and switches the root configuration to RAMDISK’s (swapconf_rdroot) beforemain:diskconf(each machine’s, no boot device under Limine) then runssetroot, which printsroot on rd0a swap on rd0b dump on rd0b, anddk_mountrootmounts ffs fromrd0a;start_initexecs/sbin/initfrom it. Without a ramdisk the kernel stays generic and, having no boot device to ask about (setroot’sRB_ASKNAMEprompt is not ported), says it cannot mount root and runs its init boot module (the Rust self-test). - boot(8)’s
BOOTARG_BOOTDUID(efiboot’sopenbsd,bootduidon arm64), the DUID of the disk the kernel came from, is abootduid=<16 hex digits>word of the Limine command line (M13a):BootInfo::bootduidparses it andsys/standwrites the kernel’sbootduidbeforemain.setrootthen finds the boot disk by its label’s DUID, as in OpenBSD, and a kernel without a ramdisk mounts its root from that disk’sapartition (root on sd0a (<duid>.a)). The kernel itself sits on the boot image’s FAT partition, not on that disk, so the DUID is the only boot device there is:just smoke-nvmeboots from an NVMe diskcargo xtask nvme-rootlays out as OpenBSD’s installer does (MBR, OpenBSD partition at 64, disklabel, the userland’s ffs inawith its fstab naming/dev/sd0a;tools/xtask/src/hwopts.rs). Flags go after it on the command line (boothowtoreads every letter from the first-on). - amd64’s FPU state uses
fxsave64/fxrstor64only (amd64/fpu.rs): the XSAVE family and its codepatches are not ported, so there is no AVX state; the switch is eager as in C (CPUPF_USERXSTATE, saved incpu_switchto, reloaded on the way back to user mode). - Limine instead of
boot(8)/efiboot, untilboot(8)boots the same kernel (M14): efiboot is ported (amd64’s BOOTX64.EFI, “Boot loaders” above) and boots to the kernel’s load; the kernel’s own entry from it is M14 track A2. - The application processors are started by Limine (M11a), not by the kernel’s own
trampoline: amd64’s
mptramp.S(real mode, INIT/SIPI/SIPI fromcpu_start_secondary) and arm64’s PSCICPU_ONintolocore.S’scpu_hatchare replaced. TheMULTIPROCESSORkernel, the only one built since decision 40, puts the MP request in.requests(a kernel without it, as before decision 40, left the other processors halted); Limine brings each processor to long mode or EL1 with the boot page tables and parks it on itsgoto_address.sys/standturns the response intomachine::BootMp(the processors’ hardware IDs and astart(i, arg)), and the machine’scpu_start_secondarycallsstartwith the processor’sstruct cpu_info; the AP runsstand::ap_starton the bootloader’s 64 KiB stack, which callsCpu::cpu_hatch(arg), the machine’scpu_hatch. Why: Limine already owns the boot path (above), and a real-mode or MMU-off trampoline would need identity mappings the kernel otherwise never makes. With no ACPI MADT (M13) the processor list comes from the same response on amd64; arm64 still enumerates/cpusfrom the device tree and matches eachregto a response entry by MPIDR. On amd64,cpu_hatch_entrydoesmptramp.S’scpu_spinup_finish(x2APIC if the boot processor runs it,EFER.NXE, the CPU’s GDT, the kernel’s%cr3,CR0, the idle thread’s stack) and loads the IDT first, which the C does later incpu_hatch. On arm64 it loads the boot processor’sMAIR,TCR,TTBR0/TTBR1andSCTLR(copied into statics: thecpu_infoitself ismalloced kernel memory the bootloader’s tables do not map), setsTPIDR_EL1,VBAR_EL1andCPACR, moves to the CPU’s own stack and runscpu_init_secondary. - amd64 processor enumeration: since M13
acpimadt0attaches the processors from the MADT at mainbus, as OpenBSD does (CPU_ROLE_BPfor the one whose local APIC is running, the othersCPU_ROLE_AP,mp_cpu_funcswithMULTIPROCESSOR; with one processor (smoke-up’s-smp 1)cpu0isapid 0 (boot processor), asbsd.spprints it). The bootloader still starts the application processors (mp_cpu_startfinds the MADT’s APIC ID in Limine’s list). Without a MADT (a kernel without ACPI, untilmpbios.cis ported) the M11a stand-in remains: mainbus maps the local APIC atLAPIC_BASEand theMULTIPROCESSORkernel attaches onecpuper processor the bootloader found (BootInfo::mp), the boot processor first asCPU_ROLE_BP, the others asCPU_ROLE_APin the bootloader’s order; one processor attaches asCPU_ROLE_SP. Without I/O APICs the application processors maskLINT0(QEMU wires the 8259’s ExtINT to every local APIC); with them every CPU masks it, as the C does. - amd64 interrupt routing (M13):
ioapic.cdrives the I/O APICsacpimadtattaches; every ISA interrupt goes throughmp_isa_bus(the MADT’s overrides, the rest identity-mapped), every PCI INTx throughmp_busses[bus](acpiprt), MSI and MSI-X straight to the local APIC (msi_pic,msix_pic, with the I/O APIC’s edge stubs, as in C); the 8259 is left masked (nothing is established on it) andLINT0’s ExtINT is masked on every CPU. Routes are recorded whileioapic_coldand programmed byioapic_enableat the end ofcpu_configure, as in C, so no device interrupt arrives during autoconfiguration. All device interrupts still go to the boot processor (apic_set_redir’s destination, as in C).acpimadtandacpiprtare x86 code indev/acpi: they reach the I/O APICs, themp_*globals and the x86 attach arguments through themachine::mpconfigcontract (<machine/mpconfig.h>;struct mp_bus/struct mp_intr_mapare defined there because the generic drivers build them, the MP-spec and I/O APIC constants are associated constants), which arm64 and the host double implement as a machine without I/O APICs. - The kernel lock (M11a, audited in M11e). M11a took it around everything ported against one
CPU, OpenBSD’s own way of bringing code under MP. M11e audited every
MULTIPROCESSORsite and everyKERNEL_LOCKthe port had kept as a comment, module by module, and now the lock covers what it covers in OpenBSD: eachKERNEL_LOCK/KERNEL_UNLOCK/KERNEL_ASSERT_LOCKEDis a real call (nothing withoutMULTIPROCESSOR),SIF_MPSAFEsoft interrupts,TASKQ_MPSAFEtask queues (systqmp, the softnet queues, wg’s) andTIMEOUT_PROC | TIMEOUT_MPSAFEtimeouts (thesoftclockmpthread: TCP, ARP, TDB, syn cache, socket timers) run without it, and so doIPL_MPSAFEinterrupt handlers (vio’s).mi_syscallhonoursSY_NOLOCK, except for the system calls inSY_NOLOCK_DEFERRED(sys/sys/syscall_mi.rs), each group with the reason it still takes the lock; a host test keeps the unlocked set equal to the audited list.uvm_faultanduvm_growrun unlocked in both machines’ traps,exit1drops the lock arounduvm_purgeand the reaper runs unlocked. printf takeskprintf_mutexand the message bufferlog_mtx, so CPUs do not interleave characters. Unlocked as in OpenBSD since M11a: the scheduler and the idle loop,mi_switch, the clock interrupt (clockintr_dispatch), the SMR thread and the IPIs. Deviations: poll’s rate-limit static (poll_lasterr,sys_generic.rs) and wg’swg_last_underloadare guarded (by the kernel lock and a mutex): the C touches them unlocked, which is a data race in Rust. The network takes SMR as in C for the ART, the rtable maps (the C’s SRP issmr_callhere),rt_next, bpf’s listener lists and filters, pflow’s list and pfsync’s softc (SMR_SLISTis ported for them). Open, a deviation: the statistics counters the C bumps with a plain++from several softnet threads (pf’s rule, state and table counters,rmx_pksent) and a few words the C reads unlocked from softnet (rt_flags,rt_priority,rt_gateway, bpf’sbd_dirfilt/bd_fildrop,if_bpf) stayCells like the C’s plain words; making them atomics changes every user. Theqemu-onlyuptime went backwardscheck compares each CPU’s readings with that CPU’s previous one (kern_clockintr.rs); since M11b it counts them per CPU, and the MP boot self-testclockintr_percpuchecks that every CPU runs its own clock interrupts. - Memory allocators under MP (M11a): the pool lock is the C’s mutex or rwlock with and without
MULTIPROCESSOR(the uniprocessor C kernel takes the same mutex);malloc_mtxanduvm.fpageqlockare real mutexes atIPL_VM. WithMULTIPROCESSORthe pools’ per-CPU caches are ported (pool_cache_initon the anon pool, theselftest=mpstresspools and, since M11e, the knote pool and the mbuf, tag, ext-ref and cluster pools; pfsync’s deferral pool cache is commented out in the C too) andpool_gc_pagesruns every second;evcountandmbstathave per-CPU counters. APR_WAITOKpool_getwith no memory sleeps for a request as in C, except while cold or on proc0, where it fails (the C panics underDIAGNOSTIC). M11e:uvm.pageqlockis the C’s mutex atIPL_VM; the pmaps take theirpm_mtxwhere the C does (amd64’spmap_map_ptes/pmap_unmap_ptesonly lock: the page tables are walked through the direct map, with no%cr3borrow); pmap anduvm_objectreference counts are atomics. The per-CPU page cache (__HAVE_UVM_PERCPU,uvm_pmr_cache_*) is ported, its magazines in an array indexed bycpu_number()rather than instruct cpu_info. arm64’spmap_purge(__HAVE_PMAP_PURGE) is amachine::Pmapmethod, a no-op on amd64. amd64’s mainbus counts every application processor inncpusfound, asacpimadtdoes. - arm64 turns on the generic timer’s event stream (
CNTKCTL_EL1.EVNTEN, awfewake-up about every 130 us) on every CPU, which OpenBSD does not (M11e). Reason: QEMU’s TCG can lose thesevthat ends awfewait (its sev helper kicks the halted vCPU without the global lock), and an application processor waiting forCPUF_GO, whose GIC CPU interface is not enabled yet, then never wakes. The event stream bounds every lost event, as Linux keeps it on for the same reason. - ddb on MP (M11c):
db_ktrapstays atsplhighfor its wholedb_enter_ddbloop, where the C drops back to the trapped level between iterations. Reason: a CPU that handed ddb to another (machine ddbcpu) waits in that loop with interrupts on, and at a low level it runs the console’s interrupt and eats the active CPU’s input. Withoutlongjmp, a fault inside a ddb command printsFaulted in DDBand panics instead of returning to the prompt. - Cargo features and
xtaskinstead ofconfig(8), Makefiles andnewvers.sh; the autoconfiguration tablesconfig(8)generates are written by hand (“Autoconfiguration”, below). aarch64-unknown-none-softfloattarget; Intel syntax for amd64 inline assembly.Result<T, Errno>instead ofintreturns; RAII guards forspl/mutex.- A host test double (
arch/host), which OpenBSD does not have. It has no MMU: itsPmap::PMAP_NOMMUmakeskm_allocandkmeminitserve everything through the direct map (the test process’s memory), where amd64 and arm64 mapkernel_map/kmem_mapas OpenBSD does. - Console attach before autoconfiguration exists (M2 to M4):
consinit()attachescom(4)atCONADDR(amd64,consinit.rs) directly instead ofcninit()’sconstab[]walk; arm64 finds its PL011 in the device tree since M4 (pluart_init_cons). This stays after M8: the console’s tty is the one autoconfiguration attaches later (com0 at isa0,pluart0 at mainbus0), which recognises the console’s registers and takes it over as OpenBSD’s drivers do. On arm64,initarminstalls a one-block identity map of the first GiB inTTBR0_EL1with Device-nGnRnE attributes, because the Limine protocol maps RAM but not devices;bus_space_mapis the identity inside it untilpmapmaps devices (M3, page tables). delay(9)before the clocks: amd64 polls the i8254 (isa/clock.rs) throughdelay_func, whichdelay_inithands totsc_delaywhen the TSC frequency is known from CPUID or an MSR, as in OpenBSD (under QEMU it is measured, soacpitimer_delayand thenacpihpet_delaytake over when they attach, M13); arm64 usesintr.c’sarm_dflt_delayuntilagtimerattaches (M4).- ddb-lite:
db_enter()is a breakpoint trap (int3,brk #0xf000) that lands indb_ktrapanddb_trap, which printStopped at <pc>and the stack trace fromddb_regsand then return, as theccommand would, because there is no command loop (db_command.c,db_run.c). A panic prints its trace throughdb_stack_dump.db_panictherefore defaults to 0: a fatal trap is printed bykerntrap/do_el1h_syncand panics instead of entering a debugger that could not be left. No symbols in the kernel yet (db_sym.c): traces are addresses, symbolised byxtask symbolize. - Traps (M4, part a): the entry stubs are OpenBSD’s
vector.S/locore.Sandexception.S, kept as.Sfiles and included byglobal_asm!with theassym.hsymbols (frame offsets, selectors, trap numbers) passed asconstplaceholders. amd64 builds its GDT, TSS and IDT ininit_x86_64(the IDT is a static page;cpu_init_msrsruns first thing because there is nolocore0.S), NMI and double fault takealltrapson their IST stacks (thecalltrap_specstkpath exists for user-mode GS/CR3, M6), andalltraps_kerndoes not re-enable interrupts until the interrupt stubs exist. arm64’sinitarmswitches toSP_EL1(Limine enters withSPSel = 0, whose vectors are empty, as in C), setstpidr_el1andVBAR_EL1itself;x18is a general register here, so the EL1 paths ofexception.Ssave and restore it instead of keepingcurcpu()in it, anddo_el1h_synckeeps interrupts masked. Without processes every kernel page fault or data abort is fatal (kpageflttrapreturns 0 whencurprocis NULL, as in C;kdata_aborthas nopcb_onfaultanduvm_faultis reported), which is what theselftest=trapboot ofsmokeasserts on both archs. - Interrupts (M4, part b1, amd64):
spl(9)is OpenBSD’s:splraise/spllowerinintr.c,Xspllower/Xdoretiinspl.S, the per-source masks incpu_info, theINTRSTUBstubs ofvector.Sfor the sixteen legacy IRQs and the MI soft interrupts (kern_softintr.c, theXsoft*stubs). What autoconfiguration would do was done bycpu_configuredirectly untilconfig_rootfoundexisted (M7b, “Autoconfiguration” below):lapic_boot_initat the architectural LAPIC base (the MADT and MP tables are not ported),cpu_intr_init,intr_enable.lapic_set_lvtprograms LINT0 as ExtINT and LINT1 as NMI, the MP default configuration, because the firmware leaves LINT0 masked and there are no tables to read it from; the IOAPIC stays off, so the 8259 is the PIC. The mutex is the uniprocessor one (kern_lock.c),evcounthas no per-CPU counters yet. Until M8 the console’s receive interrupt was armed by the machine (Console::cn_rx_intr_establish) for theselftest=uartboot, which types a line on the serial console and expects it echoed; since M8com_isa’s attach establishes it and the boot reads the line through the console’s tty (comintr,comsoft,ttyinput,ttread). - Interrupts (M4, part b2, arm64): the device tree is the one Limine hands over (
fdt.cparses it in place); QEMUvirtboots withacpi=off, because EDK2 installs the device tree only when it does not publish ACPI tables, and OpenBSD arm64 needs the tree. The console is found through/chosen(pluart_init_cons), which retires the fixed PL011 address.mainbus_attachpre-registers the interrupt controllers (arm_intr_init_fdt) and attaches the GICv2 (ampintc) from the device tree (built by hand incpu_configureuntil M7b);ampintcthen ownssplthrougharm_set_intr_handler.do_el1h_syncenables interrupts as the C does. The console’s receive interrupt goes througharm_intr_establish_fdt(since M8 frompluart_fdt_attach,fdt_intr_establish), so theselftest=uartboot exercises the same path on arm64 as on amd64. - Clocks (M5-a): the time code is OpenBSD’s (
kern_tc.cover the timehands ring,kern_clockintr.c’s per-CPU queue,kern_timeout.c’s timing wheel,kern_clock.c), reached from the machine through theCputrait’sCpuInfo/ClockFrameassociated types and accessors.mainbrings up the wheel, the clock queue, the four per-CPU clock interrupts (sched_init_cpu’s binds) andinitclocks. amd64 starts the i8254, calibrates the LAPIC timer against it (lapic_calibrate_timer, as the boot CPU’scpu_attachdoes) and drivesclockintr_dispatchfromXintr_lapic_ltimer. The timecounter is the TSC (tsc.c, withidentcpu.c’s TSC part;kern.timecounter.hardware=tsc), the i8254 the fallback. Behind the LAPIC timer the i8254 counts 15 bits (i8254_inittimecounter_simple) and wraps every 27.46 ms: uptime moves forward only while hardclock winds the timehands up within each wrap, so a clock interrupt held off longer (a QEMU vCPU descheduled by a busy host) stepsnanouptimeback a period, as it would on OpenBSD with this counter; the time code keeps the C’s modular arithmetic for it. The TSC’s 32-bit count wraps after seconds. Under featureqemu,clockintr_dispatchprintsuptime went backwardsif a reading is behind the previous one, and every smoke run rejects that line (--reject). Since M13acpitimer0andacpihpet0(amd64, under acpi0) are timecounters too and the TSC’s reference (kern.timecounter.choice=i8254(0) acpihpet0(1000) tsc(2000) acpitimer0(1000): since acpimadt attaches the CPUs from acpi0, the TSC registers between the HPET and the PM timer). arm64 attachesagtimerfrom the device tree (through mainbus since M7b) and takes the virtual timer’s PPI throughampintc. Theselftest=clockboot waits forhzhardclocks and atimeout(9). The host double owns acpu_infoof its own so the clock queue and the wheel are unit-tested over the dummy timecounter. - Processes (M5-b, part 1):
struct proc/struct processare OpenBSD’s with the members the scheduler and the kernel threads use; the machine-dependent parts (mdproc,pcb) come throughmachine::proc(associated types with associated-constant initialisers, soproc0is astatic).proc0paddris a static u-area per arch (Uarea,USPACEbytes, page aligned, aslocorereserves it in C): proc0’s kernel stack stays the boot stack Limine gave us, its pcb and the trap framecpu_forkcopies live in the static.mainsetscurprocfirst and builds process 0 asinit_main.cdoes. - Processes (M5-b, part 2, the scheduler): the sleep queues,
mi_switch, the run queues,fork1and the kernel threads are OpenBSD’s, single-CPU (MULTIPROCESSORpaths such as stealing,SPCF_SHOULDHALTand the barrier task are not configured,sched_choosecpuiscurcpu()). The machine contract gainedcpu_switchto,cpu_fork,clear_resched,cpu_unidle, the idle hooks,cpu_info_foreachand the mutex nesting counter. The context switches are the kernel-thread subsets oflocore.S/cpuswitch.S: stack pointers,curproc/curpcb/p_cpu/p_statand, on amd64,%cr3; the FPU/xstate and user segment handling, the Meltdown CR3s, retguard and the RSB refill come with user mode.proc_trampolinehands the thread function and its argument to a Rustproc_trampoline_runinstead of calling the function itself (Rustfnpointers have no C calling convention); the syscall return path after it is M6. Every thread runs on the kernel pmap until vmspaces exist: amd64pmap_activateloads it, arm64pmap_setttbrecordsci_curpmand leavesTTBR0_EL1(still the bootloader’s) alone.uvm_uarea_allochands outUSPACEblocks from the direct map without the guard page (km_alloccannot punch a hole in the direct map; the guard returns withkernel_map).coldandsafepriaresys/systm.rsstatics likephysmem. Theselftest=kthreadboot runs two kernel threads passing a turn withmsleep/wakeupthrough the run queues and the idle thread. - System calls (M6-a): the tables are generated, as in C, but by
cargo xtask gen-syscallsfromsyscalls.masterinstead ofmakesyscalls.sh; every syscall the tree does not define issys_nosysininit_sysent.rs, and the generator’s--checkkeeps the four files current injust ci. The entry paths are OpenBSD’s: amd64Xsyscall(syscallinstruction,MSR_LSTAR) building the trap frame onci_kern_rsp,syscall(),mi_syscalland the AST loop beforesysretq; arm64handle_el0_sync→do_el0_sync→svc_handler,do_astanderet. Not here: the Meltdown U-K page andXsyscall_meltdown, the xstate/FS.base restores and the Spectre code patches on amd64; the trampoline vectors (trampoline.S) on arm64, soVBAR_EL1keeps the kernel vectors.pin_checkis the C’s since M8: a system call must come from the site the executable’sPT_OPENBSD_SYSCALLS(ps_pin) orpinsyscalls(2)(ps_libcpin) names for its number, or besigreturnfrom the trampoline (machine::signal’ssigcodecall/sigcoderetgive the instruction’s length), else the process getsSIGABRT.copyin(9)is each arch’scopy.Sbehind themachine::copycontract, withpcb_onfaultrecovery in both page fault handlers (amd64 validates it against the.nofaulttable the linker script collects); amd64 runs without SMAP’sstac/clac(nocodepatch,CR4.SMAPnot set). - The first user program (M6-b): there is no filesystem, so
initis a Limine module (module_path: boot():/initinlimine.conf, whichcargo xtask imageadds when theinitbinary exists) that the boot glue hands over asBootInfo::modulesandstart_initwill exec from memory.init/is a freestanding Rust crate (#![no_std], static ELF at0x400000, rawsyscall/svcwith OpenBSD’s carry-flag convention) built for the two bare targets byjust build-init-*; it is not OpenBSD code and lives outsidesys/. Sincepin_checkis real (M8) it carries aPT_OPENBSD_SYSCALLStable like any OpenBSD program: its one system call instruction (syscall6,inline(never)) emits a.openbsd.syscallsentry for every system call number, all naming that instruction, andinit.ldputs the section in a segment of type0x65a3dbe9. Its_startis assembly that hands the initial stack pointer to the program, which checksargc,argvand the auxiliary vectorexecvebuilt (init: argv and auxv okinsmoke). - Process exit (M6-b):
kern_exit.c’sexit1/exit2/reaper/process_zapare OpenBSD’s with the pieces that need signals, file descriptors, limits, credentials or a vmspace reported;initprocessis null untilinitexists and process 0 adopts orphans meanwhile. Theselftest=kthreadthreads nowkthread_exitand proc0 checks the reaper freed them. - User address spaces (M6-b, then M7a): M6 built
exec’s segments from wired pages outside the entry tree; M7a-3b retired those stand-ins.execmaps each segment withuvm_map: a vnode’s text and data copy-on-write from itsuvn_attachobject, as OpenBSD does, and the boot module’s anonymous with the image bytes copied in (sys/kern/exec_subr.rs); either way the pages are faulted in byuvm_fault. - Raw disk I/O (M10a):
kern_physio.cis OpenBSD’s. A disk’s character device (rdread/rdwrite,sdread/sdwrite) callsphysio(strategy, dev, B_READ|B_WRITE, minphys, uio), which wires eachminphys-sized piece of the user buffer withuvm_vslock_device(bouncing it throughdma_constraintpages when the device cannot reach them), maps it intophys_mapwith the machine’svmapbuf(machine::cpu::Cpu,vm_machdep.con each arch) and runs one private buffer through the strategy routine.phys_map, likeexec_map, is a static inuvm_extern.rs(PHYS_MAP) that each machine’scpu_startupsets (VM_PHYS_SIZE=USRIOSIZEpages). - Exec (M8):
kern_exec.c,exec_elf.candexec_subr.care OpenBSD’s.sys_execvefinds the file withnamei(EXECPATH: the realpath becomesAUX_openbsd_execpath), checks it (VOP_GETATTR,VOP_ACCESS,VOP_OPEN), reads the header withvn_rdwr, copiesargv/envpinto anNCARGSbuffer ofexec_map(a submap every machine’scpu_startupmakes, as in C) and builds the stack:argc, the vectors, room for the twelve auxiliary vector entries, the strings, a random stack gap (stackgap_random),ps_stringsand the execpath.exec_elf_makecmdsloadsET_EXECand static PIE (ET_DYN, base fromuvm_map_pie) executables withPT_OPENBSD_RANDOMIZE,PT_OPENBSD_MUTABLE,PT_GNU_RELRO,DT_TEXTRELand thePT_OPENBSD_SYSCALLSpin table;exec_elf_fixupwrites the auxiliary vector (AUX_baseis the executable’s own base for a static PIE, whichrcrt0relocates itself from). APT_INTERPprogram loadsld.sothroughelf_load_file(M14:userlandbuilds it, “Shared libraries and ld.so”).exec_timekeep_mapmaps the shared timekeep page (wired inkernel_map, written bytc_update_timekeep); where the timecounter has no user-mode reader (tk_user0, the i8254 on amd64) libc falls back toclock_gettime(2); amd64’s TSC has one (TC_TSC_LFENCE/TC_TSC_RDTSCP). Until a root file system exists,start_inittriesinitpaths[]throughsys_execve(eachENOENT) and then execs theinitboot module with the same arguments throughexec_image, the same body withep_imageset (docs/C_TO_RUST.md). What a file system must giveexecve:nameiof the path, a regular-file vnode whoseVOP_GETATTR,VOP_ACCESS,VOP_OPEN/VOP_CLOSEandVOP_READwork, and pages through the vnode pager (uvn_attach,uvn_getoverVOP_READ). Reported: the profiling reset,cancel_all_itimersuntilkern_time.c, theNOTE_EXECknote and the/dev/nullfix-up of a set[ug]id exec with a closed standard descriptor (the device switch). - Process system calls (M8):
fork,vfork,__tfork(kern_fork.c, the child returning through the machine’schild_return),wait4,waitid,__threxit(kern_exit.c),futex(sys_futex.c),sched_yield,getentropy(dev/rnd.c; the syscall generator also scanssys/dev),reboot,utrace(noKTRACE: it succeeds and records nothing),setrtable/getrtableandsendsyslogare OpenBSD’s.option ACCOUNTINGis configured, as in GENERIC:kern_acct.cis whole and the generator takes the#ifdef ACCOUNTINGbranch ofsyscalls.master, soinit’sacct(NULL)succeeds.pledge(2)parses and records the promises but sets neitherPS_PLEDGEnorPS_EXECPLEDGE: the enforcement (pledge_syscall,pledge_namei,pledge_ioctl, …) is not ported, andinit(8)andksh(1)pledge early.unveil(2)(kern_unveil.c) andprofil(2)beyond its checks are reported. Withoutsyslogd(8)(no log device, no sockets)sendsyslogwrites aLOG_CONSmessage to the console and answersENOTCONN, the rest goes to the log stash;ypconnectanswersEAFNOSUPPORTwithout a YP domain. The stand-ininitforks children and waits for them, one of which makes a system call from an unpinned site and dies ofSIGABRT(pinsyscalls addr ...andinit: processes okinsmoke). - Time system calls (M8):
kern_time.cis OpenBSD’s whole file:clock_gettime,clock_settime,clock_getres,nanosleep,gettimeofday,settimeofday,adjtime,adjfreq, the interval timers (setitimer/getitimer;ITIMER_REALthrough the process’sps_realit_totimeout andrealitexpire, the virtual and profiling timers throughitimer_updateand the machine’sneed_proftick) and the periodicresettodr. No time-of-day chip driver is ported (todr_attachhas no caller), so the clock starts at the epoch andresettodrhas nothing to write. The stand-ininitchecks the clocks, a sleep and aSIGALRMfromITIMER_REALinterruptingnanosleep(init: time ok). select(2),pselect(2),poll(2),ppoll(2)(M8,sys_generic.c): OpenBSD builds them on the thread’s poll kqueue (there is nofo_pollinstruct fileopsany more), andkern_event.cis not ported. The system calls and their conversions are, withkqueue_registera reporting stand-in that fails: without descriptors they sleep for their timeout as OpenBSD’s do; with descriptorsselectfails withENOSYSandpollanswersPOLLERRfor each. They become real withkern_event.cand each file type’skqfilter(pipes already compute their filters,PipeFilter).- Signals (M7,
kern_sig.c): the whole file is OpenBSD’s, and the traps of both archs call itstrapsignal. The machine half (sendsig,sys_sigreturn, thesigcodetrampoline of eachlocore.S) is themachine::MachineSignalcontract;sys_sigreturnis entered from the table through a forwardingsys_sigreturninkern_sig.rs, because the syscall generator only scanssys/kernandsys/uvm.exec_imagemaps the trampoline with the C’sexec_sigcode_map(one shared aobj,PROT_EXEC, immutable) and draws a newps_sigcookie. amd64 has no FPU code yet (fpu.c):sendsigcopies out the pcb’sfxsave-sized area as it is andsigreturncopies it back withoutxrstor, so a handler shares the interrupted code’s FPU/SSE registers. arm64’s trampoline saves theqregisters itself, which needsfpu_load(the first FP use of a thread traps):fpu_saveandfpu_loadare ported, SVE is reported. The kqueue notes, ptrace stops (the code is there; nothing setsPS_TRACED), core dumps (vn_openis reported, so no core is ever written) andpledge_killare reported. The stand-ininitcheckssigaction,kill, delivery on the way back from a system call,sigreturn,sigprocmaskandsigpending(init: signals okinsmoke). - User pmaps (M6-b2): amd64 walks a user pmap’s tables through the direct map
(
pmap_get_ptp,pmap_enter,pmap_do_remove) instead of borrowing its%cr3for the recursive mapping (pmap_map_ptes), has no pv entries yet and nopmapslist, andpmap_pdp_ctorcopies the kernel’s whole upper half of the PML4;cpu_init_msrssetsEFER.SCE(the C’slocore0.Sdoes). On arm64 the bootstrap device map lived inTTBR0(the lower half), which user pmaps now own:pmap_initinitialises the pools, remaps the console into the kernel half (pluartcn_remap), switchesbus_space_mapto kernel-half mappings from thevmmaprange (a 4 MiB window belowvirtual_avail, so device mappings never overlap whatkernel_maphands out; the C takes them fromkernel_mapwithkm_alloc(kv_any)), setsTCR_EL1.T0SZforUSER_SPACE_BITSand pointsTTBR0_EL1at the empty table, as the C’slocoreandpmap_initdo between them; user pmaps are three-level, their tables come from the same two-page allocator as the kernel’s (nopmap_vp_pool), and ASIDs are an 8-bit bitmap without rollover.initis linked with-z nobtcfisosetregsleavespm_guardedclear (no BTI landing pads yet). - Autoconfiguration (M7b):
subr_autoconf.cand<sys/device.h>are OpenBSD’s andcpu_configurestarts them withconfig_rootfound("mainbus")on both archs.config(8)is not ported: what it would generate intoioconf.c(cfdata[]with its locators and parent vectors,cfroots[]) is written by hand per architecture insys/arch/<arch>/conf/ioconf.rs, followingconfig(8)’s layout, for the GENERIC lines whose drivers exist:mainbus0 at root,cpu0 at mainbus?,bios0 at mainbus0,acpi0 at bios0(M13) andpci* at mainbus0on amd64;mainbus0 at root,ampintc* at fdt? early 1andagtimer* at fdt?on arm64. The tables,mainbus_cdanddevice_registerreachsubr_autoconf.rsthroughmachine::autoconf, so generic code never names an arch; the host double serves whatever table a test installs. A device that GENERIC configures but whose driver is not ported is reported withunported!where its bus would probe or attach it (amd64’sefi0,mpbios0, …); on arm64 every device-tree node and on amd64 every PCI function without a driver prints OpenBSD’snot configuredline. The countsconfig(8)writes into<dev>.hfollow the tables:NMPATHis 0, thehotplug(4)calls are reported. Without ACPI or MP tables, amd64’s mainbus attaches the boot CPU asCPU_ROLE_SP, as the C does on such a machine;cpu_configurekeeps doing afterconfig_rootfoundwhat the boot processor’s attach would add (lapic_enable,lapic_calibrate_timer) and mainbus maps the LAPIC at its architectural base (with ACPI,acpimadtdoes both from the MADT, M13). Adding a driver means itscfattach/cfdriverand oneCfdatarow in eachioconf.rsthat has it in GENERIC. - UKC,
boot -c(M16e):kern/subr_userconf.ceditsioconf.c’s tables before autoconfiguration, so they cannot be immutable statics. Eachioconf.rskeepsCFDATA,CFROOTSandPDEVINITinStaticCells:cpu_startup(both archs, as the C’s machdep.c) callsuser_configwhenboothowtohasRB_CONFIG, which takes them once throughmachine::autoconf::ioconf_mutbefore anything reads them; afterwardscfdata()hands out shared slices as before.cfdata[]ends inconfig(8)’s eight free slots (Cfdata::free(), whosecf_attachisCFATTACH_NULL, the C’s NULL) for UKC’sadd, andmachine::autoconf::ioconf_cfdatacuts them off as the C’s loops stop at a NULLcf_attach. The tables also carryLOCNAMES,LOCNAMP(one run per locator attribute, the compressionmkioconf.c’s XXX asks for) andPDEVNAMES, and every entry itscf_locnames; an entry with locators andcf_locnames0 prints none of them in UKC (and fails akassert!underdiagnostic).cf_locandcf_parentsstay&'staticslices of constants: UKC’schangeandaddreplace them withmalloced copies instead of writing into them. The option is the default cargo featureboot_config(option BOOT_CONFIG). - ACPI (M13, amd64):
acpi0 at bios0 at mainbus0, as in GENERIC. bios0 (bios.c) gets the RSDP from Limine’s RSDP request (BootInfo::rsdp, kept byinit_x86_64asBIOS_EFIINFO_CONFIG_ACPI, the C’sbios_efiinfo->config_acpi), soacpi_probefinds it as on an EFI boot. Its SMBIOS half (M16e) takes the SMBIOS 2 entry point from Limine’s SMBIOS request (BootInfo::smbios,bios_efiinfo->config_smbios; after a boot by boot(8) efiboot’sSMBIOS_TABLE_GUIDtable), maps the structure table and setshw.vendor,hw.product,hw.version(QEMU,Standard PC (Q35 + ICH9, 2009),pc-q35-11.1on q35, as on OpenBSD 8.0 there); ipmi(4)’s mainbus probe reads its IPMI record (type 38) withsmbios_find_table. acpi0 copies the tables, loads the DSDT and the SSDTs into the AML interpreter at boot, and owns power:boot(RB_HALT | RB_POWERDOWN)enters S5 (acpi_powerdown),cpu_resettriescpuresetfn(acpi_reset, the FADT’s reset register) before the keyboard controller and the triple fault. acpi0’s children:acpimadt0(the MADT: processors,ioapic*at mainbus, the ISA overrides and the local APIC NMIs,mp_busses),acpiprt*(each PCI bus’s_PRT: INTx pins on the I/O APIC, link devices through_CRS/_PRS/_SRS) andacpipci*(the host bridges:acpi_haspci,_OSC, MSI enabled for the bus mainbus then attaches) are ported (M13; see “amd64 interrupt routing”);acpitimer,acpihpetand the rest print OpenBSD’snot configuredline. The SCI is on its ISA line, whichisa_intr_establishmaps to the I/O APIC pin throughmp_isa_bus. arm64 attaches no acpi0 until M14 (EFI ACPI boot,efiacpi.c); itsmachine::acpi_machdepanswers as a machine without ACPI. The S3/hibernate machinery (acpi_x86.c,acpi_wakecode.S,subr_suspend.c) is not M13’s and is reported where reached.just smoke-powercheckshalt -p(QEMU powers off, status 0) andreboot(QEMU without-no-reboot, xtask--reboot, boots a second time), on both archs: arm64 goes through psci(4) (dev/fdt/psci.c,psci0 at mainbus0: SYSTEM_OFF and SYSTEM_RESET aspowerdownfnandcpuresetfn;hvc_call/smc_callareasm!inarm64/cpufunc.rs, reached throughmachine::fdt).just smoke-rtccomparesdate +%safter boot with the host within 60 s (amd64: mc146818rtcinit; arm64: efi0 GetTime, EDK2 disables the pl031 node). - File descriptors (M7b):
kern_descrip.c,<sys/file.h>,<sys/filedesc.h>and the read/write/ioctl paths ofsys_generic.care OpenBSD’s: process 0 getsfdinit(),fork1copies or shares the table,execrunsfdprepforexec,exit1runsfdfree, and everyread/write/ioctlgoes throughfd_getfile_modeand the file’sfileops. What needs kqueues or pledge is reported (knote_fdclose,pledge_*); the vnode paths (VOP_ADVLOCK,VOP_PATHCONF,fd_cdir/fd_rdir) are the vfs core’s. - Pipes (
sys_pipe.c,<sys/pipe.h>; John S. Dyson’s licence, accepted by the user on 2026-10-03): OpenBSD’s whole file. A pair is apipe_pair_poolitem freed with its second pipe (pipe_destroyisunsafe), the buffers are pageable kernel memory (km_alloc(kv_any, kp_pageable)) thatuvm_faultfills onkernel_map, and a write to a pipe without a reader getsEPIPEandSIGPIPE(dofilewritev). Withoutkern_event.cthere are no knotes:pipe_wakeup’sknote_lockedandpipe_kqfilterare reported, the filter bodies return what they would set (PipeFilter) forkern_event.cto apply. The host double has no pageable kernel memory, sopipe_pair_createfails there and the host tests build their pairs with heap buffers; the stand-ininitchecks pipes from user mode (init: pipes okinsmoke). - The console as a file (M7b, stand-in): in OpenBSD
init(8)opens/dev/console, a vnode of the console’s character device whose tty does the I/O. Without a root file system to hold that node,start_initinstallssys/dev/consfile.rsinstead: onestruct fileof typeDTYPE_CONSFILE(127, outside OpenBSD’s range), put at descriptors 0, 1 and 2 of process 1 byfalloc/fdinsert/fdalloc. Since M8 itsfileopsare whatvn_read/vn_write/vn_ioctl/vn_closewould do for the console’s vnode:cnopenwhen it is installed, thencnread,cnwrite,cnioctl,cnkqfilterandcnclosethrough the device switch, so the descriptors are the console’s tty (line discipline,TIOCGETA,TIOCSCTTY;init: tty okinsmoke).TIOCSCTTYrecords the tty in the session but no vnode (s_ttyvpstays NULL), so/dev/ttycannot reach it. A console that is not a tty falls back to the polledcnputc/cngetcpath. It goes away wheninitcan open/dev/console. - The device switch (M8):
<sys/conf.h>issys/sys/conf.rs(Cdevsw,Bdevsw,Linesw, thecdev_*_initinitialisers asconst fns); the tables are each architecture’sconf.c(arch/<arch>/<arch>/conf.rs) behindmachine::conf(Conf:nchrdev,cdevsw,cdevsw_set,nblkdev,bdevsw,chrtoblktbl,swapdev,mem_no,iskmemdev,iszerodev,getnulldev). Every slot keeps OpenBSD’s major number (they are ABI:MAKEDEV(8)uses them); a slot whose driver is not ported holdscdev_notdef(). Present today on both archs:cn0,ctty1,mm2 (mem.c),pts/ptc5/6,com8,filedesc22 (its entry points arekern_descrip.c’s),ptm81; no block device. The tables areCells so that arm64’spluartcnattachcan do the C’s KLUDGE (cdevsw[com's major] = pluartdev) at boot; entries are read by copy. Generic code reaches them throughcrate::machine::conf(spec_vnops.rs,cons.rs,subr_xxx.rs), never an arch module. - The tty layer (M8):
tty.c,tty_subr.c,tty_conf.c,tty_tty.c,tty_pty.cand their headers are OpenBSD’s;struct ttyis a structure ofCells shared by the reading process, the driver’s interrupt and soft interrupt and the clock (docs/C_TO_RUST.md). The console is a tty on both archs: amd64 attachesisa0 at mainbus0andcom0 at isa0(isa.c,com_isa.c, the ISA machine hooks behindmachine::isa_machdep), arm64pluart* at fdt?(pluart_fdt.c, the attach arguments behindmachine::fdt’sFdtAttachArgs). Kernelprintfstays polled (cnputc) unlessTIOCCONSredirects it; tty output is interrupt driven (comstart/pluart_start). What needskern_event.c(ttkqfilter,ptckqfilter, theklistofstruct selinfo) is reported or left out, andPTMGET/TIOCCONSlook their/devnodes up withnamei, which fails withENOENTuntil there is a root file system. - The VFS core (M7+, stage 1):
vfs_init.c,vfs_subr.c,vfs_vops.c,vfs_default.c,vfs_cache.c,vfs_lookup.c,vfs_vnops.c,vfs_getcwd.c,vfs_syscalls.c,spec_vnops.c,miscfs/deadfs/dead_vnops.cand their headers (vnode.h,mount.h,namei.h,specdev.h,dirent.h,lock.h,pledge.h) are OpenBSD’s, with no file system, no buffer cache and no vnode pager yet. A vnode is avnode_poolitem that is never freed (&'static Vnode, recycled through the free lists); a mount ismalloced and reference counted (&'static Mount). A file system plugs in with astatic Vfsops, astatic Vops(oneOption<fn(&mut VopXArgs)>per operation,NoneansweringEOPNOTSUPP;docs/C_TO_RUST.md), its node behindv_data(*mut c_void, read back withVnode::data::<T>) and aVfsconf::new(...)line invfsconflist[](vfs_init.rs; ffs is the first) behind a cargo feature named after itsoption(4).mountroot(sys/systm.rs) is NULL untilsetroot(subr_disk.c) and a disk driver exist, so where OpenBSD panics “cannot mount root”mainprintscannot mount root: no root file systemand goes on without arootvnode: everynameithen fails withENOENT(the C never runs one before root is mounted),check_consolewarns that/dev/consoledoes not exist, and init is still exec’d from its boot module. The stand-ininitchecks that the path system calls reachnameiand fail that way (init: vfs ok (no root file system)insmoke). What stage 2 must bring is reported where the C calls it: the first file system. The device switch arrived in M8 (“The device switch” above), sospec_open, the character-device paths ofspec_vnops.candspec_strategycall the drivers.pledgeandunveil(kern_pledge.c,kern_unveil.c) are reported for a pledged process or an unveiled vnode, which none can be yet; the unveil hooks ofnameireturn at theirps_uvpaths == NULLtest. The host tests mounttestfs(the TESTS zone ofkern/vfs_subr.rs), a fixed in-memory tree with a real lock discipline, to drivenamei, the name cache,getcwdand the vnode life cycle. - VFS stage 2 (M7+): the buffer cache (
vfs_bio.c,vfs_biomem.c,kern_bufq.c,<sys/buf.h>), the syncer (vfs_sync.c) and advisory record locks (vfs_lockf.c,<sys/lockf.h>) are OpenBSD’s. Eachcpu_startupcallsbufinit, which reserves the buffer arena (bufkvm, a tenth of the kernel’s space capped atbufpagespages) inkernel_map;mainstarts thecleaner(buf_daemon) andupdate(syncer_thread) threads; a vnode carries its buffer tree and clean/dirty lists, andgetblk,bread,bwrite/bawrite/bdwriteandbrelseare what a disk file system calls (itsVOP_STRATEGYmapsb_lblknoand hands the buffer to its device vnode’sspec_strategy). A buffer is abufpoolitem passed as&'static Buf(docs/C_TO_RUST.md).spec_strategyhands the buffer tobdevsw[].d_strategy(the device switch, M8), but no block device is configured yet; theDIOCGPARTblock size ofspec_read/spec_writeis reported (<sys/disklabel.h>; they useBLKDEV_IOSIZE), and no disk driver usesbufq(9)yet. On the host double (no MMU) the arena is only counted and a buffer’s pages are one physical segment reached through the direct map. A boot self-test writes and reads back anonymous buffers (selftest: buffer cache okinsmoke). The vnode pager (uvm_vnode.c,<uvm/uvm_vnode.h>) and the vnode half ofuvm_pager.c(the pager mapuvm_pseg_*/uvm_pagermapin,uvm_mk_pcluster,uvm_pager_put) are OpenBSD’s:uvn_attach(vp, prot)gives the object a mapping uses, pages come in one at a time throughVOP_READand go out in clusters throughVOP_WRITE, and an unmapped object persists with its pages untilvcleancallsuvm_vnp_terminate.pmap_is_modifiedandpmap_clear_referencejoined themachine::pmapcontract for it (and foruvm_pagedeactivate).execmaps executables through it since M8. Still missing formmap(2)of files:uvm_mmapfileand the device pager (uvm_device.c); the async swap pageout ofuvm_pager.cwaits for the swap pager. A boot self-test maps a three-page cluster through the pager map (selftest: pager map ok). A device vnode keeps itsstruct lockf_state *inspecinfo(si_lockf, aLockfStateSlot), sospec_advlockandvgonel’s purge are the C’s; a file system’s inode will keep one the same way.pool_get(PR_WAITOK)cannot sleep yet, so a lock allocation can fail withENOLCKwhere the C would wait. - The fast file system (M8):
sys/ufs/ufs(the UFS layer:ufs_bmap.c,ufs_ihash.c,ufs_inode.c,ufs_lookup.c,ufs_vfsops.c,ufs_vnops.canddinode.h,dir.h,inode.h,quota.h,ufsmount.h,ufs_extern.h) andsys/ufs/ffs(ffs_alloc.c,ffs_balloc.c,ffs_inode.c,ffs_subr.c,ffs_tables.c,ffs_vfsops.c,ffs_vnops.c,fs.h,ffs_extern.h) are OpenBSD’s whole files, FFS1 and FFS2, behind featuresffs/ffs2(both default) withffsinvfsconflist[]. OpenBSD has no soft updates any more. The on-disk structures keep their C layout:struct ufs1_dinode/ufs2_dinodeare plain#[repr(C)]integers,struct fsandstruct cgare#[repr(C)]structures ofCells (the C changes them in place through shared pointers), checked offset by offset at compile time and against the headers intest-ref. An inode is anffs_ino_poolitem reached byvtoi(vp), its dinode a pool item behindDIP-like accessors (dip_size(),dip_set_size()); the in-core super-block andufsmountaremalloc(M_UFSMNT)ed and reached byvfstoufs(mp). Mounting the root isffs_mountroot:bdevvp(rootdev)(sys/systm.rs’sROOTDEV, set bysetroot), thenffs_mountfs, which opens the device (VOP_OPEN->spec_open->bdevsw[].d_open), reads the super-block at 64 KB, 8 KB or 256 KB through the buffer cache (spec_strategy->bdevsw[].d_strategy) and needs nothing else from the device: noDIOCGPART, no disk label. Until the device switch and a disk driver exist,mountrootstays NULL. Not configured or not ported, and so reported:option QUOTA(quota.rsanswers as a kernel without quotas;ufs_quota_stub.chas no licence block and is skipped),option UFS_DIRHASH(directories are searched linearly),option FIFO(fifofs; a fifo on an FFS is refused withEOPNOTSUPP), the knotes ofkern_event.c(VN_KNOTE,ufs_kqfilter),disk_mapandinittodr. The host tests build FFS1 and FFS2 images with anewfs-like helper (the TESTS zone offfs_vfsops.rs), mount them on a block device vnode whose strategy reads aVec, use them through the system calls, and check the counters of the unmounted image asfsckwould. - The system’s identity (the user’s decision, 2026-10-03): the system is EmiBSD, release
8.0 (the release number tracks the OpenBSD release the reference pin follows: the
pin’s
newvers.shhasosr="8.0"withSTATUS "-current"; it was 7.8 until 2026-10-03, a misreading corrected the same day). OpenBSD’sconf/newvers.shwritesostype,osrelease,osversion,sccsandversioninto a generatedvers.cat every build, from a counter file,date,lognameandhostname. Heresys/conf/vers.rsholds those strings, built byconcat!from whatsys/build.rspasses asEMIBSD_VERS_*compile-time variables, andbuild.rsreads only its environment:EMIBSD_BUILD(the build number),SOURCE_DATE_EPOCH(the build date, printed indate(1)’s format in UTC),USER,EMIBSD_BUILD_HOSTand thesys/directory. The justfile exports the commit count, the last commit’s time andhostname -s, so one commit gives one kernel: no clock, no counter file, no network. The result isEmiBSD 8.0 (GENERIC) #<commits>: <date>\n <user>@<host>:<dir>\n(STATUSis the release one, empty; the configuration name is alwaysGENERIC),osversionisGENERIC#<commits>.kern.ostype/kern.osrelease/kern.version/kern.osversion(kern_sysctl.c) and the line eachcpu_startupprints after the copyright, as OpenBSD’s do, come from there;kern.osrevisionstays theOpenBSDAPI date of<sys/param.h>that programs test, and the copyright notice stays OpenBSD’s text (it is the licence notice, not the identity). The stand-ininitchecks the identity throughsysctl(2)(init: EmiBSD 8.0insmoke). sysctl(2)(kern_sysctl.c): the helpers take user addresses asusizeand the name as a slice; structures are copied out as bytes throughsys::sysctl::SysctlPlain, which C does with avoid *and a size. Nodes whose variable or subsystem is not here yet report themselves withunported!, so a walk of the tree prints its gaps on the console.- DMA (M7b):
bus_dma.cis ported per architecture (amd64 bounce pages belowdma_constraint; arm64 cache maintenance unless the tag isBUS_DMA_COHERENT, thedma-coherentcopy ofmainbus_dma_tagper node) and reached throughmachine::bus’sBusDma. The host double has no DMA (its operations fail withEOPNOTSUPP);bus_dmais tested on the machines instead, by a boot self-test eachcpu_configureruns on its own tag (selftest: bus_dma ok). Sincekm_allocmapskernel_mapspace (kp_nonegives virtual space only),bus_dmamem_mapand the bounce maps work on both machines. - PCI (M7b):
dev/pci/pci.c,pci_map.c,pci_subr.c,pci_quirks.cand the headers are OpenBSD’s; the machine side ismachine::pci_machdep. On amd64, without ACPI, mainbus attachespci0for bus 0 (as the C does whenacpi_haspciis false) and configuration space is reached with mechanism #1 (ports0xcf8/0xcfc). The extents (sys/extent.h) are not ported, so a bus reserves nothing and a BAR the firmware left at 0 cannot be placed. Since M13acpimadtinstallsmp_bussesandacpiprtfills them, so INTx maps to I/O APIC pins (apic 0 int N), andacpipcienables MSI on the bus, sopci_intr_map_msi*work (msi,msix); a kernel without ACPI still maps the line register to the 8259 and refuses MSI, exactly as the C does without tables.option PCIVERBOSEis not configured: the 800 KB name tables (pcidevs_data.h) and most ofpcidevs.hare generated bydevlist2h.awkin C and would need a generator intools/xtask(asgen-syscallsis forsyscalls.master), so the attach lines print IDs (vendor 0x8086 product 0x29c0 (class bridge subclass host, rev 0x02) at pci0 dev 0 function 0 not configured) andpcidevs.rsholds only the IDs ported code names. On arm64 (M12)pciecam(dev/fdt/pciecam.c) attaches QEMUvirt’s ECAM host bridge from the device tree: it maps the 256 MiB ECAM region (beyond thevmmapwindow, sogeneric_space_maptakeskm_alloc(kv_any)space as the C does), gives the bus a copy of its parent’s bus space that translates PCI addresses throughranges, routes INTx throughinterrupt-map(arm_intr_establish_fdt_imap) and MSI/MSI-X throughmsi-parentto the GICv2m frame (ampintcmsi, attached below the GIC bysimplebus_attachas in C;arm_intr_establish_fdt_msi), loading the doorbell into a DMA map and programming the function witharm64/pci_machdep.c. Onvirt,gic-version=3(M16f,EMIBSD_GIC=3,smoke-gicv3) the GIC is agintc(4) and the pcie node’smsi-map(ACPI: the IORT) names its ITS (agintcmsi), which translates each MSI into an LPI. The ITS’s tables and the LPI tables are uncached DMA memory, sopmap_kenter_cachecleans the caches of those managed pages first (cpu_idcache_wbinv_range), as in C. agintc’s attach puts back the boot CPU’s interrupt mask as it found it (ampintc’s unmasks), so theqemuself-tests that wait for an interrupt before the first context switch (selftest=uart,selftest=clock) unmask it themselves throughmachine::cpu::intr_enable(intr_enable(), new in the machine contract). The extents are absent there too, so BARs must be assigned by the firmware (EDK2 does).virtio* at pci?is configured on arm64 as in GENERIC. PCI-PCI bridges (M16e,ppb.c, both archs): with no extents, a bridge needs its bus numbers and windows from the firmware too (EDK2 and OVMF number QEMU’spcie-root-portandpci-bridge);ppb_alloc_busrangeandppb_alloc_resourcesare ported over the extents they are given (ParentExtents), allNone. The bridge’s four INTx handles reach the bus behind it aspba_bridgeih, a slice ofOptions:Noneis the C’s unmapped handle (line = -1on amd64,PCI_NONEon arm64), so generic code needs no arch encoding of failure.PCITAG_NODE(the FDTbus-range) and thePCI_IO_START/PCI_MEM_STARTbounds aremachine::pci_machdepitems (0 and ppb.c’s defaults where the arch’s header sets none). Memory BARs are mapped by amd64’sbus_space.cmemory half (x86_mem_add_mapping:km_alloc(kv_any, kp_none)and uncachedpmap_kenter_pa, as the C does). - i2c and the SMBus controllers (M16e,
dev/i2c/,ichiic.c,piixpm.c, amd64): thei2c_controllerisI2cController, a struct ofCell<Option<fn>>hooks taking the controller’s cookie (the softc is made zero-filled and filled in by the attach),i2c_tag_ta&'staticto it;ic_exec’s(pointer, length)pairs are slices. The scan’s probe state (probe_ic,probe_addr,probe_val[],skip_fc: file statics ini2c_scan.c) is aProbepassed down, which is what lets the host tests run the identification rules on fake register files. Both controllers clear the transfer’s buffer pointer when an exec call returns, so a late interrupt after a timeout cannot write into a buffer that is gone. EDK2 leaves the ICH SMBus host controller disabled (a BIOS enables it), andichiic_attachdoes what the C does: it printsSMBus disabledand stops, as OpenBSD 8.0 does on the same q35/OVMF machine (ichiic0 at pci0 dev 31 function 3 "Intel 82801I SMBus" rev 0x02: SMBus disabled,cargo xtask diff-openbsd probe; the user’s decision of 2026-10-08, after a first port that enabled it). On--machine pcpiixpm(4) finds its controller enabled and runs the scan. QEMU’s SPD EEPROMs are blank (the memory type, register 2, reads 0), soiic_probe_eepromnames none and the scan prints nothing:smoke-iicchecks the attach lines and that no transfer fails.--machine pc(xtask,hwopts.rs) runs i440fx’spcfor piixpm(4), with the boot image on anich9-ahciinstead of the PIIX3 IDE channel (EDK2 reads that one with programmed I/O, minutes for the boot files). - virtio (M7b):
dev/pv/virtio.cand its headers are OpenBSD’s, with both transports:virtio_pci.c(virtio* at pci?, amd64; QEMU’s transitional virtio-net-pci attaches with the virtio 1.0 capabilities) andvirtio_mmio.c(virtio* at fdt?, arm64; QEMUvirt’s 32virtio,mmionodes attach, the empty ones printVirtio Unknown (0) Deviceas OpenBSD does). The rings are DMA memory the device changes, so the core reaches them only through raw pointers with volatile accesses; the barriersvirtio_membar_*are a new machine contract,machine::atomic(amd64 compiler barriers andmfence, arm64dmb st/ld/sy).virtio_mmio, a machine-independent driver, getsstruct fdt_attach_argsandfdt_intr_establishthroughmachine::fdt(a generic associated type per machine; amd64 and the host double have the members but never attach anything with them), andintr_barrierjoinedmachine::intr. The C’s#if defined(__amd64__)around forcing MSI for virtio ismachine::pci_machdep::PCI_MSI_PER_BRIDGE. Interrupts: since M13 amd64’s virtio devices take an MSI-X vector per queue (msix per-VQ); arm64’s come from the node throughampintc. - USB (M12): the machine-independent core (
dev/usb/usb.c,usbdi.c,usb_subr.c, …) runs under the kernel lock atsplusb(), as in OpenBSD. xhci(4) (dev/usb/xhci.c,dev/pci/xhci_pci.c) attaches at PCI on both archs (INTx on amd64 QEMU, MSI-X through the GICv2m frame on arm64). Its interrupt isIPL_MPSAFEas in OpenBSD: the handler only reads the status registers and schedules the USB soft interrupt, so theusbd_busmembers it touches (use_polling,dying,no_intrs) are relaxed atomics. The TRB rings, contexts and tables live in DMA memory and are reached only through bounds-checked volatile accessors (XhciTrbRef, the*_ctx_updateclosures). uhub(4) drives both the emulated root hub and external hubs. umass(4) (dev/usb/umass.c,umass_scsi.c,umass_quirks.c) attaches atuhub?and hangs ascsibusbelow it (umassis a parent of thescsiattribute, as vioblk and softraid are), so a USB stick is ansd(4)disk. The SCSI probe’s commands are polled (umass_polled_transferturns the state machine’s recursion into iteration); later I/O completes from the USB soft interrupt. uhidev(4) and ukbd(4) attach for USB keyboards over the HID parser (dev/hid/hid.c) and hidkbd; the keyboard’s interrupt pipe opens only when awskbdchild enables it (M13), so until then the keyboard attaches but is silent, as on an OpenBSD kernel withoutwskbd. ehci(4) (M16b,dev/usb/ehci.c,dev/pci/ehci_pci.c) is the second host controller:usb*takes bothxhciandehcias parents (one cfdata entry, as config(8) merges its lines). Its soft QHs, qTDs and iTDs/siTDs are carved from DMA chunks that are never freed, so they are&'static, the hardware descriptor first as#[repr(C)]Cell<u32>s read and written volatile (ehci_get/ehci_set, the siop idiom). In QEMU two behaviours stop the faithful port short of a mounted stick, left to the user: on amd64ehci_initraises INTx while the I/O APIC pin is still masked and edge-triggered (ioapic_addrouteleaves pins toioapic_enablewhile cold, as in C) and QEMU’s I/O APIC drops masked edges, so the level that never falls is never delivered; on arm64ehci_pci_attach’s 16-bitEOWRITE2(EHCI_USBINTR, 0)is refused by QEMU’s EHCI (4-byte operational registers) as a synchronous external abort. uhci(4) (M16b,dev/usb/uhci.c,dev/pci/uhci_pci.c) is the third: its soft TDs and QHs are carved from DMA chunks the same way; a TD’slink(a C union of a QH and a TD pointer) is an enum, so a walk that meets a QH where it expects a TD stops instead of reinterpreting it. On arm64virtEDK2 leaves its I/O BAR at 0; as in OpenBSD, the PCI bus now carries the host bridge’s extents (pciecam, arm64acpipci) andpci_mapreg_assignplaces such a BAR from them (amd64’s buses have no extents yet). ohci(4) (M16b,dev/usb/ohci.c,dev/pci/ohci_pci.c) is the fourthusb*parent, built the same way (soft EDs, TDs and ITDs&'staticin never-freed DMA chunks,ohci_get/ohci_set; the done queue’s bus addresses found through the C’s hash). Its interrupt is notIPL_MPSAFE, as in C, so it runs with the kernel lock. In QEMU (pci-ohci) the stick mounts and reads on both archs; the first write makes QEMU’s OHCI raise UnrecoverableError, exactly as OpenBSD 8.0 does on the same setup, andsmoke-ohciasserts that (the user’s rule for QEMU behaviours OpenBSD shares). - Audio (M12): audio(4) (
dev/audio.c) is machine-independent; drivers reach it only throughAudioHwIf,audio_attach_miandaudio_pintr/audio_rintr, called withAUDIO_LOCKheld. azalia(4) attaches QEMU’sintel-hdaon both architectures (through pciecam on arm64) and auich(4) with ac97(4) itsAC97on amd64 (GENERIC has auich on amd64 only). QEMU’s HD Audio controller stops fetching commands while a RIRB interrupt is unacknowledged, and the handler cannot run during autoconf, soazalia_get_responsedoes the handler’s RIRB work itself when the flag is up (a deviation, harmless on hardware). The generic mixer ofazalia_codec.cis ported: mixerctl(1) lists the codec’s amplifiers, selectors and pins (QEMU’s hda-output:inputs.dac-0:1,outputs.master) and sets them. Userland plays with aucat(1), which falls back to/dev/audio0(rsnd/0) when no sndiod(8) runs, assio_open(3)does; sndiod itself is not needed for the smokes. - QEMU’s disks (M10a,
boot.rs,qemu_command): besides the boot image every VM has one persistent virtio-blk disk, the raw 64 MiB sparse filetarget/disk-<arch>.img(disk-<arch>-a.img/-b.imgforsmoke2’s two VMs; intarget/smoke/<recipe>/instead oftarget/whenjust smokeruns the recipe, see “Parallel smokes”), created zero-filled when missing and reused as is, so what a guest wrote survives the next boot;--disk-fresh(qemu,smoke,smoke2) recreates it. It is added after every NIC: on amd64 it isvirtio-blk-pcion a later PCI slot (the NIC staysvirtio0/vio0, dev 2; the boot image is on port 0 of q35’s AHCI controller,ahci0at dev 31 since M13: PCI is probed by device number, so the virtio-blk disks come first and the boot image is thesdunit after them,sd1with the one disk, itsscsibusthe one after theirs; softraid’sscsibusand volumes follow); on arm64virtit takes the lowest virtio-mmio slot in use (slots go out top down, the kernel attaches bottom up), so it is the first block device found (sd0) and the boot disk (virtio31) the second (sd1).--nvme FILE(M13a,qemuandsmoke;hwopts.rs, the home of M13’s QEMU device options) adds an NVMe controller whose namespace is FILE. On amd64 it is added right after the NICs, so it takes slot 3, attaches before the virtio-blk disk (then at slot 4) and its namespace issd0. On arm64 (M13) it is the first device onvirt’s PCI bus (pci0 dev 1; its NIC and disks are virtio-mmio):pciecamcomes after thevirtio_mmionodes in QEMU’s device tree, so the virtio disks attach first and the namespace issd2(after the persistent disksd0and the boot imagesd1). - QEMU’s M13 devices (
tools/xtask/src/hwopts.rs, one option each, hooked intoboot::qemu_commandby one call):--scsi-cd ISO(qemu,smoke,smoke2) adds a virtio SCSI adapter with a read-onlyscsi-cddrive holding the file (virtio-scsi-pcion amd64,virtio-scsi-deviceon arm64) as the LAST device of the command line, so the numbering of the other virtio devices does not change: amd64’s PCI slots go up (vioscsi0 at virtio2, after the NIC and the disk), and on arm64 the adapter takes the lowest virtio-mmio slot, so the kernel finds it first (vioscsi0, and itsscsibus0) while the NIC and the disks keep the slots, hence the names, they have without it.smoke-cdmounts the ramdisk’s makefs ISO through it (cd0,mount_cd9660 /dev/cd0c).--ahci FILE(qemu,smoke) puts FILE on port 1 of q35’s built-in AHCI controller (ide-hdonide.1):sd2after the virtio-blk disk (sd0) and the boot image (sd1, port 0).virthas no AHCI controller of its own, so on arm64 (M13) it adds anich9-ahcias the first device on the PCI bus (pci0 dev 1) with FILE on port 0 (ahci0.0):sd2there too, after the two virtio-mmio disks.smoke-ahciboots from it, on both archs, a disknvme-root --root-dev sd2awrites (its fstab names the unit: diskmap(4) is not ported, so fstab cannot name the DUID).--lsi FILE(qemu,smoke; amd64 only: arm64’s GENERIC has no siop) adds QEMU’slsi53c895aafter that, with ascsi-hdat target 0 on FILE (in the run directory, made afresh, zeroed, 64 MiB, each run) and, with--lsi-cd ISO, a read-onlyscsi-cdat target 1: being last, it takes the next PCI slot (siop0 at pci0 dev 4beside the NIC and the disk) and itsscsibusattaches after vioblk’s and before ahci’s (dev 31), so its disk issd1(smoke-siop).--pci-serial FILE(qemu,smoke) adds QEMU’spci-serial(1b36:0002) last, its line a file chardev in the run directory, and--expect-pci-serial TEXTchecks the file once the serial expectations passed (smoke-puc, amd64 only: arm64’s GENERIC has nopuc*; the guest’secho ... >/dev/cua04reaches the host). For a future arm64puc*: the card’s BAR is I/O space that EDK2 leaves unassigned and the kernel cannot place without extents, andcomsharescdevswmajor 8 with the PL011 console (pluartcnattach’s KLUDGE).--nic MODEL(qemu,smoke;e1000,e1000eorigb) makes the NIC on the user network an Intel PRO/1000 of that model, for em(4), in vio0’s place on the command line and on its netdev: no other device moves, em0 is the only Ethernet interface and the kernel’s network self-test configures it as it does vio0 (on arm64 it sits onvirt’s PCIe bus). Not with--vio-mq.smoke-empings QEMU’s gateway through the 82574L on both archs and the 82540EM on amd64; QEMU’s 82576 (igb) attaches and links up but passes no traffic, probably because its model writes back only advanced receive descriptors and em(4) programs legacy ones, as OpenBSD’s does (not checked against QEMU’s source).--nic rtl8139is QEMU’s Realtek 8139C+ (PCI 10ec:8139 revision 0x20), which re(4) drives;smoke-repings the gateway through it on both archs, on INTx.--nic vmxnet3is QEMU’s VMware VMXNET3 (PCI 15ad:07b0), which vmx(4) drives;smoke-vmxpings the gateway through it on both archs, with 4 queues on MSI-X. em(4)(M13):dev/pci/if_em.cis OpenBSD’s whole driver over the shared code ofif_em_hw.c. The C changessc->hwonly under the kernel lock and readsmac_typeand the registers unlocked from its MP-safe paths (the send queue, the interrupt’s ring work); heresc->hwis handed out as a guard that asserts the kernel lock and refuses a second borrow, and the unlocked paths use a second, read-onlystruct em_hwholding the register handles andmac_type(C_TO_RUST.md).kstat(4)is compiled out (NKSTAT0) andvlan(4)is not configured (NVLAN0). MSI-X stays off as in C (em_enable_msix): the 82574L and 82576 run on MSI, the 82540EM on INTx.ifmedia,mii(4)andre(4)(M13):net/if_media.ckeeps a driver’s media list (struct ifmedia, now in em, vio and re) and answersSIOCGIFMEDIA/SIOCSIFMEDIA.dev/mii/is the MII layer:mii_attachprobes the PHY addresses through the driver’smii_readregand attaches the PHY drivers at themiiattribute (locatorphy;rlphy,rgephy,ukphyare ported). re(4) (dev/ic/re.cwithdev/pci/if_re_pci.c) drives the Realtek 8139C+/8169/8168 family; QEMU’srtl8139is an 8139C+ at revision 0x20, whichre_pci_probetakes (rl_pci_matchtakes the RT8139 only at revision 0x10, so rl(4) stays deferred). As in em(4), the descriptors are read and written whole and volatile, and what the MP-safe send queue and interrupt share (rl_flags, the transmit producer and consumer) are atomics. One deviation fixes a race QEMU exposes:re_initacknowledgesRL_ISRbefore it starts the simulated-moderation timer, which can expire into the C’s later acknowledgement and leave the timer-only interrupt mask silent.__STRICT_ALIGNMENTbecameMachineParam::STRICT_ALIGNMENT(arm64 true), forRE_ETHER_ALIGN.vmx(4)(M13):dev/pci/if_vmx.candif_vmxreg.hwhole, for QEMU’svmxnet3. As in em(4) and re(4), the descriptors are read and written whole and volatile, the shared structures member by member, and the transmit ring’s producer and consumer (the send queue’s and the interrupt’s) are atomics; each receive ring’s fill state stays under its mutex. QEMU offers 25 MSI-X vectors: vector 0 takes the device’s events and intrmap(9) gives each queue its own vector on its own CPU, 4 queues with-smp 4on both archs (transmit spreads over them by flow id; QEMU receives on queue 0 only).kstat(4)is compiled out (NKSTAT0),vlan(4)is not configured (NVLAN0). Itsinitcallsintr_barrierfor each queue vector, which waits for that vector’s CPU to go through the scheduler; so the kernel’sqemunetwork self-test (selftest::ping_gateway, which configures the first Ethernet interface) runs aftercpu_boot_secondary_processors, as OpenBSD’s netstart(8) runs with every processor up.disklabel(8)andfdisk(8)embed their manual page in a generatedmanual.crendered with mandoc(1); the userland build takes their Makefiles’ own.ifdef NOMANbranch (NOMAN_PROGRAMSintools/xtask/src/userland.rs), so the embedded page readsno manual. Only that text differs.vio(4)(M7b):dev/pv/if_vio.cis OpenBSD’s whole driver (vio* at virtio?), attached with the NIC API of the network-interface layer (if_attach,ether_ifattach, one send and one receive queue). QEMU’s user-mode network gives it no offloads (slirp has no virtio-net header), so it runs withMRG_RXBUF, event indexes, indirect descriptors and the control queue. Multi-queue (VIRTIO_NET_F_MQ, asked for with more than one CPU and four MSI-X vectors) goes throughintrmap(9)since M13, as in C; QEMU’s user network offers it only withmq=on(xtask--vio-mq) and with one queue pair, so the default boots keep virtio_pci’s per-queue vectors. Not ported and reported where called:ifmedia(net/if_media.c: only the five media words of<net/if_media.h>it reports are here),tcpstat. The interrupts work on both machines: amd64’s through MSI-X since M13 (INTx through the 8259 before), arm64’s SPI throughampintc; theselftest=vioboot bringsvio0up throughifioctl(the control queue’s answers arrive only through the interrupt oncecoldis over), stops the receive tick, sends an ARP request built by hand and sees QEMU’s answer reachifiq_inputthroughvio_rx_intr, whereether_inputreportsarpinputuntil netinet is here.- Network interfaces (M7b):
net/if.c,net/ifq.c,net/if_ethersubr.candnet/if_loop.care OpenBSD’s.netlocklives innet/if_.rs(as inif.c) and theNET_LOCK()family issys/systm.rs’s functions over it;mainrunsifinitandsoftnet_init(one softnet task queue,NET_TASKQ1 withoutMULTIPROCESSOR) and attaches the pseudo-devices frompdevinit[], which eachioconf.rslists (onlyloop, solo0attaches at boot) andmachine::autoconf::pdevinit()hands over. SMR is not ported: the interface index map is read without a lock and replaced under its rwlock as in C, but the old map is freed at once (smr_call) andsmr_barrieris empty, which is sound on one CPU with a kernel that is not preempted.MPLS(and, until M9+,INET6) and the pseudo-devices that are not ported (vlan,bridge,carp,pf,bpfilter,kstat,af_frame, …) are not configured: their code is a comment at each site. A driver embeds astruct arpcom(all-zero valid, so it fits anM_ZEROsoftc;Rwlock’s name became anOptionfor this), callsif_attach(&ac.ac_if)andether_ifattach(&ac), and hands received frames toif_input. - IPv4 and routing (M7b):
net/art.c,net/rtable.c,net/route.c,netinet/in.c,if_ether.c(ARP),ip_input.c,ip_output.c,ip_icmp.cand the checksums are OpenBSD’s.maincallsrtable_initbefore the pseudo-devices anddomaininitafter them, as in C;domains[](kern/uipc_domain.rs) holdsinetdomainandroutedomain, andunixdomainis reported until sockets exist. There are no sockets: the routing socket half ofnet/rtsock.cthatroute.ccalls (rtm_miss,rtm_addr,rtm_ifchg, …) builds its messages and reports their delivery, and a kernel caller ofin_controlpasses a NULL socket, which counts as privileged. SMR and SRP are not ported, so routes and ART tables are freed at once instead of after a grace period, sound on one CPU with a kernel that is not preempted.ip_ctloutputand the other socket options wait for sockets; TCP, UDP, raw IP, IGMP, IPv6, IPsec,pf,carp, multicast routing and divert are not configured or report themselves (netinet/in_proto.rs). ARP’srt_expire0 means “permanent”, so an entry made whiletime_uptimeis still 0 never expires and never re-asks; the boot ping selftest (kern/selftest.rs, featureqemu) waits for the first second of uptime before it configures10.0.2.15/24on the first Ethernet interface, adds the default route through10.0.2.2and sends an ICMP echo. unported!("name")(sys/kern/unported.rs) marks every call into a subsystem that is not here yet: it prints once per site and yieldsENOSYS. The serial transcript of a boot is therefore an honest list of what the kernel skipped.- Physical memory and the direct map (M3): the memory handed to
uvm_page_physloadis the boot protocol’s usable regions (already without the kernel, the firmware and the bootloader’s data), so the BIOS/EFI map walks,avail_end, the ISA hole and arm64’smemreg_*bookkeeping have nothing to do. Both pmaps use the bootloader’s higher-half direct map (BootInfo::hhdm_offset) as__HAVE_PMAP_DIRECTuntil the kernel owns its page tables:pmap_direct_baseis that offset,pmap_bootstrapdoes not build the direct map’s tables, andvirtual_availon amd64 starts above the direct map when Limine places it atVM_MIN_KERNEL_ADDRESS. OpenBSD arm64 has no direct map and noPMAP_STEAL_MEMORY(it usespmap_steal_availand maps page by page); here it has both, souvm_pageboot_allocworks before anypmap_kenter_paexists. Thevm_physmem[]half of amd64’spmap_steal_memoryisuvm_page_physsteal(uvm/uvm_page.rs), shared by amd64, arm64 and the host double instead of being written three times. - Kernel page tables (M3, part 2): both kernels keep running on the bootloader’s tables and
extend them. amd64 adopts the PML4 in
CR3aspmap_kernel()->pm_pdir, installs the recursive mapping in slot 255 itself (the C’slocore0.Sdoes) and counts the kernel’s PTPs fromvirtual_avail(above the direct map, which shares PML4 slot 256);pmap_alloc_levelkeeps the page-table pages the bootloader already installed. arm64 copies the bootloader’s level-0 table and the level-1 table of the kernel’s slot intopmapvp0/pmapvp1so the vp shadow exists, switchesTTBR1_EL1to the copy and fillsMAIR_EL1indices 2 to 4 around the bootloader’s 0 (write-back) and 1 (device); the kernel pmap is four-level where OpenBSD’s is three-level, andpmap_growkernelpopulates the first GiB that the C’spmap_bootstrappre-allocates. The level-2/3 tables of the kernel image and of the direct map stay the bootloader’s on both archs.pg_nxcomes fromEFER.NXEas the bootloader left it.uvm_km_initonly records the kernel map’s bounds untiluvm_map.c;kern/selftest.rs(featureqemu) maps a page there at boot andsmokeassertsselftest: pmap kernel mapping okon both archs. - Kernel allocators (M3, part 3):
km_allochas nokernel_map/kmem_mapyet (uvm_map.cis M6), so every request is served physically contiguous through the direct map, which the C does only for single pages and single segments;kmem_mapis therefore the direct map andkmemusagehas one entry per loaded page frame.pool(9)andmalloc(9)are ported on top with their locks reduced to assertion flags (M5), no sleeping (PR_WAITOK/M_WAITOKfail where the C would wait), no idle-page timestamps (getnsecuptimeis inkern_tc.c, whose beerware licence needs the user’s decision) and the freelist poison (subr_poison.c) reported.dev/rnd.rswas a placeholder stream (SplitMix64, constant seed, NOT random) behindarc4random, which pools andXSIMPLEQneed for their cookies, until M16d portedrnd.cwhole (the entropy pool, ChaCha20,random_start,/dev/random).kern/rust_alloc.rsis the RustGlobalAllocovermalloc(9)(M_TEMP,M_NOWAIT); featureallocis on by default.physmemlives insys/systm.rs(the C defines it per arch) and<machine/intr.h>’sIPL_*are themachine::Intrcontract. uvmexpis a static of atomics (exported under its C name so the amd64 interrupt stubs can countV_INTR) and the page-queue locks (uvm_lock_pageq,uvm_lock_fpageq) are no-ops until the pools and uvm take the mutex (M5): the boot CPU is alone.wakeupanduvm_waitreport themselves unported, so aUVM_PLA_WAITOKallocation that cannot be met fails withENOMEMinstead of sleeping.-
Licences:
ddb/and thedb_*arch files carry the Mach licence (Carnegie Mellon);dev/ic/comvar.hand amd64include/bus.hhave a BSD block with the 4-clause advertising clause. Since 2026-10-04 every licence in the pinned OpenBSD tree is accepted (the user’s rule,.claude/rules/scope-and-stubs.md). A translation is still a derivative work, so each ported file keeps its original licence block whatever the language; code from outside the tree needs the user’s decision. - The crypto framework (
sys/crypto, M9b/M9c) is the software driver only:crypto.c,cryptosoft.c,xform.c,criov.cand the primitives they and WireGuard use.cryptop_poolis not ported (a request is a value, seecrypto/crypto.rs). The IPCOMP transform (CRYPTO_DEFLATE_COMP,comp_algo_deflate,xform_ipcomp.c’sdeflate_global) runs on thelibzcrate’s deflate and inflate (M9+).crypto_initandswcr_initrun inmainafter the pseudo-devices, asinit_main.ccalls them under#ifdef CRYPTO(GENERIC’soption CRYPTO, M9b). Primitives whose C is public domain (chacha_private.h,poly1305,rijndael,sha1,md5,cast) keep their notice verbatim between the licence markers (accepted 2026-10-03).
Every file-level deviation is in that file’s //! ## Deviations list and in ports.toml notes.
-
rd(4)’s image (M8): OpenBSD links a RAMDISK kernel with an
rd_root_image[]array thatrdsetroot(8)fills with a file system image. Here the image is a Limine module,/ramdisk.ffson the ESP (target/userland/<arch>/ramdisk.ffs, made bymakefs(8)injust userland;cargo xtask imageadds it when it exists,--ramdisk noneleaves it out), whichsys/standhands tord_root_image_setbeforemain. The bootloader maps modules read-write in its direct map and never reclaims them, so rd(4) reads and writes the image in place, as the C does with its array. Why: the kernel stays one ELF whatever the userland, and no tool has to patch it.pseudo-device rd 1(from RAMDISK, not GENERIC) is in eachioconf.rs; without a module rd0 attaches with an empty image and the boot saysrd: no ramdisk module. -
pf(4) (M9d):
pseudo-device pfandpflogare configured as in GENERIC (/dev/pfis character major 73 on both archs,pfattachandpflogattachrun frompdevinit[]), and so isstoeplitz(pf’s state hashes,inp_flowid), and so arepseudo-device pfsyncandpflow(the user’s decision of 2026-10-03):pfsyncattachandpflowattachinpdevinit[],IPPROTO_PFSYNCininetsw[],net.pflowinnet_sysctl, the pf and IPsec hooks.INET6is configured since M9+ (featureinet6), so pf’s IPv6 and NAT64 (af-to,netinet/inet_nat64.c) paths are real.bpf(4)is not configured, sopflog0exists and counts butpflog_packettaps nothing. Divert sockets (netinet/ip_divert.c) are not ported:divert-packetrules report themselves and drop the packet. The ABI structures pf shares with pfctl(8) keep the C layout to the byte; the TESTS zone ofnet/pfvar.rschecks their sizes and offsets against clang’s. -
amd64’s TSC under QEMU (the user’s decision of 2026-10-04). OpenBSD registers the TSC timecounter only with
CPUF_CONST_TSCandCPUF_INVAR_TSC, which on AMD both come from cpuid 0x80000007%edxbit 8 (invariant TSC). QEMU’s TCG never sets it (QEMU 11.1.2, Apple Silicon host, so TCG only):-cpu0x80000007 %edxcpuid(1) CPUID_TSChighest leaf qemu640 set 0xd qemu64,+invtsc0 set 0xd max,+invtsc0 set 0xd With
+invtscQEMU warnsTCG doesn't support requested feature: CPUID[eax=80000007h].EDX.invtsc [bit 8], soboot.rskeeps plainqemu64. TCG’s TSC is monotonic all the same (it follows the host clock). Under featureqemuonly,identifycpusets both flags whenever cpuid(1) reportsCPUID_TSC. The frequency is then unknown to CPUID (no leaf 0x15, and the P0 MSR is AMD family 17h/19h hardware), sotsc_timecounter_inittakesidentifycpu’scpu_freqand recalibrates it as OpenBSD does (M13):acpitimer0andacpihpet0attach under acpi0 at bios0, before mainbus attaches cpu0, and each callscpu_recalibrate_tsc, sodelay(9)isacpihpet_delaywhencpu_freqcounts the TSC over 100 ms andmeasure_tsc_freqmeasures it against acpihpet0 (three 100 ms rounds with interrupts off, at least two within 50 us). The quality follows the C: 2000 after a good measurement, -1000 if every round was disturbed (a busy host descheduling the vCPU), in which case acpihpet0 (1000) is the timecounter. The M11 stand-in (quality 2000 for a TSC no reference had recalibrated, measured against the i8254, which under load read 1.2-1.3 GHz for ~1.0 and ran the clock 20-30 % slow) is gone; the invariant-TSC flags above are what remains of this deviation. Underqemuonly,calibrate_tsc_freqprints its result (tsc: calibrated against acpihpet0: 1000000000 Hz), besidemachdep.tscfreq(cpu_sysctl, ported in M13);smoke-clockchecks thatdatekeeps the host’s rate acrosssleep 45, within 2 s, on both archs. - amd64’s TSC synchronisation test with
MULTIPROCESSOR(M11b).cpu.crunstsc.c’s test against each application processor where the C does, and a failure prints the C’stsc: cpu0/cpuN: sync test failedand drops the TSC to quality -1000. The C prints nothing when the test passes, and nothing for the APs it skips after a failure. So under featuresqemuandmultiprocessoronly,cpu_start_secondaryadds one line per AP:tsc: cpu0/cpuN: sync test passed, or... sync test not run: <why>(tsc_report_verdict).smoke-mpthen expects onetsc: cpu0/cpuN: sync testline per AP, whatever the verdict. Under TCG the test passes: every vCPU reads its TSC from one host clock that QEMU keeps monotonic across vCPUs. - softraid’s boot keys (
sr_bootuuid,sr_bootkey) have no source under Limine (M10f). OpenBSD’s own loaders set them: amd64 boot(8) throughbios_bootsr, arm64 efiboot through theopenbsd,sr-bootuuidandopenbsd,sr-bootkeyproperties. Here they stay zero (replaced-by-limine; comments mark bothmachdep.rssites), so no crypto volume is unlocked at boot. It is unlocked afterwards withbioctl -c C -p <passfile> -l <chunk>. - Frame buffer under Limine (M13, the video console’s first step). OpenBSD’s loaders hand
the kernel the UEFI GOP frame buffer: amd64 boot(8)/efiboot in
bios_efiinfo’sfb_*fields (BOOTARG_EFIINFO), arm64 efiboot as asimple-framebuffernodeframebufferunder/chosen(efi_framebuffer()). Under Limine the kernel asks for Limine’s framebuffer response, which the boot glue turns intoBootInfo::framebuffer(physical address, geometry, channel sizes and shifts). On amd64init_x86_64fillsmachdep.rs’sbios_efiinfo()from it as efiboot fillsbios_efiinfofrom the GOP mode (the reserved mask is the rest of the pixel), soefifb(4)is ported unchanged; its early map is the bootloader’s direct map, which covers the frame buffer (pmap_set_pml4_earlyis not needed). On arm64 the glue builds a copy of the device tree with the node efiboot would add (sys/stand/fdtfb.rs: same properties, same skip rules, in a static buffer instead of efiboot’s in-place edit), sosimplefbandmainbus_attach_framebufferare ported unchanged; QEMU’svirttree, as EDK2 passes it on, has no frame buffer node of its own (ramfbis set up through fw_cfg). The display QEMU gives the GOP: q35’s standard VGA on amd64 (present even with-display none; 1280x800 under OVMF),-device ramfbon arm64 (800x600 under ArmVirtQemu; avirtio-gpuGOP is blit-only and Limine needs a linear frame buffer).xtask’s--fbadds it,--screenshot-afterchecks a QEMUscreendumpagainst the kernel’sselftest=fb(smoke-fb). Under featureqemuthe drivers also hand theirwsdisplayattach arguments to that self-test. - The frame buffer is a plain display, the serial line the console (M13, wsdisplay). OpenBSD
picks the console in
cninit(amd64:constab[]withwscnprobeatCN_MIDPRIandcomcnprobeatCN_HIGHPRIwhen boot(8) saysset tty com0) or in arm64’sconsinit(simplefb_init_constakes the frame buffer only when/chosen’sstdout-pathnames it). With a serial console, OpenBSD attacheswsdisplay0 at efifb0 mux 1(orat simplefb0) without: console, makes its six screens (ttyC0..ttyC5), and the kernel’s messages do not appear on it. EmiBSD’s console is always the serial line, which every smoke reads, so it behaves as OpenBSD with a serial console: amd64’sconsinitattaches com0 directly (cninit,constab[]andwscons_machdep.care not ported, soefifb_cnattachis never called), arm64’s callssimplefb_init_consafterpluart_init_consas the C does, and it returns at once because QEMU’sstdout-pathnames the PL011.wsdisplay_cnattachand the console paths of efifb/simplefb are ported and wait for a frame buffer console. A boot without--fbhas no frame buffer, so nothing attaches and nothing changes; with--fbjust smoke-wsconswrites to/dev/ttyC0and checks the screendump (--screen-text, with the gridselftest=wsconsprints). - vga(4) under UEFI (M13).
vga0 at isa?,vga* at pci?andwsdisplay0 at vga? console 1are in amd64’s ioconf, as in GENERIC (arm64’s GENERIC has no vga). On the smokes’ q35 with OVMF the std VGA is in the graphics mode OVMF set for the GOP: its I/O and memory decoding are on (command register 0x7), but the legacy text memory at 0xb8000 reads back 0xffff, sovga_common_probefails at both buses and the device stays “not configured”, which is what an OpenBSD 8.0 snapshot prints on the same machine (just diff-openbsd’s VM:"Bochs VGA" rev 0x02 at pci0 dev 1 function 0 not configured, no vga line,efifb0takes the display);smoke-vgachecks it. Text mode would need a BIOS boot (SeaBIOS), which EmiBSD does not have. The probe maps the ISA hole: Limine’s direct map (base revision 3) covers only its memory map’s regions and leaves the VGA window out, so amd64’sbus_space_maptakes the hole shortcut (atdevbase, whichlocore0.Smaps after a boot by boot(8)) only for pages that are mapped there and maps the others like any device memory. Without theioport_exextent a second probe of claimed ports does not fail: where a PCI VGA does attach,vga0 at isa?would attach too (OpenBSD’s ISA probe fails there). - smmu(4) on an SMMUv3 without EL2 support (
SMMU_IDR0.Hypclear, QEMU’svirt,iommu=smmuv3) uses the non-secure EL1 regime:STRWNS-EL1, noCR2.E2H, andTLBI_NH_ASID/TLBI_NH_VAinstead ofTLBI_EL2_*, the formssmmu.ckeeps commented out; withHypset the C’s EL2 forms stay (M16f;v3.sc_has_hypis not in the C). QEMU ignores the EL2 invalidations, so a map’s IOTLB entries outlived its unload and the next load of the same IOVA read the old pages: the NVMe root’s disklabel came back wrong.
Testing architecture
See .claude/rules/testing.md. Host tests exist because of arch/host. QEMU smoke tests exist
because custom_test_frameworks is unstable. Reference-backed tests exist because constants copied
by hand drift.
Two-machine tests (the M9b/M9c tunnels) are cargo xtask smoke2 (tools/xtask/src/twovm.rs):
two QEMUs of one arch run at once, each with its own image and EDK2 variable store, each
driven by its own send-after/expect script; the run passes when both are done. The second
virtio-net NIC of each VM (vio1) is on QEMU’s dgram netdev, a pair of UDP sockets on
localhost: unlike socket,mcast= it does not depend on the host’s multicast routing, and
unlike socket,listen=/connect= neither VM has to start first. No kernel change is needed:
vio* at virtio? already attaches a second device on both archs. One harness detail follows
from the kernel finding virtio-mmio slots bottom up while QEMU virt hands them out top down:
on arm64 the link NIC is added to QEMU’s command line before the user-mode one, so that
vio0 is still the user-mode NIC. just smoke-link is the first user and is not part of
smoke.
QEMU’s command line makes EDK2 boot Limine at once: the boot image’s device has
bootindex=0 (QEMU’s bootorder fw_cfg file puts it first in the firmware’s BootOrder, so
the blank persistent disk is no longer tried first: BdsDxe: failed to load Boot0001 "UEFI
Misc Device"), and -boot menu=on,splash-time=0 sets the boot manager’s timeout to 0 through
etc/boot-menu-wait. ArmVirtQemu otherwise waits its platform default: about 5 s of every
arm64 boot (firmware start to BdsDxe: starting went from 5.5 s to 0.5 s). OVMF’s default is
already 0, so amd64 boots gain nothing measurable there.
Host tests of arch code
sys/arch/amd64 and sys/arch/arm64 are compiled for their bare targets only, so the tests
in their files do not run under just test. Where an arch file holds plain logic worth
testing that has no machine-independent home in OpenBSD, the host double compiles that file
for its tests (M16e: amd64’s bios.c, the SMBIOS structure-table walk and its strings, which
OpenBSD writes again in arm64’s dev/smbios.c): sys/arch/host/mod.rs has a #[cfg(test)]
module whose #[path] is ../amd64, holding the file and the self-contained headers it reads
in their own layout (include/biosvar.rs, include/smbiosvar.rs, amd64/bios.rs), so the
file reaches its headers by relative paths (super::super::include) in both builds. What
reaches the machine (bios0’s attach, its mappings, bios_efiinfo) is #[cfg(target_os =
"none")] in the file. This adds no logic to the host double, which only names the files.
hal does the same since N3.1: sys/hal/arch/host/mod.rs compiles, for their tests, each
arch’s include/param.rs, include/pte.rs, include/vmparam.rs and the pure constants of
its pmap (pmap/layout.rs) in their own layout (#[path = "../amd64"], "../arm64").
Parallel smokes
just smoke (and so just ci) runs its recipes several at a time, four by default, since a
sequential run had grown to about 35 minutes of boots under TCG. Decided on 2026-10-04 (the
user’s plan). The shape is one build phase, then one run phase:
smoke-buildbuilds everything a recipe boots, once and in order: the MULTIPROCESSOR kernels with--features qemu(smoke-upboots them too, with-smp 1, since decision 40), the init stand-ins, the media kernels and cd80.iso. No recipe builds while others boot.cargo xtask smoke-all -j N(tools/xtask/src/smokeall.rs) runs each recipe of the justfile’ssmokeslist asjust --no-deps <recipe>, N at a time, longest first by the time each took last. A recipe runs exactly asjust <recipe>would, with the same expectations;just <recipe>alone still builds what it needs and runs intarget/.- Every recipe gets a run directory,
target/smoke/<recipe>/(EMIBSD_RUN_DIR, read byboot::run_dir): its boot images, EDK2 variable stores and persistent disks are there, so no two recipes write the same file (QEMU’s image locking would refuse the second writer, andsmoke-diskandsmoke-ufsoptsboth formatsd0). The disks persist between runs astarget/disk-*.imgdo; the boot images and variable stores of a passed recipe are deleted. Its output goes tologthere: one line is printed per recipe as it ends, and the whole log of every failed recipe after the last one; the command fails if any recipe failed. - The other shared resources were already per run:
smoke2’s link is a pair of free UDP ports asked of the system (asked again, up to three times, if QEMU finds one taken in between); the HTTPS test servers’ ports (8443-8445) are fixed, but onlysmoke-httpsuses them, and a server waits for a port another run holds (another worktree’sjust ci). - Time limits (180 s per boot, or a recipe’s
--timeout) are multiplied byEMIBSD_TIMEOUT_SCALE, whichsmoke-allsets to N/2 rounded up (2 for N = 4): the VMs share the host’s cores, and the limits are there to catch hangs. No expectation changes. - A watchdog outside each recipe (the user’s decision of 2026-10-09). The limits above live
inside the recipe’s xtask process, so a process that hangs before it gets to them is stopped
by nothing: in one
ci-fullrun anxtask smokeofsmoke-softraidsat in macOS’s dynamic loader (_dyld_start, beforemain, so QEMU never started) for 2 h 30 min whilesmoke-allwaited and the log stayed still. Nowsmoke-allpolls every recipe once a second and stops it when it runs longer than its limit, or when its log has not grown for ten minutes. The limit is the sum of the recipe’s boot limits, read fromjust --no-deps --dry-run <recipe>(180 s perxtask smoke, asmoke2’s--timeout; an hour when none shows), times the scale, plus five minutes for what runs between boots;smoke-boot’s 16 boots give 101 min at N = 4,smoke-ntfs’s one boot 11 min. The log’s first line says both. For the still-log rule a waiting boot printsxtask: <arch>: still running after Ns, B serial bytesonce a minute (boot::Heartbeat, also insmoke2and in the serial VMs ofinstallanddiff-openbsd), since a boot otherwise prints nothing until it ends. Stopping a recipe SIGKILLs its whole process tree, read frompsand killed parents first (just, its shell,cargo, xtask, QEMU, swtpm), not a process group: in a group of its own a recipe would no longer get the terminal’s Ctrl-C. The cause goes at the end of the log, with whatpsshowed of each process of the tree just before (state, CPU, wait channel, command), the recipe is reportedTIMEOUT(a failure, its log printed with the others’), the other recipes go on, andsmoke-allexits non-zero. - The architectures (decision 44, 2026-10-18; N4.0):
EMIBSD_ARCHS(defaultarm64) names the archs booted.xtask smokeandsmoke2of a disabled arch print one line,smoke: amd64 SKIPPED (decision 44): EMIBSD_ARCHS=unset, and exit 0 before they look at a kernel or an image; the recipes and their amd64 expectations stay as they are.smoke-allcounts those lines in each recipe’s log ([N skipped]beside the firmware-retry note); a recipe that passed with no boot run is reportedSKIPPEDand is not counted as passed, and the last line readsP of T passed, S skipped (decision 44), B boot(s) skipped in ..., the skipped recipes named like the failed ones. The exit status is unchanged: only failures fail. The watchdog’s limit still counts the skipped boots of a recipe (a larger limit, harmless). N10 runs withEMIBSD_ARCHS=amd64,arm64, which gives today’s commands back.
JOBS=N just smoke (or just jobs=N smoke, or JOBS=N just ci) picks another N; JOBS=1
runs the recipes one after the other, still each in its own directory.
How many processors the VMs get (the user’s decision of 2026-10-07, replacing the -smp 4
of every recipe decided on 2026-10-03 for M11):
- Every recipe boots the MULTIPROCESSOR kernel with the justfile’s
smp,-smp 2by default.ncpucomes fromEMIBSD_NCPU(2 or 4; anything else stopsjustwith an error) and is exported, becausesmoke-allruns each recipe as a newjust --no-depsprocess: an environment variable reaches it, ajust ncpu=4override on the command line alone would not (the export carries it). Expectations that name the count followncpu:aps(the application processors, also the last CPU’s number) insmoke-efiboot,smoke-acpiandsmoke-ddbmp, whosemachine ddbcpuwalk andcpuinfolines depend on it. The install recipes (smoke-install-*) followsmptoo. - The
smp4group stays on-smp 4whateverncpusays, to stress MP:smoke-mp(mpstress, the kthread ping-pong, the vio multiqueue boot),smoke-vmxandsmoke-net-mp(multi-queue and softnet networking; the latter’s-smp 8boot stays) andsmoke-softraid(the disk smoke under load).diff-openbsdalso keeps four processors, the configuration its expected differences were recorded with.smoke-upstays the one single-processor boot, of the same MP kernel with-smp 1since decision 40. just ci-fullrunsEMIBSD_NCPU=4 just ci: every recipe on four processors, as before 2026-10-07. Then, still on four, the installer end to end on both archs (the user’s decision of 2026-10-07):smoke-install-<arch>andsmoke-install-boot-<arch>for amd64, arm64 and arm64 on ACPI, which make the install media from the tree (sojust compmust have run), so every milestone close regenerates and checks the installer. Mandatory before a milestone is marked met; its result goes in the closing commit.- Why two is enough by default: every MP bug found until then needs only two CPUs (one
CPU sleeping in vio_ctrl_submit while the interrupt lands on another; one SMR reader and
one freer in art, rtable, bpf and pfsync; soreceive’s sender and receiver; the consfile tty
race; QEMU’s lost
sevon arm64 with any MTTCG of two or more vCPUs; vmx’s intr_barrier on an AP that is not running yet, which intrmap reaches with-smp 2too). Fewer CPUs make them rarer, not impossible, henceci-fullbefore every milestone close.
diff-openbsd
just diff-openbsd (cargo xtask diff-openbsd, tools/xtask/src/diffopenbsd.rs, M12+) runs
the same scenarios on EmiBSD and on a real OpenBSD and compares them step by step. The smokes
check that EmiBSD does what we expect; this checks that it does what OpenBSD does.
- The OpenBSD VM (the user’s decision of 2026-10-05): the official -current snapshot
nearest the pin, amd64 and arm64. The mirrors keep only the latest snapshot; the one used
was built a day (amd64, 2026-10-03) and two days (arm64, 2026-10-04) after the pin.
tools/xtask/openbsd-snapshot.tomlrecords the mirror, the build dates and two SHA256s per arch: the snapshot’s ownSHA256file, andinstall80.imgas that file lists it.fetchdownloads the image once intotarget/openbsd/<arch>/and refuses it unless both match. Once the mirror moves on, a fresh clone cannot fetch it: copytarget/openbsd/from a machine that has it, or record a new snapshot (the user’s decision). - Install: autoinstall(8), headless. The image boots under EDK2 (on amd64
set tty com0is typed at efiboot’sboot>); the installer is toldAand givenhttp://10.0.2.2:<port>/install.conf(tools/xtask/diff-openbsd/install.conf), served by a small HTTP/1.0 file server of xtask (diffopenbsd/http.rs) with a disklabel template (/and swap). Sets:bsd*,base,comp. The image’s set directory has noSHA256.sig, so the installer is told to go on without it: the whole image was checked on the host. The installer gets two processors (it offersbsd.mponly on a multiprocessor). A first boot, without network (restrict=on, so rc.firsttime’s fw_update and syspatch reach nothing), setslibrary_aslr=NOand removes/usr/share/relink/kernel, so later boots do not relink, and halts. The disk is kept with aninstalledmarker. - A run: EmiBSD (the smokes’ MP kernel and ramdisk,
--smp 4) and OpenBSD (the installed disk under-snapshot, never changed) boot side by side, each with a blank 64 MiB scratch disk (sd0on EmiBSD,sd1on OpenBSD;$Din the scripts). Both log in as root on the serial console and fetch each set’s script and the test program with ftp(1) from the HTTP server. Each script runs every step between@@B <k>and@@E <k> <status>markers. - Scenarios (
tools/xtask/diff-openbsd/*.scn, format indiffopenbsd/scenario.rs): one shell line per step,$compares output and status,?the status,!nothing.setuppartitions, labels, formats and mounts the scratch disk;syscallsrunstools/difftest(ISC, our own: 291 probes printing return values and errno names, inode numbers as labels) one section per step;fsuses OpenBSD’s own utilities, built into the ramdisk for it (cp, ln, mv, rm, rmdir, readlink, stat, touch, wc). The same static difftest binary runs on both systems. - Normalized: host names, the scratch disk’s name,
prog[pid], tabs (expanded on both), and, where a step asks, ls dates, inode numbers, or all numbers. - Expected differences (
tools/xtask/diff-openbsd/expected.toml): each with the step id, its command and a reason. Any other difference fails, and so does an entry that no longer differs. An entry covers a whole step, so a step that hits an unported part stays alone. - Environment, not kernel: the two root file systems differ (the ramdisk has no
/usr/mdec/mbr, its own/etc/fstab,/etc/groupand gettytab, and no root.profilerunning tset(1)). The scenarios avoid or normalize those, and each place says why.
First results (2026-10-05): one stale stub fixed (amap_copy chunking, uvm_amap.c now
ported); five expected differences on both archs: fifofs, exec_script.c, file mmap, core
dumps (all visible EmiBSD stubs) and kern.ostype (branding). Since M14 exec_script.c and
file mmap are ported, so their two entries are gone: three expected differences remain
(fifofs, core dumps, branding).
Timings on the M-series Mac with the other milestone agents running: download about 1 minute
for both images (1.5 GB); install and first boot 272 s (amd64) and 426 s (arm64), once. A run
after that: about 35 to 65 s for amd64 and 80 s for arm64, image build included. It stays
beside just ci, a documented recipe, not part of it: the first run needs the network and
a snapshot that the mirrors drop within days, so ci could not stay reproducible on a fresh
clone; and it adds no build step ci does not already do. testing.md says when to run it.
bench
just bench (cargo xtask bench, tools/xtask/src/bench.rs, N3) measures EmiBSD against
OpenBSD 8.0 on the same emulated machine, so that a redesign of a hot path (N3’s uvm and pmap:
page faults, fork, exec, mmap) has a before and an after. PHASE2’s goal 3 (“measured
improvements only”) rests on it; a milestone close that moved a number carries Bench:
trailers (git-commits.md), printed by cargo xtask bench compare BEFORE AFTER.
- Two systems, one machine. Per arch, EmiBSD (the smokes’ MP kernel, a debug build at
opt-level = 1, with the ramdisk) anddiff-openbsd’s installed snapshot (under-snapshot, never installed or downloaded bybench) boot ondiff-openbsd’s QEMU configuration: four processors, the same CPU model, firmware, virtio disks and NICs, and the same memory (boot::VM_MEMORY, 512 MiB;diff-openbsdgives OpenBSD 1 GiB, which a ratio cannot have). They boot one after the other, never side by side, so neither steals the host’s cores from the other, and the order alternates from run to run, so a drift of the host’s speed falls on both. Ten runs each. - What is measured.
boot: QEMU’s start to the firstlogin:(firmware and boot loader included: OpenBSD’s efiboot waits atboot>, Limine does not).tools/bench(ISC, ours, one static binary for both, in the ramdisk asdifftestis and fetched by OpenBSD overdiff-openbsd’s HTTP server) prints ns per op ofpagefault(the first touch of a page of a fresh anonymous mapping),forkexit,forkexec(the program executes itself as a trivialtrue: the ramdisk has no true(1) and OpenBSD’s is dynamic),pipe(a one-byte round trip between two processes) andfileread(64 KiB reads of a 256 MiB file on a fresh 384 MiB scratch disk, FFS, mounted again after the file is written, so it is read cold through vioblk). Each boot runs the program twice: a warm-up pass, checked and discarded, then, with the scratch disk mounted again, the recorded one.tcpbench:smoke-tcpbench’s two VMs onsmoke2’s private link, for each system, pf disabled on both, a 2 s run retried until the server answers, one 10 s run as a warm-up, discarded, and then ten 10 s runs in the same boot (Mbps, tcpbench(1)’s average). - Warm-up and sizes (the user’s decision of 2026-10-17, after the first baseline): five
runs of one pass each left six of fourteen spreads above 15 %, most from one sample, the
first after boot (the first tcpbench run, arm64’s first
forkexit). So a pass is discarded per boot, and the noisiest measurements do more work per sample:forkexit500 -> 2,000 ops,forkexec200 -> 800, the file 64 -> 256 MiB. - Ten runs, the worst left out (the user’s decision of 2026-10-17, after the second baseline): with five runs one disturbed boot set the max-min spread on amd64 (EmiBSD’s first run once, OpenBSD’s second the next time, on whichever side). So each side boots ten times and, per measurement, its worst sample (the slowest for times, the lowest for Mbps) is left out of the statistics; the 15 % limit stays.
- Why ratios. Under TCG (amd64 always on this Mac, arm64 too: no hvf) an absolute number says how fast QEMU translates, not how fast the kernel is; the same emulation running both kernels cancels that out to first order, as TRACTOR reports a translation’s overhead against the C it came from. The ratio is EmiBSD’s median over OpenBSD’s: below 1 is faster for times, above 1 for Mbps. It is only indicative of real hardware (M17), and EmiBSD’s side is a debug kernel against OpenBSD’s release one.
- Spread. Each side’s spread is
(max - min) / medianover the nine samples it keeps of its ten runs, and its median is theirs. A ratio is printed only when both are at most 15 %; otherwise the table saysspreadand the report gives the numbers. The limit is not tuned to make a run pass: a measurement that stays above it is reported as it is, and the cause found. - Report. A table per arch on stdout and in
<run dir>/<arch>/report.txt, and<run dir>/<arch>/report.toml(schema = 1): the kernel’s commit, whethersys/was clean, its SHA-256, the QEMU command lines, the date, the configuration and the method (reps,discard_worst), and every raw sample, all ten, with the index of the discarded one (emibsd_discarded,openbsd_discarded) and each side’s median, minimum, maximum and spread over the others; the table lists the discarded samples.comparereads two of them, refuses two of different methods (runs,discard_worst, the file’s size, tcpbench’s length), and prints aBench: <measurement>-<arch> <before> -> <after> <unit>line for each EmiBSD median that moved by more than the larger spread of the two, and why not for the others. - Host diagnostic. Around every run of each side (tcpbench’s runs too) the host’s
one-minute load average at the start and the end and its five busiest processes but QEMU
are printed and kept in
report.toml([[host]]), so a sample can be read against the host: a debugger traced a step in amd64’spipeandfilereadto host contention (VS Code’s cpptools bursts, macOS’s idle-time daemons). It changes no sample, rule or limit. - Where it runs. Beside
just ci, not in it, likediff-openbsd: under the machine lock, with nothing else running on the Mac (a concurrent build moves every number). The baseline and each milestone’s after go in that milestone’s section ofdocs/JOURNAL.md. - The release diagnostic (decision 42, the user’s of 2026-10-18).
just bench-release(cargo xtask bench --profile release) builds the kernel alone in Cargo.toml’s[profile.release](LTO, one codegen unit, symbols kept,panic = "abort") and bootstarget/<triple>/release/bsdin place of the debug kernel; init,abi_openbsd.koand the ramdisk are the filesjust benchboots (debug), so only the kernel’s profile differs. The method is the same (ten runs, the worst left out, the 15 % limit, the warm-up, the host diagnostic);report.tomlrecordsprofile = "release"(a report without the key is a debug one, so the debug reports are written as before and stay readable), the table’s first line names it, andcomparereads two release reports as two debug ones and says so first when the two differ in profile. It tells how much of a ratio is the debug build’s rather than the design’s; it never replaces the baseline, which stays the debug kernel’s. Its reports go totarget/bench-release/<arch>/unlessEMIBSD_RUN_DIRsays otherwise. - Host microbenchmarks (decision 42, phase 1; decision 43 runs them per slice).
sys/kernel/benches/vm.rs, aharness = falsebench target ofkernel(featuredoubles, an[[extra]]oflineage.toml, never a kernel image), drives the realkernel::vmcode overkernel::testutil’s doubles, with recycling pool(9) and malloc(9) doubles of its own: the anonymous and lower faults, fork, exit, exec, map/unmap and the primitives (aPoolArcclone, the map lock, a page allocation), ns per op, seven repetitions of about 300 ms, median and spread.cargo build -p kernel --features doubles --bench vm --profile dev(the kernel’sopt-level = 1) and--release; the binary takes bench names,list, or--profile NAME SECSfor a profiler. What it cannot see: the real pmaps (the host pmap is aBTreeMap), TLB shootdowns, the trap and syscall entry, the page contents and TCG’s costs; so it attributes and compares, andjust benchstays the measurement of record.