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

Backtracer frame walk trusts stack content β€” INKERNEL() defined but never used AND semantically wrong for the vkernel; walk escapes into guest-user VAs (guaranteed fault on this platform) and prints attacker-chosen kernel symbols (DF-5470 twin, vkernel-amplified)

Summary

The fork shares the pc64 DF-5470 defect and aggravates it: INKERNEL() is never referenced AND the inherited >= USRSTACK definition is itself wrong for the vkernel (kernel KVA lives inside the user range; the correct bound is [KvaStart,KvaEnd)). Any adopted guest-user frame pointer guarantees a SIGSEGV in db_read_bytes (vkernel copyin never raw-derefs user VAs) - the escape doesn't merely read attacker memory, it detonates DF-5473; kernel-KVA values are steerable (rig-chosen kprintf() symbol printed). In-vkernel-proven: clean chain prints rig-chosen frames, bad chain adopts 0x100000000 -> Fatal trap 12 at the f_retaddr read -> DF-5473 cascade kills the vkernel (host: exited signal 4, core dumped). KvaStart/KvaEnd + forward-progress validation fix validated (walk returns cleanly).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5472 Β· 13 files
FileTypeDescriptionSize
README.md β€” 4.2 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
verdict.json β€” 4.2 KB view raw
src/vk_trace_harness.c β€” 3.8 KB view raw
src/vktrigger.c β€” 2.2 KB view raw
src/run_vk.sh β€” 1.9 KB view raw
src/build_image.sh β€” 2.1 KB view raw
vkrun_clean.log β€” 2.2 KB view raw
vkrun_bad.log β€” 2.1 KB view raw
vkrun_fixed.log β€” 2.3 KB view raw
vkrun_smoke.log β€” 3.7 KB view raw
core_gdb.txt β€” 496 B view raw
env.txt β€” 250 B view raw

DF-5472 β€” vkernel64 backtracer frame walk trusts stack content; INKERNEL() dead AND semantically wrong for the platform; walk escapes into guest-user VAs and attacker-chosen kernel symbols are printed (pc64 DF-5470 twin, with vkernel-specific amplification)

File: sys/platform/vkernel64/x86_64/db_trace.c (pass 2) Severity: Medium Β· Confidence: certain Β· Verified: reproduced on the QEMU guest (in-vkernel rig)

What

Identical defect class to the pc64 twin (DF-5470), plus two vkernel-specific aggravations:

  1. INKERNEL(va) is defined at db_trace.c:132 and never referenced. The only validation of an adopted frame pointer is (unsigned long)*fp < PAGE_SIZE (db_trace.c:227). Whatever QWORD sits at [rbp] becomes the next frame and is dereferenced (db_trace.c:231-232), and the "trapframe" heuristic reads *fp+16 with a raw deref (db_trace.c:268).
  2. Even if INKERNEL() were wired in, the inherited definition is wrong for the vkernel: INKERNEL(va) = va >= USRSTACK with USRSTACK = VM_MAX_USER_ADDRESS β‰ˆ 0x7FFF_FFFF_F000 (vmparam.h:94,100) β€” but the vkernel's kernel lives at KERNEL_KVA_START = 512GB .. +512GB (vmparam.h:80-81), i.e. inside [0, USRSTACK). The inherited test would reject every real kernel frame and accept most guest-user addresses. The correct bound is [KvaStart, KvaEnd) (platform/init.c:609-621, vm/pmap.h:141). The tree's generic helper kva_p() (vm/pmap.h:162-171) is also useless here: for _KERNEL_VIRTUAL it is hardcoded addr != NULL.
  3. The trap/syscall/interrupt frame heuristics (calltrap, Xresume*, _Xsyscall, db_trace.c:244-248) are dead code on vkernel64 β€” those symbols do not exist anywhere in the platform (traps arrive as host signals via exc_segfault β†’ kern_trap, exception.c:263, trap.c:583). The walk structurally cannot recognize a kernel↔user boundary.

Vkernel-specific impact (guest-proven)

Guest-user memory of the process whose syscall is being traced lives in a different host vmspace than the vkernel kernel: vkernel copyin (platform/copyio.c:311-322) never raw-dereferences a user VA (it uses vm_fault_page + kernel-window lwbuf copies). Consequently, unlike pc64 where the escaped walk reads user memory, on vkernel64 any adopted guest-user frame pointer guarantees a fault in db_read_bytes β€” which falls into the broken no-fault recovery (DF-5473) and destroys the vkernel process with zero further diagnostics. Values that do stay in kernel KVA are fully attacker-steerable: the trace prints the symbol of any kernel address the corrupted stack happens to hold (kprintf() at 0x50d850 printed from a rig-chosen return address in the clean run), and raw hex for the rest (the leak aspect is DF-1078's; the steering + escape + guaranteed fault is this finding).

Proof (this pack)

In-vkernel rig: a VKERNEL64 kernel (built on the guest, plus rig harness vk_trace_harness.c) boots on a minimal UFS image; an ordinary guest process (vktrigger) sprays a fake frame chain at guest-user VA 0x100000000 and asks the harness to walk a kernel chain whose second frame points there (db_stack_trace_cmd(chain, TRUE, 6, NULL) β€” the exact entry print_backtrace() uses, db_trace.c:449 β€” from live syscall context with db_active == 0, the panic-path state).

  • vkrun_clean.log β€” control: chain stays in kernel KVA; walk completes, prints rig-chosen symbols, returns; vkernel survives.
  • vkrun_bad.log β€” walk adopts 0x100000000: Fatal trap 12: page fault, fault virtual address = 0x100000008 (= the f_retaddr read at db_trace.c:231) in process vktrigger, then the DF-5473 cascade.
  • vkrun_fixed.log β€” with fix.diff (KvaStart/KvaEnd walk validation + forward-progress check) the same trigger stops cleanly at the boundary, "DFVK: walk RETURNED without crashing", vkernel survives.

Fix

fix.diff (shared with DF-5473): redefine INKERNEL to [KvaStart, KvaEnd), reject non-kernel/non-forward frames at every adoption point (db_nextframe entry :227, NORMAL path :256, trap epilogue :308, have_addr entry :335). Validated: bad trigger returns cleanly, no crash, vkernel stays up (vkrun_fixed.log).

Build / run

See DF-5473 pack (build_image.sh, run_vk.sh); sources in src/.

VERDICT.md
↓ download raw

DF-5472 VERDICT β€” reproduced (in-vkernel rig)

Bottom line: The vkernel64 backtracer adopts arbitrary 64-bit frame pointers from stack content. INKERNEL() (db_trace.c:132) is never called, and β€” a pass-2 discovery β€” it is semantically wrong for this platform anyway (kernel KVA 512GB..1TB lies inside [0, USRSTACKβ‰ˆ128TB); vmparam.h:80,94,100). On vkernel, guest-user memory lives in a separate host vmspace (platform/copyio.c:311 never raw-derefs user VA), so any adopted guest-user frame guarantees a fault in db_read_bytes β€” the walk does not merely read attacker memory like pc64 (DF-5470), it detonates the broken no-fault path (DF-5473). Kernel-KVA values remaining in the chain are fully steerable: the clean run prints the rig-chosen symbol (kprintf() at 0x50d850) β€” an unprivileged guest process corrupting its own stack content (or any kernel stack corruption) chooses which "functions" appear in the panic trace.

How reproduced (2 deliberate runs + 1 accidental, fresh boots each)

  • rig: guest-built VKERNEL64 kernel + in-kernel harness (debug.df_vk_walk sysctl runs db_stack_trace_cmd(chain, TRUE, 6, NULL) β€” the exact entry of print_backtrace(), db_trace.c:443-450 β€” from live syscall context, db_active=0, db_jmpbuf all-zero, verified in the log line DFVK: db_active=0 db_jmpbuf[0]=0000000000000000 db_jmpbuf[3]=0000000000000000).
  • vkrun_clean.log: chain kept in kernel KVA β†’ 2Γ—(null)() at 0x8002037030
  • kprintf() at 0x50d850 (rig-chosen return address symbolized), walk returned, vkernel survived (VK_EXIT=0).
  • vkrun_bad.log: chain points into guest-user VA 0x100000000 (mapped for the guest process β€” vtrigger's mmap succeeded β€” but not dereferenceable from kernel context) β†’ Fatal trap 12: page fault, fault virtual address = 0x100000008 β€” the f_retaddr read at db_trace.c:231 (faulting insn verified in the disassembly: 0x71ca40: movzbl (%rdi),%ecx, the db_read_bytes copy loop, db_interface.c:239-240) β†’ DF-5473 cascade kills the vkernel (signal 4, core dumped).

Exploit chain

Not a host-privesc class: the vkernel is an unprivileged host process; impact is inside the vkernel's own threat model (steered/destroyed panic forensics, kernel-process death from a live debug-print site). Chain: guest process influences stack content walked during a vkernel panic/debug-print β†’ walk escapes kernel KVA β†’ guaranteed SIGSEGV β†’ broken no-fault longjmp β†’ vkernel process destroyed with zero diagnostics (anti-forensics amplifier).

Fix validation

fix.diff (in DF-5473 pack, shared): INKERNEL β†’ [KvaStart,KvaEnd), frame adoption validated at db_trace.c:227/256/308 + have_addr entry :335. Rebuilt vkernel, identical bad trigger β†’ 2 frames printed, walk stops at the guest-user boundary, DFVK: walk RETURNED without crashing, sysctl returned, clean halt, no signal-4 death (vkrun_fixed.log, guest dmesg clean). fix_status: fixed.

Negative results (classes hunted, killed)

  • db_sym_numargs/argnames OOB (db_trace.c:421-426): DEAD β€” vkernel uses db_kld.c:107 X_db_sym_numargs which is hardcoded return FALSE; narg comes from db_numargs() which returns 0 (db_trace.c:163-167).
  • trapframe heuristics (db_trace.c:244-248): dead code β€” calltrap, Xresume*, _Xsyscall exist nowhere in vkernel64 (signal-based traps); contributes to the boundary-blindness covered by this finding.
  • watchpoint paths (kx86_64_set_watch etc., db_trace.c:473-652): DDB-console only (operator), not attacker-reachable.
  • dl_symbol_values/host dladdr() on garbage callpc (db_trace.c:659-669): dladdr walks link_map ranges without dereferencing callpc β€” no memory safety issue found.
  • db_numargs instruction-decode OOB read: disabled at db_trace.c:166 (#if 1 return 0).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

patched vkernel with identical bad trigger: walk stops at the guest-user boundary, prints 2 frames, 'DFVK: walk RETURNED without crashing', clean halt, no signal death (vkrun_fixed.log); guest dmesg shows no new core dump

findings/poc/DF-5472/vkrun_fixed.log lines 34-44 vs findings/poc/DF-5472/vkrun_bad.log lines 36-54; fix.diff in findings/poc/DF-5473/fix.diff (git apply --check clean against repo tree)
per-fix-DF-5472

Confirmed kernel references

Detail

Exploit chain

guest process / stack corruption steers frame pointers -> walk escapes kernel KVA -> guaranteed SIGSEGV in db_read_bytes -> broken longjmp -> vkernel process killed with zero diagnostics (anti-forensics: the panic trace that trace_on_panic exists for is destroyed). No host privesc: the vkernel is an unprivileged host process.

Evidence (decisive lines)

vkrun_clean.log lines 34-44 (walk mechanics + steered symbol + survival), vkrun_bad.log lines 36-54 (escape -> Fatal trap 12 -> silence), VERDICT.md negative-results section (db_sym_numargs hardcoded FALSE at db_kld.c:107, dead trap heuristics, dladdr safe)

PoC changes

authored fresh in-vkernel rig (no seed): kernel-built harness sysctl debug.df_vk_walk driving db_stack_trace_cmd from live syscall context; guest-user spray via mmap MAP_FIXED at 0x100000000; required building the VKERNEL64 kernel + a minimal UFS root image on the guest (DF-5474 build fix needed first)

Verified recommended fix

redefine INKERNEL to [KvaStart,KvaEnd) and validate every adopted frame (kernel KVA + strictly forward) in db_nextframe and the have_addr entry β€” see fix.diff in DF-5473 pack

Verdict

The vkernel64 fork shares the pc64 DF-5470 defect and aggravates it: INKERNEL() (db_trace.c:132) is defined but never called, the only frame validation is *fp < PAGE_SIZE (db_trace.c:227), and the inherited INKERNEL definition (>= USRSTACK) is itself wrong for the vkernel (kernel KVA 512GB..1TB lies inside [0,USRSTACK~128TB); the correct bound is [KvaStart,KvaEnd)). Guest-proven on the QEMU guest with an in-vkernel rig: a walk whose chain leaves kernel KVA adopts a guest-user VA, and because vkernel copyin (copyio.c:311) never raw-derefs user VAs, the walk's db_read_bytes deref is a guaranteed fault (Fatal trap 12, VA=0x100000008, the f_retaddr read at db_trace.c:231, faulting insn movzbl (%rdi),%ecx at 0x71ca40) which then detonates the broken no-fault recovery (DF-5473) and kills the vkernel process. Kernel-KVA values in the chain are steerable: the clean run printed the rig-chosen symbol 'kprintf() at 0x50d850'.