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)
PoC verification
Evidence pack
findings/poc/DF-5579 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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, bytes41 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().
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 theuiomove()and is the only gate. /dev/kmemis 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)
sys/platform/vkernel64/x86_64/vm_machdep.c:322-325β the validator loopspmap_kextract(addr)over every page of the requested range with zero prior knowledge that the paging structures exist.sys/platform/vkernel64/platform/pmap.c:717-747βpmap_kextract():KKASSERT(va >= KvaStart && va < KvaEnd)thenpde = *vtopde(va)(:732). The KKASSERT passes: the trigger address IS inside [KvaStart, KvaEnd).sys/platform/vkernel64/platform/pmap.c:353-358βvtopde():x = pmap_pde(kernel_pmap, va); assert(x != NULL);(:357).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 thepmap_growkernel()frontier (kernel_vm_end, pmap.c:1563-64, grows fromKvaStart; 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).- 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/kmemopens;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.._endshortcut, vm_machdep.c:314-317) βpread returned 8, bytes41 56 41 55 41 54 53 48(push r14/r13/r12/rbx; sub rsp β genuine kernel prologue) β /dev/kmem reads work normally;controlagain 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
fixedApplied 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)
Confirmed kernel references
- sys/platform/vkernel64/x86_64/vm_machdep.c:310
- sys/platform/vkernel64/x86_64/vm_machdep.c:322
- sys/platform/vkernel64/platform/pmap.c:732
- sys/platform/vkernel64/platform/pmap.c:357
- sys/platform/vkernel64/platform/pmap.c:273
- sys/platform/vkernel64/platform/pmap.c:1563
- sys/platform/vkernel64/include/vmparam.h:80
- sys/platform/vkernel64/platform/init.c:609
- sys/kern/kern_memio.c:285
- sys/platform/vkernel64/x86_64/exception.c:226
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.
No comments yet.