Roadmap
Native milestones, numbered N0.. so they never collide with the port’s M numbers (LZ’s
docs/ROADMAP.md, M0..M17; M0..M14 are also this repository’s history). Every milestone has a
mechanical exit criterion: just ci green (which includes cargo xtask lz check, lz drift
--strict and unsafe-report --check), just ci-full green, just diff-openbsd equal with the
same expected list, cargo xtask lz drift --security empty, and the subsystem’s unsafe totals
against the baseline of N0. The numbers in the criteria are filled at N0 from the tools (real
data only, docs.md); until then they read <n>.
Order (recommended, the user’s call at the go): leaves first, where the tests are strongest and the risk lowest; the security subsystems last, with their own review rule. LZ sync is a rolling gate for every milestone, never a milestone of its own.
Since decision 44 (the user, 2026-10-18; docs/ZERO_UNSAFE.md section 10) only arm64 is built,
booted and measured until N10 homologates amd64: the smokes, ci, ci-full, diff-openbsd and
bench run on the architectures of EMIBSD_ARCHS (default arm64, the switch N4.0 lands); no
amd64 code is written or fixed before N10, and a hal boundary type is still designed with the
amd64 C in view (the planner’s “amd64 constraints”). The order after N8 is N9 (the close, on
arm64, the amd64 tree frozen), N10 (amd64), N11 (verification) and N12 (performance).
| Milestone | Scope | Exit criterion |
|---|---|---|
| N0 Bootstrap | In EmiBSD/, in place, from lz-origin: the governance rewrite applied (CLAUDE.md, .claude/rules/, docs/PHASE2.md, this file, docs/IDIOMS.md, docs/SYNC.md, the README); lz/PINNED.md; reference/emibsd-lz/ cloned; lineage.toml generated 1:1 from ports.toml (every LZ .rs maps to itself, inherited, licences and wip notes carried over; the adapted status and the [[module.fn]] rows in the schema from the first commit); cargo xtask lz {check,status,drift,trace}, unsafe-report --check, unsafe-budget.toml at the current counts, just lz-sync; lz-sync.toml with [meta] only; ports.toml, the ports subcommands and docs/PORTING.md retired; the RCS ident lines removed (decision 20); the baseline in docs/STATUS.md, unsafe-budget.toml and the N0 section of docs/JOURNAL.md (unsafe totals, smoke count, diff-openbsd result, the inherited blockers; the benchmarks when just bench exists); the memory directory curated |
cargo xtask lz check passes; cargo xtask lz drift prints nothing; cargo xtask unsafe-report --check passes; just ci and just ci-full green; just diff-openbsd equal to LZ’s last run; the baseline numbers recorded in docs/JOURNAL.md |
| N1 Leaves | sys/lib/libkern, sys/lib/libz, the primitives of sys/crypto/ (ciphers, hashes, MACs; not the framework): slice and &mut [u8] APIs, no raw pointers, unsafe to zero or a justified list, the C’s test vectors as table-driven tests, property tests for the parsers. It keeps 15 modules inherited: the crypto framework (crypto, cryptodev, criov; N8’s), the constant tables (DES podd, sk, spr; libz inffixed, zconf), libkern’s crate root and six helpers forbid as they stand |
unsafe total of lib/libkern, lib/libz and crypto at 15 (from 10, 2 and 5 at lz-origin: 10, 0 and 5; the 10 of libkern are StaticCell, API frozen by the timing rule, and the explicit_bzero volatile store; the 5 of crypto are in the framework, N8), each remaining unsafe with a soundness argument; host test count (just ci) 2605 -> 2695; every crypto smoke passes (smoke-wg, smoke-esp, smoke-ipcomp, smoke-softraid, smoke-https) |
| N2 Foundations (met 2026-10-16; replanned the same day, docs/ZERO_UNSAFE.md sections 0 and 0.9) | The crates sys/hal/ (the 24 bsd-free machine headers and 18 constant headers of sys/sys/, moved in inherited (types.rs follows uvm_param’s page arithmetic at N3); the rest of the hardware enters hal with the kernel owner of each boundary, decisions 26-28; the new runtime: Mutexed<T>, Rwlocked<T>, TypedPool<T>, PoolArc<T>, MBox<T>, KmBuf, OnceStatic<T>, List/Tree written new with their first kernel user (the lists and trees of sys/sys/queue.rs, sys/sys/tree.rs, sys/kern/subr_tree.rs stay in bsd), UserPtr<T> with abi_pod!, the bridge registry), sys/kernel/ (#![forbid(unsafe_code)]; crypto from sys/crypto/; pipe(2) from sys/kern/sys_pipe.rs, sys/sys/pipe.rs, the first subsystem by the new path) and sys/abi/, built as the loadable module abi_openbsd.ko (#![forbid(unsafe_code)]; the syscall table, all entries Legacy but pipe; the constants through hal; loaded by hal’s ELF loader at boot, registered as the syscall handler, given KernelApi, decision 22); bsd links kernel and hal; cargo xtask hal-check, abi-check, unsafe-report v2, /progress v2, bridges.toml |
kernel and abi build with forbid at the root on both archs; cargo xtask hal-check and abi-check pass in just check; hal ≤ 2,150 unsafe, every site with // SAFETY:; abi_openbsd.ko loaded at boot on both archs (smoke-abi); sys/kern/sys_pipe.rs and sys/sys/pipe.rs absent from bsd; the 24 + 18 headers under sys/hal/; just ci and just diff-openbsd equal (ci-full at N3’s close, decision 24). Met 2026-10-16: both forbid crates build on both archs (just build, just check rc=0); hal-check (74 files, 35 sites <= hal_ceiling 35, every block proved) and abi-check in just check; hal 35 <= 2,150; abi_openbsd.ko: 331 syscalls, 2 native on both archs (smoke-abi, smoke-efiboot; cd80.iso without it, decision 29); sys/kern/sys_pipe.rs and sys/sys/pipe.rs absent from bsd; 24 + 18 headers under sys/hal/; just jobs=3 ci rc=0 (85 of 85 smokes, 1734 s) and just diff-openbsd rc=0 (107 steps per arch, 104 equal, 3 expected, 0 unexpected); ci-full at N3 (decision 24) |
| N3 Memory (met 2026-10-18; re-planned at N3.4, docs/ZERO_UNSAFE.md 0.10) | sys/uvm/ -> sys/kernel/vm/ in owner slices (km, object, anon, amap, aobj, the vnode pager, page, pmemrange, addr, map, mmap, glue; uvm_fault with the owners, before the map, since the owners could not flip before the fault held their witnesses: N3.4, security-review.md); the pmap behind a safe hal::Pmap API: no file of the pmap cluster of sys/arch was movable as it stood (all 14 name bsd), so N3.1 redesigned the two pmaps into hal (the LZ pmap files and pmap_module.rs left sys/arch; the two cpufunc.rs and vm_machdep.rs wait for N4; decision 26) Headers and machine contract it owns: sys/sys/types.rs, sys/sys/swap.rs, sys/machine/pmap.rs. |
sys/uvm/ absent from bsd; the pmap files absent from sys/arch/; sys/machine/pmap.rs an adapter over hal::pmap with no unsafe block (its legacy unsafe fn signatures leave with their last caller); cargo xtask unsafe-report shows the uvm row of bsd gone; hal has no pub unsafe fn (hal-check). Met 2026-10-18: sys/uvm/ absent (its 26 modules redesigned into the 22 of sys/kernel/vm/ and hal’s pieces); no pmap file in sys/arch/*/; sys/machine/pmap.rs without an unsafe block (three legacy unsafe fn signatures left); unsafe-report has no uvm row (215 at the N2 close); hal-check 111 files, 100 sites <= hal_ceiling 110, no pub unsafe fn; the swap device, the page daemon and the coredump stay visible stubs (unported: lines, expected.toml’s coredump entry); just jobs=3 ci at a043de71: 86 of 86 smokes in 18 min 32 s, rc=1 only at check-drift (the record of LZ 602141098d1d named the deleted sys/uvm/uvm_init.rs), fixed in 614d65af, the steps after it rc=0; just jobs=3 ci-full at 614d65af rc=0 in 2,248 s (its ci on four CPUs, 86 of 86 smokes in 21 min 18 s, no retry, then the eight installer recipes); just diff-openbsd rc=0 (107 steps per arch, 104 equal, 3 expected, 0 unexpected, as at N2); cargo xtask lz drift --security empty |
| N4 Processes and scheduling | sys/kern/ by owner -> sys/kernel/: timeout, task, kqueue (kern_event.rs, sys/sys/event.rs), descriptors (sys/sys/file.rs, kern_descrip.rs), signals, proc, the scheduler, sysctl’s core, tty; StaticCell uses in kern -> OnceStatic; the cpu, clock, interrupt, trap, syscall-entry and copy files of sys/arch (97) into hal (decision 26; arm64’s: the amd64 half is N10’s, decision 44), after N4.0 lands first: the EMIBSD_ARCHS switch in xtask (smoke, smoke2, smoke-all, diff-openbsd, bench) and the justfile (build, clippy, ci-full), an amd64 boot logged SKIPPED (decision 44) and counted apart, never as a pass. Headers and machine contract it owns: sys/sys/proc.rs, sys/sys/sched.rs, sys/sys/sig* (signal, signalvar, siginfo, sigio), sys/sys/selinfo.rs, sys/sys/poll.rs, sys/sys/event*, sys/sys/fil* (file, filedesc, filio), sys/sys/fcntl.rs, sys/sys/resource*, sys/sys/ucred.rs, sys/sys/user.rs, sys/sys/task.rs, sys/sys/time* (time, timeout, timetc), sys/sys/_time.rs, sys/sys/clockintr.rs, sys/sys/evcount.rs, sys/sys/syscall* (syscall_mi, syscallargs), sys/sys/sysctl.rs, sys/sys/systm.rs, sys/sys/param.rs, sys/sys/syslimits.rs, sys/sys/acct.rs, sys/sys/msgbuf.rs, sys/sys/tprintf.rs, sys/sys/syslog.rs, sys/sys/tty*, sys/sys/termios.rs, sys/sys/ioccom.rs, sys/sys/ioctl.rs, sys/sys/uio.rs; sys/machine/cpu.rs, sys/machine/copy.rs, sys/machine/intr.rs, sys/machine/proc.rs, sys/machine/signal.rs, sys/machine/tcb.rs, sys/machine/atomic.rs, sys/machine/cons.rs (decision 26: cpu, clock, interrupts, trap, syscall entry, copy). |
sys/kern/ in bsd holds only vfs_* (N5), uipc_*/sys_socket.rs (N6) and the N8 files; the Proc, Fd and Kq bridges removed from bridges.toml |
| N5 VFS and file systems | sys/kern/vfs_*, sys/ufs/, sys/isofs/, sys/msdosfs/, sys/nfs/, sys/ntfs/, sys/tmpfs/, sys/miscfs/ -> sys/kernel/fs/ Headers it owns: sys/sys/vnode.rs, sys/sys/mount.rs, sys/sys/namei.rs, sys/sys/buf.rs, sys/sys/dirent.rs, sys/sys/stat.rs, sys/sys/specdev.rs, sys/sys/lockf.rs, sys/sys/fusebuf.rs. |
those directories absent from bsd; the Vfs bridge removed |
| N6 Network stack | sys/net/, sys/netinet/, sys/netinet6/, sys/kern/uipc_*, sys/kern/sys_socket.rs -> sys/kernel/net/, except the files of N8 Headers it owns: sys/sys/mbuf.rs, sys/sys/sock* (socket, socketvar, sockio), sys/sys/protosw.rs, sys/sys/domain.rs, sys/sys/un.rs, sys/sys/unpcb.rs. |
those directories absent from bsd but N8’s files; the Sockets bridge removed |
| N7 Devices | sys/dev/ and sys/scsi/ -> sys/kernel/dev/, one bus directory at a time, over hal::Mmio<Regs> and hal::DmaBuf; sys/dev/rnd.rs last (security-review.md); the bus, autoconf and ACPI/FDT glue of sys/arch (74 files) into hal, its 19 drivers into sys/kernel/dev/ (decisions 26, 28; arm64’s: the amd64 half is N10’s, decision 44) Headers and machine contract it owns: sys/sys/device.rs, sys/sys/conf.rs, sys/sys/disk.rs, sys/sys/disklabel.rs, sys/sys/*io.rs (ataio, audioio, cdio, dkio, mtio, scsiio), sys/sys/gpio.rs, sys/sys/extent.rs, sys/sys/sensors.rs; the rest of sys/machine/ (bus, autoconf, acpi_machdep, fdt, mpconfig, pci_machdep, pci_chipset, pciide_machdep, isa_machdep, bootinfo, disklabel, x86: decision 26, bus, autoconf and the ACPI/FDT glue). |
sys/dev/ and sys/scsi/ absent from bsd but sys/dev/softraid_crypto.rs (N8) |
| N8 Security subsystems | Under security-review.md, each with Security-Review: and the area’s smokes before every commit: sys/net/pf*, sys/net/if_pf*, sys/net/if_wg.rs, sys/net/wg_*, sys/netinet/ip_ipsp.rs, sys/netinet/ip_esp.rs, sys/netinet/ip_ah.rs, sys/netinet/ip_ipcomp.rs, sys/netinet/ipsec_*, sys/dev/softraid_crypto.rs, sys/kern/kern_pledge.rs, sys/kern/kern_unveil.rs, sys/kern/exec_*, the crypto framework -> sys/kernel/ Headers it owns: sys/sys/exec* (exec, exec_script). |
those files absent from bsd; cargo xtask lz drift --security empty |
| N9 Close | What is left: sys/ddb/ with the ddb glue and the configuration tables of sys/arch/, sys/conf/, sys/reftest.rs, init/, the old runtime files (sys/kern/subr_pool.rs, sys/kern/kern_lock.rs, sys/kern/kern_rwlock.rs, sys/kern/kern_malloc.rs, sys/kern/kern_smr.rs, sys/kern/subr_percpu.rs, their sys/sys/ headers) deleted with their last user; sys/sys/ emptied into abi and kernel; the bsd crate deleted; the kernel binary is kernel + abi + hal; Miri on hal’s host tests; the amd64 tree frozen (decision 44): sys/arch/amd64/, sys/hal/arch/amd64/ and the amd64 implementations of sys/machine left as they are, N10’s row owning them, inherited, outside every crate, counted as frozen by /progress and unsafe-report (that tooling is this row’s). Headers and machine contract it owns: sys/sys/malloc.rs, sys/sys/pool.rs, sys/sys/lock.rs, sys/sys/mutex.rs, sys/sys/rwlock.rs, sys/sys/mplock.rs, sys/sys/percpu.rs, sys/sys/smr.rs, sys/sys/refcnt.rs, sys/sys/queue.rs, sys/sys/tree.rs, sys/kern/subr_tree.rs (the old runtime’s headers, and the lists and trees that leave with their last user); sys/machine/db_machdep.rs, sys/machine/conf.rs (the ddb glue and the configuration tables). |
sys/ holds hal/, kernel/, abi/, stand/, lib/ and the frozen amd64 tree, nothing else; cargo xtask unsafe-report: no bsd, kernel 0, abi 0, hal at its recorded floor, frozen with its count; bridges.toml empty; just ci-full and just diff-openbsd equal on arm64 |
| N10 amd64 homologation (decision 44; after N9) | The compile-and-fix pass of the frozen tree (sys/arch/amd64/, sys/hal/arch/amd64/, the amd64 implementations of sys/machine) against hal’s and kernel’s APIs at N9; the amd64 halves N4 and N7 deferred: cpu, clock, trap, syscall entry and copy into hal; bus, autoconf and the ACPI glue into hal, the amd64 drivers of sys/arch into sys/kernel/dev/; the module window of decision 37 revisited; the open amd64 performance of decision 42 (pagefault +49 %, the pmap side of the fault, fileread’s spread); the deferred-to-N10 LZ records applied |
EMIBSD_ARCHS=amd64,arm64 just ci rc=0 with the recipes’ amd64 expectations unchanged; just ci-full with the amd64 installer recipes (smoke-install-amd64, smoke-install-boot-amd64, smoke-install80-amd64) and cd80.iso; just diff-openbsd on amd64 with the same expected.toml, 0 unexpected; just bench amd64 against docs/bench/n3-perf-after-amd64.toml and n3-before-amd64.toml, every ratio at or under its value before N3 or a reason written per measurement; frozen 0 files; cargo xtask hal-check under hal_ceiling; no deferred-to-N10 record open |
| N11 Verification | The measurement gaps deferred on 2026-10-17 after the comparison with DARPA TRACTOR: the semantic count of unsafe operations (raw-pointer dereferences, calls to unsafe fn, mutable statics, union fields) beside the syntactic one in cargo xtask unsafe-report; the six-dimension idiomaticity rubric (project structure, ownership and data flow, errors and unsafe, types, control flow, API and docs) as the reviewer’s scorecard, the score in the commit body; an organic and hidden battery, OpenBSD’s regress through diff-openbsd (widening the reference clone is the user’s decision); decision 8’s renames (RawRwlock, the plain names for the owning types); -accel hvf for the arm64 VMs tried once through xtask and recorded |
unsafe-report prints operations and keywords; every module redesigned from N11 on carries its six scores; the regress battery runs under diff-openbsd with its own expected.toml and 0 unexpected; the hvf result (works or not, and why) in docs/ARCHITECTURE.md |
| N12 Performance | What decision 42 left open, on both archs: the QEMU profile of the fault and of fork (the pmaps in hal, the page system’s locks under TCG, the trap and the TLB shootdowns), the lookups beyond the C that the review listed for sys/kernel/vm/map.rs, zero_page/copy_page over typed pages; whether the smokes’ kernel moves to the release profile is the user’s question |
just bench on both archs: every ratio at or under its value before N3 (docs/bench/n3-before-<arch>.toml), or a written reason per measurement; Bench: on every commit (decision 43); no new unsafe, no check relaxed |
Every exit also requires:
- each file of the scope has left
bsd: redesigned intosys/kernel/(or moved intosys/abi/orsys/hal/where the row says so) and[[dropped]]inlineage.tomlwithabsorbed by ..., or a 0-unsafecall-site adapter listed as such (docs/ZERO_UNSAFE.md 0.3, 0.4); just check, onejust ciandjust diff-openbsdequal, with the same expected list;just ci-fullat every close but N2’s (decision 24: N3’s close runs it for both); on arm64 only from N4 to N9 (decision 44), on both archs from N10 on;cargo xtask lz drift --securityempty, and the numbers in the closing commit.
Since 2026-10-16 the goal is zero unsafe in the kernel: the crates sys/kernel/ and sys/abi/
forbid it at their roots, sys/hal/ holds the only unsafe, and bsd is emptied
(docs/ZERO_UNSAFE.md section 0); a row’s scope resolves as /progress resolves it, the most specific path winning. A header of
sys/sys/ belongs to the row whose C fills its fields (proc.h to N4, vnode.h to N5),
so it is redesigned with its owner into kernel; the constant headers nobody fills move into hal, inherited, and kernel and abi name them through hal (decision 27: 18 at N2, types.rs at N3.1’s first commit, into sys/hal/sys/types.rs and sys/hal/vm/param.rs).
Rolling: LZ sync
After every LZ milestone close and every LZ pin bump, and before any N is marked met:
cargo xtask lz drift --security, one lz-sync.toml record per LZ commit, lz-sync: commits,
then lz: bump pin to <12-hex> with the user’s OK (.claude/rules/lz-sync.md). The ABI grows
only this way (M16’s and M17’s devices, syscalls and sysctls arrive as applied records).
Adding or changing a milestone
State the subsystem it claims, the LZ modules it derives from (they turn redesigned in
lineage.toml), and one mechanical exit criterion with numbers from the tools. Update
docs/STATUS.md when a milestone starts and when it closes; docs/JOURNAL.md gains its section
at the close.