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

Backtrace fault outside an established DDB session longjmps into the BSS-zeroed (or stale) global db_jmpbuf β†’ double fault β†’ panic: double fault; every stock panic with a garbage frame pointer is upgraded to an undiagnosable crash

Summary

Every walk deref arms db_nofault=&db_jmpbuf, but the global jmp_buf is set only inside an established DDB session - and on the stock config (options DDB_TRACE) EVERY panic runs print_backtrace BEFORE Debugger() with db_jmpbuf still zeroed (plus live m_copyback/mfree/INVARIANTS debug sites call it with no debugger at all): a garbage frame pointing at unmapped memory longjmp-restores rsp=0/rbp=0 -> write at rsp=0 faults -> double fault -> panic('double fault') -> DDB wedge, destroying the original panic's dump. Reproduced twice on the stock guest (Fatal trap 12 -> Fatal double fault rip inside longjmp rsp=0 -> panic, vm down); walk-side validation fix validated (2 frames, clean return, guest up). Full fix wants a print_backtrace-local setjmp.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5471 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
run.log β€” 2.8 KB view raw
run.sh β€” 207 B view raw
build.sh β€” 169 B view raw
build.log β€” 6.1 KB view raw
env.txt β€” 298 B view raw
panic.txt β€” 1.8 KB view raw
fix_validation.txt β€” 395 B view raw
fix.diff β€” 1.5 KB view raw
src/df_trace_harness.c β€” 3.9 KB view raw
src/user_chain.c β€” 1.8 KB view raw
src/Makefile β€” 72 B ↓ download

DF-5471 β€” backtrace fault outside a DDB session longjmps into uninitialized/stale global db_jmpbuf β†’ double fault β†’ panic

File: sys/platform/pc64/x86_64/db_trace.c (pass 2, cross-file with db_interface.c, db_command.c) Severity: Medium Β· Bucket: base:ddb / DoS-amplification Status: REPRODUCED on stock guest (twice), crash signature in panic.txt

What

Every dereference the backtracer performs goes through db_get_value() β†’ db_read_bytes(), which arms the global no-fault recovery db_nofault = &db_jmpbuf (db_interface.c:242) around a raw pointer dereference. On a fault, kdb_trap() executes longjmp(*no_fault, 1) (db_interface.c:140-143).

db_jmpbuf (db_command.c:62, BSS-zeroed) is only initialized by setjmp() at db_command.c:467 / db_trap.c:68 β€” i.e. inside an established DDB session. But db_stack_trace_cmd() also runs:

  1. On every panic, before DDB exists β€” stock X86_64_GENERIC has options DDB_TRACE, so trace_on_panic=1 and panic() (kern_shutdown.c:880-881) calls print_backtrace(6) before Debugger("panic") ever runs setjmp(db_jmpbuf).
  2. From live kernel debug sites with no debugger involved, e.g. m_copyback() failure (uipc_mbuf.c:2413, unguarded) and mfree() anomaly printing (uipc_mbuf.c:1339, unguarded), plus INVARIANTS lock_test_mode sites (kern_lock.c:389, indefinite2.h:116 β€” INVARIANTS is in the stock config).

If the walk adopts a garbage frame pointer (precisely what happens when the panic was caused by stack/memory corruption β€” the moment you need the dump most) and dereferences an unmapped page:

#PF in db_read_bytes
  β†’ trap_pfault β†’ trap_fatal (trap.c:1096, debugger_on_panic=1 default)
  β†’ kdb_trap: db_nofault armed β†’ longjmp(db_jmpbuf)
  β†’ db_jmpbuf all-zero (or stale from a previous DDB session on another
    thread/stack) β†’ longjmp restores rsp=0, rbp=0, rip=0
    (support.s:802-814; first faulting insn is the write `movq %rdx,0(%rsp)`
     at support.s:811 with rsp=0)
  β†’ DOUBLE FAULT β†’ dblfault_handler β†’ panic("double fault")

The original panic's dump/forensics are destroyed or hopelessly interleaved; a live debug print site becomes a full system crash.

Proof (this pack)

KLD harness: sysctl -w debug.df_kfault=1 walks a kmalloc'd chain whose 2nd frame pointer is 0xffffdead00000000 (unmapped kernel VA), from live syscall context, on a fresh boot (db_jmpbuf zeroed β€” same state as the panic path). Observed on serial (panic.txt): Fatal trap 12 at 0xffffdead00000008 (the f_retaddr read, db_trace.c:231) β†’ kernel: type 12 trap β†’ Fatal double fault: rip=0xffffffff80bcb71f (longjmp), rsp=0x0, rbp=0x0 β†’ panic: double fault β†’ auto-trace β†’ DDB wedge, guest down. Reproduced twice.

fix.diff (shared with DF-5470) makes the walk refuse out-of-kernel-space and non-forward frames, so db_read_bytes is never pointed at the unmapped VA from the walk. Verified on a rebuilt kernel: the same trigger prints two valid frames and returns cleanly β€” DFTRACE: kfault: walk returned without crashing, zero panic lines, guest stays up (run.log, fix_validation.txt).

Complete remediation additionally needs a db_interface.c change (arm a jmpbuf that is valid whenever db_nofault can fire, e.g. setjmp a local buffer in print_backtrace()/db_stack_trace_cmd() entry, or make kdb_trap fall back to db_global_jmpbuf when the DDB loop never ran) β€” out of this file's scope, noted in the finding's recommended fix.

Build / run

./build.sh          # push src/ to guest, build KLD + user_chain, load
./run.sh            # kfault trigger; CRASHES the stock guest (by design)
VERDICT.md
↓ download raw

DF-5471 VERDICT β€” REPRODUCED (impact: panic β€” deterministic crash from the backtrace fault path; forensic destruction)

Result

phase kernel observation
baseline (run 1) stock, fresh boot, DDB never entered sysctl -w debug.df_kfault=1 β†’ Fatal trap 12 at 0xffffdead00000008 β†’ kernel: type 12 trap β†’ Fatal double fault, rip=0xffffffff80bcb71f (inside longjmp), rsp=0x0, rbp=0x0 β†’ panic: double fault β†’ auto-trace β†’ DDB wedge; ssh dead; vm.sh status β‡’ down
baseline (run 2) same binary (reboot had silently not happened β€” see narrative) identical cascade, identical signature
fix.diff rebuilt #1 Oct 6 01:12:20 same trigger: two valid frames printed, then DFTRACE: kfault: walk returned without crashing; dmesg | grep -c "Fatal\|DOUBLE FAULT\|panic" = 0; guest stays up

Mechanism (all source-cited)

  1. db_stack_trace_cmd() β†’ db_nextframe() adopts garbage rbp (0xffffdead00000000) β€” only guard is < PAGE_SIZE (db_trace.c:227) β€” and reads (*fp)->f_retaddr (db_trace.c:231).
  2. db_get_value() β†’ db_read_bytes() arms db_nofault = &db_jmpbuf (db_interface.c:242) and dereferences (db_interface.c:244-246).
  3. #PF β†’ kernel-mode fault β†’ trap_fatal() (trap.c:1019) β†’ (debugger_on_panic || db_active) && kdb_trap(...) (trap.c:1096; debugger_on_panic=1 by default, kern_shutdown.c:110).
  4. kdb_trap() default case sees armed db_nofault β†’ longjmp(*no_fault, 1) (db_interface.c:140-143) = longjmp(db_jmpbuf).
  5. db_jmpbuf is BSS (db_command.c:62), set ONLY by setjmp inside an established DDB session (db_command.c:467, db_trap.c:68). On a fresh boot it is all zeros β€” exactly the state of the stock panic path, where print_backtrace(6) (kern_shutdown.c:880-881; trace_on_panic=1 because options DDB_TRACE is in X86_64_GENERIC) runs BEFORE Debugger("panic"). Also the state at every unguarded live debug site (uipc_mbuf.c:2413 m_copyback, uipc_mbuf.c:1339 mfree; INVARIANTS sites kern_lock.c:389 / indefinite2.h:116).
  6. longjmp restores rsp=0/rbp=0/rip-from-zeroed (support.s:803-810); the very next instruction movq %rdx,0(%rsp) (support.s:811) writes to VA 0 β†’ #DF. Serial confirms: rip=0xffffffff80bcb71f (inside longjmp), rsp=0x0, rbp=0x0.
  7. dblfault_handler() (trap.c:1125) β†’ panic("double fault") β†’ nested-panic path β†’ DDB wedge. The original panic context (or, on the live-path variant, a merely-diagnostic warning) is destroyed; no clean dump of the original event.

Narrative honesty / deviations

  • Trigger is a harness sysctl standing in for the kernel's own invocation of the same walk (print_backtrace/ddb trace); the exercised code (db_stack_trace_cmd + db_nextframe + db_read_bytes + kdb_trap + longjmp) is stock, unmodified.
  • "Run 2": the fix-validation cycle initially failed to reboot the guest (my && chain aborted on a kenv non-zero exit), so the "patched" run in fact executed the stock kernel again β€” producing a second, independent baseline crash with an identical signature before the true patched run. Both are documented in run.log; the true patched run followed after a forced power cycle (uname confirmed #1 Oct 6 01:12:20, fix string present in /boot/kernel/kernel).
  • The stale-jmpbuf variant (DDB entered and exited earlier in the boot; db_jmpbuf then holds a dead db_command_loop context on a possibly reused kernel stack β†’ longjmp resumes a ghost frame in arbitrary thread context) is real by the same code path but was not separately demonstrated; the zeroed variant is deterministic and fully proven.
  • No attacker-controlled register restore is available in the zeroed variant (BSS is not attacker-writable), so there is no uid0 chain from this primitive alone; impact is crash/DoS + forensic destruction.

Fix

fix.diff (shared with DF-5470) prevents the walk from adopting out-of-kernel-space / non-forward frames, eliminating the demonstrated trigger from the trace side (validated: crash gone). Complete remediation should additionally make the no-fault recovery safe in db_interface.c (e.g. a setjmp'd local buffer established by print_backtrace()/db_stack_trace_cmd() before any db_get_value, or a validity flag for db_jmpbuf checked in kdb_trap), since other DDB read paths can fault outside a DDB session too.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in guest /usr/src, kernel rebuilt (uname #1 Oct 6 01:12:20) and rebooted; identical trigger now terminates gracefully ('DFTRACE: kfault: walk returned without crashing'), zero Fatal/DOUBLE FAULT/panic lines in dmesg, guest stays up and answers ssh. Baseline double-fault cascade observed twice on stock kernel.

findings/poc/DF-5471/run.log (baseline serial cascade + patched clean run); panic.txt; fix_validation.txt (patched serial excerpt)
↓ fix.diffper-fix-DF-5471

Confirmed kernel references

Detail

Exploit chain

corrupted/garbage stack (the very condition being diagnosed) -> panic -> stock DDB_TRACE print_backtrace(6) walks before DDB exists -> db_nextframe adopts unmapped VA -> db_read_bytes arms db_nofault=&db_jmpbuf and faults -> kdb_trap longjmps into zeroed (or stale-from-previous-DDb-session) db_jmpbuf -> rsp=0 write at support.s:811 -> double fault -> panic-in-context reboot without usable dump; live-variant: m_copyback/mfree debug site with corrupt stack -> full crash from a diagnostic print

Evidence (decisive lines)

panic.txt (full untrimmed serial cascade: Fatal trap 12 at 0xffffdead00000008 -> kernel: type 12 trap -> Fatal double fault rip=0xffffffff80bcb71f rsp=0x0 rbp=0x0 -> panic: double fault -> db> wedge); run.log (baseline twice incl. the documented inadvertent second stock run, then patched 'walk returned without crashing' + grep -c Fatal/panic = 0 + uptime); fix_validation.txt

PoC changes

harness authored fresh for this file (no seed PoC existed): KLD sysctl debug.df_kfault builds a kmalloc'd 2-frame chain ending in unmapped kernel VA 0xffffdead00000000 and calls production db_stack_trace_cmd() from live syscall context on a fresh boot (db_jmpbuf zeroed, matching the stock panic path where print_backtrace runs before Debugger() sets db_jmpbuf)

Verified recommended fix

db_trace.c: INKERNEL()+monotonic-forward frame validation so the walk never points db_read_bytes at unmapped/user VA (fix.diff, validated); plus db_interface.c: never longjmp db_jmpbuf unless a DDB session initialized it (setjmp a local recovery buffer in print_backtrace()/db_stack_trace_cmd() entry, or gate on a db_jmpbuf-valid flag in kdb_trap).

Verdict

REPRODUCED twice on stock X86_64_GENERIC (fresh boots, DDB never entered = exactly the panic-path state): db_read_bytes' no-fault recovery longjmps into the BSS-zeroed global db_jmpbuf whenever the backtrace walk faults outside an established DDB session. Trigger: walk adopts an unmapped kernel VA (0xffffdead00000000) as frame pointer (only guard is *fp < PAGE_SIZE, db_trace.c:227) -> #PF at 0xffffdead00000008 (f_retaddr read, db_trace.c:231) -> trap_fatal (trap.c:1096, debugger_on_panic=1 default) -> kdb_trap longjmp (db_interface.c:140-143) -> longjmp restores rsp=0/rbp=0 from zeroed jmp_buf (support.s:803-811, first faulting write at :811) -> Fatal double fault rip=0xffffffff80bcb71f rsp=0x0 -> panic: double fault -> nested auto-trace -> DDB wedge, guest down, ssh dead. Stock reachability: every panic (options DDB_TRACE => trace_on_panic=1; kern_shutdown.c:880 print_backtrace(6) runs BEFORE Debugger() ever sets db_jmpbuf) and unguarded live debug sites (uipc_mbuf.c:2413 m_copyback, :1339 mfree; INVARIANTS sites kern_lock.c:389, indefinite2.h:116) - i.e. any panic caused by stack/memory corruption (garbage rbp on the stack) is upgraded to an undiagnosable double-fault reboot that destroys the dump context. fix.diff (walk-side validation, same diff as DF-5470) validated on rebuilt kernel: identical trigger prints 2 frames and returns cleanly, zero panic lines, guest stays up.