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

vkernel64 kvm_access_check() /dev/kmem validator faults on ungrown KVA: pmap_kextract()'s vtopde() assert() SIGABRTs the whole vkernel from inside the residency check (DF-5358 pc64 twin)

Summary

The /dev/kmem residency gate walks pmap_kextract over every page before proving any mapped: beyond the grown frontier (kernel_vm_end - a few hundred MB of the fixed 512GB KVA window) pmap_pde returns NULL and vtopde's assert SIGABRTs - unhandled by the vkernel, killing the whole VM (all vcpus, all guest processes) from one pread. Verified on the vkernel rig: pread at 0xE400000000 -> 'Assertion failed (x != NULL) vtopde pmap.c:357' -> vkernel DEAD, with controls proving /dev/kmem works normally (image-shortcut read returns real bytes; grown-KVA read clean EFAULT). DF-5358 pc64 twin; root-in-vkernel availability loss only. kernel_vm_end gate fix validated (trigger -> EFAULT, vkernel alive).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5579 Β· 19 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
vkmem.c β€” 2.2 KB view raw
build.sh β€” 226 B view raw
build_image.sh β€” 1.2 KB view raw
run.sh β€” 226 B view raw
run_ctl.sh β€” 1.2 KB view raw
run_fixed.sh β€” 1.4 KB view raw
vkbuild.log β€” 934.6 KB ↓ download
vkfixbuild.log β€” 933.3 KB ↓ download
run.log β€” 2.5 KB view raw
run.baseline.log β€” 2.5 KB view raw
run.ctl.log β€” 2.3 KB view raw
run.fixed.log β€” 2.6 KB view raw
panic.txt β€” 383 B view raw
env.txt β€” 1.2 KB view raw
fix.diff β€” 883 B view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 5.3 KB view raw

DF-5579 β€” vkernel64 kvm_access_check() /dev/kmem validator kills the vkernel on ungrown KVA (vtopde() assert β†’ SIGABRT; DF-5358 pc64 twin)

File: sys/platform/vkernel64/x86_64/vm_machdep.c (pass 2) Severity: Low (root-in-vkernel required) Β· Confidence: certain Status: reproduced (panic of the whole vkernel) Β· fix validated

What breaks

kvm_access_check() (sys/platform/vkernel64/x86_64/vm_machdep.c:310-329) is the residency gate for /dev/kmem (kern_memio.c:285). For every page of the requested range it calls pmap_kextract(addr) (vm_machdep.c:322-325) before any of the range has been proven mapped. The vkernel's pmap_kextract() (sys/platform/vkernel64/platform/pmap.c:717-747) does pde = *vtopde(va) (pmap.c:732), and vtopde() (pmap.c:353-358) is:

x = pmap_pde(kernel_pmap, va);
assert(x != NULL);          /* pmap.c:357 β€” fires on ungrown KVA */

pmap_pde() (pmap.c:273-280) returns NULL when the 1 GB PDPT entry covering the VA is invalid β€” i.e. for any KVA beyond the pmap_growkernel() frontier (kernel_vm_end, absolute VA, initialized to KvaStart at pmap.c:1563-64). The vkernel's kernel VA space is a fixed 512 GB [0x8000000000, 0x10000000000) (vmparam.h:80-81, MAP_FIXED at init.c:609-621) of which only a few hundred MB is ever grown. A single pread(2) on /dev/kmem at e.g. 0xE400000000 (512 GB + 400 GB) therefore SIGABRTs the entire vkernel from inside the validator. assert() is live (libc assert, no -DNDEBUG in the vkernel build) and SIGABRT is not in the vkernel's handled signal set (exception.c:226-250).

This is the vkernel twin of DF-5358 (pc64 pmap_kextract recursive-PDE fault); same class (validator-fault on ungrown KVA), different mechanism (assert/abort vs page-fault-in-recursive-map).

Reproduce (guest, as root)

# one-time: DF-5474 link workaround (see findings/poc/DF-5474)
#   sed -i.bak -e "s|vers.o -pthread$|& -Wl,--defsym,__build_id_start=0 -Wl,--defsym,__build_id_end=0|" \
#       /usr/src/sys/platform/vkernel64/conf/Makefile
#   sysctl vm.vkernel_enable=1 ; mkdir -p /var/vkernel
cc -static -O2 -o /root/vkmem vkmem.c      # build.log
sh build_image.sh                          # /vkroot.img with /root/vkmem
sh run.sh                                  # boots kernel.debug, runs control+trigger

Expected (unfixed):

  • /root/vkmem control (pread at 512GB+1MB): pread returned -1 errno=14, DF5579_control_SURVIVED β€” validator safe on grown KVA.
  • /root/vkmem 0x454780 (vkernel image text, shortcut path): pread returned 8, bytes 41 56 41 55 41 54 53 48 β€” normal /dev/kmem reads work.
  • /root/vkmem trigger (pread at 512GB+400GB): Assertion failed: (x != NULL), function vtopde, file .../pmap.c, line 357. β†’ vkernel process dies (=== vkernel alive? === DEAD).

Fixed kernel (fix.diff applied, rebuilt): trigger returns pread returned -1 errno=14 + DF5579_trigger_SURVIVED, run reaches VKDONE, vkernel ALIVE; both controls byte-identical to baseline (run.fixed.log).

Fix

fix.diff β€” gate the page loop on kernel_vm_end (declared in vm/vm_kern.h, already included) after the KvaStart/KvaEnd bounds, before the first pmap_kextract().

VERDICT.md
↓ download raw

DF-5579 β€” VERDICT

status: reproduced (panic β€” fatal crash of the whole vkernel) Β· fix validated Β· impact: root-in-vkernel unprivileged-to-that-vkernel? NO β€” requires root inside the vkernel (PR:H) β†’ availability kill of the vkernel host process

Bottom line

A single 8-byte pread(2) on /dev/kmem at a kernel virtual address inside the vkernel's fixed 512 GB KVA window but beyond the grown portion of the kernel page tables aborts the entire vkernel process from inside the kvm_access_check() residency validator itself:

DF5579 mode=trigger offset=0xe400000000 (KVA=[0x8000000000,0x10000000000))
open ok, preading 8 bytes...
Assertion failed: (x != NULL), function vtopde, file /usr/src/sys/platform/vkernel64/platform/pmap.c, line 357.
=== vkernel alive? ===
DEAD

(run.baseline.log / panic.txt)

Reachability and privilege

  • Caller: memrw() minor 1 (/dev/kmem), sys/kern/kern_memio.c:270-296 β€” kvm_access_check(saddr, eaddr, prot) at :285 runs before the uiomove() and is the only gate.
  • /dev/kmem is root-only (0640 root:kmem, kern_memio.c:841). So the attacker is root inside the vkernel (or root on the host driving the vkernel's /dev/kmem through any file channel). The victim is the vkernel process itself β€” every guest, every user process in it, all vcpus die with no core state preserved for the guest. Not a guest-user privesc; a privileged-user DoS. Severity Low, matching DF-5358 (pc64 twin).

Root cause chain (path:line)

  1. sys/platform/vkernel64/x86_64/vm_machdep.c:322-325 β€” the validator loops pmap_kextract(addr) over every page of the requested range with zero prior knowledge that the paging structures exist.
  2. sys/platform/vkernel64/platform/pmap.c:717-747 β€” pmap_kextract(): KKASSERT(va >= KvaStart && va < KvaEnd) then pde = *vtopde(va) (:732). The KKASSERT passes: the trigger address IS inside [KvaStart, KvaEnd).
  3. sys/platform/vkernel64/platform/pmap.c:353-358 β€” vtopde(): x = pmap_pde(kernel_pmap, va); assert(x != NULL); (:357).
  4. sys/platform/vkernel64/platform/pmap.c:273-280 β€” pmap_pde() returns NULL when the PDPT entry is invalid β€” exactly the case for any VA beyond the pmap_growkernel() frontier (kernel_vm_end, pmap.c:1563-64, grows from KvaStart; a 512 MB vkernel grows a few hundred MB of the 512 GB window defined by KERNEL_KVA_START/SIZE, vmparam.h:80-81, mapped MAP_FIXED at init.c:609-621).
  5. libc assert() is live in the vkernel build (no -DNDEBUG anywhere in sys/conf or the platform makefiles) β†’ __assert_fail β†’ SIGABRT β†’ not in the vkernel's handled signal set (exception.c:226-250) β†’ default action β†’ vkernel process terminates, core dumped.

Secondary observation (same loop, benign outcome): for a KVA whose PDPT entry IS valid but whose PDE is zero, pmap_pde_to_pte() (pmap.c:283-289) computes PHYS_TO_DMAP(0)+idx*8 and reads physical page 0 through the DMAP β€” the returned garbage-PA is non-zero so the page passes the == 0 check; the subsequent kernacc() (vm_machdep.c:326, vm_glue.c:106) kernel_map entry check rejects it with EFAULT. So only the PDPT-invalid case crashes; the observed control (pread at KvaStart+1MB β†’ clean EFAULT, run.ctl.log) is consistent.

What was run (rig: DF-5337/5538/5556 proven vkernel rig)

Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (env.txt). VKERNEL64 built with the DF-5474 --defsym __build_id_* link workaround; booted kernel.debug -m 512m -r /vkroot.img -n 1 -e init_path=/bin/vkinit (1 vCPU). PoC vkmem.c (static) runs inside the vkernel as root.

Three boots (all logs in this pack):

  • run.baseline.log (unfixed kernel): devcheck /dev/kmem opens; control β†’ EFAULT, SURVIVED; trigger β†’ assertion β†’ vkernel DEAD.
  • run.ctl.log (unfixed kernel, second boot): positive control /root/vkmem 0x454780 (vkernel image text via the _start.._end shortcut, vm_machdep.c:314-317) β†’ pread returned 8, bytes 41 56 41 55 41 54 53 48 (push r14/r13/r12/rbx; sub rsp β€” genuine kernel prologue) β€” /dev/kmem reads work normally; control again EFAULT/SURVIVED.
  • run.fixed.log (fix.diff applied, kernel rebuilt, vkfixbuild.log): text control β†’ same 8 bytes; KVA control β†’ EFAULT; trigger β†’ pread returned -1 errno=14, DF5579_trigger_SURVIVED, VKDONE reached, vkernel ALIVE.

Fix

fix.diff (git-apply-able against sys/): after the KvaStart/KvaEnd gates in kvm_access_check(), reject eaddr > kernel_vm_end (vm/vm_kern.h:107, already included via vm_machdep.c:70) β€” the grown frontier β€” before the first pmap_kextract() call. Everything legitimately reachable through /dev/kmem (the _start.._end image shortcut returns earlier; grown KVA is below kernel_vm_end by construction of pmap_growkernel()) is unaffected; validated by run.fixed.log with byte-identical positive control.

Why not higher severity

No guest-user reachability: /dev/kmem open requires root. No memory corruption, no info leak (the zero-PDE garbage read is discarded; kernacc backstops). Same class/severity as the pc64 twin DF-5358 (Low, CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H).

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

Applied fix.diff to the guest /usr/src copy, rebuilt VKERNEL64 (vkfixbuild.log, BUILD_RC=0), re-ran the exact trio (run_fixed.sh -> run.fixed.log): trigger now returns pread=-1/EFAULT with DF5579_trigger_SURVIVED, session reaches VKDONE, vkernel ALIVE; positive text control returns the identical 8 bytes and KVA control behaves identically to baseline. The previously-observed bad behavior (assertion + vkernel death) is gone.

run.fixed.log (DF5579_trigger_SURVIVED / ALIVE / VKDONE) vs run.baseline.log (Assertion failed pmap.c:357 / DEAD); vkfixbuild.log (fixed kernel rebuild); fix.diff (git-apply-able)
↓ fix.diffper-fix-DF-5579

Confirmed kernel references

Detail

Exploit chain

root inside vkernel -> open /dev/kmem -> pread at KvaStart+400GB (any address beyond kernel_vm_end within the 512GB MAP_FIXED KVA window) -> memrw -> kvm_access_check (vm_machdep.c:322-325) -> pmap_kextract -> vtopde assert -> SIGABRT -> whole vkernel process dies (all vcpus, all guest processes). No memory-safety primitive; no guest-user reachability; availability only.

Evidence (decisive lines)

panic.txt + run.baseline.log (assertion line + '=== vkernel alive? === DEAD'); run.ctl.log (positive control: pread at 0x454780 returns 8 bytes 41 56 41 55 41 54 53 48 via the _start.._end shortcut; control EFAULT SURVIVED); run.fixed.log (post-fix: trigger EFAULT SURVIVED, VKDONE, ALIVE); vkfixbuild.log (fixed kernel build); VERDICT.md for the full chain

PoC changes

vkmem.c written fresh (no seed); rig rebuilt from scratch on a reset guest: DF-5474 SYSTEM_LD defsym workaround re-applied, VKERNEL64 kernel rebuilt, /vkroot.img regenerated (DF-5337/5538/5556 rig), vm.vkernel_enable=1 + /var/vkernel created (both required and missing on a fresh guest).

Verified recommended fix

In kvm_access_check(), reject eaddr > kernel_vm_end (grown page-table frontier, vm/vm_kern.h) before the pmap_kextract() page loop (fix.diff).

Verdict

Reproduced on the guest vkernel rig: a single 8-byte pread(2) on /dev/kmem at 0xE400000000 (inside the vkernel's fixed 512GB KVA window [0x8000000000,0x10000000000) but beyond the pmap_growkernel() frontier) aborts the entire vkernel from inside the kvm_access_check() residency validator: pmap_kextract() (pmap.c:732) derefs vtopde() whose assert(x != NULL) (pmap.c:357) fires because pmap_pde() (pmap.c:273-280) returns NULL for the ungrown 1GB PDPT entry; libc assert -> SIGABRT (unhandled, exception.c:226-250) -> vkernel process DEAD with all its guests. Controls prove /dev/kmem works normally in the same boot (image-shortcut text read returns real kernel prologue bytes; grown-KVA read returns clean EFAULT). Root-in-vkernel required (kern_memio.c:841, 0640 root:kmem) -> Low severity, availability-only, pc64 DF-5358 twin. fix.diff (gate the page loop on kernel_vm_end, vm/vm_kern.h:107) validated by rebuild: trigger now returns EFAULT and the vkernel stays ALIVE to VKDONE with byte-identical positive control.