kvm_access_check()'s /dev/kmem residency gate is built on pc64 pmap_kextract(), which validates no paging level: a pread at any ungrown kernel VA page-faults the validator itself (Fatal trap 12 -> panic), and PTE-empty queries return (va & PAGE_MASK) garbage instead of 0
Summary
The /dev/kmem gate requires pmap_kextract(addr)!=0 for every page, but pc64 pmap_kextract derefs the recursive-map PDE slot before any PG_V test: a KVA whose PDPT/PD page is ungrown (most of the 8TB KVA window) takes an unrecoverable kernel fault inside the VALIDATOR - one 8-byte root pread panics (reads allowed at any securelevel); with PT present and PTE==0 it returns offset-garbage (proven 0x30) instead of 0 - the pc64-kextract twin of DF-5245/DF-5290, saved today only because in-tree queries are aligned/always-mapped. Guest-verified end-to-end: baseline Fatal trap 12 at pmap_kextract+0x16 (fault VA = the unmapped recursive-map slot) guest down; DMAP-4-level-walk+PG_V fix rebuilt -> identical trigger returns EFAULT, guest up, mapped reads unaffected. Root-gated DoS; no corruption primitive. 4-level-validated-walk fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5358 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| pmxtest.c | β | 4.1 KB | view raw | |
| pmx_user.c | β | 1.6 KB | view raw | |
| build.sh | β | 200 B | view raw | |
| run.sh | β | 202 B | view raw | |
| build.log | β | 1.1 KB | view raw | |
| fix_build.log | β | 12.5 KB | view raw | |
| build.tail.log | β | 12.5 KB | view raw | |
| run.log | β | 2.0 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| fix_run.log | β | 1.3 KB | view raw | |
| run.fixed.log | β | 1.3 KB | view raw | |
| env.txt | β | 290 B | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| VERDICT.md | β | 7.6 KB | β raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 6.9 KB | view raw |
VERDICT β DF-5358
Status: reproduced (impact: panic β root-gated local DoS via /dev/kmem read; confidence: certain) Fix: validated (fix_status=fixed β panic eliminated, clean EFAULT, no regression).
What was claimed
Pass-2 audit of sys/platform/pc64/x86_64/vm_machdep.c flagged
kvm_access_check() (vm_machdep.c:1276-1291) as the pc64 consumer of a
pmap_kextract() (pmap.c:1865-1888) that performs no paging-level validity
check β the pc64 twin of DF-5245 (pmap_extract, "no live in-tree KVA caller")
and DF-5290 (vkernel pmap_extract/pmap_kextract, whose live caller is the
vkernel kvm_access_check). Two predicted failure modes:
- ungrown KVA β
*vtopt(va)recursive-map deref β kernel page fault β panic; - PT-present/PTE-0 β non-zero garbage β "pages resident" gate passes vacuously.
How it was proven (guest: DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026, X86_64_GENERIC)
Baseline (stock kernel)
pmxtest.ko (build.sh, root):
pmxtest: KvaStart=fffff80040000000 KvaEnd=ffffffffff1ff000 KvaSize=000007ffbf1ff000 pmxtest[1]: CONTROL pmap_kextract(&dummy=0xffffffff826002d8) = 0000000135ab1278 (mapped, sane) pmxtest[2]: va=ffffffff82601000 while-mapped pa=0000000134641000 pmxtest[2]: PT-present/PTE-0: aligned pmap_kextract(va) = 0000000000000000 pmxtest[2]: PT-present/PTE-0: unaligned pmap_kextract(va+0x30) = 0000000000000030 <-- garbage pmxtest[2]: restored + freed pmxtest[3]: kernel_vm_end=fffff80118800000 pmxtest[3]: PML4[496] present, PDPT[5] ABSENT (0000000000000000) -> /dev/kmem test KVA fffff80140000000
- The PTE-0 hole is manufactured deterministically:
kmem_alloc3()one page,pmap_kremove()its PTE (kernel PT pages are never freed, so the level above stays present), extract, restore, free. - Honest correction to the initial analysis: for the aligned addresses
kvm_access_check()walks,(pa & PG_FRAME)|(va & PAGE_MASK)evaluates to 0 (offset-in-page is 0), so the aligned PTE-0 case actually fails closed on pc64. The non-zero garbage appears only for unaligned callers (0x30observed) β exactly the DF-5245 defect class, but those in-tree callers (kern_memio.c:784/797/821user_kernel_mapping) only query always-mapped kernel objects. So failure mode 2 is a latent landmine on pc64 today, not a live gate bypass. - Failure mode 1 is live and lethal.
pmx_user(pread /dev/kmem, 8 bytes) at the probed absent-PDPT KVA0xfffff80140000000(inside [KvaStart,KvaEnd), abovekernel_vm_end, covered by theVM_SUBSYS_RESERVEDVM_PROT_ALL entrykmem_init()(vm_kern.c:494-537) installs over all unallocated KVA β sokernacc()would NOT have rejected it either):
Fatal user address access from kernel mode from pmx_user at ffffffff80c0cdc6 Fatal trap 12: page fault while in kernel mode fault virtual address = 0xffff80403e005000 <- recursive-map PDE-slot address (unmapped) fault code = supervisor read data, page not present current process = 1016 Stopped at pmap_kextract.part.23+0x16: movq (%rax,%rdx,1),%rdx
Call chain: pread β mmrw (kern_memio.c:257-290, minor 1) β
kvm_access_check (vm_machdep.c:1284 loop) β pmap_kextract (pmap.c:1872
pt = *vtopt(va)) β page fault, pcb_onfault == NULL β db> panic. Guest
down. REPRODUCED.
Reachability gating: /dev/kmem open requires
caps_priv_check(SYSCAP_RESTRICTEDROOT|__SYSCAP_NOROOTTEST) (kern_memio.c:152);
reads are allowed at any securelevel/kernel_mem_readonly (only FWRITE is
blocked, kern_memio.c:155-158). So the panic is root-gated, not
securelevel-gated. Impact ceiling: privileged local DoS (Low per the audit
rubric). No memory-corruption primitive: the faulting access is a read of an
unmapped address inside the validator itself; the garbage-PA variant returns
a bogus PA that pc64 kvm_access_check only compares against 0 (it is not used
as a DMA target), and the aligned walk fails closed. No uid=0 chain exists or
was pursued (triggering already requires root).
Fix validation
fix.diff replaces the recursive-map deref with a DMAP walk of the four levels
with PG_V required at each (returning 0 when absent). Applied to the guest's
/usr/src (patch -p1), make -j6 nativekernel KERNCONF=X86_64_GENERIC +
make installkernel (rc=0, Werror clean), reboot.
An intermediate fix (v1) skipped the PDPTβPD level (fed the PDPE to
pmap_pt_to_pte) and panicked at boot (vm_page_free_contig: wire_count == 1
via ata_dmaalloc) β my authoring bug, not a kernel issue; recorded here for
honesty. The final fix.diff (v2) walks all four levels.
Patched kernel (build #1 Oct 3 2026):
pmxtest[1]: CONTROL pmap_kextract(&dummy=...) = 000000013218e2d8 (sane) pmxtest[2]: PT-present/PTE-0: unaligned pmap_kextract(va+0x30) = 0000000000000000 (garbage gone) pmxtest[3]: ... -> /dev/kmem test KVA fffff80140000000 $ ./pmx_user 0xfffff80140000000 8 pmx_user: pread returned -1 errno=14 (Bad address) pmx_user: kernel survived (no panic) $ ./pmx_user 0xffffffff811449e0 24 # control: kernel `version` symbol pmx_user: pread returned 24 errno=0 # /dev/kmem still reads mapped KVA
Baseline panic eliminated; clean EFAULT; no false negatives on mapped KVA; the whole system boots and runs (vtophys() == pmap_kextract(), so every busdma/ contigfree path exercised the new walk during boot + build).
Why the other cited paths in the file are clean (pass-2 negative results)
- cpu_fork LDT paths (vm_machdep.c:118-123, 221-227) β dead on pc64:
pcb_ldthas no writer;user_ldt_alloc/free/set_user_ldtare panic stubs (userldt.c:42-58); sysarch.c has no LDT ops on x86_64. - cpu_fork PCB copy β
union savefpu pcb_saveis embedded in struct pcb (pcb.h:80) andtd_savefpu = &td_pcb->pcb_save(pmap.c:2250), so*pcb2 = *pcb1copies FPU state by value with no aliasing; pcb_ext is NULLed (vm_machdep.c:218) and never set on pc64 (cpu_lwp_exit's KKASSERT at :311 can't fire); pcb_onfault is NULL in fork context. - cpu_prepare_lwp's user-controlled copyout (vm_machdep.c:262-265) β std_copyout (support.s:228-279) rejects dst/len outside user range (carry + VM_MAX_USER_ADDRESS), so a kernel-range lwp_stack returns EFAULT, not a write.
- PCB_DBREGS/dbregs β set_dbregs (machdep.c:3133) re-validates addresses vs VM_MAX_USER_ADDRESS for unprivileged users; fork inheritance copies within identical credentials (no privilege crossing); swtch.s:210-236/592-618 save/restore protocol is flag-consistent with cpu_lwp_exit's reset_dbregs (vm_machdep.c:316-319).
- spectre/mds sysctl parsers (vm_machdep.c:654-752, 1045-1146) β root-only
(kern_sysctl.c:1447 SYSCAP_NOSYSCTL_WR), 128-byte buffer with
len >= sizeof(buf)rejection and NUL termination; strsep loop safe. - cpu_vmspace_alloc/free β empty stubs on pc64 (vm_machdep.c:1266-1273); the VMSPACE_CTL_* intercept lives in vm_vmspace.c (already pass-2 audited).
- kernacc int-truncation at vm_machdep.c:1288 (
eaddr - saddrβ int param of kernacc) β real type defect, but unreachable in isolation: any /dev/kmem transfer β₯ 2^31 spans unmapped KVA, so the (correct or panicking) page walk decides first; with a correct walk it returns EFAULT before kernacc's truncated length matters. Killed (negative note), consistent with DF-0081's INVALID resolution of the uiomove side.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src (patch -p1), make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (rc=0, Werror), reboot into build #1 Oct 3 2026. Identical trigger (./pmx_user 0xfffff80140000000 8): baseline stock kernel panicked (Fatal trap 12 at pmap_kextract+0x16, guest down in DDB); patched kernel returns EFAULT with the guest up and responsive. Unaligned PTE-0 extraction now returns 0 (was 0x30). No regression: /dev/kmem read of a mapped kernel VA (kernel version symbol) still returns data (24 bytes, errno=0), and the patched kernel boots and runs the full system (vtophys() alias exercised by busdma/contig paths during boot and the kernel build itself). Bad behavior eliminated.
fix_run.log (EFAULT + control read + module output on patched kernel); fix_build.log (nativekernel+installkernel tail, INSTALL_DONE); panic.txt (baseline Fatal trap 12); fix.diff (git-apply-clean vs this tree)
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt: 'Fatal trap 12 ... Stopped at pmap_kextract.part.23+0x16: movq (%rax,%rdx,1),%rdx' with fault VA 0xffff80403e005000 (recursive-map PDE slot for KVA 0xfffff80140000000), current process = pmx_user (the /dev/kmem pread trigger); run.log (baseline transcript incl. module proofs); pmxtest.c output lines: 'PT-present/PTE-0: unaligned pmap_kextract(va+0x30) = 0000000000000030' (stock garbage) vs '0000000000000000' (patched); fix_run.log: 'pread returned -1 errno=14 (Bad address); kernel survived (no panic)' + control read 'pread returned 24 errno=0' on the patched kernel; fix.diff; fix_build.log (nativekernel+installkernel rc=0).
PoC changes
Seed-less (pack built from scratch). Three iterations were needed vs the initial design: (1) first module version passed the handler function to DECLARE_MODULE where a moduledata_t is required -> linker strcmp GPF during kldload (my bug, fixed); (2) honest analysis correction: aligned PTE-0 extraction returns 0 on pc64 (va & PAGE_MASK == 0), so the 'gate bypass' half of the claim is only live for UNALIGNED queries - module extended to prove both aligned and unaligned; absent-level probe constrained to KVA >= KvaStart (first probe landed below KvaStart where the range check rejects); (3) fix.diff v1 skipped the PDPT->PD level and panicked at boot (vm_page_free_contig wire_count assert via ata_dmaalloc) - corrected to a full 4-level DMAP walk (v2) which boots and passes.
Verified recommended fix
pmap_kextract(): walk the kernel page tables through the DMAP (pmap_pdp -> pmap_pdp_to_pd -> pmap_pd_to_pt -> pmap_pt_to_pte) requiring PG_V at each level and returning 0 when absent, instead of the unvalidated recursive-map *vtopt() dereference (fix.diff, applied and validated on the guest).
Verdict
REPRODUCED on the QEMU DragonFly guest (X86_64_GENERIC #0 Jul 2 2026): pc64 pmap_kextract() (pmap.c:1865-1888) dereferences the recursive map (*vtopt(va), pmap.c:1872) and tests no paging level, so kvm_access_check() (vm_machdep.c:1284-1287) - whose whole job is to verify /dev/kmem ranges page-by-page - page-faults unrecoverably (no pcb_onfault) on any kernel VA whose PDPT/PD page is not yet allocated. A single 8-byte pread() on a root-opened /dev/kmem at such a KVA panics the box: 'Fatal trap 12 ... Stopped at pmap_kextract.part.23+0x16', fault VA = the unmapped recursive-map PDE-slot address. Read access to /dev/kmem is allowed at ANY securelevel (only FWRITE is gated), but opening it requires SYSCAP_RESTRICTEDROOT, so impact is a root-gated local DoS (Low), not unprivileged. Second mode confirmed by module: with the PT page present and PTE==0, pmap_kextract() returns (va & PAGE_MASK)-style garbage (0x30 for va+0x30) instead of 0 - the pc64 kextract twin of DF-5245/DF-5290 - meaning the documented 'all pages currently resident' guarantee (kern_memio.c:266-270) is enforced only by the accident that the gate queries page-aligned VAs (aligned garbage == 0); unaligned in-tree callers (kern_memio.c:784/797/821) query always-mapped objects today, so this half is a latent landmine. FIX VALIDATED: fix.diff (DMAP-based 4-level walk + PG_V at each level, return 0 when absent) built via make nativekernel + installkernel; the identical trigger then returns EFAULT with the guest up, mapped-KVA /dev/kmem reads still succeed (kernel version symbol read: 24 bytes, errno=0), and the system boots and runs (vtophys()==pmap_kextract() globally exercised). No memory-corruption primitive: the fault is a read of an unmapped address inside the validator, and the garbage PA is only compared against 0 - never used as an access target - so no uid=0 chain exists; the trigger itself already requires root.
No comments yet.