Backtracer frame walk trusts stack content as frame pointers β INKERNEL() defined but never used; walk escapes into user-space, arbitrary mapped, and unmapped memory (attacker-steerable panic-time trace, msgbuf forensic eviction)
Summary
The frame walk validates only *fp < PAGE_SIZE - the INKERNEL() macro is defined but NEVER referenced (the FreeBSD ancestor's kernel-space validation was dropped in the x86-64 port): whatever 64-bit value sits at [rbp] is adopted and dereferenced, and the trap heuristic reads the 'trapframe' with direct derefs. During a panic in an attacker's syscall the walk descends into the attacker's own address space and prints attacker-steered content: guest-proven with uid-1001-sprayed fake frames - 22754+ '(null)() at' lines flooding msgbuf past 1MB and evicting the genuine boot/panic evidence (anti-forensics amplifier distinct from DF-1077's exposure); an unmapped VA additionally triggers the DF-5471 double fault. INKERNEL + strictly-forward frame validation fix validated ('is not in kernel space', boot context survives).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5470 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| run.log | β | 2.2 KB | view raw | |
| run.sh | β | 229 B | view raw | |
| build.sh | β | 151 B | view raw | |
| build.log | β | 6.1 KB | view raw | |
| env.txt | β | 298 B | view raw | |
| leak_flood_sample.txt | β | 1.4 KB | 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-5470 β DDB backtracer frame walk trusts stack content as frame pointers (INKERNEL() defined, never used); walk escapes into user/mapped/unmapped memory
File: sys/platform/pc64/x86_64/db_trace.c (pass 2)
Severity: Medium Β· Bucket: base:ddb / anti-forensics
Status: REPRODUCED on stock guest (see run.log, leak_flood_sample.txt)
What
db_stack_trace_cmd()/db_nextframe() walk the frame-pointer chain with a
single sanity check β (unsigned long)*fp < PAGE_SIZE (db_trace.c:227). The
file defines INKERNEL(va) at db_trace.c:131 and never uses it β the
kernel-space validation that the ancestor FreeBSD code performs was dropped
in the x86-64 port. Adopted frames (rbp read from stack/memory content)
can therefore be:
- user-space addresses (the panicking/traced process's own mappings are live during panic and DDB traces), or
- arbitrary mapped kernel addresses (heap, other stacks), or
- unmapped addresses (β fault inside
db_read_bytes, see DF-5471).
The convenience scan for trace <addr> (db_trace.c:336-352) likewise reads
8-byte words at addr..addr+4088 with no validation of addr.
Proof (this pack)
A KLD harness (src/df_trace_harness.c) calls the production
db_stack_trace_cmd() from a live syscall context. An unprivileged user
(uid 1001, src/user_chain.c) sprays 40000 fake frames in its own address
space at 0x60000000; the walk runs in that process's context (identical to
the context of a panic inside the attacker's syscall β print_backtrace()
runs on the panicking thread with its page table active):
- 30000 frames walked through user memory, one msgbuf line each;
- 22754+ lines (~1.07MB β₯
MSGBUF_SIZE=1MB) flood the message buffer; - the walk's own start line and the entire pre-existing msgbuf content (boot context; on a real system: the panic message, trap registers, and the useful head of the backtrace) are evicted β forensic evidence destroyed, output fully attacker-steered.
fix.diff restores the check: frames must satisfy INKERNEL() and move
strictly up the stack; trace <addr> refuses non-kernel addresses.
Verified on a rebuilt kernel: the user-VA walk is refused
("0x60000000 is not in kernel space"), zero flood lines, msgbuf intact.
Build / run
./build.sh # push src/ to guest, build KLD + user_chain, load ./run.sh # baseline sequence with expected outputs inline
Requires the dfbsd-qemu guest (stock X86_64_GENERIC: DDB, DDB_TRACE,
INVARIANTS). For the fix validation: apply fix.diff in the guest
/usr/src, make nativekernel KERNCONF=X86_64_GENERIC && make installkernel,
reboot, re-run run.sh.
DF-5470 VERDICT β REPRODUCED (impact: dos / anti-forensics)
Result
| phase | kernel | observation |
|---|---|---|
| baseline | stock #0 Jul 2 06:02:54 (X86_64_GENERIC, DDB+DDB_TRACE+INVARIANTS) |
production db_stack_trace_cmd() walked 30000 frames sprayed by an unprivileged user (uid 1001) in user VA 0x60000000; 22754+ lines flooded msgbuf (~1.07MB β₯ MSGBUF_SIZE=1MB); walk start line + entire prior msgbuf content (boot context) evicted |
| fix.diff | rebuilt #1 Oct 6 01:12:20 |
walk refused: 0x60000000 is not in kernel space; 0 flood lines; boot context intact; guest healthy |
Why it is real
INKERNEL()is defined atsys/platform/pc64/x86_64/db_trace.c:131and referenced nowhere in the file β the kernel-space validation of the ancestor code was dropped in the x86-64 port.- The only frame validation in the walk is
(unsigned long)*fp < PAGE_SIZE(db_trace.c:227).db_nextframe()adopts whatever 64-bit value sits at[rbp]as the next frame (db_trace.c:232,258) β user addresses, mapped kernel heap, cycles, or unmapped VA all pass. - During a panic or DDB
traceon a thread whose process has user mappings, user VA is directly readable by the walk: the panic-timeprint_backtrace()(stock DDB_TRACE, kern_shutdown.c:880-881) runs on the panicking thread with its page table still active. An attacker whose syscall panics the kernel (user-triggerable panics exist independently) fully controls every subsequent "frame": itsf_retaddr(printed), and via thecalltrap/Xresume*/_Xsyscallsymbol-name heuristic (db_trace.c:240-251) the fake "trapframe" contents atfp+16(db_trace.c:268-302) β printed as trapno/rip/rsp/rbp. - Output goes to msgbuf:
db_putchar()routes throughkprintf()whendb_active==0(db_output.c:107-116) β true for the whole panic-time walk (db_active is only incremented later in kdb_trap). - Demonstrated consequence: ~1MB of attacker-steered output evicts the real panic evidence from the 1MB msgbuf (run.log: pre-walk boot lines and the walk's own start line both gone). This is the anti-forensics amplifier of the already-reported address exposure DF-1077 (not re-reported here); it is also the enabling half of DF-5471 (unmapped-VA adoption β db_read_bytes fault β longjmp hazard).
Honest scope notes
- The walk itself performs no writes; there is no memory-corruption primitive in this finding, hence no uid0 chain. The demonstrated impact is denial-of-service of forensic data (msgbuf eviction) plus attacker-steered kernel output. The crash consequence of the same missing validation is DF-5471.
- The harness trigger is an instrumentation sysctl (root-loadable KLD)
standing in for the kernel's own invocation sites (panic()/DDB). The
unprivileged-reachability claim rests on (a) user-triggerable panics
existing elsewhere in the kernel and (b) the walk running in the
attacker's address-space context β both cited; the walk mechanics
demonstrated are the production code path (
db_stack_trace_cmd+db_nextframeunmodified). db_numargs()returns 0 (db_trace.c:166) andX_db_sym_numargs()is hard-wired FALSE (db_kld.c:107-111), so theargnames[MAXNARG]stack-array path is dead on this platform β no overflow there.
Fix
fix.diff (git-apply-able, validated): frames must be INKERNEL() and
strictly forward; trace <addr> refuses non-kernel addresses. Same diff
fixes DF-5471's trigger from the walk side.
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 refuses the user-VA walk ('0x60000000 is not in kernel space'), zero flood lines, boot context survives in msgbuf. Baseline flooding observed twice on stock kernel.
findings/poc/DF-5470/run.log (baseline vs patched sections); findings/poc/DF-5471/run.log patched section (same kernel build)
Confirmed kernel references
- sys/platform/pc64/x86_64/db_trace.c:131
- sys/platform/pc64/x86_64/db_trace.c:227
- sys/platform/pc64/x86_64/db_trace.c:232
- sys/platform/pc64/x86_64/db_trace.c:258
- sys/platform/pc64/x86_64/db_trace.c:268
- sys/platform/pc64/x86_64/db_trace.c:295
- sys/platform/pc64/x86_64/db_trace.c:336
- sys/platform/pc64/x86_64/db_trace.c:444
- sys/ddb/db_access.c:73
- sys/ddb/db_output.c:107
- sys/kern/kern_shutdown.c:880
Detail
Exploit chain
unprivileged user mmaps fake frame chain at 0x60000000 (f_frame=i+1, f_retaddr=kernel-text) -> any user-triggerable panic in that process's syscall (stock DDB_TRACE auto-runs print_backtrace) or DDB trace adopts user VA as frames -> walk reads/walks attacker memory -> 1MB+ of attacker-steered lines evict the real panic evidence from msgbuf; steering the adopted frame to an unmapped VA additionally yields the DF-5471 double-fault crash
Evidence (decisive lines)
run.log (baseline: 22754 '(null)() at' lines, walk-start line and 'swap low/high-water' boot line both evicted; patched: '0x60000000 is not in kernel space', 0 flood, boot line survives); leak_flood_sample.txt; panic-time context citations in VERDICT.md; fix validation in DF-5471/run.log patched section
PoC changes
harness authored fresh for this file (no seed PoC existed): KLD module calls production db_stack_trace_cmd() via sysctl; user_chain.c (uid 1001) sprays 40000 fake frames and triggers the walk in its own context via CTLFLAG_ANYBODY instrumentation sysctl standing in for panic()/ddb invocation
Verified recommended fix
db_trace.c: validate frames with INKERNEL() (>= USRSTACK) at db_nextframe entry and before adopting rbp in NORMAL/trap paths, require strictly-forward progression, and refuse non-kernel addrs in db_stack_trace_cmd's have_addr scan (see fix.diff).
Verdict
REPRODUCED on stock X86_64_GENERIC: the production db_stack_trace_cmd()/db_nextframe() frame walk (INKERNEL defined-but-unused at db_trace.c:131; only check is *fp < PAGE_SIZE at :227) adopted 30000 frames sprayed by an unprivileged user (uid 1001) in user VA 0x60000000 when triggered in that process's context (the exact context of panic()'s print_backtrace, which runs on the panicking thread with its page table active). 22754+ walk lines (~1.07MB >= MSGBUF_SIZE) flooded msgbuf and evicted all prior content including the boot/panic context - forensic-evidence destruction with fully attacker-steered output (attacker also controls printed 'trapframe' contents via the calltrap/Xresume/_Xsyscall heuristic at :240-302). Same missing validation is the enabling half of DF-5471. fix.diff (INKERNEL + monotonic-forward frame validation, validated on a rebuilt kernel) eliminates the walk into user/unmapped space: refusal printed, 0 flood lines, msgbuf intact.
No comments yet.