Repository navigation
x86-64's boot map holds no page writable and executable - #1212
Conversation
Review r1 of #1212 at
|
…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>
|
Round two is at Blocker 1 ( Blocker 2 (early fault). The record is rewritten and renamed to 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.
On OVMF the new loader line reads |
Review r2 of #1212 at
|
…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>
|
Round three is at Blocker (the pool withheld on x86-64 alone). Resolved in shared code: the pool is the eighth entry of Suggestions. All three taken: "bit 63 of an entry", the clause in Evidence. |
Review r3 of #1212 at
|
…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>
…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>
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>
Review r4 of #1212 at
|
|
Orchestrator, before queueing at 388be26 (the head review r4 gave LAND).
Queueing. |
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 untilmm::init, and every AP from its trampoline untilap_entryloads 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.rsblock()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.
lib/bootmap:Access::Datasets XD on x86-64.Plan::newtakes 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.EFER.NXEandCR0.WP, says what firmware handed over of the two, and refuses a CPU without execute-disable by name.EFERbefore turning paging on; it now turns paging on withcontrol_regs::CR0_ENTERING_PAGING, the declaration'sWPandPG. Its temporary GDT's descriptors are marked accessed, so loading them writes nothing to the page (SDM Vol. 3A §3.4.5.1).kernel_main's shared list of the loader's allocations and checked against the firmware map asLoaderDatalike 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 insidemm::init, and on x86-64 the frames every AP later loads as its firstCR3. Found by the reviewer, confirmed by reading (is_usable_typetakesLoaderData; the pool was in no reserved region); not reproduced, since the allocator hands out low memory first and the pool sits high.Where this departs from the record's exit
The record asked that "the kernel's entry applies its declared
EFERbefore 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'scontrol_regsstill writes and asserts the whole declaredCR0andEFERon every CPU afterwards.Checks
Booted head
4f3efa786; landing head388be2666, 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 at7355706db; round three changes only which regions the kernel withholds at boot, and was rechecked by clippy and three guest rows. Logs: evidencepr-1212atd66bc554.cargo test -p toyos-bootmap, in the host run)r2/ci-host.logstd_threads_count_the_cpus_and_give_back_ended_threads(x86-64),virt_smp,virt_user_moder3/guest-rows-42952063386.logcargo run -- --clippyat the headr3/clippy.log7355706db, run 42946955150std_threads_count_the_cpus_and_give_back_ended_threads,libc_children,kernel_wx_leaf_is_refused) and AArch64virt(virt_smp,virt_user_mode,virt_kernel_wx_leaf_is_refused)r2/guest-rows-42946955150.logCR0.WPin force on an APdiff,log66a5575a3control-no-xd.log,control-code-writable.logcargo run -- --ci hostllvm::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.logOn 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:
AMD bench:
EFER.NXEclear, so its boot is the first where the loader's ownNXEwrite mattered. No firmware seen hands overCR0.WPclear, so the loader'sWPwrite is still established by reading only; the trampoline's is shown by the descriptor control.LoaderDatacheck on both firmware maps.clock: TSC STEPline 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): head388be2666. The rebase touched built code: main's own changes since (#1210, #1214, #1200, #1213, among themkernel/src/main.rsand bothboot.rs), and one hand-resolved line inkernel/src/main.rs, where main's newnamesregion and this branch'sboot_mapregion both join theloaderlist. The manual'sboot.mdkeeps both bullets.git range-diffshows the three commits unchanged across the two later rebases. The benches booted4f3efa786, not this head. At388be2666:cargo run -- --ci host, clippy includedrebased/ci-host.logstd_threads_count_the_cpus_and_give_back_ended_threads(x86-64),virt_smp(AArch64)rebased/guest-rows-42988634474.logRisks
issues/a-fault-before-the-kernels-idt-resets-the-machine-and-says-nothing.mdis 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.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 holdsorboot: the reserved regions … share memory), not a reset.FINE_TABLESgrew 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).kernel/src/arch/x86_64/smp.rsand in the list inissues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md.🤖 Generated with Claude Code