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:
-
db_nextframe(lines 280-282, 289-291, 299-301) unconditionally prints raw kernel addresses viadb_printfwith%016lxformat specifiers forrip,rsp, andrbpin trap/syscall/interrupt frame markers. These values are vkernel kernel virtual addresses. -
db_stack_trace_cmd(line 345) printsdb_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. -
The vkernel version is missing the
Xfast_syscallboundary 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'swhile(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 viadb_print_stack_entry) then prints raw hex addresses (db_sym.c:289-292: whenname == NULLoroffset >= db_maxoff, it printsdb_format_radixof 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/klogordmesgto 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
VKERNEL64config (DDB+DDB_TRACE+INVARIANTSenabled). No special privileges needed inside the vkernel to read/dev/klog. - Reachability: Triggered whenever
print_backtrace()is called withdb_active == 0. Primary trigger: vkernel panic (trace_on_panic=1by default). Additional triggers:INVARIANTS-enabled debug paths that callprint_backtrace()(VKERNEL64config line 60 enablesINVARIANTS), includinglwkt_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.
Recommended fix
Two changes in db_trace.c:
-
Add the missing
Xfast_syscallboundary stop (port from pc64db_trace.c:430-435) to prevent the stack walker from continuing past the syscall boundary into junk frames. -
Gate all raw-address (
%016lx,%p) prints ondb_active(the DDB-is-interactive flag, declaredexterninddb/ddb.h:78). Whendb_active == 0(print_backtracecontext), output goes to msgbuf viakprintf, so raw addresses must be suppressed. Replace rawripprints withdb_printsym(symbolic), and only printrsp/rbpwhendb_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
- DF-1077 β pc64 sibling finding (same msgbuf leak class)
sys/platform/vkernel64/x86_64/db_trace.c:280-302β rawrip/rsp/rbpprintssys/platform/vkernel64/x86_64/db_trace.c:345, 352β%pstack-frame printssys/platform/vkernel64/x86_64/db_trace.c:428β missingXfast_syscallboundary stopsys/platform/pc64/x86_64/db_trace.c:430-435β pc64 has the boundary stop that vkernel lackssys/ddb/db_output.c:108-119βdb_putcharroutes tokprintfβ msgbuf when!db_activesys/config/VKERNEL64:58-60βDDB+DDB_TRACE+INVARIANTSenabled by defaultsys/kern/kern_shutdown.c:881, 110, 116β panic callsprint_backtracebeforeDebuggersys/platform/vkernel64/platform/console.c:437βvconsputcwrites viapwrite(1, ...)- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1078 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 |
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 withtf->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_testablecompile 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.
No comments yet.