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

vkernel DDB backtrace leaks kernel addresses to world-readable msgbuf (DF-1077 class) + missing Xfast_syscall boundary stop

Field Value
ID DF-1078
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
File sys/platform/vkernel64/x86_64/db_trace.c
Lines 280-302 (raw rip/rsp/rbp prints), 345/352 (%p stack-frame prints), 428 (missing Xfast_syscall stop)
Area platform/vkernel64 (virtual-kernel DDB backtrace)
Confidence likely
Discovered 2026-07-14
Reported pending
Known CVE DF-1077 (pc64 sibling)
CVE match dfly_specific

Summary

db_nextframe() prints raw rip/rsp/rbp values via db_printf for trap/syscall/interrupt frames (lines 280-302), and db_stack_trace_cmd prints stack-frame addresses via %p (lines 345, 352). When print_backtrace() is called from non-DDB paths β€” e.g. panic with trace_on_panic=1 (the default for VKERNEL64: config line 59 options DDB_TRACE sets trace_on_panic=1 at kern_shutdown.c:116) β€” db_active is 0, so db_putchar (db_output.c:108-119) routes output through kprintf, which writes to the vkernel's msgbuf (initialised at init.c:702) AND to host stdout (vconsputc β†’ pwrite(1, ...) at console.c:437). Vkernel guest processes read msgbuf via /dev/klog or dmesg (world-readable), leaking the vkernel kernel's virtual address layout (KASLR bypass).

The vkernel also omits the Xfast_syscall stack-walk boundary stop that pc64 has (pc64 db_trace.c:430-435), so the walker continues past the syscall boundary into junk frames, potentially leaking additional unresolved addresses via db_printsym (db_sym.c:289-292 prints raw hex when no symbol matches).

Root cause

Three compounding defects in sys/platform/vkernel64/x86_64/db_trace.c:

  1. db_nextframe (lines 280-282, 289-291, 299-301) unconditionally prints raw kernel addresses via db_printf with %016lx format specifiers for rip, rsp, and rbp in trap/syscall/interrupt frame markers. These values are vkernel kernel virtual addresses.

  2. db_stack_trace_cmd (line 345) prints db_printf("%p does not look like a stack frame, skipping\n", ...) and (line 352) db_printf("Trace beginning at frame %p\n", frame), leaking stack-frame addresses.

  3. The vkernel version is missing the Xfast_syscall boundary stop present in pc64 (sys/platform/pc64/x86_64/db_trace.c:430-435): if (name && strcmp(name, "Xfast_syscall") == 0) break;. Without this, db_stack_trace_cmd's while(count--) loop at line 357 continues walking past the syscall entry point into user-space or junk stack frames, where return addresses are unlikely to resolve to symbols. db_printsym (called at line 211 via db_print_stack_entry) then prints raw hex addresses (db_sym.c:289-292: when name == NULL or offset >= db_maxoff, it prints db_format_radix of the raw address).

The output path: db_printf (db_output.c:181) β†’ kvcprintf β†’ db_putchar (db_output.c:102). When db_active == 0 (print_backtrace context, not interactive DDB), db_putchar (db_output.c:108) calls kprintf("%c", c), which sends to both the console driver and msgbuf. The vkernel's console is host stdout (console.c:433-437: vconsputc writes via pwrite(1, ...)). The vkernel's msgbuf is set up at init.c:696-702 and is readable inside the vkernel via /dev/klog (standard DragonFlyBSD device, world-readable by default) or the dmesg command.

The VKERNEL64 kernel config (sys/config/VKERNEL64 lines 58-59) enables both DDB and DDB_TRACE by default, so trace_on_panic=1 (kern_shutdown.c:116) and debugger_on_panic=1 (kern_shutdown.c:110). Any vkernel panic triggers print_backtrace(6) at kern_shutdown.c:881 BEFORE DDB is entered, so addresses reach msgbuf while db_active is still 0.

Threat model & preconditions

  • Attacker position: An unprivileged process running inside a vkernel (a vkernel guest process).
  • Privileges gained or impact: Read the vkernel's msgbuf via /dev/klog or dmesg to learn the vkernel kernel's virtual address layout. The vkernel kernel runs as a user-space process on the host, so these "kernel" addresses are user-space virtual addresses from the host's perspective β€” they reveal the vkernel process's code / data / stack layout, defeating host ASLR (if the vkernel binary is PIE) or the vkernel's own address randomisation. This KASLR bypass can be chained with a separate vkernel kernel vulnerability (e.g. a syscall bug) to achieve reliable code execution within the vkernel process, potentially escaping the vkernel sandbox to gain the privileges of the host user running the vkernel.
  • Required config or capabilities: Default kernel with vkernel64 running the default VKERNEL64 config (DDB + DDB_TRACE + INVARIANTS enabled). No special privileges needed inside the vkernel to read /dev/klog.
  • Reachability: Triggered whenever print_backtrace() is called with db_active == 0. Primary trigger: vkernel panic (trace_on_panic=1 by default). Additional triggers: INVARIANTS-enabled debug paths that call print_backtrace() (VKERNEL64 config line 60 enables INVARIANTS), including lwkt_token.c:710/774, vfs_lock.c:223, lwkt_thread.c:605, kern_lock.c:389/444, vfs_bio.c:4112/4408, uipc_mbuf.c:1339/2413, kern_jail.c:785/801 β€” many reachable by unprivileged guest processes causing lock-order or assertion conditions.

Proof of concept

# PoC: vkernel kernel address leak via msgbuf
#
# Setup: Boot a DragonFlyBSD vkernel (vkernel64) on a host.
# The vkernel is built with the default VKERNEL64 config which
# enables DDB, DDB_TRACE (trace_on_panic=1), and INVARIANTS.
#
# Step 1: As an unprivileged guest process inside the vkernel,
#         trigger a code path that calls print_backtrace().
#         The simplest is to trigger a kernel panic. For example,
#         many INVARIANTS assertions call print_backtrace() before
#         panicking. Alternatively, trigger a debug warning path
#         (e.g., lock debugging in kern_lock.c) that calls
#         print_backtrace() without panicking.
#
#         A reliable non-fatal trigger: set a debug sysctl that
#         enables a code path calling print_backtrace(). For
#         example, vfs_bio.c:4408 calls print_backtrace(10) when
#         a buffer cache invariant is violated.
#
# Step 2: After print_backtrace() has run (either the vkernel
#         recovered from a non-fatal debug warning, or rebooted
#         after a panic with a persistent msgbuf), read the msgbuf:
#
#         dmesg | grep -E 'rip = |rsp = |rbp = |0x[0-9a-f]{12,16}'
#
#         Or read /dev/klog directly:
#         cat /dev/klog &
#         # wait for a backtrace to appear

Build & run

No compilation needed β€” this is a runtime exploit using standard vkernel guest utilities (dmesg, cat /dev/klog).

# Inside a running vkernel, as an unprivileged user:
dmesg | grep -E 'rip = |rsp = |rbp = |0x[0-9a-f]{12,16}'

Expected output

Raw vkernel kernel addresses leaked:

--- trap 000000000000000c, rip = 0x0000000801234abc,
    rsp = 0x0000000805678def, rbp = 0x0000000805678d00 ---
some_function(0x801234abc) at some_symbol+0x45
--- syscall 0000000000000004, rip = 0x000000080abcdef0,
    rsp = 0x0000000805679000, rbp = 0x0000000805678f00 ---

The leaked rip values reveal the vkernel kernel's text segment base address. rsp / rbp values reveal the kernel stack layout. Use these to defeat KASLR and exploit a separate vkernel kernel vulnerability.

Impact

KASLR-bypass / address-layout leak from an unprivileged vkernel guest process via msgbuf after any panic or INVARIANTS-warning path that calls print_backtrace(). Medium severity per "info leak of limited kernel memory" + "vkernel default config exposes this without any opt-in". The vkernel kernel runs as a user-space process so the leak reveals host user-space addresses, compounding with host ASLR if PIE. Sibling of DF-1077 (pc64) but more severe because the vkernel process address space is directly useful for guest-to-host exploitation.

Two changes in db_trace.c:

  1. Add the missing Xfast_syscall boundary stop (port from pc64 db_trace.c:430-435) to prevent the stack walker from continuing past the syscall boundary into junk frames.

  2. Gate all raw-address (%016lx, %p) prints on db_active (the DDB-is-interactive flag, declared extern in ddb/ddb.h:78). When db_active == 0 (print_backtrace context), output goes to msgbuf via kprintf, so raw addresses must be suppressed. Replace raw rip prints with db_printsym (symbolic), and only print rsp / rbp when db_active == 1.

Note: the root cause is that db_putchar (db_output.c:108) routes to kprintf β†’ msgbuf when !db_active. A more complete fix would change db_putchar to use cnputc (console only, no msgbuf) instead of kprintf when !db_active, but that is in db_output.c, outside this file. The fix below addresses the leak within db_trace.c itself.

--- a/sys/platform/vkernel64/x86_64/db_trace.c
+++ b/sys/platform/vkernel64/x86_64/db_trace.c
@@ -276,26 +276,35 @@ db_nextframe(struct x86_64_frame **fp, db_addr_t *ip)
    switch (frame_type) {
    case TRAP:
        {
            rip = tf->tf_rip;
            rbp = tf->tf_rbp;
-           db_printf(
-    "--- trap %016lx, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
-               tf->tf_trapno, rip, rsp, rbp);
+           db_printf("--- trap %ld, rip = ", tf->tf_trapno);
+           db_printsym(rip, DB_STGY_PROC);
+           if (db_active)
+               db_printf(", rsp = %016lx, rbp = %016lx", rsp, rbp);
+           db_printf(" ---\n");
        }
        break;
    case SYSCALL:
        {
            rip = tf->tf_rip;
            rbp = tf->tf_rbp;
-           db_printf(
-    "--- syscall %016lx, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
-               tf->tf_rax, rip, rsp, rbp);
+           db_printf("--- syscall %ld, rip = ", tf->tf_rax);
+           db_printsym(rip, DB_STGY_PROC);
+           if (db_active)
+               db_printf(", rsp = %016lx, rbp = %016lx", rsp, rbp);
+           db_printf(" ---\n");
        }
        break;
    case INTERRUPT:
        tf = (struct trapframe *)((long)*fp + 16);
        {
            rip = tf->tf_rip;
            rbp = tf->tf_rbp;
-           db_printf(
-    "--- interrupt, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
-               rip, rsp, rbp);
+           db_printf("--- interrupt, rip = ");
+           db_printsym(rip, DB_STGY_PROC);
+           if (db_active)
+               db_printf(", rsp = %016lx, rbp = %016lx", rsp, rbp);
+           db_printf(" ---\n");
        }
        break;
    default:
        break;
@@ -342,13 +351,15 @@ db_stack_trace_cmd(db_expr_t addr, boolean_t have_addr, db_expr_t count,
            ) {
                break;
            }
-           db_printf("%p does not look like a stack frame, skipping\n", (char *)&frame->f_frame + i);
+           if (db_active)
+               db_printf("%p does not look like a stack frame, skipping\n", (char *)&frame->f_frame + i);
        }
        if (i == 4096) {
            db_printf("Unable to find anything that looks like a stack frame\n");
            return;
        }
        frame = (void *)((char *)frame + i);
-       db_printf("Trace beginning at frame %p\n", frame);
+       if (db_active)
+           db_printf("Trace beginning at frame %p\n", frame);
        callpc = (db_addr_t)db_get_value((long)&frame->f_retaddr, 8, FALSE);
    }
@@ -428,6 +439,12 @@ db_stack_trace_cmd(db_expr_t addr, boolean_t have_addr, db_expr_t count,

        db_print_stack_entry(name, narg, argnp, argp, callpc);

+       /*
+        * Stop at the system call boundary (else we risk
+        * double-faulting on junk).
+        */
+       if (name && strcmp(name, "Xfast_syscall") == 0)
+           break;
+
        if (actframe != frame) {
            /* `frame' belongs to caller. */
            callpc = (db_addr_t)

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1078 Β· 8 files
FileTypeDescriptionSize
verify.sh trigger-source static source-verification script (6 checks) 1.4 KB view raw
verify.log run-log verify.sh output on audit commit 2.1 KB view raw
VERDICT.md verdict full narrative: mechanism + why-not-reproduced 5.8 KB ↓ raw
fix.diff suggested-fix gate raw prints on db_active + add Xfast_syscall stop 2.7 KB view raw
env.txt environment uname, cc, sysctls 713 B view raw
README.md readme how to reproduce 1.2 KB ↓ 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 how to reproduce
↓ download raw

DF-DF-1078 β€” reproduce

This finding was verified by static source tracing (see VERDICT.md). Runtime reproduction on the audit's default QEMU guest is not possible because the precondition is outside the unprivileged-guest-user threat model (see VERDICT.md "Why it cannot be triggered from the audit guest").

How to verify (static source check)

# from the repo root (sys/ must be present)
sh findings/poc/DF-DF-1078/verify.sh

The script walks the cited code path in sys/ with grep/sed and confirms every claim in the finding markdown.

Files

File Purpose
verify.sh static source-verification script
verify.log output of verify.sh on the audit commit (the evidence)
VERDICT.md full narrative: mechanism, why-not-reproduced, fix rationale
fix.diff git-apply-able fix (validated with git apply --check)
env.txt guest environment (uname, cc, sysctls, modules)
manifest.json machine-readable artifact catalog
VERDICT.md verdict full narrative: mechanism + why-not-reproduced
↓ download raw

DF-1078 β€” vkernel64 DDB backtrace leaks kernel addresses to msgbuf

Verdict

NOT REPRODUCED (runtime) β€” STATIC VERIFICATION CONFIRMED.

The cited code path and bug exist verbatim in sys/platform/vkernel64/x86_64/db_trace.c. The runtime trigger requires a running vkernel64 binary β€” a separate user-mode kernel build that is not the running GENERIC kernel. The audit's running kernel is 6.5-DEVELOPMENT #0 X86_64_GENERIC (confirmed via sysctl kern.version); there is no vkernel binary in /boot/kernel/ (ls /boot/kernel/ | grep -i vkernel returns nothing) and the vkernel64 platform is built only when the operator explicitly configures VKERNEL64. The audit does not build or run a vkernel.

So this is a non-default-kernel defect: real in source, present in the vkernel64 platform tree, but not exercisable on the GENERIC kernel that the audit guest runs. The finding itself classifies this correctly as Medium with CVSS AV:L/AC:L/PR:L (an unprivileged process inside a vkernel guest reading /dev/klog).

Mechanism (confirmed by source trace)

db_nextframe() (vkernel64/db_trace.c:264-) prints raw rip/rsp/ rbp values via db_printf with %016lx format specifiers in three cases:

  • TRAP at :280-283: db_printf("--- trap %016lx, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n", tf->tf_trapno, rip, rsp, rbp);
  • SYSCALL at :289-292: identical pattern with tf->tf_rax
  • INTERRUPT at :299-302: db_printf("--- interrupt, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n", rip, rsp, rbp);

db_stack_trace_cmd (:345): db_printf("%p does not look like a stack frame, skipping\n", ...) and (:352) db_printf("Trace beginning at frame %p\n", frame) β€” both leak stack-frame addresses via %p.

When print_backtrace() is called from non-DDB paths (panic with trace_on_panic=1, the default for VKERNEL64 β€” sys/config/VKERNEL64:59 options DDB_TRACE sets trace_on_panic=1 at kern_shutdown.c:116), db_active is 0, so db_putchar (sys/ddb/db_output.c:108-119) routes output through kprintf("%c", c) which writes to both the vkernel's msgbuf and host stdout (console.c:437 vconsputc β†’ pwrite(1, ...)). Vkernel guest processes read msgbuf via /dev/klog or dmesg (world-readable) β€” leaking the vkernel kernel's virtual address layout. The audit verified db_output.c:108-119 end-to-end:

/* db_output.c:108-119 */
if (!db_active) {
    if (c == '\r' || c == '\n' || c == '\t' || isprint(c)) {
        kprintf("%c", c);              /* <-- routes to msgbuf */
    } else {
        kprintf("?");
    }
    if (!db_active)
        return;
    ...
}

The vkernel64 platform is also missing the Xfast_syscall boundary stop that pc64 has (sys/platform/pc64/x86_64/db_trace.c:434: if (name && strcmp(name, "Xfast_syscall") == 0) break;). Confirmed by grep -n Xfast_syscall sys/platform/vkernel64/x86_64/db_trace.c β€” no match. Without it, db_stack_trace_cmd's while(count--) loop continues past the syscall entry point into user-space / junk stack frames, where return addresses are unlikely to resolve to symbols. db_printsym (db_sym.c:289-292) then prints raw hex addresses when name == NULL or offset >= db_maxoff β€” additional unresolved-address leak.

Why it cannot be triggered from the audit guest

The audit guest runs the X86_64_GENERIC kernel, not a vkernel. The vkernel64 platform is a separate kernel build that must be configured (VKERNEL64) and compiled; even then it runs as a userland process on the host, not as the guest kernel. Building and running a vkernel is a non-default-kernel action; per the audit's bright-line rule, results that hold only on a non-default kernel build (without labelling) are circular. The audit therefore confines the demonstration for this finding to the static-verification fallback.

Exploit chain

None developed β€” the leak is from a vkernel64 binary (not running), and the primitive is a KASLR-bypass / address-layout leak rather than memory corruption. The finding's threat model chains this with a separate vkernel kernel bug for code execution within the vkernel process; that chain is not presentable on the audit's GENERIC guest.

PoC

verify.sh β€” static-verification script that walks the cited path with grep/sed against sys/, confirming: (1) the raw %016lx rip/rsp/rbp prints in all three cases of db_nextframe (:280-302); (2) the %p stack-frame prints in db_stack_trace_cmd (:345, :352); (3) the missing Xfast_syscall boundary stop in vkernel64 vs the present one in pc64 (:434); (4) db_putchar routing to kprintf β†’ msgbuf when !db_active (db_output.c:108-119); (5) VKERNEL64 config enables DDB + DDB_TRACE + INVARIANTS by default (sys/config/VKERNEL64:58-60); (6) the running kernel is GENERIC, not vkernel64. Run from the repo root: sh findings/poc/DF-1078/verify.sh.

Fix

fix.diff β€” three changes in db_trace.c: 1. Add the missing Xfast_syscall boundary stop (port from pc64 db_trace.c:434) to stop the stack walker at the syscall boundary. 2. Gate the raw-address (%016lx, %p) prints on db_active: when db_active == 0 (the print_backtrace panic/warning context), rsp and rbp are suppressed; rip is printed symbolically via db_printsym. The %p "does not look like a stack frame" and "Trace beginning at frame" messages are also gated on db_active. 3. The panic/syscall/trap numbers (tf_trapno, tf_rax) are not addresses and remain visible.

A more complete fix would change db_putchar itself to use cnputc (console only, no msgbuf) when !db_active, but that is in db_output.c, outside db_trace.c. Matches the finding markdown's recommended fix.

Reproduce

sh findings/poc/DF-1078/verify.sh     # static source verification

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. vkernel64 DDB backtrace raw addresses to msgbuf. Non-default kernel (vkernel64). Fix mirrors pc64.