# 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

```c
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

```sh
# 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 require
  `SYSCAP_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_cputime` with
  `kern.sniff_enable=1` (kern_clock.c:161-174, root-armed, *any user can then
  poll* `kern.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.
