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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5470 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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 at sys/platform/pc64/x86_64/db_trace.c:131 and 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 trace on a thread whose process has user mappings, user VA is directly readable by the walk: the panic-time print_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": its f_retaddr (printed), and via the calltrap/Xresume*/_Xsyscall symbol-name heuristic (db_trace.c:240-251) the fake "trapframe" contents at fp+16 (db_trace.c:268-302) β€” printed as trapno/rip/rsp/rbp.
  • Output goes to msgbuf: db_putchar() routes through kprintf() when db_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_nextframe unmodified).
  • db_numargs() returns 0 (db_trace.c:166) and X_db_sym_numargs() is hard-wired FALSE (db_kld.c:107-111), so the argnames[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

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 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)
↓ fix.diffper-fix-DF-5470

Confirmed kernel references

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.