β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5358

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5358 Β· 16 files
FileTypeDescriptionSize
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.md
↓ download 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:

  1. ungrown KVA β†’ *vtopt(va) recursive-map deref β†’ kernel page fault β†’ panic;
  2. 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 (0x30 observed) β€” exactly the DF-5245 defect class, but those in-tree callers (kern_memio.c:784/797/821 user_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 KVA 0xfffff80140000000 (inside [KvaStart,KvaEnd), above kernel_vm_end, covered by the VM_SUBSYS_RESERVED VM_PROT_ALL entry kmem_init() (vm_kern.c:494-537) installs over all unallocated KVA β€” so kernacc() 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_ldt has no writer; user_ldt_alloc/free/set_user_ldt are panic stubs (userldt.c:42-58); sysarch.c has no LDT ops on x86_64.
  • cpu_fork PCB copy β€” union savefpu pcb_save is embedded in struct pcb (pcb.h:80) and td_savefpu = &td_pcb->pcb_save (pmap.c:2250), so *pcb2 = *pcb1 copies 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5358

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.