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

FRAMEUP stack-walk heuristic accepts non-monotonic, non-aligned, non-contained frame pointers β€” a stale kernel %rbp can fault the chain walk (panic-only ceiling; hardening)

Summary

cpu_ktr_caller walks the saved-%rbp chain with only a ~16KB proximity test: a stale kernel rbp (left by a frame-pointer-less assembly stub) landing in the window is accepted and the next frameptr[0]/[1] read can hit unmapped kernel VA (adjacent freed kstack) -> panic. Attacker-controlled rbp impossible (kernel/user split defeats the acceptance test every step); writes confined to the fixed ring slot; the facility compiles only under options KTR (absent from the stock kernel - live-verified debug.ktr unknown oid). monotonic-up + alignment fix proposed. Analysis pack only; PoC waived (facility not on guest).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5665 Β· 6 files
FileTypeDescriptionSize
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 7.1 KB ↓ raw
verdict.json β€” 4.2 KB view raw
env.txt β€” 1.6 KB view raw
fix.diff β€” 861 B view raw
manifest.json β€” 1.0 KB view raw

DF-5665 β€” FRAMEUP unwinder heuristic accepts non-monotonic, non-aligned, non-contained frame pointers (hardening)

File audited (pass 2): sys/cpu/x86_64/misc/ktr.c (95 LOC, sha256 a86d4a65a53feb7ba6324f018049986eac1c99c13224fa1b73a8a58a3754e2ee).

What the finding is

cpu_ktr_caller() (sys/cpu/x86_64/misc/ktr.c:78-95) walks the saved-%rbp chain of the running kernel stack to record two caller return addresses into the per-CPU KTR ring entry. Each step goes through FRAMEUP() (sys/cpu/x86_64/misc/ktr.c:60-70), whose only validation is

if (((uintptr_t)newframeptr ^ (uintptr_t)frameptr) & ~16383ULL)
        newframeptr = frameptr;      /* reject: differs outside low 14 bits */

i.e. the candidate next frame must lie within the same ~16 KiB neighborhood of the current frame pointer. There is no check that the chain moves monotonically up the stack, that the pointer is aligned, or that it stays inside the current kernel stack. A stale kernel %rbp (left in a saved-rbp slot by an assembly stub / trampoline that does not maintain frame pointers β€” e.g. fork/execve trampoline context, hand-written exception or IPI entry paths) that happens to land within the Β±16 KiB window is accepted, and the next step then reads frameptr[0] / frameptr[1] (sys/cpu/x86_64/misc/ktr.c:66,92,94) at that address β€” which can be unmapped kernel VA (e.g. an adjacent, freed per-thread kstack) β†’ kernel page fault β†’ panic.

Why this is only Info

  • No attacker control of the walked value. The user-controlled-%rbp route (interrupt/syscall taken from user mode leaves the user's %rbp in a saved slot) is killed by construction: the acceptance baseline is always a genuine kernel-stack address obtained from cpu_ktr_caller's own frame (ktr.c:86-88), and a user-space candidate (< 0x0000800000000000) XOR a kernel kstack address (>= 0xffff800000000000) always has high bits set β†’ rejected every step. The walk can never cross into user space, and every read address is within Β±16 KiB of an address already proven readable.
  • Panic-only ceiling even in the stale-kernel-rbp case: the function only performs reads (frameptr[0], frameptr[1]) and two writes into the pre-allocated ring entry (ktr.c:92,94).
  • Debug-only facility: compiled only under options KTR (sys/platform/pc64/conf/files:110 β€” cpu/x86_64/misc/ktr.c optional ktr; sys/conf/files:1377 β€” kern/kern_ktr.c optional ktr). The stock X86_64_GENERIC kernel does not include it (live-verified below), and the only caller is ktr_finish_write_entry() gated by ktr_stacktrace (sys/kern/kern_ktr.c:531-532).
  • No demonstrated trigger from any real kernel call path; the precondition (KTR_LOG site within 2 frames of a stub that leaves a kernel rbp within 16 KiB of the live frame) is contrived.

Why no PoC was run (status: untested)

Per the review contract, PoC verification is waived for Info-severity hardening findings. Additionally the target code is absent from the audit guest's kernel, so the path cannot be exercised without a custom options KTR kernel build, which is not warranted for a speculative, panic-only, debug-facility hardening note.

Live guest evidence collected anyway (see env.txt):

$ sysctl debug.ktr
sysctl: unknown oid 'debug.ktr'          # KTR not compiled into X86_64_GENERIC
$ sysctl -a | grep -c "debug.ktr"
0
$ ls -l /dev/mem /dev/kmem
crw-r-----  1 root kmem 13, 0x00000001 ... /dev/kmem    # ktrdump(1) path is root-only
crw-r-----  1 root kmem 13, 0x00000000 ... /dev/mem

Fix

fix.diff (git-apply checked against sys/): require the candidate frame to be strictly above the current frame and 8-byte aligned, in addition to the existing ~16 KiB proximity test. Genuine x86-64 frame chains always grow upwards, so no legitimate walk is affected.

Reproduce the analysis (no build required)

  1. rg -n "cpu_ktr_caller" sys/ β†’ only caller sys/kern/kern_ktr.c:532.
  2. Read sys/cpu/x86_64/misc/ktr.c:60-95 (FRAMEUP + walk).
  3. grep -n "ktr" sys/platform/pc64/conf/files β†’ optional ktr.
  4. On the guest: sysctl debug.ktr β†’ unknown oid (facility absent).
VERDICT.md
↓ download raw

DF-5665 β€” VERDICT

Status: untested (Info-severity hardening finding; PoC waived by contract β€” and the code is not compiled into the audit guest's kernel). Impact ceiling: kernel panic (local, speculative). No privilege escalation, no info leak, no memory-corruption write primitive.

Scope of the pass-2 audit

Target: sys/cpu/x86_64/misc/ktr.c (95 LOC). The prompt's checklist items (ring index arithmetic, buffer allocation, dump copyouts, version counters) live in sys/kern/kern_ktr.c / sys/sys/ktr.h; they were pulled in and analyzed as callers/consumers of this file. Every class was hunted and each candidate was proven or killed with path:line evidence below.

The one finding (mechanism certain, trigger speculative)

FRAMEUP() (sys/cpu/x86_64/misc/ktr.c:60-70) validates a candidate frame pointer only for ~16 KiB proximity to the previous frame pointer:

  • no monotonic-up requirement (kernel frames grow upward; a genuine chain never goes down),
  • no 8-byte alignment requirement,
  • no containment within the current kernel stack.

cpu_ktr_caller() then dereferences the accepted pointer (frameptr[0] at ktr.c:66; frameptr[1] at ktr.c:92,94). A stale kernel %rbp value left in a saved-rbp slot by a frame-pointer-less assembly stub (trampolines, exception/IPI entry) that lands inside the acceptance window but on unmapped KVA (e.g. an adjacent freed per-thread kstack) makes the next read fault β†’ panic. Attackers control neither the stale value nor (absent a separate kernel bug) the call path shape; the ceiling is a reboot. Filed as Info hardening, confidence speculative (trigger), mechanism verified by line-level reading.

Attacker-route analysis (why this is not exploitable)

  1. User %rbp injection is structurally impossible. When an interrupt/syscall is taken from user mode, the user's %rbp can end up in a saved slot; the walk might pick it up as a candidate. FRAMEUP compares it against the current baseline, which always starts from cpu_ktr_caller's own genuine kernel frame (ktr.c:86-88: frame = rbp; frameptr = frame->f_frame). A user candidate (< 0x0000_8000_0000_0000) XOR a kernel kstack address (>= 0xFFFF_8000_0000_0000) always has bits β‰₯ 14 set β†’ & ~16383ULL β‰  0 β†’ rejected. The baseline only ever updates to accepted (kernel-canonical, close) values, so no sequence of steps can cross into user space. Worst case on a broken chain: ktr_caller1 == ktr_caller2 == the traced function's return address (fallback stays on the current frame) β€” no fault, garbage-free.
  2. Writes are confined by construction. The only stores are ktr->ktr_caller1/2 (ktr.c:92,94) into the ring entry returned by ktr_begin_write_entry() (sys/kern/kern_ktr.c:496-526), which is bounds-derived (kcpu->ktr_buf + (kcpu->ktr_idx & ktr_entries_mask), kern_ktr.c:511) and NULL-guarded against panicstr and early boot (kern_ktr.c:505-508). No attacker-supplied size, count, or offset reaches this file.
  3. Ring payload sizes are compile-time constants. The macro-side struct copy (*(struct ktr_info_..._args *)&entry->ktr_data[0], sys/sys/ktr.h:193/214) is bounded by CTASSERT(sizeof(...) <= KTR_BUFSIZE) (sys/sys/ktr.h:155). Tracepoint arguments fill a fixed struct; they never feed sizes/counts.

Negative results (classes considered and killed)

  • Ring index arithmetic / wraparound (kern_ktr.c:511-512): int ktr_idx overflow after 2^31 entries wraps in practice; (negative) & mask on two's complement stays in 0..mask β†’ in-bounds. Not this file; no OOB.
  • Per-CPU buffer allocation (kern_ktr.c:231-239): compile-time power-of-2 KTR_ENTRIES (enforced, kern_ktr.c:99), M_ZERO β†’ no uninit leak; boot-buffer memcpy sized 256 entries into a β‰₯256-entry destination. No overflow at any sane KTR_ENTRIES.
  • Boot publication ordering: already filed as DF-0104 (cpu_sfence at kern_ktr.c:240) β€” not re-reported.
  • Dump/copyout surface: the kernel never copies ring entries to userspace. DDB show ktr only prints (kern_ktr.c:557-680; indices masked at :570/:649, tstate[MAXCPU] bounded, printcpu checked at :595). ktrdump(1) reads via kvm β†’ /dev/mem, root:kmem 0640 (live-verified). debug.ktr.* sysctls export four ints only.
  • Sysctl write abuse (debug.ktr.resynchronize, test knobs, verbose): all writes pass SYSCAP_NOSYSCTL_WR privilege gate (sys/kern/kern_sysctl.c:1445-1448) β†’ root-only. Test knobs are #if KTR_TESTLOG-gated off unless KTR_ALL.
  • ktr_caller1/2 kernel-text pointers in the ring: exposure requires DDB console or ktrdump/root. No unprivileged leak path (live-verified: no unpriv OID even exposes the ring).
  • Entry lifetime/UAF: per-CPU buffers are kmalloc'd once (kern_ktr.c:233) and never freed anywhere (no teardown, not a module); no panicstr-time use (NULL-return guard). No UAF.
  • Torn/stale caller fields (caller writes happen after crit_exit, kern_ktr.c:524 vs :532): trace incoherence only; same-slot reuse needs 2048 same-CPU intervening logs inside the beginβ†’finish window β€” not achievable in the straight-line macro body.
  • KTR_VERSION handling: static const 4 (kern_ktr.c:179); consumers (ktrdump nlist switches) are userland; no runtime version mutation.

Guest state

Live checks on dfbsd-qemu guest DragonFly dfbsd 6.5-DEVELOPMENT #0 ... X86_64_GENERIC x86_64:

  • sysctl debug.ktr β†’ unknown oid β€” KTR (and hence this file) is not compiled into the stock kernel (sys/platform/pc64/conf/files:110: optional ktr). The file's entire runtime surface on the default configuration is dead code.
  • /dev/mem, /dev/kmem: root:kmem 0640.

Guest left up and clean (no dirt: no kernel build, no crash, no writes).

Fix validation

Not performed on the guest (would require a custom options KTR kernel build for a speculative panic-only Info finding). fix.diff is git apply --check-verified against the unmodified sys/ tree: adds strictly-above + 8-byte-alignment requirements to FRAMEUP. Genuine frame chains always grow upward on x86-64, so the hardening cannot break any legitimate walk; it converts "accept any near pointer" into "accept only a plausible caller frame".

Bottom line

The file is memory-safe against attacker input by construction: the only attacker-influenceable pointer candidate class (user %rbp) is unreachable past the kernel/user address-space split baked into the FRAMEUP test, writes go to a fixed ring slot, and all sizes are compile-time constants. The residual weakness β€” the heuristic's tolerance of stale kernel rbp values β€” has a panic-only ceiling in a debug-only facility and is filed as Info hardening with a one-hunk fix. Everything else on the pass-2 checklist (ring math, allocation, dump paths, ordering) is either in-bounds, root-gated, or already filed (DF-0104).

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored and git-apply-checked against the unmodified sys/ tree (adds monotonic-up + alignment requirements to FRAMEUP, single hunk). Fix validation build not performed: would require a custom 'options KTR' kernel on the guest to exercise, unwarranted for a speculative panic-only Info hardening note with no demonstrated trigger.

findings/poc/DF-5665/fix.diff (git apply --check passed against sys/cpu/x86_64/misc/ktr.c, sha256 476a25a0b76dcf7d773ffe5ed4e9705bf80802d372293b7a720cbe4a65773a39); no fix_build.log/fix_run.log β€” no kernel build performed.
↓ fix.diffper-fix-DF-5665

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5665/VERDICT.md (full per-class kill list with path:line); README.md (mechanism + live guest probes); env.txt (guest uname + sysctl//dev/mem checks); fix.diff (git-apply-checked one-hunk hardening). Decisive lines: sys/cpu/x86_64/misc/ktr.c:66-69 (proximity-only validation), :86-94 (walk + reads), sys/kern/kern_ktr.c:505-511 (entry allocation/guards), :531-532 (sole caller).

PoC changes

No PoC run β€” Info-severity hardening finding (PoC waived per contract); the audited code is not compiled into the guest kernel (optional ktr), so the path cannot be exercised without a custom kernel build.

Verified recommended fix

Harden FRAMEUP in sys/cpu/x86_64/misc/ktr.c to also require the candidate frame pointer to be strictly above the current frame and 8-byte aligned (genuine x86-64 chains grow upward), preventing a stale/corrupted saved %rbp from walking the chain into unrelated unmapped kernel memory.

Verdict

Pass-2 audit of sys/cpu/x86_64/misc/ktr.c (cpu_ktr_caller + FRAMEUP stack unwinder): memory-safe against attacker input by construction. The user-controlled-%rbp route is structurally dead β€” FRAMEUP's XOR/~16383 acceptance test (ktr.c:67) always rejects user-space candidates against a genuine kernel-stack baseline (kernel/user address-space split), all writes go to the fixed per-CPU ring slot from ktr_begin_write_entry (kern_ktr.c:511, NULL/panicstr-guarded at :505-508), and every payload size is a compile-time constant (CTASSERT at ktr.h:155). Residual weakness: FRAMEUP accepts non-monotonic, non-aligned, non-contained frame pointers, so a stale kernel %rbp left by a frame-pointer-less assembly stub and landing in the +-16KiB window could make the next frameptr[0]/[1] read fault on unmapped KVA β€” panic-only ceiling, no attacker control of the value, debug-only facility (optional ktr; not compiled into the stock guest kernel, live-verified: 'sysctl debug.ktr' -> unknown oid). Filed as Info hardening; PoC waived for Info severity and not exercisable on the guest without a custom options-KTR kernel build. Ring math, allocation, dump/copyout, and ordering checklist items are in-bounds or root-gated; boot-publication ordering already filed as DF-0104 and not re-reported.