Skip to content

x86-64's boot map holds no page writable and executable - #1212

Merged
Japabu merged 3 commits into
mainfrom
boot-map-wx
Nov 11, 2026
Merged

Japabu merged 3 commits into
mainfrom
boot-map-wx

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The loader's x86-64 boot map mapped the kernel's text read-only and every other page writable, and every page executable: bit 63 is execute-disable only under IA32_EFER.NXE, and the loader switched to the map from firmware's state. The boot CPU ran on that map until mm::init, and every AP from its trampoline until ap_entry loads the kernel's tables (issues/x86-64s-boot-map-is-writable-and-executable.md). AArch64's boot map was already clean.

Confirmed on main before changing anything: lib/bootmap/src/x86_64.rs block() set no bit 63 and said why in its own comment.

What it does now

The map is built once with its final permissions; there is no later fix-up pass. A leaf is either code (read-only, executable) or data (writable, execute-disabled), and both halves are in force in Ring 0 from each CPU's first instruction under the map.

            before                      now
text        R  X                        R  X
0x8000      RW X   (AP trampoline)      R  X
switch page RW X   (in the loader)      R  X
all else    RW X                        RW -
  • lib/bootmap: Access::Data sets XD on x86-64. Plan::new takes up to two 4 KiB code pages beside the kernel's text; the 2 MiB page around each is split, so its neighbours stay data.
  • Loader: the switch is an eight-byte stub of its own, 16-byte aligned so it lies in one page, and that page is the only part of the loader's image the map holds executable. Before the switch the loader sets EFER.NXE and CR0.WP, says what firmware handed over of the two, and refuses a CPU without execute-disable by name.
  • AP trampoline: it already wrote the kernel's declared EFER before turning paging on; it now turns paging on with control_regs::CR0_ENTERING_PAGING, the declaration's WP and PG. Its temporary GDT's descriptors are marked accessed, so loading them writes nothing to the page (SDM Vol. 3A §3.4.5.1).
  • The map's tables are withheld from the allocator on both architectures, named in kernel_main's shared list of the loader's allocations and checked against the firmware map as LoaderData like the rest. They were a loader allocation the kernel did not reserve: on main the allocator owned the frames the boot CPU is still running under inside mm::init, and on x86-64 the frames every AP later loads as its first CR3. Found by the reviewer, confirmed by reading (is_usable_type takes LoaderData; the pool was in no reserved region); not reproduced, since the allocator hands out low memory first and the pool sits high.
  • AArch64 names no code pages: its kernel entry turns translation on from its own text.

Where this departs from the record's exit

The record asked that "the kernel's entry applies its declared EFER before it switches to the boot map". On x86-64 the switch is the loader's, under firmware's tables, so the loader sets the two bits the map needs and the kernel's control_regs still writes and asserts the whole declared CR0 and EFER on every CPU afterwards.

Checks

Booted head 4f3efa786; landing head 388be2666, rebased, with its own checks at the end of this section. The table below is the pre-rebase evidence: the host run and the descriptor control are at 7355706db; round three changes only which regions the kernel withholds at boot, and was rechecked by clippy and three guest rows. Logs: evidence pr-1212 at d66bc554.

check result log
the plan's host tests (cargo test -p toyos-bootmap, in the host run) 59 passed. The tables are walked as a CPU walks them: no address of either view is writable and executable, exactly the text and the two code pages run, a switch page above 4 GiB among them r2/ci-host.log
guest rows at the head, run 42952063386 success, 3 passed: std_threads_count_the_cpus_and_give_back_ended_threads (x86-64), virt_smp, virt_user_mode r3/guest-rows-42952063386.log
cargo run -- --clippy at the head exit 0, 26 invocations clean r3/clippy.log
guest rows at 7355706db, run 42946955150 success, 6 passed: x86-64 q35 under KVM (std_threads_count_the_cpus_and_give_back_ended_threads, libc_children, kernel_wx_leaf_is_refused) and AArch64 virt (virt_smp, virt_user_mode, virt_kernel_wx_leaf_is_refused) r2/guest-rows-42946955150.log
negative control, run 42946958444: the trampoline's descriptors left unaccessed builds; the guest resets as the boot CPU starts its first AP. This is what shows CR0.WP in force on an AP diff, log
negative controls on the encoding, at 66a5575a3 XD dropped: 6 tests red. Code pages left as data: 2 tests red. The encoding is unchanged since control-no-xd.log, control-code-writable.log
cargo run -- --ci host exit 1: 1 of 110 steps red, the macOS-only llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key (issues/the-llvm-environment-test-breaks-git-on-macos.md) r2/ci-host.log

On OVMF the loader's line reads firmware hands over EFER.NXE set and CR0.WP set, so no boot has yet shown the loader's own writes mattering. Each bench's log will say what its firmware hands over.

Metal: both benches green at 4f3efa786, 4 of 4 rows each (smp_roster_and_tsc_trail, topology_readback, pmm_accounting, kernel_wx), judged from the readbacks: T14, AMD bench. The landing checklist's lines, cut from each readback (T14, AMD):

T14:

CPU: firmware hands over EFER.NXE set and CR0.WP set; the loader sets both before its switch
Scanout: 0x4000000000+0x800000 mapped as the scanout in 2 MiB pages at identity and at PHYS_OFFSET, in 6 page directories under 1 second-level tables
Loader image: 0x140000000+0x7e000, mapped at identity as 0x140000000+0x200000
Code pages beside it, mapped read-only: [140002000, 8000]; every other page is never executed
pmm: the firmware map calls 16885280768 bytes usable in 74 entries; managed=16800129024 withheld=85151744 unaligned=0, and the three sum to it
SMP: 8 of 8 MADT cpus online
control_regs: 8 of 8 cpus hold cr0=0x80010033 cr4=0x00360ee8 efer=0x0d01 xcr0=0xe7 save=xsaves apic=X2apic
paging: 9974 kernel leaves, none writable and executable, none executable outside the text …, none writable over the RELRO …

AMD bench:

CPU: firmware hands over EFER.NXE clear and CR0.WP set; the loader sets both before its switch
Scanout: 0xfc90000000+0x800000 mapped as the scanout in 2 MiB pages at identity and at PHYS_OFFSET, in 6 page directories under 2 second-level tables
Loader image: 0x140000000+0x7e000, mapped at identity as 0x140000000+0x200000
Code pages beside it, mapped read-only: [140002000, 8000]; every other page is never executed
pmm: the firmware map calls 16505958400 bytes usable in 40 entries; managed=16422281216 withheld=83677184 unaligned=0, and the three sum to it
SMP: 16 of 16 MADT cpus online
control_regs: 16 of 16 cpus hold cr0=0x80010033 cr4=0x00340ee8 efer=0x0d01 xcr0=0x7 save=xsavec apic=Xapic
paging: 9835 kernel leaves, none writable and executable, none executable outside the text …, none writable over the RELRO …
  • The AMD bench's firmware hands over EFER.NXE clear, so its boot is the first where the loader's own NXE write mattered. No firmware seen hands over CR0.WP clear, so the loader's WP write is still established by reading only; the trampoline's is shown by the descriptor control.
  • Both loaders sat above 4 GiB, so the switch ran from a page outside the low map on both.
  • The pool passed the LoaderData check on both firmware maps.
  • The AMD judge log carries a clock: TSC STEP line from that boot; no row judged it and this change touches no clock code.

After the metal boots the branch was rebased onto main, last onto cb3f9c062 (#1213): head 388be2666. The rebase touched built code: main's own changes since (#1210, #1214, #1200, #1213, among them kernel/src/main.rs and both boot.rs), and one hand-resolved line in kernel/src/main.rs, where main's new names region and this branch's boot_map region both join the loader list. The manual's boot.md keeps both bullets. git range-diff shows the three commits unchanged across the two later rebases. The benches booted 4f3efa786, not this head. At 388be2666:

check result log
cargo run -- --ci host, clippy included exit 1: 1 of 110 steps red, the same macOS-only llvm test (384 passed, 1 failed in that step) rebased/ci-host.log
guest rows, run 42988634474 success, 2 passed: std_threads_count_the_cpus_and_give_back_ended_threads (x86-64), virt_smp (AArch64) rebased/guest-rows-42988634474.log

Risks

  • How a failure of this change presents on a bench: a reset, not a frozen panel. Until the kernel loads its own IDT, a fault vectors through firmware's table to handlers the map now holds execute-disabled, so the machine triple-faults and resets, clearing what the panel showed (read from the encoding, not observed). issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md is renamed and rewritten to say so. A bench that reboots in a loop at the handover, or as its first AP starts, is this change.
  • The pool on real firmware maps. These boots are the first time either bench's firmware map is asked about the boot map's tables. If a bench's map does not hold them as LoaderData, or they share memory with another reserved region, the failure is a named panic (boot: reserving …, which no LoaderData descriptor in the firmware map holds or boot: the reserved regions … share memory), not a reset.
  • Real firmware. Before the bench boots below, q35's OVMF was the only firmware that had booted this.
  • FINE_TABLES grew from 4 to 6, so the loader's page-table pool is 8 KiB larger, and it is now withheld for the machine's life on both architectures (20 pages, 80 KiB).
  • Not taken: dropping the rest of the loader's image from the map. It is not a clean reduction here: the plan claims directories from the loader's extent, so replacing the extent with one page rewrites the directory accounting and most of the plan's tests, on both architectures' shared path.
  • Unverified citation. I did not re-read the SDM for the bit 63 rule, so the comment no longer cites a section for it.
  • Other open PRs. Touches none of A child's arguments and environment are memory its parent lays out #1200, A move names the rights it delivers: a ring and a manifest's SysCap stay where they were given #1204, A journal entry is one flush and a forced write: FUA through the block path #1202 or nvme: the controller's own idle power states, by APST #1131's files. Every kernel stack sits above unmapped memory, and an overflow names its stack #1211 overlaps textually in kernel/src/arch/x86_64/smp.rs and in the list in issues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1212 at 66a5575a3: SEND BACK

The table encoding is right and the host tests are a sound oracle for it. Two things stop it landing: read-only is not in force in ring 0 on the CPUs that run the code pages, and the change alters what an early fault does without updating the record and comment that describe it. No metal boot exists yet.

Architecture

The design fits. One map with final permissions, no fix-up pass, and the loader as the owner of the one bit its own switch needs are the right calls: the switch runs under firmware's tables, where the kernel's entry cannot run first, and the kernel still writes and asserts the whole declared EFER afterwards. The departure from the record's exit wording is ordinary implementation and is stated truthfully in the PR body and at lib/bootmap/src/x86_64.rs:53. Refusing a CPU without execute-disable is consistent with the manual's 2020-and-later range and with the kernel's existing assertion; it prints before it panics, on the same path AArch64's refusal uses.

Net: production +139/−39, tests +133/−66, manual +3, records −24. The growth is the code-page plumbing and the switch stub.

BLOCKERS

1. kernel/src/arch/x86_64/smp.rs:307-309, 340-342, loader/src/arch/x86_64.rs:146-168 — CR0.WP is not set before either kind of CPU loads the map, so the "read-only" pages are writable in ring 0 while they are executable.

  • APs (certain). The trampoline ORs only PE and PG into the CR0 that INIT left, which has WP clear (SDM Vol. 3A Table 10-1; APM Vol. 2 Table 14-1). issues/ap-control-registers-inherit-init.md records the measured value after the trampoline: cr0=0x80000011 under KVM. With WP clear a supervisor write succeeds whatever R/W says (SDM §4.6.1). So from paging-on until control_regs::init_paging writes CR0 (control_regs.rs:266), each AP holds the trampoline page and the whole kernel text writable and executable. The trampoline page is executed only in that window, so its read-only bit is never in force on any CPU that runs it. The deleted record named exactly this page.
  • Boot CPU (unestablished). It runs on firmware's CR0 from the switch until init_paging(0) at kernel/src/arch/x86_64/boot.rs:60, which is after pat::init and the panel arm. edk2-derived firmware usually hands over WP set, but nothing in the loader makes it so, and nothing records what either bench hands over.
  • Consequences in the tree. The manual sentence at docs/src/architecture/boot.md:22-24 is false for APs. The comments at smp.rs:16-18 and smp.rs:154-155 ("a page the AP holds read-only") are false. The accessed-bit change is inert at head, since the write it avoids would not fault, so no evidence shows it is needed or sufficient. lib/bootmap/tests/walk.rs:30-33 states the write rule without its CR0.WP condition, though it states NXE for the fetch rule.
  • What resolves it. WP in force before each CPU's first instruction under the map, from the kernel's one CR0 declaration as EFER_BEFORE_LONG_MODE already is for EFER, and set by the loader beside NXE. Cheapest evidence: the x86-64 SMP guest row green with it, and one negative control restoring the unaccessed descriptors (0x9A/0x92), which should then stop the AP.

2. issues/a-fault-before-the-kernels-idt-vectors-into-the-firmwares.md, kernel/src/arch/x86_64/boot.rs:51-52 — both now describe behaviour this change removes.

The record says a fault before the kernel's IDT "vectors into firmware code, which on EDK2 dead-loops"; the comment says it "would reach firmware's handlers". Firmware's handlers sit in memory the boot map now holds execute-disabled, so from the switch to mm::init the fetch itself faults and the machine triple-faults and resets (inference from the encoding, not observed). On metal that turns a frozen panel showing the last record into a reset that clears it. Root CLAUDE.md requires directly affected records to stay accurate; update both, and name this in the PR's risks, because it is also how a failure of this change will present on the benches.

SUGGESTIONS

  • loader/src/arch/x86_64.rs:69 — have cpu_as_entered return a line with firmware's EFER.NXE and CR0.WP. OVMF likely hands over NXE already set, so no boot so far shows the loader's write mattering; the line makes each bench's log say which path ran.
  • lib/bootmap/src/lib.rs:350-354 — the loader's image is in the map only because the switch runs from it, and the switch now runs from one named page. Everything handed to the kernel is checked against the low map, so the rest of the image appears to be mapped for nothing. Dropping loader in favour of the code page would shrink the plan and the map. Read, not built.
  • lib/bootmap/tests/walk.rs:231 — walk one plan whose switch page lies above 4 GiB. The plan test covers the entry; no walked table does, and a bench may load the loader there.
  • loader/src/main.rs:674-681 — the call's SAFETY comment does not discharge enter_kernel's new precondition (the plan was built with arch::code_pages()).
  • lib/bootmap/src/x86_64.rs:35 — the reserved-unless-NXE rule is in the bit 63 row of Tables 4-15 to 4-20, as I recall the SDM; §4.1.4 is the CPUID enumeration. Not re-read here.
  • loader/src/arch/x86_64.rs:75 — on Intel the refusal can be firmware's "XD bit" setting (IA32_MISC_ENABLE[34]); naming it gives the owner the remedy.

What I checked and found sound

  • Mappings. Every leaf is code (R, X) or data (RW, XD); table entries grant everything, so the leaf decides. Kernel entry moves to the kernel's own stack at once; boot_map_write_combining preserves bit 63; the kernel calls no firmware service; the boot CPU writes the trampoline through the kernel's direct map, where it is data.
  • NXE ordering. The loader sets it before mov cr3. The trampoline loads CR3 with paging off, which walks nothing, then writes EFER, then sets PG.
  • Implicit CPU writes. The temporary GDT is the only descriptor table on a read-only page. The kernel's GDT and TSS are data; A/D updates go by physical address; 64-bit IDT delivery writes nothing.
  • Stub. Eight bytes (0F 22 DF, 48 89 D7, FF E6), 16-byte aligned, so one page.
  • Tests. The walk reads R/W as an AND and XD as an OR across levels, handles 1 GiB, 2 MiB and 4 KiB leaves, and scans every 4 KiB address of both views. Both controls are meaningful: I read the logs, 6 and 2 reds, each at the assertion the mutation should hit.
  • Host red. One of 110 steps, the macOS-only LLVM environment test with its own record; independent of this branch.
  • Records and manual. No reference to the deleted slug remains; the manual line names no code.

Metal boots required before landing

At the head that lands, since the fix touches the trampoline and the loader:

  1. Intel bench (T14): one boot of the testcases image with smp_roster_and_tsc_trail and kernel_wx green. The first passes only if every MADT CPU came through the trampoline.
  2. AMD bench: the same boot and rows. No AMD CPU has executed this revision (the guest host was Intel), and its scanout sits under a second-level table of its own.

The readback should carry the loader's Code pages beside it line and the kernel's control_regs: N of N cpus hold line.

Interaction with #1211

No semantic conflict. #1211 starts an AP on its idle stack; that stack and the per-CPU block must still be reachable through the boot map, as the bring-up stack must today, and a guard page is not unmapped there. Textual conflicts are in the umbrella record and smp.rs.

Outside this change

  • The boot map's table pool is LoaderData (loader/src/main.rs:469-471) and is not among the regions kernel/src/main.rs:354 withholds, so after mm::init the allocator owns the frames each AP later loads as CR3. Present on origin/main; read, not reproduced; I found no record. It needs one.
  • For the orchestrator: the evidence README says the tested head is "on main 9349705ce"; its parent is fb88cec2a. The PR body's list of other open PRs omits Every kernel stack sits above unmapped memory, and an overflow names its stack #1211.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…its tables are withheld

CR0.WP was not set before either kind of CPU loaded the boot map, so its
read-only leaves were writable in Ring 0: on an AP certainly, since INIT
leaves WP clear and the trampoline set only PE and PG, and on the boot
CPU by firmware's grace. The trampoline now turns paging on with
control_regs::CR0_ENTERING_PAGING, the declaration's WP and PG, and the
loader sets WP beside NXE before its switch. The loader says what
firmware handed over of the two, and its refusal names Intel's XD-bit
setting.

The boot map's tables are a loader allocation the kernel did not
withhold, so after mm::init the allocator owned the frames every AP
later loads as its first CR3. x86-64 now reserves them beside the
trampoline page.

A fault before the kernel's IDT vectors to firmware handlers the map
holds execute-disabled, and so resets the machine rather than stopping
in firmware: the record is renamed to say so
(issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md)
and boot.rs's comment with it.

The walk states CR0.WP as the write rule's condition and walks a switch
page above the low map.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Round two is at 7355706db. Evidence: pr-1212 at 6b6674a7.

Blocker 1 (CR0.WP). Fixed. The trampoline turns paging on with control_regs::CR0_ENTERING_PAGING (the declaration's WP | PG), and the loader sets WP beside NXE before its switch. The control you named is red: with 0x9A/0x92 restored the guest resets as the boot CPU starts its first AP (run 42946958444); the same row is green at the head (run 42946955150). The false comments and the walk's write rule are corrected.

Blocker 2 (early fault). The record is rewritten and renamed to issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md, boot.rs's comment follows, and the PR's risks lead with it.

Outside finding (the map's tables). Real on main by reading, not reproduced. Fixed here: x86-64 withholds the pool beside the trampoline page for the machine's life.

Suggestions.

  • Taken: the cpu_as_entered line, the walk of a switch page above 4 GiB, the SAFETY comment, the XD-setting hint.
  • Citation: I did not re-read the SDM, so the bit 63 comment no longer cites §4.1.4 or any table.
  • Not taken: dropping the loader's image from the map. The plan claims directories from the loader's extent, so it is a rewrite of the directory accounting and its tests rather than a deletion.

On OVMF the new loader line reads EFER.NXE set and CR0.WP set, so the loader's writes have not yet been seen to matter on any firmware.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1212 at 7355706db: SEND BACK

Scope: 66a5575a3..7355706db only. Both round-1 blockers are resolved. One new blocker, in the part this round added: the boot map's tables are withheld on x86-64 only, AArch64's boot CPU still runs under frames the allocator owns inside mm::init, and the branch says AArch64 is unaffected. Metal is still not run.

Architecture

CR0.WP has the right owner. There is one declaration (control_regs::CR0); the trampoline takes WP | PG of it as it already takes EFER less LMA, and the loader sets the bit its own switch needs beside NXE. No new mechanism.

Withholding the table pool is right and holding it for good is right, but it sits with the wrong owner. The pool is a loader allocation made by shared code for both architectures; the branch hands it through an x86-64 architecture hook, which returns a dummy second region on AArch64 and bypasses the check every other loader allocation gets.

Net this round: production +99/−36 (47 of it the loader's two register reads and its handover line), tests +21/−2, records +30/−24 (one rename and rewrite).

Round-1 blockers

1. CR0.WP — resolved.

  • APs. kernel/src/arch/x86_64/smp.rs:345-347 ORs the low dword of TrampolineData.cr0 (+0x88, size assertion moved to 0x90) into CR0. The field holds CR0 & (WP | PG), bits 16 and 31, so PG and WP arrive in one mov cr0 and the far jump, the first instruction fetched through the map, runs under both. The write is in the .code32 part with flat DS; or eax, [0x8F88] is the absolute-disp32 form (0B 05), the same form as mov edx, [0x8F84] three lines above it. Read, not disassembled; the two runs below settle it.
  • Boot CPU. loader/src/arch/x86_64.rs:186-201 sets NXE then WP under firmware's tables, after ExitBootServices and before the call into the stub, whose first instruction under the map follows mov cr3.
  • Nothing clears it. The only other CR0 writers are pat::init (pat.rs:60-70, which restores the value it read) and init_paging (control_regs.rs:265,273, (live | CD) & !NW then the whole declaration). Under no-ap-control-regs an AP now keeps WP as well; no judge expects it clear (tests/toyos.rs:6915 requires it set).
  • Do the two runs establish it? Yes, for an AP on Intel under KVM. The control (run 42946958444, 60680370b, the head plus three descriptor bytes) boots to mtrr: the boot processor's range registers, the line before boot_aps, and QEMU exits under -no-reboot. Unaccessed descriptors change one thing: the CPU writes the accessed bit on load. code32 and data are loaded with paging off; code64 is loaded by the far jump after mov cr0. That write faults only if the leaf is read-only and CR0.WP is set on that CPU. The head is green on the same row (run 42946955150, std_threads: 2 CPUs). The pair also shows the operand is read correctly: without PG the AP would not come up, and without WP the control would not go red.
  • What they do not show. OVMF hands over both bits set (CPU: firmware hands over EFER.NXE set and CR0.WP set, in the control's serial), so the loader's write is a no-op there and rests on reading. One AP ran; the others take the same path.
  • Comments. smp.rs:16-18,155-157,342-344, lib/bootmap/src/x86_64.rs:54-58 and lib/bootmap/tests/walk.rs:30-34 now state the WP condition. The manual's sentence is now true for APs.

2. Early-fault record and comment — resolved. The record reads true against the code: the window is _start to init_bsp, firmware's handlers are execute-disabled or unmapped in the boot map, and the reset is labelled as read from the encoding. Its claim about the kernel's own tables is what the W^X audit asserts. Frontmatter is valid, opened is kept, and git grep at the head finds no citation of the old slug. kernel/src/arch/x86_64/boot.rs:51-54 follows, and the PR's risks lead with it.

BLOCKER

kernel/src/arch/aarch64/boot.rs:330-335, kernel/src/main.rs:348-356, PR body — the pool is withheld for x86-64 only; AArch64's boot CPU still runs under un-withheld tables, and the branch says it does not.

  • The defect has two windows. The pool comes from shared start_kernel (loader/src/main.rs:469-471) and is LoaderData on both architectures. A CPU runs under it (a) inside mm::init until the kernel's own tables are loaded, and (b) on x86-64, each AP long afterwards. The x86-64 fix closes both there.
  • AArch64 has window (a) at the head. Its entry installs the pool's root (kernel/src/arch/aarch64/boot.rs:67) and the boot CPU stays on it until join() at kernel/src/arch/aarch64/paging.rs:909. Before that, in the same mm::init, pmm::init describes every usable frame, then places its records in the lowest usable memory no reserved region touches and zeroes them (kernel/src/mm/pmm.rs:183-192). paging::init then takes a frame from pmm::alloc_frame() for every table it builds (paging.rs:141,873,885). Neither knows the pool.
  • Consequence. The allocator fills from the bottom (kernel/pure/frames.rs:472-473) and edk2 puts the pool high (root 0x7d298000 against records at 0x100000 in the control's serial), so no boot here reaches it; that is why main's x86-64 defect was never seen either. On firmware that allocates from the bottom, the pool is among the lowest usable memory and pmm::init's own zeroing lands on live tables. Read, not reproduced.
  • The comment and the PR sentence. "No CPU loads the loader's tables after mm::init" is true and beside the point: one CPU is running under them during it. The PR body's "its other CPUs start on the kernel's own bring-up root" has the same gap.
  • main.rs:348-350. The old comment gave a reason, "not a loader allocation". The new one is circular, because the second region is a loader allocation. It therefore skips the LoaderData check at main.rs:332-347, and its extent is this kernel's own MAX_PAGES (which this PR changes from 18 to 20) rather than anything checked against the firmware map.
  • What resolves it. Simplest: name the pool in kernel_main's loader list, in shared code, as boot_pml4_addr .. + MAX_PAGES * PAGE_4K. Both architectures then withhold it, it gets the LoaderData check, arch::boot::reserved goes back to one region and no argument on both, and the comment at 348 can say what it said before. That is shared code, not AArch64-only work. Alternatively keep the code, record AArch64's window in issues/, and correct the comment; the PR sentence is the orchestrator's.

Suggestions

  • lib/bootmap/src/x86_64.rs:35-36 — "bit 63 of each entry's table" reads as the paging table that holds the entry, in a file where "table" means that everywhere else. "Bit 63 of an entry" says it.
  • kernel/src/arch/x86_64/boot.rs:104-106 — says a later fault would stop "in a handler the firmware left behind". The rewritten record now says the opposite for the same code path. It predates this branch; one clause fixes it.
  • Staged metal boots — add pmm_accounting. It is the same image and the same boot, and it reads the pmm: line with the pool withheld on each bench's real map.

The questions on the table pool

  • Is the withheld set exactly the tables? Yes, on x86-64. Plan::write returns the pool's first page as the root and lays root, second-level tables, directories and fine tables into the one [Table; MAX_PAGES] (1 + 5 + 8 + 6 = 20 pages); the kernel withholds [root, root + 20 pages). Unused pool pages are inside the same allocation. The kernel adds no table: boot_map_write_combining rewrites directory entries in place (kernel/src/arch/x86_64/paging.rs:1356), and everything after mm::init maps into the kernel's own tables.
  • For the machine's life? Needed, not only simpler. boot_aps abandons an AP that misses its deadline without parking it (smp.rs:233-236), so that AP may still load this CR3 at any later time; AArch64 leaks bringup_root for the same stated reason. The trampoline page is already held for good, and giving 80 KiB back would need a release path the allocator does not have.
  • Its own record or regression test? Neither, for the fixed half. A fixed defect is a deleted record, and the commit message carries the story. A guest test could expose a regression only by steering the allocator to one frame; let [own, tables] = … already makes dropping the region a compile error, and the rest is plain to read. What is owed is the AArch64 half above.

Suggestions taken and declined

  • Taken: the firmware NXE/WP line, the walk of a switch page above 4 GiB (which is where OVMF actually loads the loader on these rows: 0x140000000), the SAFETY comment, the XD-setting hint. All read correctly.
  • Citation at x86_64.rs:35: not required. Root asks that a hardware requirement code cannot express be stated, and calls a citation useful where it explains the constraint; it does not demand one per site. The module header already names the entry-format tables and walk.rs:28 cites "Table 4-15 onwards" for this bit. As I recall the SDM, the rule is the bit 63 row of those tables; I did not re-read it here either.
  • Declined, dropping the loader's image from the map: accepted. The reason holds: Plan::new claims directories and second-level tables from the loader's extent (lib/bootmap/src/lib.rs:399-416) and every fine table is filled whole, so one mapped page in an otherwise absent 2 MiB page is a new shape, not a deletion. The rest of the image is data, so W^X does not depend on it.

Evidence

  • Host log, read in full for failures. Exit 1; 110 steps, one red: the build system: cargo test --lib, 383 passed, 1 failed, llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key, on the macOS xcodebuild -find git failure its record describes. The branch touches nothing under build/; independent.
  • Plan tests. 9 + 11 + 32 + 6 and one doctest make 59, the new walk among them. The host log names no revision; the new test's presence ties it to this head.
  • Guest rows. Run 42946955150 checked out 7355706db, /dev/kvm opens, 6 passed. Run 42946958444 checked out 60680370b, exit 1 where expected.
  • Identifiers. I scanned the README and r2/* for MAC, IPv4, serial-number and personal-hostname shapes and for home paths: none, and the runner's machine name is masked.

Metal boots required before landing

Not run, so nothing here is established on silicon. One boot per bench (T14, AMD) of the testcases image at the head that lands:

  1. Green rows: smp_roster_and_tsc_trail (every MADT CPU online and control_regs: N of N), kernel_wx, topology_readback.
  2. From loader.log, quoted into the PR for each bench:
    • CPU: firmware hands over EFER.NXE … and CR0.WP …; the loader sets both before its switch. A clear on either bench makes that boot the first evidence of the loader's own write; set and set on both leaves that write established by reading only.
    • Loader image: … and Code pages beside it, mapped read-only: [<switch page>, 8000].
    • Scanout: … (the AMD bench's sits under a second-level table of its own).
  3. From the kernel log: the pmm: line (the boot CPU got through mm::init with the pool withheld), SMP: N of N MADT cpus online, control_regs: N of N cpus hold, and the audit's none writable and executable line.
  4. Failure signature: a reset with nothing on the panel, either after Loader log: the kernel handoff begins (the switch or the boot CPU) or after mtrr: the boot processor's range registers (the first AP).

The AMD boot is also the only check that an AMD CPU leaves an already-accessed descriptor unwritten; the guest host was Intel and the site cites only the SDM.

If the blocker is resolved in code, the images are restaged and the x86-64 SMP row, virt_smp and virt_user_mode redispatched. If it is resolved by a record and comments, the images staged at 7355706db stand.

For the orchestrator

  • The PR body's AArch64 sentence needs the boot CPU's window, whichever way the blocker is resolved.
  • The evidence logs carry the guest runner's CPU model and core count on the harness's instrument line. It is no shape build/sourcegate.rs names and the public Actions log has it too; implementer.md lists "machine model strings", so the call is yours.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…chitectures withhold

The pool was withheld on x86-64 alone, through the architecture's hook,
which bypassed the LoaderData check; AArch64's boot CPU runs under the
same un-withheld frames inside mm::init. It is now named in kernel_main's
shared list of the loader's allocations, checked against the firmware
map like the rest, and arch::boot::reserved is one region again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Round three is at 4f3efa786. Evidence: pr-1212 at 000bc819.

Blocker (the pool withheld on x86-64 alone). Resolved in shared code: the pool is the eighth entry of kernel_main's loader list, boot_pml4_addr .. + MAX_PAGES * PAGE_4K, so it gets the LoaderData check and both architectures withhold it. arch::boot::reserved is one region with no dummy, and the comment over the destructuring is main's again. The PR body's AArch64 sentence is corrected.

Suggestions. All three taken: "bit 63 of an entry", the clause in boot.rs's interrupts, and pmm_accounting in both staged boots.

Evidence. std_threads_count_the_cpus_and_give_back_ended_threads, virt_smp and virt_user_mode are green at the head (run 42952063386), which is each architecture passing the LoaderData check for the pool on its firmware's map; clippy is clean. The evidence branch was recreated to mask the runner's CPU model and core count, so earlier links by hash are dead.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3 of #1212 at 4f3efa786: LAND

Scope: 7355706db..4f3efa786 only, one commit, +20 −22 over kernel/src/main.rs, both boot.rs and one comment in lib/bootmap/src/x86_64.rs. Read: the commit, the changed files at the head, the loader's pool allocation and allocator, Plan::write, the LoaderData check, AArch64's entry, pmm::init, and r3/* at evidence 000bc819. Nothing was built or run. The reviewer that died posted nothing on this PR.

Round 2's blocker is resolved. Landing still waits on the two metal boots, which are not run.

Architecture

The pool now sits with the owner round 2 asked for. It is a loader allocation made by shared code, so shared kernel_main names it beside the other seven and it gets the check they get. The architecture hook is back to what only the architecture knows: x86-64's trampoline page, and nothing on AArch64. The dummy second region and the hook's argument are gone. Production +20/−22, no tests, no records.

The blocker: resolved

  • The extent is exactly the pool, on both architectures.
    • The allocation is shared: Layout::new::<[Table; MAX_PAGES]>() in start_kernel (loader/src/main.rs:469-471), not per architecture.
    • Plan::write fills the whole pool and returns phys(0), the pool's first byte, as the root (lib/bootmap/src/lib.rs:496,518). The loader hands that as boot_pml4_addr on both.
    • A Table is [u64; 512] aligned to 4096 (lib/bootmap/src/lib.rs:171), so MAX_PAGES * PAGE_4K is the allocation's size. MAX_PAGES is one constant with no architecture arm.
    • AArch64's entry loads that root into both TTBR0_EL1 and TTBR1_EL1 (kernel/src/arch/aarch64/boot.rs:67,140-141). There is no second root and no table outside the pool.
  • AArch64's window is closed. mm::init receives the region at kernel/src/main.rs:376; pmm::init places its records outside every reserved region and withholds their frames (kernel/src/mm/pmm.rs:183,211,246). That is the path round 2 found zeroing live tables on bottom-up firmware.
  • Can the check refuse a legitimate map? Not more than it already could.
    • The pool comes from the loader's global allocator, which is AllocatePool(EfiLoaderData, size + align) for an over-aligned layout (loader/src/efi/mod.rs:503-511). The aligned 80 KiB lie inside that one allocation.
    • The kernel's own memory, the kernel section and the copied map come from the same allocator and already pass the same check; held asks for one LoaderData descriptor holding the extent, with no alignment (lib/rootimage/src/handoff.rs:20-35, block 1).
    • The pool is allocated before the final map is taken. Both rows that booted passed it on edk2's map for their architecture.
    • A refusal is a named panic after the slot line, not a reset.
  • One assumption, stated not blocking. The extent is the kernel's constant, so it equals the loader's allocation only when both are one build. KernelArgs is unversioned repr(C) and already assumes that.

Comments

All read true against the code.

  • kernel/src/main.rs:331-333 — says who runs under the tables and why they are kept for good. It names x86-64 in prose only; no generic code names an architecture.
  • kernel/src/main.rs:355-359 — main's reason again ("not a loader allocation"), no longer circular.
  • kernel/src/arch/aarch64/boot.rs:330, kernel/src/arch/x86_64/boot.rs:84-85 — one line and two; the false AArch64 sentence is gone.

Suggestions: all three taken

  • lib/bootmap/src/x86_64.rs:35-36 — "bit 63 of an entry".
  • kernel/src/arch/x86_64/boot.rs:100-101 — now agrees with the early-fault record: a reset through firmware's table.
  • pmm_accounting is in both staged commands (r3/metal-prepare-t14.log, r3/metal-prepare-amd.log, each exit 2, staged).

The PR body's sentence

Corrected and true: the tables are withheld on both architectures; on main the allocator owned the frames the boot CPU runs under inside mm::init, and on x86-64 the frames each AP later loads.

Evidence

  • Rows at the head. Run 42952063386 fetched and checked out 4f3efa786…; x86-64 under KVM and AArch64 emulated; 3 passed, 3 total. Each boot got past the assertion at kernel/src/main.rs:339-353, so each firmware's map holds the pool as LoaderData.
  • Clippy at the head. 26 invocations clean, exit 0, the kernel in every feature shape for both targets among them. The log names no revision.
  • Is the host run from 7355706db sufficient? Yes. No host test executes kernel_main or either boot.rs, and the lib/bootmap change is a comment. Clippy compiles the changed code at the head and the rows run it. The merge queue runs the host checks on Linux regardless.
  • Is the descriptor control from 7355706db sufficient? Yes. It shows CR0.WP in force on an AP through the trampoline, which this commit does not touch.
  • Identifiers. The r3 logs carry <cpu model> and <n> core(s); I found no MAC, IPv4 or home-path shape.

SUGGESTION

  • kernel/src/main.rs:336 — size_of::<[toyos_bootmap::Table; toyos_bootmap::MAX_PAGES]>() would name the loader's layout instead of recomputing it. Optional.

Metal boots before landing: what changed

Round 2's checklist stands. Three changes:

  1. The images are the ones staged at 4f3efa786; those staged at 7355706db no longer stand.
  2. pmm_accounting is a fourth green row on each bench.
  3. One more failure signature, and it is not a reset: a panic after the kernel's slot line reading boot: reserving …, which no LoaderData descriptor in the firmware map holds, or boot: the reserved regions … share memory. These boots are the first time either bench's firmware map is asked about the pool. Either line is this commit, and the line's addresses with the firmware map from that boot are the evidence to keep.

For the orchestrator

  • The body's Risks section says a failure of this change presents as a reset. Since this round it can also present as the named panic above.
  • x86-64's boot map holds no page writable and executable #1212's base is fb88cec2a; main's commits since touch none of its files, and a trial merge with 1191cef01 is clean.

LAND

Japabu added a commit that referenced this pull request Nov 11, 2026
…its tables are withheld

CR0.WP was not set before either kind of CPU loaded the boot map, so its
read-only leaves were writable in Ring 0: on an AP certainly, since INIT
leaves WP clear and the trampoline set only PE and PG, and on the boot
CPU by firmware's grace. The trampoline now turns paging on with
control_regs::CR0_ENTERING_PAGING, the declaration's WP and PG, and the
loader sets WP beside NXE before its switch. The loader says what
firmware handed over of the two, and its refusal names Intel's XD-bit
setting.

The boot map's tables are a loader allocation the kernel did not
withhold, so after mm::init the allocator owned the frames every AP
later loads as its first CR3. x86-64 now reserves them beside the
trampoline page.

A fault before the kernel's IDT vectors to firmware handlers the map
holds execute-disabled, and so resets the machine rather than stopping
in firmware: the record is renamed to say so
(issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md)
and boot.rs's comment with it.

The walk states CR0.WP as the write rule's condition and walks a switch
page above the low map.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…chitectures withhold

The pool was withheld on x86-64 alone, through the architecture's hook,
which bypassed the LoaderData check; AArch64's boot CPU runs under the
same un-withheld frames inside mm::init. It is now named in kernel_main's
shared list of the loader's allocations, checked against the firmware
map like the rest, and arch::boot::reserved is one region again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…its tables are withheld

CR0.WP was not set before either kind of CPU loaded the boot map, so its
read-only leaves were writable in Ring 0: on an AP certainly, since INIT
leaves WP clear and the trampoline set only PE and PG, and on the boot
CPU by firmware's grace. The trampoline now turns paging on with
control_regs::CR0_ENTERING_PAGING, the declaration's WP and PG, and the
loader sets WP beside NXE before its switch. The loader says what
firmware handed over of the two, and its refusal names Intel's XD-bit
setting.

The boot map's tables are a loader allocation the kernel did not
withhold, so after mm::init the allocator owned the frames every AP
later loads as its first CR3. x86-64 now reserves them beside the
trampoline page.

A fault before the kernel's IDT vectors to firmware handlers the map
holds execute-disabled, and so resets the machine rather than stopping
in firmware: the record is renamed to say so
(issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md)
and boot.rs's comment with it.

The walk states CR0.WP as the write rule's condition and walks a switch
page above the low map.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…chitectures withhold

The pool was withheld on x86-64 alone, through the architecture's hook,
which bypassed the LoaderData check; AArch64's boot CPU runs under the
same un-withheld frames inside mm::init. It is now named in kernel_main's
shared list of the loader's allocations, checked against the firmware
map like the rest, and arch::boot::reserved is one region again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu and others added 3 commits November 11, 2026 10:38
The loader's x86-64 boot map mapped the kernel's text read-only and
every other page writable, and every page executable: bit 63 is
execute-disable only under IA32_EFER.NXE, and the loader switched to the
map from firmware's state. The boot CPU ran on it until mm::init and each
AP from its trampoline until ap_entry loads the kernel's tables.

The map is now built once with its final permissions. A leaf is code,
read-only and executable, or data, writable and execute-disabled
(lib/bootmap, Access). Code is the kernel's text and at most two 4 KiB
pages the architecture names: on x86-64 the page the loader's switch runs
from, now an eight-byte stub of its own, and the AP trampoline's page at
0x8000; on AArch64 none. The rest of the loader's image is data.

Whoever loads the map sets NXE first: the loader, which refuses a CPU
with no execute-disable by name, just before its switch; an AP's
trampoline already wrote the declared EFER before it turned paging on.
The kernel's control_regs still writes and asserts the declared EFER
whole on every CPU. The trampoline's temporary GDT is marked accessed,
so an AP writes nothing to the page it now holds read-only.

Host tests walk the written tables as a CPU does: no address of either
view is writable and executable, exactly the text and the two pages run,
and their neighbours are data. Dropping XD from the encoding reds them.

Closes issues/x86-64s-boot-map-is-writable-and-executable.md.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…its tables are withheld

CR0.WP was not set before either kind of CPU loaded the boot map, so its
read-only leaves were writable in Ring 0: on an AP certainly, since INIT
leaves WP clear and the trampoline set only PE and PG, and on the boot
CPU by firmware's grace. The trampoline now turns paging on with
control_regs::CR0_ENTERING_PAGING, the declaration's WP and PG, and the
loader sets WP beside NXE before its switch. The loader says what
firmware handed over of the two, and its refusal names Intel's XD-bit
setting.

The boot map's tables are a loader allocation the kernel did not
withhold, so after mm::init the allocator owned the frames every AP
later loads as its first CR3. x86-64 now reserves them beside the
trampoline page.

A fault before the kernel's IDT vectors to firmware handlers the map
holds execute-disabled, and so resets the machine rather than stopping
in firmware: the record is renamed to say so
(issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.md)
and boot.rs's comment with it.

The walk states CR0.WP as the write rule's condition and walks a switch
page above the low map.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…chitectures withhold

The pool was withheld on x86-64 alone, through the architecture's hook,
which bypassed the LoaderData check; AArch64's boot CPU runs under the
same un-withheld frames inside mm::init. It is now named in kernel_main's
shared list of the loader's allocations, checked against the firmware
map like the rest, and arch::boot::reserved is one region again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r4 of #1212 at 388be2666: the rebases only

Scope: the series reviewed at 4f3efa786 (base fb88cec2a) as it now sits on main cb3f9c062, after three rebases: over #1210, over #1200 (36898f8d4) and over #1213 (388be2666). Read: git range-diff of the reviewed series against each rebased one, the whole diffs against their bases compared line for line, kernel_main at the head from bootimage::init to the clock, both boot.rs, the manual's boot page, what main took since fb88cec2a in the loader, kernel/src/bootimage.rs, kernel/src/main.rs and kernel/src/arch, guest run 42986543196's log, and evidence d152d9e8. Nothing was built or run.

The rebases are faithful

  • The change is the reviewed one. Against its base, the diff at 388be2666 differs from the one at 4f3efa786 in two places only: the resolved pair of lines in kernel/src/main.rs, and three lines of context in docs/src/architecture/boot.md. Every other hunk of the 16 files is identical, so production and test line counts are unchanged from round 3.
  • The last rebase is mechanical. git range-diff 2ec4ad950..36898f8d4 cb3f9c062..388be2666 marks all three commits =, and 388be2666's tree is a0f40f3d0, bit for bit the tree git merge-tree writes for 36898f8d4 and cb3f9c062 with no conflict. Nothing was edited by hand in it.
  • kernel/src/main.rs:353-354, the hand-resolved lines: correct.
    • The loader array literal holds nine regions in this order: kernel memory, kernel section, supervisor, file server, ROOT, boot names, the copied memory map, microcode, the boot map's tables. The destructuring names nine in that order, so its arity check still fails the build on a region added without being named.
    • reserved is those nine and arch::boot::reserved(), ten. Both names and boot_map reach mm::init.
    • Both are in loader before the LoaderData loop, so both get the check; the loop and held are untouched.
    • The overlap assertion cannot trip on the pair. The boot names are whole pages from AllocatePages that the loader never frees (loader/src/payload.rs:24-35, no Drop); the tables are a pool allocation made after it (loader/src/main.rs:479-482). Two live firmware allocations share no byte. overlapping skips empty extents.
  • docs/src/architecture/boot.md:22-28: correct. Main's bullet on where the loader booted from and this branch's bullet on the mapping are both kept, in that order; main replaced the "typed description" bullet this branch's hunk sat under, which is why it conflicted. The text names no code.
  • No stale reference. Neither the deleted record's name nor the renamed record's old name appears anywhere in the tree at the head.

What main took, against the boot map, the trampoline and CR0.WP

Do the bench boots of 4f3efa786 stand for 388be2666? Yes

What those boots established is unchanged code on unchanged inputs:

  • the loader's EFER.NXE and CR0.WP writes and the switch from a code page above 4 GiB (the AMD bench's NXE clear boot among them): loader/src/arch/x86_64.rs is byte-identical and main did not touch it;
  • every AP through the read-only trampoline with WP in force: smp.rs and control_regs.rs likewise;
  • the pool held as LoaderData on each firmware's map and past mm::init: the pool's allocation, extent and check are the same lines. Where a boot came from is the supervisor's to read, not the kernel's #1210 adds one page allocation before it, which moves addresses and changes nothing the check depends on.

What a rebase could break is the merged list compiling and both regions passing the two assertions together, which does not depend on the firmware. One boot per architecture settles it:

  • Guest run 42986543196, read from the run itself, at 36898f8d4. It fetched and checked out 36898f8d4f2f…; filter std_threads_count_the_cpus_and_give_back_ended_threads virt_smp; 2 passed, 2 total. Each boot built the merged kernel_main and got past the LoaderData loop and the overlap check with both regions, on x86-64 under KVM and on AArch64.
  • At 388be2666 nothing has run yet. Run 42988634474 (the same dispatch at this head) was queued when I posted. By inspection it can differ from the run above only through The x86-64 wall clock is the loader's reading of firmware, and the CMOS driver is gone #1213's own code, which main already carries with its own evidence. It is the direct confirmation and costs nothing to read before arming; I do not make the verdict wait on it, since a tree that failed to build would stop in the queue, not land.
  • The evidence branch (d152d9e8) has no section for either rebased head. The host run the author is repeating adds little: the resolved lines sit in no feature arm, main did not touch lib/bootmap, so its tests and controls stand, and the queue runs the host checks on the merge result.

Repeating the bench boots would exercise nothing these rebases changed.

For the orchestrator

  • The body is stale: it names head 4f3efa786 and evidence 000bc819, and still says "Metal, AMD bench: not yet run". The evidence at d152d9e8 has both benches 4 of 4 with the AMD bench's EFER.NXE clear hand-over; the body should carry that, the head 388be2666 and the guest runs at the rebased heads.
  • The AMD judge log carries a clock: TSC STEP line from the boot of 4f3efa786; the evidence says no row judged it and this change touches no clock code. I agree it is independent of this PR. The x86-64 wall clock is the loader's reading of firmware, and the CMOS driver is gone #1213 has since changed that code, so the sighting belongs with the clock's records at main's revision, not here.

No blocker. Round 3's optional suggestion is not reopened.

LAND

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator, before queueing at 388be26 (the head review r4 gave LAND).

Queueing.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 08:54
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 422915e Nov 11, 2026
11 checks passed
@Japabu
Japabu deleted the boot-map-wx branch November 11, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant