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

pmap_unwire dereferences NULL pte when page table page does not exist -> vkernel crash

Summary

pmap_unwire at pmap.c:2637 pte=pmap_pte(pmap,va); :2638 if((*pte&VPTE_V)==0) derefs pte without NULL check. pmap_pte returns NULL when pmap_pde returns NULL (no PDE/PDPE/PML4E page table page exists for the VA). pc64 equivalent at sys/platform/pc64/x86_64/pmap.c:5588-5618 explicitly handles NULL. Reachable: mlock/munlock with sparse VA ranges (VPAGETABLE entries, vm_map_entry_unwire_all during exit/exec on sparse entries, race window where vm_fault_wire unlocks map allowing concurrent pmap_remove to free PT pages, error cleanup in vm_fault_wire). NULL-page fault delivered to vkernel user process -> vkernel panic takes down all virtual user processes. No memory corruption (read of address 0 trapped by real kernel). Fix: if(pte==NULL || (*pte&VPTE_V)==0).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0996 Β· 6 files
FileTypeDescriptionSize
fix.diff suggested-fix NULL check for pte in pmap_unwire (mirrors pc64) 400 B view raw
VERDICT.md verdict full narrative: why bug is real, why not testable on default guest 3.6 KB ↓ raw
README.md readme summary + fix 1.1 KB ↓ raw
manifest.json manifest this catalog 2.3 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme summary + fix
↓ download raw

DF-0996 β€” vkernel64 pmap_unwire NULL pte dereference

Summary

pmap_unwire() at sys/platform/vkernel64/platform/pmap.c:2637-2638 calls pmap_pte() then dereferences the returned pointer without a NULL check. pmap_pte() returns NULL when no page-table page exists for the VA (sys/platform/vkernel64/platform/pmap.c:317), so an unwire walk over a sparse range crashes the vkernel.

The pc64 equivalent at sys/platform/pc64/x86_64/pmap.c:5565-5619 correctly handles the NULL case β€” proving the vkernel64 version is the outlier.

Why not tested on default guest

The bug is in sys/platform/vkernel64/, not compiled into the default X86_64_GENERIC kernel. Triggering it requires building and booting a VKERNEL64 target (the DragonFly virtual kernel), which is outside this run's scope. The fix is verified by source review and parity with pc64.

Fix

Apply fix.diff:

pte = pmap_pte(pmap, va);
-    if ((*pte & VPTE_V) == 0) {
-        *pva = pmap_nextva(pmap, va);
+    if (pte == NULL || (*pte & VPTE_V) == 0) {
+        if (pte == NULL)
+            *pva = pmap_nextva(pmap, va);
VERDICT.md verdict full narrative: why bug is real, why not testable on default guest
↓ download raw

DF-0996 β€” vkernel64 pmap_unwire dereferences NULL pte

Verdict

NOT TESTABLE on default guest. Bug is real and the fix is correct (verified by source-level trace and by parity with the equivalent pc64 implementation), but the vulnerable code lives in sys/platform/vkernel64/, which is not compiled into the running X86_64_GENERIC kernel on this guest. The vkernel64 is a separate kernel target (a virtual kernel that runs as a userspace process on the real kernel); exercising this path would require building/booting a VKERNEL64 binary that hosts user processes β€” outside the scope of the default guest and not a default-kernel config.

The bug and fix are confirmed by code review (below). Filed severity (Medium) and CVSS AV:L accurately reflect the on-host trigger model for a system running vkernel64.

Mechanism (cited)

pmap_unwire() at sys/platform/vkernel64/platform/pmap.c:2621-2665:

2636:    vm_object_hold(pmap->pm_pteobj);
2637:    pte = pmap_pte(pmap, va);
2638:    if ((*pte & VPTE_V) == 0) {        /* <-- derefs pte with no NULL check */
2639:        *pva = pmap_nextva(pmap, va);

pmap_pte() at sys/platform/vkernel64/platform/pmap.c:311-322 returns NULL when pmap_pde() returns NULL (no PDE/PDPE/PML4E page-table page exists for the VA) or when the PDE is not present:

316:    pde = pmap_pde(pmap, va);
317:    if (pde == NULL || (*pde & VPTE_V) == 0)
318:        return NULL;

Line 2638 then dereferences pte unconditionally β€” a NULL dereference.

The equivalent function in the bare-metal pc64 pmap (sys/platform/pc64/x86_64/pmap.c:5565-5619) does handle the NULL case correctly (lines 5588, 5610–5618), proving the vkernel64 version is the outlier and the fix pattern is canonical.

Reachability (in a vkernel64 environment)

pmap_unwire() is called from sys/vm/vm_fault.c:2636 and sys/vm/vm_fault.c:2691 during vm_fault_wire()/vm_fault_unwire() β€” i.e. on mlock/munlock, and on vm_map_entry_unwire_all() during exit/exec of processes that have wired sparse VA ranges. The finding summary cites multiple realistic trigger paths: sparse VPAGETABLE entries, race window where vm_fault_wire unlocks the map allowing concurrent pmap_remove to free PT pages, and error cleanup paths.

In a vkernel64 (which maps a guest physical address space onto host user memory), a NULL PDE is the normal state for vast ranges of the virtual address space β€” pmap_unwire is called per page during unwire-walks, so any unwire that walks into a region with no PT page triggers the NULL deref.

Impact

  • A vkernel64 crash takes down all virtual user processes it hosts (vkernel is single-process-from-host-perspective, multiplexing many guest user processes). This is a host-local DoS of the vkernel's guest.
  • Per the finding summary, the fault is delivered as a NULL-page fault trapped by the real (host) kernel β€” no memory corruption of the host.

Fix

fix.diff β€” adds the missing NULL check at line 2638, matching the pattern used by the pc64 equivalent. When pte == NULL, advance *pva via pmap_nextva() and return NULL (same as the existing VPTE_V==0 path).

This fix is not validated by a built kernel because the default guest kernel does not include vkernel64 code. Applying the diff and rebuilding a VKERNEL64 target (with a workload that issues mlock over a sparse VA range) would validate it; that is outside this run's scope.

Files in this evidence pack

  • fix.diff β€” NULL check for pte in pmap_unwire()
  • VERDICT.md β€” this narrative
  • README.md β€” overview
  • manifest.json β€” artifact catalog

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. vkernel64 pmap_unwire NULL pte deref. vkernel64 not compiled into GENERIC. Fix mirrors pc64.