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)
PoC verification
Evidence pack
findings/poc/DF-5471 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- On every panic, before DDB exists β stock
X86_64_GENERIChasoptions DDB_TRACE, sotrace_on_panic=1and panic() (kern_shutdown.c:880-881) callsprint_backtrace(6)beforeDebugger("panic")ever runssetjmp(db_jmpbuf). - From live kernel debug sites with no debugger involved, e.g.
m_copyback()failure (uipc_mbuf.c:2413, unguarded) andmfree()anomaly printing (uipc_mbuf.c:1339, unguarded), plus INVARIANTSlock_test_modesites (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)
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)
db_stack_trace_cmd()βdb_nextframe()adopts garbagerbp(0xffffdead00000000) β only guard is< PAGE_SIZE(db_trace.c:227) β and reads(*fp)->f_retaddr(db_trace.c:231).db_get_value()βdb_read_bytes()armsdb_nofault = &db_jmpbuf(db_interface.c:242) and dereferences (db_interface.c:244-246).#PFβ kernel-mode fault βtrap_fatal()(trap.c:1019) β(debugger_on_panic || db_active) && kdb_trap(...)(trap.c:1096;debugger_on_panic=1by default, kern_shutdown.c:110).kdb_trap()default case sees armeddb_nofaultβlongjmp(*no_fault, 1)(db_interface.c:140-143) =longjmp(db_jmpbuf).db_jmpbufis BSS (db_command.c:62), set ONLY bysetjmpinside 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, whereprint_backtrace(6)(kern_shutdown.c:880-881;trace_on_panic=1becauseoptions DDB_TRACEis in X86_64_GENERIC) runs BEFOREDebugger("panic"). Also the state at every unguarded live debug site (uipc_mbuf.c:2413m_copyback, uipc_mbuf.c:1339mfree; INVARIANTS sites kern_lock.c:389 / indefinite2.h:116).longjmprestores rsp=0/rbp=0/rip-from-zeroed (support.s:803-810); the very next instructionmovq %rdx,0(%rsp)(support.s:811) writes to VA 0 β #DF. Serial confirms:rip=0xffffffff80bcb71f(inside longjmp),rsp=0x0,rbp=0x0.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/ddbtrace); 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 akenvnon-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_jmpbufthen holds a deaddb_command_loopcontext 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
fixedfix.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)
Confirmed kernel references
- sys/platform/pc64/x86_64/db_trace.c:227
- sys/platform/pc64/x86_64/db_trace.c:231
- sys/platform/pc64/x86_64/db_trace.c:444
- sys/platform/pc64/x86_64/db_interface.c:140
- sys/platform/pc64/x86_64/db_interface.c:242
- sys/ddb/db_command.c:62
- sys/ddb/db_command.c:467
- sys/ddb/db_trap.c:68
- sys/platform/pc64/x86_64/support.s:802
- sys/platform/pc64/x86_64/support.s:811
- sys/platform/pc64/x86_64/trap.c:1096
- sys/kern/kern_shutdown.c:880
- sys/kern/uipc_mbuf.c:1339
- sys/kern/uipc_mbuf.c:2413
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.
No comments yet.