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)
PoC verification
Evidence pack
findings/poc/DF-5665 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
%rbpin a saved slot) is killed by construction: the acceptance baseline is always a genuine kernel-stack address obtained fromcpu_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 isktr_finish_write_entry()gated byktr_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)
rg -n "cpu_ktr_caller" sys/β only callersys/kern/kern_ktr.c:532.- Read
sys/cpu/x86_64/misc/ktr.c:60-95(FRAMEUP + walk). grep -n "ktr" sys/platform/pc64/conf/filesβoptional ktr.- On the guest:
sysctl debug.ktrβ unknown oid (facility absent).
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)
- User
%rbpinjection is structurally impossible. When an interrupt/syscall is taken from user mode, the user's%rbpcan 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 fromcpu_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. - Writes are confined by construction. The only stores are
ktr->ktr_caller1/2(ktr.c:92,94) into the ring entry returned byktr_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 againstpanicstrand early boot (kern_ktr.c:505-508). No attacker-supplied size, count, or offset reaches this file. - 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 byCTASSERT(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_idxoverflow after 2^31 entries wraps in practice;(negative) & maskon 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 ktronly prints (kern_ktr.c:557-680; indices masked at :570/:649,tstate[MAXCPU]bounded,printcpuchecked 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 passSYSCAP_NOSYSCTL_WRprivilege gate (sys/kern/kern_sysctl.c:1445-1448) β root-only. Test knobs are#if KTR_TESTLOG-gated off unless KTR_ALL. ktr_caller1/2kernel-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_testablefix.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.
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.
No comments yet.