hard_sniff() flame-graph frame walk reads 8 bytes past the interrupted thread's kernel stack (fatal trap 12 / kernel-heap qword disclosure)
Summary
The walk guard checks rbp > top-8 but the load at rbp+8 spans [rbp+8, rbp+16): rbp==top-8 (equal passes) reads [top, top+8) - 8 bytes past the kstack. Unmapped adjacent page (~1 in 43 fresh-process kstacks) = fatal trap 12 in hard_sniff (reproduced twice: 'Stopped at hard_sniff+0x9e: movq 0x8(%rdx),%rax', guest dead); mapped = adjacent kernel-heap qword silently recorded into the flame entry (observed kprintf scratch) readable via the root-only sink. Root must arm the diagnostic, but afterwards every sampled thread - including unprivileged ones - executes the walk via the user-pollable kern.sniff_enable path and the ipiq-stall debug sniffs. Same guard bug duplicated in pcpu_timer_always (machdep.c:3656). rbp>top-16 fix validated on a rebuilt kernel (150 fresh-process loads, zero panics, >=111 unmapped-neighbour survivals). NOTE: file is the flame-graph stack sampler, not the tsc/PIPT display the task brief described - audited as-found.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5624 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| flameoob.c | β | 4.8 KB | view raw | |
| flame_dump.c | β | 2.1 KB | view raw | |
| Makefile | β | 56 B | β download | |
| build.sh | β | 141 B | view raw | |
| run.sh | β | 193 B | view raw | |
| decisive.sh | β | 1020 B | view raw | |
| decisive2.sh | β | 1.4 KB | view raw | |
| build.log | β | 7.2 KB | view raw | |
| fix_build_tail.log | β | 2.0 KB | view raw | |
| panic.txt | β | 1.0 KB | view raw | |
| panic-v1-module.txt | β | 1012 B | view raw | |
| run_fixed.log | β | 1.9 KB | view raw | |
| env.txt | β | 190 B | view raw | |
| fix.diff | β | 763 B | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-5624 β hard_sniff() off-by-8 OOB read past kernel stack top (panic / heap disclosure)
File: sys/platform/pc64/x86_64/mp_flame.c (flame-graph stack sampler, Xsniff vector target)
Lines: 135β143 (the bug is the guard at :136 vs the load at :138)
Class: CWE-125 out-of-bounds read Β· Severity: Medium Β· Confidence: certain
Verified on guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (baseline)
and patched #1: Thu Oct 8 22:24:57 UTC 2026 (fix validation), x86_64, 6 CPUs.
The bug
bot = (char *)td->td_kstack + PAGE_SIZE; /* mp_flame.c:124 */
top = (char *)td->td_kstack + td->td_kstack_size; /* mp_flame.c:125 */
...
for (n = 1; n < FLAME_GRAPH_FRAMES - 1; ++n) {
if (rbp < bot || rbp > top - 8 || ((intptr_t)rbp & 7)) /* :136 */
break;
fge->rips[n] = (intptr_t)*(char **)(rbp + 8); /* :138 */
The guard admits rbp == top - 8 (equal is not greater), but the x86-64 frame
at rbp spans [rbp, rbp+16) (saved %rbp + return address), so the load at
rbp+8 reads [top, top+8) β 8 bytes past the end of the interrupted
thread's kernel stack. When the page after the kstack is unmapped (kstacks
are kmem allocations placed against holes β seen naturally in ~1/43 fresh
processes on this guest) this is a fatal kernel-mode page fault in the
Xsniff path β panic, all CPUs stopped. When mapped, the qword of adjacent
kernel heap is silently copied into the flame entry (rips[1]), readable via
debug.flame_graph_data (RESTRICTEDROOT).
Reproduce
# in guest, as root:
cd /root/df5624
sh build.sh # builds flame_dump (userland) + flameoob.ko (in-tree kmod)
ITERS=150 sh decisive2.sh # loop: fresh-process kldloads with tf_rbp == kstack-top-8
Buggy kernel (expected, observed): first iteration whose kstack has an unmapped neighbour page panics:
DF5624: ... top=0xfffff801185da000 NEIGHBOR UNMAPPED Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff801185da000 (== top) fault code = supervisor read data, page not present Stopped at hard_sniff+0x9e: movq 0x8(%rdx),%rax (== mp_flame.c:138)
(see panic.txt β v2 rig; panic-v1-module.txt β first, incidental capture)
Mapped-neighbour iterations instead record the OOB qword: earlier sampling
runs observed rips[1] = 0x203a343236354644 = "DF5624: " β kprintf scratch
in the adjacent heap page (serial-console transcript of the 2026-10-08 run).
Fixed kernel: loop completes; UNMAPPED-neighbour loads survive
("survived UNMAPPED-neighbour sniff"); all entries have rips[1]=0
(guard refuses rbp==top-8). See run_fixed.log.
How hard_sniff is reached
- Root arms:
sysctl -w debug.flame_graph_enable=1(sysctl writes requireSYSCAP_NOSYSCTL_WR, kern_sysctl.c:1446-1448). - Sniff IPIs (Xsniff β
hard_sniff) then fire from:sysctl_flame_graph_sniff(mp_flame.c:193, root),sysctl_cputimewithkern.sniff_enable=1(kern_clock.c:161-174, root-armed, any user can then pollkern.cputime), and the automatic ipiq-stall debug path (lwkt_ipiq.c:264,272). The interrupted thread can be ANY thread on the box, including unprivileged user threads whose kstack neighbour state is attacker-influenced only by allocation churn.
Threat model
Diagnostic feature must be armed by root once; afterwards any sampled thread
with rbp == top-8 and an unmapped neighbour panics the machine (local DoS),
and mapped neighbours leak one kernel-heap qword per sample into a
root-readable buffer (no privilege boundary crossed). No write primitive; a
uid=0 chain is not constructible from this bug (read-only sink) β see
VERDICT.md "Exploitability".
Fix
fix.diff: require rbp + 16 <= top (guard becomes rbp > top - 16).
Validated by full kernel build + reboot + identical loop (see VERDICT.md).
Twin code with the same guard exists in pcpu_timer_always()
(sys/platform/pc64/x86_64/machdep.c:3656) β out of this file's scope, flagged
for that file's audit.
Artifacts
flameoob.c (PoC kmod, v2 β safe DMAP page-table present-check), flame_dump.c (userland observer), Makefile, build.sh, run.sh, decisive.sh (v1 loop), decisive2.sh (v2 loop, baseline+fixed), build.log, fix_build_tail.log, panic.txt, panic-v1-module.txt, run_fixed.log, env.txt, fix.diff, manifest.json, verdict.json.
DF-5624 VERDICT β REPRODUCED (panic; fixed by fix.diff)
Bottom line
hard_sniff() (the Xsniff flame-graph sampler) bounds-checks the frame-pointer
chain with rbp > top - 8 but then dereferences rbp + 8, so a frame pointer
exactly at top - 8 β admitted by the guard β makes the kernel load
[top, top+8), 8 bytes past the end of the interrupted thread's kernel stack
(sys/platform/pc64/x86_64/mp_flame.c:136-138). On a guest DragonFly
6.5-DEVELOPMENT kernel this was reproduced twice as a fatal trap 12 page
fault inside hard_sniff (hard_sniff+0x9e: movq 0x8(%rdx),%rax) panicking
the machine, with an instrumented rig proving causality end to end, and the
one-line guard fix (top - 8 β top - 16) was built, installed, and shown to
eliminate both the panic and the silent out-of-bounds capture.
Reproduction narrative (all on the audit QEMU/KVM guest)
- Direct proof the guard admits the boundary (mapped neighbour): KLD
flameoob.kocalls the productionhard_sniff()with a crafted trapframetf_rbp = top - 8of the current thread's kstack. Guest output (run.sh): guard inputs printed (tf_rbp=0xfffff801166dbff8 == top-8, 8-aligned, >= bot), entry recorded (windex0β1,rips[0]= magic), the OOB qword captured inrips[1]. Across a 24-load sampling loop one neighbour contained live kernel heap: the module's reference read printedqword at [top, top+8) = 203a343236354644= ASCII"DF5624: "β kprintf scratch β i.e. adjacent-heap disclosure is real, not theoretical. - Panic proof #1 (v1 rig, incidental): during the nonzero-neighbour hunt
the loop hit a kstack followed by an unmapped page:
kstack=[0xfffff80118810000,0xfffff80118814000), fatal fault at0xfffff80118814000== top,Stopped at hard_sniff+0x9e: movq 0x8(%rdx),%rax(panic-v1-module.txt). 43 loads this boot, 1 unmapped neighbour β ~1/43 occurrence rate under ordinary process churn. - Panic proof #2 (v2 rig, instrumented, matched to the fix run): v2 adds
a safe read-only 4-level page-table walk (through the DMAP, present-bit
checked at every level β cannot itself fault) so the console records
whether the neighbour page is mapped before
hard_sniffis called. Baseline loop:NEIGHBOR UNMAPPEDprinted fortop=0xfffff801185da000, thenFatal trap 12 ... fault virtual address = 0xfffff801185da000 ... supervisor read data, page not present ... Stopped at hard_sniff+0x9e(panic.txt). Causality is now airtight: the faulting load is the return-address read at mp_flame.c:138 with rbp == top-8, the exact case the guard fails to exclude. - Fix validation:
fix.diff(guardrbp > top - 16) applied to the guest's/usr/src,make -j8 nativekernel KERNCONF=X86_64_GENERIC(RC=0),make installkernel(RC=0), reboot into#1: Thu Oct 8 22:24:57 UTC 2026. Identical v2 loop, 150 iterations: no panic, guest stayed up, β₯111 unmapped-neighbour loads survived ("survived UNMAPPED-neighbour sniff"), and the flame dump showed 0 entries with rips[1] β 0 β the fixed guard refuses rbp==top-8 before the load, so no OOB read occurs at all (run_fixed.log). Baseline vs patched behaviour differs exactly as the fix predicts.
Honest impact classification
- Observation = guest dead in DDB after fatal kernel-mode page fault, ssh
gone,
vm.sh statusβ down β reproduced / impact=panic (local DoS). - The read is 8 bytes, value lands in a buffer readable only with
SYSCAP_RESTRICTEDROOTβ no privilege boundary is crossed by the leak.
Exploitability (why no uid=0 chain)
The primitive is a read-only 8-byte overshoot whose sink (the flame entry)
is root-only. There is no write primitive, no register control, no
use-after-free; the faulting address is chosen by stack layout, not the
attacker. An unprivileged user cannot arm the sampler (sysctl writes require
SYSCAP_NOSYSCTL_WR, sys/kern/kern_sysctl.c:1446-1448); once an
administrator arms debug.flame_graph_enable, unprivileged activity
(including the automatic ipiq-stall sniff at sys/kern/lwkt_ipiq.c:264,272
and any user polling kern.cputime with kern.sniff_enable=1,
sys/kern/kern_clock.c:161-174) supplies the interrupts, and any sampled
thread whose kstack abuts an unmapped page crashes the machine. Hard blocker
for escalation: read-only sink β ceiling is DoS + root-only heap-qword
disclosure. Severity Medium ("local DoS requiring unusual config/privilege"
per the rubric), confidence certain.
Kernel references
- sys/platform/pc64/x86_64/mp_flame.c:124-129 (bot/top, windex entry select)
- sys/platform/pc64/x86_64/mp_flame.c:135-143 (buggy guard :136, OOB load :138)
- sys/platform/pc64/x86_64/mp_flame.c:119-120 (enable gate), :148-176 (data sysctl, RESTRICTEDROOT), :182-195 (sniff sysctl, RESTRICTEDROOT)
- sys/kern/kern_sysctl.c:1439-1448 (root-only sysctl writes)
- sys/kern/kern_clock.c:161-174, sys/kern/lwkt_ipiq.c:264,272 (sniff triggers)
- sys/platform/pc64/x86_64/machdep.c:3651-3660 (twin guard bug, other file)
- sys/platform/pc64/apic/lapic.c:255 (Xsniff IDT wiring)
Notes / discrepancies
- The task brief described this file as "tsc Frequency/PIPT frame buffer scrolling / mp relay-barrier helpers" β the file contains none of that; it is the flame-graph sampler. Audited as-found (see final JSON negative_notes for the classes checked and killed).
ebytesheader at mp_flame.c:161-162 understates the per-CPU record size (sizeof(fge)*256is what's emitted); harmless β the only consumer (usr.bin/flame_graph/collect.c:83-97) ignores it and usesfg->nentries.- Guest left clean:
vm.sh reset with-srcafter validation (guest_dirty=0).
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 to guest /usr/src; make -j8 nativekernel KERNCONF=X86_64_GENERIC RC=0; make installkernel RC=0; rebooted into #1 Thu Oct 8 22:24:57 UTC 2026. Baseline (stock kernel): fatal trap 12 in hard_sniff+0x9e at first unmapped-neighbour load (panic.txt). Patched: identical 150-iteration loop, >=111 unmapped-neighbour loads survived, no panic, flame dump 0 entries with rips[1]!=0 -- bad behavior gone.
run_fixed.log; fix_build_tail.log (nativekernel RC=0); panic.txt (baseline); VERDICT.md sections 3-4
Confirmed kernel references
Detail
Exploit chain
No escalation chain: primitive is an 8-byte read-only overshoot whose sink (debug.flame_graph_data) requires SYSCAP_RESTRICTEDROOT; no write primitive, no register control, fault address not attacker-chosen. Ceiling = panic (DoS) once an admin arms debug.flame_graph_enable; unprivileged users then supply triggers via kern.cputime polling (kern_clock.c:161-174) or the automatic ipiq-stall sniff (lwkt_ipiq.c:264,272).
Evidence (decisive lines)
panic.txt (v2 rig: NEIGHBOR UNMAPPED for top=0xfffff801185da000 then Fatal trap 12, fault VA == top, Stopped at hard_sniff+0x9e); panic-v1-module.txt (first incidental panic, same signature, 1-in-43 loads); run_fixed.log (patched kernel: survived UNMAPPED-neighbour sniffs, 0 nonzero rips[1]); build.log (kmod build); fix_build_tail.log (nativekernel RC=0); flame_dump.c output in transcripts (entry windex=1 rips[0]=magic rips[1]=OOB qword); VERDICT.md for the full narrative.
PoC changes
Rewrote the seed concept into a KLD that calls the exported production hard_sniff() with a crafted trapframe (tf_rbp == kstack-top-8, magic tf_rip) instead of relying on probabilistic natural sampling; v2 added a safe read-only 4-level page-table walk via PHYS_TO_DMAP (present-bit checked each level, never dereferences a non-present target) so the console records NEIGHBOR MAPPED|UNMAPPED before the call, plus a userland flame_dump.c observer that parses debug.flame_graph_data to read back the recorded OOB qword. Module writes no kernel memory.
Verified recommended fix
Change the hard_sniff() chain-walk guard from 'rbp > top - 8' to 'rbp > top - 16' (frame spans [rbp, rbp+16)); same one-liner for the twin in machdep.c pcpu_timer_always().
Verdict
hard_sniff()'s frame-chain guard (mp_flame.c:136 'rbp > top - 8') admits rbp == top-8, and the return-address load at rbp+8 (mp_flame.c:138) then reads [top, top+8) -- 8 bytes past the interrupted thread's kernel stack. Reproduced twice on the stock guest kernel as a fatal trap 12 page fault inside hard_sniff (hard_sniff+0x9e: movq 0x8(%rdx),%rax) with the faulting VA exactly == kstack top and the neighbour page recorded UNMAPPED by the instrumented rig; mapped neighbours silently copy an adjacent kernel-heap qword into the flame entry (observed 0x203a343236354644 = kprintf scratch 'DF5624: '). One-line guard fix (top-8 -> top-16) built via nativekernel, installed, rebooted: identical 150-iteration loop completes with >=111 unmapped-neighbour survivals, zero panics, and zero entries with rips[1] != 0. Local DoS ceiling: read-only sink, root-gated arming (SYSCAP_NOSYSCTL_WR), so no uid=0 chain exists from this bug.
No comments yet.