Journal
One section per milestone of docs/ROADMAP.md, N0 onwards: what went well, what failed, the
idioms that took more than one attempt, the rules that had to be corrected, and reproducible
numbers. Only what the history shows; Effort and Time are left for the user.
Origin
This repository is EmiBSD.LZ’s history up to lz-origin (f5985f1d055a, docs: M16b met,
2026-10-08), the faithful port of OpenBSD: milestones M0..M16b, 542 commits, 1860 Upstream:
trailers, 1042 ported and 140 wip entries of its ports.toml. Their journal is LZ’s
docs/JOURNAL.md at that commit (git show lz-origin:docs/JOURNAL.md). From here on the port
continues in LZ and this journal records the native system.
N0 Bootstrap
Boundary: the docs: N0 met commit. Range lz-origin.. that commit (f5985f1d055a onwards).
- Went well: the governance,
lineage.toml1:1 fromports.tomlandcargo xtask lzheld up: every check that failed did so on its own tooling, never on the kernel. The firstjust ciin this tree passed all 67 smokes on its first run. The author’s ISC block reached 1200 files through LZ 44edb2c with the lz-sync machinery the milestone built, and 1017 of the 1028 inherited modules matched LZ byte for byte with the native script as well. - Failed: three tooling bugs from the RCS-ident drop (3a7e877), found by the first runs:
lz checkkept the blank line an ident left (15 false “differs from LZ”),gen-syscallsstill wrote the ident line, and ten modules of licence-less C files carried an empty LICENSES zone withoutlicense = "none". Stopping ajust cimid-build corrupted cargo’s incremental cache (undefinedanon.*symbols at link);cargo clean -p bsdper target fixed it. Runningjust comp, a subagent and the smokes at once took the load to 50 on 11 cores. The author’s block was first planned “after” the notices, then “first”; and the push the user needed at once waited on ajust ci: an explicit push order is now pushed at once. - Idioms: none settled (N1 settles the first).
- Rules: the governance rewrite (
CLAUDE.md,.claude/rules/), decisions 16 to 20 (docs/PHASE2.md); the authorship rule of 2026-10-09 (the author’s ISC block first in every.rs), first planned as a milestone N0b, then absorbed from LZ 44edb2c and pushed at once at the user’s request; the milestone was dropped once it was in. - LZ sync:
f5985f1d055a..44edb2c8323e, 2 commits (the logo, the author’s block): cherry-pick 1, cherry-pick-conflicts 1, reimplemented 0. - Numbers (at
lz-originunless said):cargo xtask unsafe-report: kernel 8198 blocks, 1041 fn, 768 impl, 27 trait, 323 other, total 10357; tests 816 more. After libz’s redesign (N1, already in): 10355.cargo xtask lz status: 1028 modules, all inherited, 35 extras, 0 dropped (git ls-filesofsys/andtools/: 1200.rs); at this commit 1011 inherited, 4 adapted, 13 redesigned.just ci: 67 smokes on both archs, host tests 2605 passed, 0 failed, 229 ignored, 1150 s.just ci-full: green, 1655 s: the 67 smokes on-smp 4, the installer end to end on amd64, arm64 and arm64 ACPI (install 141, 153 and 228 s, each then booted).just diff-openbsd: amd64 and arm64, 102 steps each, 99 equal, 3 expected (fifofs, thekern.ostypebranding, core dumps), 0 unexpected.just comp: both archs, the second (resumed) run 465 s.- LZ sync: 2 commits absorbed (see Rules);
lz drift --strict0 open. - Commits:
git log --oneline f5985f1d055a..this commit.
Effort: (user)
Time: (user)
N1 Leaves
Boundary: the docs: N1 met commit. Range 8e090e1.. that commit (N1’s libkern and libz
commits, 33785a0..d6cfca6, landed just before N0’s close and belong here).
- Went well: one subagent per area (libkern on Sonnet; libz, crypto-hash and crypto-cipher on
Opus), each in its own worktree with a brief and a handoff note, and an independent Opus
reviewer per security-sensitive branch. Every output stayed bit-identical: zlib’s byte-for-byte
vectors, the standards’ known-answer vectors (FIPS 180-4, RFC 1321, 2104/4231, 7693, FIPS 197,
SP 800-38D, RFC 8439, 7748, FIPS 81, RFC 2144) and
diff-openbsdequal on both archs after every branch. libz lost its twounsafe(fallible allocation throughVec::try_reserve_exactandBox<[T; 1]>). The reviews found an inherited gap and closed it: LZ’s cryptosoft “wiped” key schedules and HMAC states by storing an enum tag, leaving keys in freed session memory; every schedule and keyed context now zeroes itself on drop, as the C’sexplicit_bzerodoes. - Failed: the first reviewer’s trailer was pasted with its heading (“Security-Review: (trailer
text) Security-Review: …”) into the seven published crypto-hash commits (8abd30a..417452c);
left as published. A subagent’s rebase left intermediate commits unbuilt; a per-commit
cargo checkon host, amd64 and arm64 proved all twelve. The cipher reviewer approved with four minor findings (a zero-round AES panic, three contexts without a wipingDrop, a stack copy of softraid’s mask-key schedule,PartialEqon a key schedule); fixed in five commits and re-reviewed. The worktrees started atlz-origin, notmain: every agent reset tomain. - Idioms: three rows in docs/IDIOMS.md: a C status code ->
Result(libz), a hash/MAC context with free functions -> a type withnew/update/finalize(&mut self)that wipes in place (finalize(self)would wipe a copy), and a key schedule wiped by its ownDrop, no longerCopy. - Rules: none changed; the brief for crypto (security-review.md applied in full) is the pattern for N8.
- Numbers:
cargo xtask unsafe-report: lib/libkern 10 -> 10, lib/libz 2 -> 0, crypto 5 -> 5; kernel total 10357 -> 10355.cargo xtask lz status: 37 redesigned, 26 adapted (call sites only), 965 inherited; 111[[module.fn]]rows. libkern 5 redesigned, libz 10 (adler32 and crc32 for their tests), crypto 22 (spr, sk and podd stay inherited: constant tables).- Host tests (
just ci): 2605 -> 2695 passed, 0 failed, 229 ignored; libz alone 85 -> 123 (the user found 90 too few: dictionaries, the zlib and raw wrappers, every truncation and single-bit flip of eight streams, byte-at-a-time streaming with every flush mode, inflateBack, 60480 init parameter combinations, deflateBound, resets, checksums; no bug). just ci: 67 of 67 smokes after each branch (1131-1404 s); at the close smoke-uhci failed once on arm64 with the inherited EDK2 UhciDxe ASSERT before the kernel, then passed on both archs, as did the checks after it;just ci-full: green in two parts: 66 of 67 smokes on-smp 4;smoke-softraidhung 2 h 30 min in macOS_dyld_start(xtask never reachedmain, QEMU never started, so its own time limit never ran), was killed and passed alone on 4 CPUs; then the checks and the installer end to end on amd64, arm64 and arm64 ACPI (install 98, 155 and 162 s, each booted; 554 s).just diff-openbsd: amd64 and arm64, 102 steps, 99 equal, 3 expected, 0 unexpected.- LZ sync: no LZ commit after 44edb2c;
lz drift --securityempty. - Commits: 30 (
git log --oneline 8e090e1..this commit, plus 33785a0..d6cfca6).
Effort: (user)
Time: (user)
N2 Foundations
Met 2026-10-16 (under way since 2026-10-10). Boundary: from lz: bump pin to 3c62ede68802
(7eee150) to the docs: close N2 commit.
- LZ sync before N2 (the timing rule: N2 redesigns
queue/tree, used by 222 files, so LZ’s M16 came first):f5985f1d055a..3c62ede68802, 99 LZ commits, every one recorded inlz-sync.toml(cherry-pick 39, cherry-pick-conflicts 30, reimplemented 1, the rest records of merges or not applicable), 122 new inherited modules,rnd.rsadapted to the N1 crypto API (reviewed). Wall time 01:48-05:28 on 2026-10-10 (3 h 40 min,git log --format=%ci acf8449..956c81b), four chunks by subagents and one integrator. Closed by the pin bump (7eee150), the unsafe re-baseline of the 14 rows the inherited drivers moved (c443f75, the user’s rule inunsafe-budget.md),just jobs=3 circ=0 in 1643 s (84 of 84 smokes in 17 min 43 s; host tests 2893 passed, 0 failed, 294 ignored) andjust diff-openbsdrc=0 (amd64 and arm64, 102 steps, 99 equal, 3 expected, 0 unexpected) at 154ad1d. - v1 (2026-10-10 to 2026-10-16): the N2 of “core structures” (the safe
Adapter, the typed slots ofFile.f_dataand the knote hook, theforbidratchet insidebsd). Replanned on 2026-10-16 as N2 Foundations (docs/ZERO_UNSAFE.md section 0, decisions 20-24): the kernel is a crate with nounsafe, so the typed-slot branches were not merged (0.6). - Went well: four crates where there was one, built on both archs at the first compile but one
;(just buildrc=0 after one fix; nothing of the runtime, the loader, the module or pipe had been compiled before, decision 23). The runtime inhal(N2.3),KernelApiwith the ELF loader andabi-openbsd.ko(N2.4a), crypto intokernel(N2.5) and pipe(2) as the first subsystem by the new path (N2.6) arrived as three stacked branches, each reviewed before the close, merged intomainonce (decision 31). The reviews found real defects before any build: two soundness holes inabi_pod!(unions, fields left out), boot(8)’s record not range-checked, pipe’sO_NONBLOCKread once instead of at every check, and the module window’s one-shot link not enforced.diff-openbsd’s newpipeset was equal on both archs at its first run. - Failed: the first
just ciattempt stopped insmoke-build:cd-iso-amd64refusedcd80.isobecause ISO 9660 storesabi-openbsd.koasABI_OPENBSD.KOand libsa’s cd9660 (no Rock Ridge) looks forABI-OPENBSD.KO; xtask’s model of that lookup caught it, no smoke had booted. 1ae6774d leaves the module off the CD (decision 29’s fallback); the module’s name was decided the same night,abi_openbsd.kofor N3’s first mechanical slice (decision 38; the user’s standing authority of the night) (abi_openbsd.ko, a-->_mapping in boot(8)’s cd9660, or relaxed ISO names). The second attempt booted: 7 of 85 smokes, every amd64 kernel panicked on the jump into the module (panic: trap type 6, code=11): amd64’skernel_mapdirectories carry NX as OpenBSD’spmap_alloc_levelsets them, arm64 has no PXN on its tables. A debugger found the cause (its first hypothesis, the direct map, was wrong); the user chose a window of the kernel’s L4 slot (decision 37), redesigned in 8b2ed4e1, reviewed, fixed in 2ff6d854 (the link at most once). The third run was green.smoke-fb’s “monitor.sock: No such file” in the second run was the same panic, hidden by xtask returning before it printed the transcript (tools/xtask/src/boot.rspoll_monitor, a follow-up). - Idioms that took several attempts: the lists and trees moved into
haland back (their firstkerneluser bringshal’s own);hal::errno::Errnointerim, then folded intohal::sys::errno(decision 36); the memory bridgesunsafe traitthen safe with a checked trust boundary (decision 30);KernelApityped inkernel, then inhalso the module links no kernel code; decision 13 asKlist::owned(). - Rules corrected: decision 31 (the pipeline’s repetitions cut: rules blocks in prompts, host
checks once by the integrator, one merge into
mainper milestone withjust buildandcargo check -p bsd --testsafter it); the commit hook now seesPATH=...prefixes and thecdtarget (44e48b6f);hal-checkcounts the sitesunsafe-reportcounts (7deb1419: it had countedtestutil.rs, 41 against 35), sohal_ceilingfell to 35 (f46a7e48);gen-syscallswritessys/hal/sys/syscall.rs(db35b0db). - Numbers:
cargo xtask unsafe-report(crates): before N2kernel0,abi0,hal0,bsd10,604, boot 621; at the closekernel0 (23 files),abi0 (4),hal35 (72, ceiling 35),bsd10,583 (1,103 files; legacy 8,747 -> 8,726,forbid306 -> 320; kern 1,146 -> 1,126, sys 328 -> 327, arch/amd64 863 -> 862 -> 863 with the user’s raise for decision 37), boot 621.cargo xtask lz check: 1,156 modules (1,058 inherited, 40 adapted, 58 redesigned), 157 fn rows, 60 extras; 12 bridges open (N3 2, N4 7, N7 1, N9 2).- Host tests (
just test): 3,010 passed at main f5101f5b -> 3,229 passed, 0 failed, 301 ignored at the close. just jobs=3 ciat 5716e767: rc=0 in 1734 s; 85 of 85 smokes in 22 min 36 s, one EDK2 UhciDxe retry (smoke-uhci, the known firmware flake). After the review fix 2ff6d854 (merged at 17fb1cb1):just buildrc=0 andsmoke-boot,smoke-abi,smoke-efiboot,smoke-fbrc=0 on both archs (abi-openbsd.ko: 331 syscalls, 2 native,abi-ok-42,abi-pipe-43); no second fullci(the full run covered the tree one commit before).just diff-openbsd: rc=0 in 79 s; amd64 and arm64, 107 steps, 104 equal, 3 expected, 0 unexpected (thepipeset new).cargo xtask lz drift --security: empty; no LZ commit after the pin.- Commits: 196 after the pin bump (176 without merges;
git rev-list --count [--no-merges] 7eee150..this commit’s parent), 140 of them on 2026-10-16.
Effort: (user)
Time: (user)
N3 Memory
Met 2026-10-18 (under way since 2026-10-16). Boundary: from docs: close N2 (7775163b) to
the docs: close N3 commit.
- Performance baseline before any uvm slice is merged (
just bench, docs/ARCHITECTURE.md “bench”; the brief of 2026-10-17). EmiBSD is the smokes’ MP debug kernel (opt-level = 1, kept so by the user’s decision of 2026-10-17, the caveat documented) with the ramdisk andabi-openbsd.ko; OpenBSD thediff-openbsdsnapshot; both-smp 4,-m 512M, TCG, one after the other, the order alternating. Ten runs per side, each boot running a discarded warm-up pass before the recorded one; per measurement and side the worst sample is left out, and the median, minimum and spread(max - min) / medianare those of the nine kept; the ratio is EmiBSD / OpenBSD and is refused (spread) when either spread exceeds 15 %. The reports, all ten samples and the discarded one’s index included, are kept asdocs/bench/n3-before-<arch>.tomlforbench compare, which refuses a report of another method. Each run also records the host’s one-minute load average at its start and end and its five busiest processes but QEMU (cd47fb56,[[host]]in the report), a diagnostic that changes no sample. - Superseded baselines, each run on a kernel whose
sys/equalsmain20035fd0:- a49170f2 (22:01-22:16 UTC, five runs, one pass per boot,
forkexit500 ops,forkexec200, a 64 MiB file): six of fourteen ratios refused, most of the spread from the first sample after boot (amd64’s first tcpbench run, 483.8 Mbps against 629.6 to 716.9; arm64’s firstforkexit, 2.47 ms against 1.70 to 2.03). The user chose a warm-up pass per boot, a 10 s tcpbench warm-up and more work per sample (forkexit2,000 ops,forkexec800, a 256 MiB file): b7e474e3. - 6c971a45 (22:27-22:50 UTC, five runs): arm64 seven of seven, amd64 one of seven, EmiBSD’s
first run (booted the moment
just userlandended) 1.5 to 3.5 times slow in every measurement. 580cd038 (23:10-23:19 UTC, amd64 again alone, a quiet minute first): one of seven again, this time OpenBSD’s second run the outlier (forkexec6.07 ms against 1.85 to 2.07). With five runs, one disturbed boot sets the max-min spread on either side; the user chose ten runs a side with the worst sample of each left out (fc622fed). - d55bd02a’s amd64 run (2026-10-18 00:03-00:18 UTC, ten runs): five of seven, EmiBSD’s
pipe(16.0 %) andfileread(26.0 %) refused, slow in runs 1 to 3 and fast after. A debugger traced that step to host CPU contention, not the kernel, the files or the run index: VS Code’s cpptools in bursts of 350 to 430 % and macOS’s idle-time daemons (photoanalysisd, mediaanalysisd and others, about a core each); threeyeshogs on the host reproducepipeat about 70k ns, its slow level. Its clean diagnostic run C10 (01:17-01:28 UTC, same tool and method) was made without tcpbench, so it is not a baseline. The user had amd64 run again in full with the host diagnostic on (dd0f5a75).
- a49170f2 (22:01-22:16 UTC, five runs, one pass per boot,
-
The baseline in force, arm64: the fc622fed run (2026-10-18 00:18-00:44 UTC; the whole run of both archs 2,446 s), whose
sys/is identical tomain20035fd0 (git diff 20035fd0 fc622fed -- sysis empty; the kernel’s SHA-256 changes from one commit to the next only becausesys/build.rsstamps the last commit’s time), under the machine lock with nothing else running and a quiet minute (no cargo, rustc or qemu inps) before the first boot.arm64 (kernel SHA-256 48c08004…):
measurement unit EmiBSD median (min, spread) OpenBSD median (min, spread) ratio boot s 8.33 (8.28, 2.9 %) 31.67 (30.99, 3.5 %) 0.263 pagefault ns/op 75784 (74836, 2.7 %) 122733 (121450, 2.5 %) 0.617 forkexit ns/op 1686982 (1640915, 4.1 %) 2214634 (2102787, 11.3 %) 0.762 forkexec ns/op 4900752 (4801397, 4.1 %) 6621370 (6432361, 6.6 %) 0.740 pipe ns/op 168655 (163968, 9.3 %) 177063 (175689, 7.0 %) 0.953 fileread ns/op 702139 (616961, 13.0 %) 888372 (871559, 4.1 %) 0.790 tcpbench Mbps 108.3 (102.7, 6.9 %) 87.7 (87.2, 1.2 %) 1.235 Discarded (run, value): boot EmiBSD 2 (8.62), OpenBSD 9 (32.16); pagefault 8 (92261), 4 (124531); forkexit 6 (2077130), 2 (2533415); forkexec 3 (5075677), 2 (6891198); pipe 3 (182330), 3 (188904); fileread 6 (710295), 1 (1078541); tcpbench 3 (102.1), 10 (86.9).
-
The baseline in force, amd64: the dd0f5a75 run (2026-10-18 01:45-02:01 UTC, 935 s of benchmark, 1,016 s with the build and the quiet minute),
sys/identical to 20035fd0, under the machine lock, a quiet minute before the first boot, the host diagnostic on.amd64 (kernel SHA-256 264087ac…):
measurement unit EmiBSD median (min, spread) OpenBSD median (min, spread) ratio boot s 17.35 (17.13, 2.4 %) 21.64 (21.49, 1.5 %) 0.802 pagefault ns/op 7956 (7793, 13.8 %) 10646 (10267, 8.8 %) 0.747 forkexit ns/op 459233 (432332, 7.4 %) 786786 (768463, 3.7 %) 0.584 forkexec ns/op 1214616 (1186903, 6.5 %) 1842307 (1770422, 8.6 %) 0.659 pipe ns/op 68504 (59989, 14.4 %) 73197 (67555, 11.3 %) 0.936 fileread ns/op 280928 (224734, 24.8 %) 329772 (276228, 19.7 %) spread tcpbench Mbps 767.3 (761.3, 1.2 %) 590.4 (531.7, 14.0 %) 1.300 Discarded (run, value): boot EmiBSD 1 (17.72), OpenBSD 1 (22.59); pagefault 9 (9087), 5 (11664); forkexit 9 (478218), 4 (817915); forkexec 1 (1387212), 7 (1971466); pipe 4 (70121), 7 (85702); fileread 1 (317045), 7 (342389); tcpbench 4 (728.2), 6 (517.0).
Thirteen of fourteen ratios print. The refused one is amd64’s
fileread, over the limit on both sides. EmiBSD’s samples sit at 277 to 317 us but for runs 2 and 5 (224.7 and 234.1); OpenBSD’s at 276 to 317 in runs 1 to 4 and 330 to 342 in runs 5 to 10. The host rows do not line up with either: cpptools ran at 64 to 100 % from OpenBSD’s run 1 to EmiBSD’s run 5 (load 2.97 to 4.14), the quiet runs after it show 5 to 30 % for the busiest process (load 2.16 to 3.09), and EmiBSD’s two fast runs both fall inside the busy part, OpenBSD’s fast runs too. As recorded:run EmiBSD fileread host at its end OpenBSD fileread host at its end 1 317045 (discarded) load 2.53 -> 3.50; WindowServer 14 %, Code Helper 14 % 276228 load 3.50 -> 3.34; cpptools 64 % 2 224734 load 2.97 -> 3.46; cpptools 91 % 285099 load 3.34 -> 2.97; cpptools 82 % 3 280702 load 3.46 -> 4.14; cpptools 95 % 289801 load 4.14 -> 3.76; cpptools 84 % 4 277067 load 3.36 -> 3.54; cpptools 100 % 317010 load 3.76 -> 3.36; cpptools 86 % 5 234109 load 3.54 -> 3.64; cpptools 85 % 333894 load 3.64 -> 2.91; Creative Cloud 5 % 6 283726 load 2.61 -> 2.73; Creative Cloud 6 % 329944 load 2.91 -> 2.61; Creative Cloud 6 % 7 294477 load 2.73 -> 2.68; WindowServer 28 % 342389 (discarded) load 2.68 -> 3.09; WindowServer 17 %, firefox 17 % 8 288055 load 2.61 -> 2.72; socketfilterfw 30 % 341235 load 3.09 -> 2.61; Creative Cloud 6 % 9 283475 load 2.72 -> 2.54; WindowServer 9 % 329772 load 2.54 -> 2.16; Creative Cloud 6 % 10 280928 load 2.43 -> 2.74; cfprefsd 10 % 338651 load 2.16 -> 2.43; socketfilterfw 8 % The limit was not tuned and the method not changed (the user’s decision); the cause of
fileread’s spread on amd64 is open. The N3 close runs the same command and carriesBench:lines fromcargo xtask bench compareagainst these reports, for what prints. - The close (2026-10-18): N3’s branch (925cce59, 152 commits from main 6da2ef08, 141 without
merges;
git rev-list --count [--no-merges] 6da2ef08..925cce59) merged into the candidate at d612f9c5 with no conflict, n3/bench (60700aff) at 29d66500 with one (OWN_PROGRAMSin tools/xtask/src/userland.rs: mmapstress and bench, both kept). Wall time of the milestone: 2026-10-16 23:01 (621a179d) to 2026-10-18 09:49 (925cce59), about 35 h, up to four agents at once. Six slices: N3.0 the module’s name; N3.1hal::Pmapand the two pmaps; N3.2kernel::vm::km; N3.3 the page system; N3.4 the owners and the fault; N3.5 the map and the rest, the flip that deletedsys/uvm/; decision 40 between N3.3 and N3.4. - Went well: every slice reviewed by an agent that did not write it before it was merged into
the next (25 Security-Review paragraphs, carried by d612f9c5’s body), and the reviews found
what no build would have: the arm64 backend’s PTE cleared before its pv lock, a pted 8 bytes
short of the C’s (visible in
kern.pool), the static-entry arena refilled throughkernel_map(a deadlock while cold), an unprivileged panic on unwiring an entry without an amap (K1, reachable throughmunlock(2)), a use-after-free window in an owned tree’s removal,with_vmspace_handlereachable from safe code, and four slips in OpenBSD’s own C (uvm_fault.c:1468-1485, confirmed;uvm_aobj.c:428-432;uvm_pager.c:292withuvm_page.c:1238;uvm_map.c:2214-2221, confirmed; for the user, never upstream). The close’sjust checkwas green at its first run: the compile-and-fix pass found nothing, since every flip had booted its own smokes (asecurity-review.mdarea).just ci-fullran for the first time under v2 (decision 24, for N2 and N3) and was green at its first run, the installer end to end on both archs included. - Failed: the first integration checkpoint (54f48bd4, N3.1 and N3.2 only) booted 0 of 85
smokes: amd64 under Limine underflowed
uvm_map_setup(4b’s clamp of the kernel window, the HHDM reaching slot 258) and both machines panicked allocating with Box beforekmeminit; fixed in 74eabc4d, 6e996212 and 799ca7ad, then 85 of 85. N3.4b stopped before its flip: the owners could not move before the fault held their witnesses, so the planner re-planned (N3.4c-e, N3.6 folded in); N3.5 S1-R1 stopped onhalpieces the kernel could not reach (arc4random,sched_pause), added in 7f2712c6. Two worktrees were created inside the LZ clone once (the session’s cwd), written nothing; the rule is never tocdintoreference/. The close’s firstjust cistopped inteston the inheriteddev::ic::pckbcflake (below). - Idioms that took several attempts:
MappedBytes, from a barePaddr(the review: safe code could alias&mut) to a claimed, busy, token-only view (three reviews); the witness-carrying fault (RwWitness, a witness consumed by a sleep, a&'v mut Option<T>slot to return a witness borrowing a value);Guarded::get_mutneeding witness parts (Divisible::split) for two cells of one lock; owning intrusive containers that find and remove by key and re-read the secondary count after adapter calls; the entry’s handle cells needing their ownAtomicBoolwindow under a read lock (smoke-mp found it);NSTATIC_POOLS4 -> 8 -> 16 as more pools moved beforekmeminit. - Rules corrected: decision 40 (one MP kernel); decision 41 recorded as studied; decision 30
extended to
MappedBytes; the redesigner’s memory trimmed under its read limit (a043de71); the bench recipe spells its features as the smokes do since decision 40 (818ed2f5). - Numbers:
cargo xtask unsafe-report(crates), main 20035fd0 -> the close:kernel0 (23 -> 48 files),abi0 (4),hal35 -> 100 (72 -> 108 files;hal_ceiling110, the user’s raise a2ec4145),bsd10,583 -> 10,196 (1,103 -> 1,069 files; legacy 8,726 -> 8,501, core 2,513 -> 2,416,forbid320 -> 348 of 1,218 modules; rows arch/amd64 863 -> 773, arch/arm64 943 -> 881, arch/host 38 -> 35, machine 85 -> 79, kern 1,126 -> 1,117, dev/pv 249 -> 246, sys 327 -> 328 with the user’s raise 6da2ef08, uvm 215 -> gone), boot 621.cargo xtask lz check: 1,156 -> 1,170 modules (1,058 -> 937 inherited, 40 -> 123 adapted, 58 -> 110 redesigned), 157 -> 594 fn rows, 60 -> 68 extras; bridges 12 -> 21 open (N4 13, N5 2, N6 1, N7 2, N9 3).- Host tests (
just check): 3,229 -> 3,775 passed, 0 failed (bsd 2,396, kernel 308, hal 292, abi 9, xtask 260, libz 123, libkern 26, the boot loaders’ 57). just jobs=3 ci: the first run (10:17) stopped inteston the inherited pckbc flake (below); the second (10:20-10:43, 1,396 s) passed 86 of 86 smokes in 18 min 32 s, no retry, and stopped atcheck-drift: the applied record of LZ 602141098d1d namedsys/uvm/uvm_init.rs, deleted by N3.5’s flip (614d65af points it atsys/kern/vm_bsd.rs, as 27432aa6 did for the pmap at the checkpoint); the steps after it rc=0.just jobs=3 ci-fullat 614d65af (decision 24, N2 and N3): rc=0 in 2,248 s, itscion four CPUs (86 of 86 smokes in 21 min 18 s, no retry) and the eight installer recipes (smoke-install-amd64106 s,-arm64164 s,-arm64-acpi165 s, the foursmoke-install-boot-*,smoke-install80-amd6475 s,-arm64123 s).just compfirst (425 s, seeded from the main checkout).just diff-openbsd: rc=0 in 77 s; amd64 and arm64, 107 steps, 104 equal, 3 expected, 0 unexpected (expected.tomlunchanged).-
just benchat 614d65af (11:37-12:20, 2,573 s with the build and a quiet minute; the same command and method as the baselines, under the machine lock, nothing else running): fourteen of fourteen ratios print. The reports are kept asdocs/bench/n3-after-<arch>.toml.cargo xtask bench compare docs/bench/n3-before-<arch>.toml target/bench/<arch>/report.tomlgives theBench:lines of the closing commit (EmiBSD medians; a row prints when it moved by more than the larger spread):measurement unit amd64 before -> after ratio to OpenBSD arm64 before -> after ratio to OpenBSD boot s 17.35 -> 17.22 (within spread) 0.802 -> 0.804 8.33 -> 8.65 0.263 -> 0.274 pagefault ns/op 7956 -> 12269 0.747 -> 1.184 75784 -> 81715 0.617 -> 0.661 forkexit ns/op 459233 -> 680755 0.584 -> 0.884 1686982 -> 2672794 0.762 -> 1.200 forkexec ns/op 1214616 -> 1820719 0.659 -> 1.034 4900752 -> 6868461 0.740 -> 1.075 pipe ns/op 68504 -> 69186 (within spread) 0.936 -> 0.942 168655 -> 171029 (within spread) 0.953 -> 0.922 fileread ns/op 280928 -> 322744 (baseline spread over 15 %) spread -> 0.973 702139 -> 879892 0.790 -> 1.001 tcpbench Mbps 767.3 -> 787.6 1.300 -> 1.313 108.3 -> 102.1 (within spread) 1.235 -> 1.143 N3 made the page fault and fork slower on both machines: amd64
pagefault+54 %,forkexit+48 %,forkexec+50 %; arm64forkexit+58 %,forkexec+40 %,pagefault+8 %,fileread+25 % (OpenBSD’s own runs moved by under 6 %). On the debug kernel (opt-level = 1) the kernel’s witnesses, counted handles and checked slices run unoptimised; where the time goes is not measured yet, and is the first question for N4’s bench. cargo xtask lz drift --security: empty; no LZ commit after the pin.- The flake of the close:
dev::ic::pckbc::tests::an_async_command_is_acked_byte_by_byte_then_freedpanicked atsys/kern/kern_timeout.rs:291(a nullprevincircq_insert_tail) in the firstjust ci, as once at N3.4e. By reading: pckbc’s testcontroller()takeskern_timeout::tests::LOCKbut never callstimeout_startup(), so it depends on an earlier test (hidkbd’s, kern_timeout’s) having initialised the wheel; both files are inherited, byte-identical to LZ, so the fix is the user’s decision. The second run passed.
Effort: (user)
Time: (user)
Decision 42: N3’s performance
Investigated 2026-10-18 after the close, before N4 (docs/ZERO_UNSAFE.md decisions 42 and 43).
Boundary: main e3cdf1d3 to the docs: commit of n3/perf-close. Three branches, 12 commits
without merges (git rev-list --count --no-merges e3cdf1d3..n3/perf-release 2,
..n3/perf-opt 10), merged at 8accf040 and 11eb98d3 with no conflict (disjoint files), and
the review’s fix c51cb2c2; git diff --shortstat e3cdf1d3 n3/perf-opt -- sys: 7 files, +1,939
-418 (sys/kernel/benches/vm.rs 1,021 of them).
-
The release diagnostic (eeb155ed,
just bench-release; the method of the baselines: ten runs a side, the worst left out, the 15 % limit). The MP kernel in the release profile at 20035fd0 (before N3, built at ef0888b7 with the flag picked onto it) and at eeb155ed (after), 2026-10-18 14:06-16:13 under the machine lock. Cell: EmiBSD median (spread), ratio to OpenBSD;spread= refused.measurement arch debug before debug after release before release after release/debug before after boot (s) amd64 17.35 (2.4 %) 0.802 17.22 (3.1 %) 0.804 16.81 (9.1 %) 0.772 16.79 (1.7 %) 0.780 0.969 0.975 pagefault (ns) amd64 7956 (13.8 %) 0.747 12269 (4.0 %) 1.184 6313 (5.0 %) 0.595 8550 (4.6 %) 0.821 0.793 0.697 forkexit (ns) amd64 459233 (7.4 %) 0.584 680755 (3.4 %) 0.884 394051 (6.4 %) 0.490 554141 (2.4 %) 0.700 0.858 0.814 forkexec (ns) amd64 1214616 (6.5 %) 0.659 1820719 (4.5 %) 1.034 999901 (7.7 %) 0.538 1431855 (4.3 %) 0.793 0.823 0.786 pipe (ns) amd64 68504 (14.4 %) 0.936 69186 (3.6 %) 0.942 53004 (3.0 %) 0.707 52262 (9.8 %) 0.709 0.774 0.755 fileread (ns) amd64 280928 (24.8 %) spread 322744 (2.8 %) 0.973 275107 (3.4 %) 0.802 273054 (21.6 %) spread 0.979 0.846 tcpbench (Mbps) amd64 767.3 (1.2 %) 1.300 787.6 (1.6 %) 1.313 1085.7 (12.5 %) 1.946 1191.4 (1.2 %) 2.004 1.415 1.513 boot (s) arm64 8.33 (2.9 %) 0.263 8.65 (1.5 %) 0.274 8.31 (2.1 %) 0.263 8.47 (3.3 %) 0.269 0.998 0.980 pagefault (ns) arm64 75784 (2.7 %) 0.617 81715 (2.3 %) 0.661 72791 (1.7 %) 0.589 78044 (7.4 %) 0.633 0.961 0.955 forkexit (ns) arm64 1686982 (4.1 %) 0.762 2672794 (2.5 %) 1.200 1572308 (1.7 %) 0.711 2225552 (3.5 %) 0.998 0.932 0.833 forkexec (ns) arm64 4900752 (4.1 %) 0.740 6868461 (2.8 %) 1.075 4436730 (3.8 %) 0.695 5918638 (3.9 %) 0.925 0.905 0.862 pipe (ns) arm64 168655 (9.3 %) 0.953 171029 (2.2 %) 0.922 161038 (4.6 %) 0.866 152366 (3.2 %) 0.821 0.955 0.891 fileread (ns) arm64 702139 (13.0 %) 0.790 879892 (2.1 %) 1.001 595441 (1.8 %) 0.678 813719 (2.3 %) 0.925 0.848 0.925 tcpbench (Mbps) arm64 108.3 (6.9 %) 1.235 102.1 (0.5 %) 1.143 105.5 (0.5 %) 1.189 107.2 (0.6 %) 1.226 0.974 1.050 The release kernel is slower after N3 in the same places as the debug one (pagefault amd64 +35 %, forkexit +41 % / +42 %, forkexec +43 % / +33 %, fileread arm64 +37 %), so the cost was the design, not
opt-level = 1. Kept as `docs/bench/n3-release-{before,after}-.toml`. Two runs were refused and run again alone: release-after amd64's first (`forkexec` OpenBSD spread 19.0 %, host daemons' bursts in runs 2-4; the re-run's `fileread` EmiBSD 21.6 %, one fast sample, reported as refused and not run a third time) and release-before arm64's first (`fileread` EmiBSD 17.6 %; the re-run 1.8 %). - Where the time went (737b7b5d,
sys/kernel/benches/vm.rs, docs/ARCHITECTURE.md “bench”): the realkernel::vmcode on the host, debug and release, profiled with sample(1) and xctrace with the inlined frames expanded. Against LZ’s legacy uvm at 20035fd0 (the same ops through a scratch copy, never a worktree) the fault was +29 % (dev) / +23 % (release) per page, a 70-entry fork +134 % / +168 %, its teardown +191 % / +163 %, a one-page map/unmap +121 % / +123 %. Share of each op without the host pmap and the doubles (dev / release), the pieces N3 added:- fork, exit, exec, map/unmap: entry looked up again by key (
ent,next_key; 12-15 tree searches per entry forked where the C follows pointers) 32-47 %; the owned tree’s per-node identity and bounds checks inside each search 10-24 %;MapState::owners()rebuilt by every free-list call 8-17 %;Guardedchecks 1.5-4.4 %; handle clones 2.5-3 %. - the anonymous fault: the page system (queue mutexes, lists, the per-CPU cache)
37-40 %, the same share as LZ’s; the fault’s own logic 24-30 %;
PoolArcclones 6-7 %;Guarded2-4 %,PG_HAL_VIEW3 %, the owned-tree checks 4-6 %. - Not seen by the host: the pmaps (the host pmap is a
BTreeMap, 45-58 % of a host fault), TLB shootdowns, the trap and syscall entry, TCG’s costs. Busy-window relocks: 0 samples.
- fork, exit, exec, map/unmap: entry looked up again by key (
- Phase 2 (n3/perf-opt, the redesigner; the user: no approval of the table needed), one
commit per piece, each with its host
Bench:lines (two runs per profile, the lowest median, against the previous commit; fork.big dev / release):- ac8b9ebf hold the map entry in hand instead of finding it again by key: 11834 -> 8804 / 10165 -> 6971 ns (forkexit 24374 -> 18064 / 20822 -> 14276, map.syscall 721 -> 556 / 600 -> 480).
- dd9dc059 compute a map operation’s owners once and lend them: 8804 -> 7214 / 6971 ->
6095 (keeping them in
MapStatewas measured and refused:Vmspace616 bytes, avmspplitem sizevmstat -mshows). - 9986eaac view each node of an owned tree’s walk once: 7214 -> 7374 / 6095 -> 5688.
- 6eb820e2 lend map entries’ handles where the write lock keeps them: 7374 -> 6956 / 5688 -> 5574.
- 9dab32dd inline
Guarded’s check and give the fault a typed witness: 6956 -> 6586 / 5574 -> 5458. - ee5bbc7b lend the element an owned tree’s insert linked (hal API, no user yet; hal tests 292 -> 293).
- f5caa6de go on from the entry an insert lends: 6586 -> 6225 / 5458 -> 5230.
Base 02f20ecb -> tip 98611cd0, dev / release: fork.big -47 % / -49 %, forkexit -43 % /
-44 %, forkexec -36 % / -38 %, exec -26 % / -29 %, map.syscall -30 % / -28 %, cmp.fork70
-49 % / -50 %, the faults -1 to -5 %; kernel tests 308 -> 310,
hal100. Refused by the user:hal::owned’s identity check once per search instead of per node (fork.big -8 % / -6 %); it stays per node.
- The review (an agent that wrote none of it) approved everything but one defect: ac8b9ebf
dropped
UVM_MAP_CLIP_END’s KASSERT fromuvm_unmap_removewhen the entry lies inside the range andmarkfreeis false (reference/openbsd-src/sys/uvm/uvm_map.h:111, uvm_map.c:1987- 1988), a tautology today but a check the C makes, and its body said “no check removed”. c51cb2c2 restores it under the C’s condition (diagnosticonly, as KASSERT; the host bench unchanged, -7 % to +0.2 %). - The close of decision 42 (n3/perf-close):
just checkrc=0 (344 s; host tests 3,775 -> 3,780),hal-check100 <= 100,abi-checkclean,cargo test -p xtask261,cargo check -p bsd --testsclean;just jobs=3 circ=0 in 1,330 s (18:34-18:56), 86 of 86 smokes in 18 min 15 s, no retry,lz drift --strict0 open;just diff-openbsdrc=0 in 76 s, 107 steps on each arch, 104 equal, 3 expected, 0 unexpected (expected.tomlunchanged). Noci-full: not an N close (it runs at N4’s). -
The final debug
just bench(the smokes’ kernel, the same method), against the N3 close’s reports (cargo xtask bench compare docs/bench/n3-after-<arch>.toml <report>) and the baseline before N3 (n3-before-<arch>.toml); kept asdocs/bench/n3-perf-after-<arch>.toml:2026-10-18 18:59-19:43 (2,643 s with the build and a quiet minute), kernel c51cb2c2, under the machine lock, nothing of ours else running; the host diagnostic shows macOS’s suggestd at 100 % of a core through the whole run (not ours, not touched). EmiBSD medians; ratio to OpenBSD in brackets; the change against the N3 close and against the baseline before N3.
measurement arch before N3 N3 close now now vs close now vs before boot (s) amd64 17.35 [0.802] 17.22 [0.804] 17.31 [0.810] within spread within spread pagefault (ns) amd64 7956 [0.747] 12269 [1.184] 11855 [1.157] -3.4 %, within spread +49 % forkexit (ns) amd64 459233 [0.584] 680755 [0.884] 597826 [0.770] -12.2 % +30 % forkexec (ns) amd64 1214616 [0.659] 1820719 [1.034] 1646020 [0.934] -9.6 % +36 % pipe (ns) amd64 68504 [0.936] 69186 [0.942] 69415 [0.948] within spread within spread fileread (ns) amd64 280928 [spread] 322744 [0.973] 318409 [0.959] within spread baseline spread over 15 % tcpbench (Mbps) amd64 767.3 [1.300] 787.6 [1.313] 781.6 [1.295] within spread within spread boot (s) arm64 8.33 [0.263] 8.65 [0.274] 8.61 [0.273] within spread +3.4 % pagefault (ns) arm64 75784 [0.617] 81715 [0.661] 81425 [0.657] within spread +7.4 % forkexit (ns) arm64 1686982 [0.762] 2672794 [1.200] 2706914 [1.225] +1.3 %, within spread +60 % forkexec (ns) arm64 4900752 [0.740] 6868461 [1.075] 6904563 [1.077] +0.5 %, within spread +41 % pipe (ns) arm64 168655 [0.953] 171029 [0.922] 171062 [0.918] within spread within spread fileread (ns) arm64 702139 [0.790] 879892 [1.001] 867319 [0.983] within spread +24 % tcpbench (Mbps) arm64 108.3 [1.235] 102.1 [1.143] 103.8 [spread] refused (15.5 %) refused Thirteen of fourteen ratios print. arm64’s
tcpbenchis refused: EmiBSD’s spread 15.5 % (OpenBSD’s 14.7 %; EmiBSD’s discarded run 3 at 58.2 Mbps, the nine kept 98.5 to 114.5, run 1 the fast one). The arm64 half was started again alone at 19:45 for it and stopped by the main session at 19:54, in its fourth run, to free the machine for the QEMU profiling the user asked for; the first run’s report is the one kept.Bench:lines (cargo xtask bench compare), against the N3 close:forkexit-amd64 680755 -> 597826 ns,forkexec-amd64 1820719 -> 1646020 ns; every other row within the larger spread or not comparable. So on amd64 the phase recovered 37 % offorkexit’s N3 regression and 29 % offorkexec’s, none of the page fault’s; on arm64 nothing moved. The host’s 36-49 % on fork and exec is thekernel::vmshare only: what N3 costs under QEMU is mostly elsewhere, in what the host doubles cannot see (the pmaps moved intohalat N3.1, arm64’s above all; the page system’s locks under TCG; the trap and the TLB shootdowns), and is the next question. - Left: the lookups beyond the C the review listed in sys/kernel/vm/map.rs (unmap: one search
per step and one after a clip;
uvm_mapent_mkfreeup to three anduvm_map_fix_space’s by key;uvm_map_mkentry’s first,fix_space’s from it and the final refind, ~8 % of a release fork; the clips’ anduvm_map_splitentry’s refinds; the owners recomputed insplitentry,clip_startand the merge; thenext_keywalks ofpageable_pgon,pageable_wire,clip_first,inherit,immutable,advice,extract,refill), a cursor over the state or a reshapedmkentrybeing the next step; the fault’s aref/object clones stay (they cross the sleeps with the map unlocked); the pmap side of the fault, which only a QEMU profile or a kernel-side counter can see (the user’s);zero_page/copy_pagetaking thePageMdwaits for typed pages. sys/kernel/benches/vm.rsusesstd(a host bench target,harness = false, never linked into a kernel image), which CLAUDE.md’sstdexceptions did not name; the review asked for the user’s ruling, and the main session relayed on 2026-10-18 that the user allowssys/*/benches/. The exception’s wording in CLAUDE.md and.claude/rules/rust-kernel.mdis the main session’s commit.
/insight: external opinions, the first smoke test (2026-10-19)
- The user’s decision of 2026-10-19: the session itself may ask external models for an opinion
when blocked, as in the performance case, and judge point by point what applies. Built as the
skill
.claude/skills/insight/(scripts/insight.py, stdlib only, OpenRouter with the user’s key from the environment;ledger.tsvfor the spend); pointers in CLAUDE.md, the third network use in.claude/rules/lz-sync.md, decision 43’s stop inre-engineering.md(“the stop reaches the user with hypotheses already verified”), the planner’s design fork in.claude/agents/planner.md. Three triggers only; it applies nothing. - The models, chosen by the user by OpenRouter link:
google/gemini-4-argon,qwen/qwen3.8-max-prime,~deepseek/deepseek-pro-latest. After the test Qwen was dropped (reasoning mandatory: HTTP 400 when disabled; twice Gemini’s price; two empty answers; the user’s doubt about its training lineage); the defaults are Gemini 4 Argon and DeepSeek Pro with reasoning off; a third family (OpenAI) waits for the user’s OK. - The smoke test: the brief of today’s chat feedback on
map.rs, rewritten to the template (question, the decision 42 numbers, the constraints, 12 line-numbered excerpts ofmap.rs, theOwnedTreeAPI anduvm_map.c, the answer format; 48.7 KB, 16.5k-21k input tokens: the bytes/4 estimator under-counted, now bytes/3). Three runs: reasoning off,low,high. Off: Gemini 8 claims, 8 correct, 2 the session did not have ($0.08, 71 s); DeepSeek 10/10, nothing new ($0.02, 12 s).low: Gemini 6/6 with 0 reasoning tokens (lost theHandlesfind, gained two implementation hints:pe’s free-tree removal hoisted inside thefpborrow inuvm_mapent_mkfree;uvm_map_fix_spacereturning the lent&VmMapEntrywhenmin == max); Qwen 10/10 ($0.15, 61 s).high: DeepSeek the same 10 claims ($0.04); Gemini and Qwen spent the whole 16k cap on reasoning and returned nothing ($0.40 for nothing). Reasoning added no finding; the structured brief is what made the claims checkable. The user then replaced DeepSeek and Qwen by GPT Sol (~openai/gpt-sol-latest, served asgpt-6.1-sol): reasoning off, 6 claims, 5 correct and one honest “no claim” (the excerpt ofuvm_unmap_removewas the guard-splitting wrapper, 3853-3875, not the loop inunmap_remove, 3880: GPT Sol was the only model to say so instead of inferring from the C; the template now asks for the inner function), nothing new, $0.06, 40 s. The whole test: $0.97. - Verified against the code and the C, all
correct: the per-entry refinds (next_key= find + next,map.rs:2216; theclip_*_atwrappers and their inner functions, 6407/6422 and 6444/6470;splitentry’sent(orig)at 4576 and theent(nkey)after the insert at 4645;clip_start’s chain, 6470-6493;mkfree’s five searches per freed entry, 3750-3770 plusFixFrom::Keyat 6761;mkentry’s loop and its finalent(&st.addr, addr)at 3468 afterfix_spacehad the reference lent). Two the session did not have:insert_gethas a user,uvm_mapent_addr_insert_parts(2348), so the ee5bbc7b “no user yet” note is stale;uvm_map_splitentry’s free-space half clonesHandlesinuvm_mapent_copy(2243-2246) and replaces them withHandles::default()at 4594: a refcount increment and decrement per handle on the mmap/fork carve where the C doesmemcpyandNULL. No claim was false and none a “regression” the C also has (the chat feedback of the same morning had one of each and one “not a regression”: the template, not the model, made the difference). What would need ahalAPI, the user’s decision: removal by reference or a cursor (take_bysearches again,owned.rs:705), and keeping a reference across a mutation. - Nothing applied. The items are N12’s (or N4.1’s where that slice rewrites the code), under
decision 43’s
Bench:lines, arm64 only; the QEMU-side arm64 regression (pmaps, trap) is untouched by all of them, as the brief said. - Three more cases the same day, one per trigger, the five models (the user: “uno solo no
ayuda mucho”, “considera también a los descartados”, reasoning on,
medium), $1.74: a design fork (A:kernel::timeoutowner-embedded vs wheel-owned, the N4.1 question, with the C, LZ’s port andhal’s owned API), a security review (B: commit 9dab32dd, the fault’s typed witness, a clean diff) and a trap (C: the fault’s handle clones, which cross sleeps). Verified against the code: Gemini 4 Argon 0 false in 27 claims over the four cases and 7 findings the session did not have; GPT Sol 0 false in 31 and 2; GPT Astra the same findings as Sol at five times the price and a blocked sign-off on B; Qwen one false claim in C (fault.rs:879relocks the amap onowned.is_some()without a map relock: “every post-unlock use is gated onlocked” is wrong) and two built on it; DeepSeek 0 false and 0 new in 38. The defaults stay Gemini + Sol. The system prompt nameduvm_map.cfor every brief (Sol and Astra answered “no claim about uvm_map.c” ten times each): fixed to “the C excerpt”. Spend for the four tests: $2.71. - What the three cases leave for the work, verified: (A, for the N4.1 planner)
OwnedListownsPoolBoxnodes (owned.rs:856-866), so an owner-embeddedTimeoutneeds a new non-owning intrusive list inhal(newunsafe);timeout_barrier’s stackstruct timeout(kern_timeout.c:510) cannot be a'staticnode in either shape (a pool node, or a cond ontctx_runningwithout a fake node);timeout_set_flagswrites without the mutex and overwritesto_flags(kern_timeout.c:300-303), so the native contract for “set while pending” must be chosen, not copied;CIRCQ_CONCAT(kern_timeout.c:781) has noOwnedListequivalent and the per-element owner id (owned.rs:888) makes a move O(n) unlesshalgrows a splice;timeout_del_barrierfrom the callback itself waits on its own context (kern_timeout.c:533-549); all five models recommend the wheel-owned shape. (C, for N12)uvm_fault_checkbuilds theEntryViewat 1162 and again at 1226 aftercopy_amap, so the COW path clones the handles twice (fault.upper-cowis that path); the resident path never unlocks the map, so the handles could be lent and cloned only beforeuvmfault_unlockall(the slow paths need them:anonget’s owned-page relock at 879,lower_io’sget_syncat 2060 and its cleanup at 2104-2110 even when the relock fails); the C re-readsuobj = pg->uobjectafter the I/O (uvm_fault.c:1641) where the Rust panics on an owner-tag mismatch (2105-2108), a deviation the module notes already keep. Nothing applied.