# DF-4777 — VERDICT

**REPRODUCED — kernel-heap out-of-bounds READ (info leak) to an
operator-group userspace process, deterministic 3/3 runs with recognizable
kernel-memory content, and the one-file fix is validated live on a rebuilt
kernel (over-read eliminated, device still functional).**

## Root cause

`sys/dev/misc/ipmi/ipmi_kcs.c` — `kcs_polled_request()`:

- reply collection loop (ipmi_kcs.c:411-431): `i` counts **every** data
  byte the BMC streams in KCS READ state. Storage is bounded
  (`if (i < req->ir_replybuflen)` at :420) but the count is not.
- `req->ir_replylen = i;` (ipmi_kcs.c:432) records the BMC-controlled total.

`sys/dev/misc/ipmi/ipmi.c` — `ipmi_ioctl()` IPMICTL_RECEIVE_MSG(_TRUNC)
(ipmi.c:448-468): `len = kreq->ir_replylen + 1` is trusted;
`len = min(recv->msg.data_len, len)` (user `data_len` up to 65535) then
`copyout(kreq->ir_reply, recv->msg.data + 1, len - 1)` reads `ir_replylen`
bytes out of a buffer of exactly `IPMI_MAX_RX = 1024` bytes
(`ipmi_alloc_request`, ipmi.c:534-546; reply length for ioctl requests is
always IPMI_MAX_RX, ipmi.c:401-403; `sys/sys/ipmi.h:35`). A BMC that streams
> ~2 KB of data bytes before returning to IDLE (trivial for a hostile BMC;
each READ byte is one port read) turns every RECEIVE ioctl into a kernel
heap disclosure of up to 64 KB past the `kmalloc` chunk. Repeatable per
request; length chosen by the BMC; readout by any process in group
`operator` (`/dev/ipmi0` 0660 root:operator, ipmi.c:831-832).
The SMIC backend has the identical defect (ipmi_smic.c:341) — same fix.

Attack class: CWE-125 (OOB read) chained from a hostile-management-
controller trust boundary (the kernel must not trust BMC-reported lengths).
Adjacent-kernel-memory disclosure (CWE-909 family) is the impact.

## How it was reproduced (transport-substituted, sink fully live)

The stock QEMU `ipmi-bmc-sim` caps responses at MAX_IPMI_MSG_SIZE=255
(QEMU hw/ipmi/ipmi_kcs.c `ipmi_kcs_handle_rsp` truncates oversized BMC
replies), so the *transport half* (a BMC streaming 16000 data bytes) cannot
be produced by the built-in sim. The verification therefore reproduces
exactly the end state a hostile BMC produces — `ir_replylen = 16000 > 1024`
on a completed request — with a root-loaded harness module (`bumpko.c`,
hot-patches ipmi0's `dev_ops.d_ioctl`, sets `ir_replylen` under IPMI_LOCK on
the head of the calling fd's completed queue, then calls the unmodified
original ioctl). **Everything from the RECEIVE ioctl onward — the length
math and the `copyout` at ipmi.c:448-468 — runs in the stock kernel, and
the readout process is unprivileged** (maxx, uid 1001, groups
maxx+operator).

Heap grooming (30 same-size-class M_IPMI chunks filled with a recognizable
pattern; the oldest 20 freed so the victim reuses a chunk *below* live
patterned chunks) makes the disclosed neighbor content identifiable.

Observed, 3/3 independent runs (run.log, run.2.log, run.3.log):

```
RECEIVE ok: msg.data_len = 16001 (compcode + 16000 reply bytes)
nonzero in [0,1024):     1          <- the BMC's real (2-byte-ish) reply
nonzero in [1024,16000): 7136-7144  (first at 0x042e)
marker fragments beyond the victim BMC data: 424
```

Leaked content at the first foreign offset (leak_sample.txt):

- `0xfffff8008dcfc8c8`-style kernel heap pointers (slab-zone linkage of the
  adjacent chunk) at ir_reply[1070+] = 70 bytes past the victim's 2048-class
  kmalloc chunk end — exactly matching the allocation geometry
  (`ir_reply` = chunk+80+900; chunk = 2048 bytes);
- 424 occurrences of the "DF4777MARK-" pattern from the *live* neighboring
  kernel allocations (~940 nonzero bytes per 2048-byte stride in the block
  census = 900 pattern bytes + allocator metadata).

Control (run.control.log): arming a *legal* reply length (200 < 1024)
yields zero bytes beyond the real reply — the over-read manifests only when
the transport-reported length exceeds the buffer, i.e. exactly the hostile
BMC condition.

Honest scope: the primitive is an info **leak**, not a write. Escalation to
uid=0 is not reachable from this bug alone; the disclosed slab-adjacent
contents (kernel pointers, neighboring allocation data) would feed a
separate exploitation chain, and on this guest (no KASLR) the pointer
disclosure is directly usable for defeating any future randomization of the
leaked region's occupants.

## Fix validation (mandatory quality bar — done)

`fix.diff` (3 hunks, `git apply`-clean on the guest's /usr/src):

1. `ipmi_kcs.c`: clamp `i` to `req->ir_replybuflen` before storing
   `ir_replylen` (fixes the real hostile-BMC path at collection time);
2. `ipmi_smic.c`: identical clamp for the twin SMIC backend;
3. `ipmi.c`: defense-in-depth — clamp `len` to `ir_replybuflen + 1` in the
   RECEIVE path (fixes any other producer of an inflated ir_replylen).

Build: `make nativekernel KERNCONF=X86_64_GENERIC && make installkernel`
(BUILD-OK, fix_build.log), reboot into
`DragonFly 6.5-DEVELOPMENT #1: Wed Sep 23 15:29:40 UTC 2026`.

Same PoC, same arming (ir_replylen=16000), 2/2 runs (fix_run.log and
confirmation):

```
RECEIVE ok: msg.data_len = 1025 (compcode + 1024 reply bytes)
nonzero in [1024,1024):  0
marker fragments beyond the victim BMC data: 0
```

The over-read is gone (in the module-assisted flow the ipmi.c sink clamp is
the defense that fires; the kcs clamp covers the real BMC path), and normal
IPMI traffic over the KCS sim continues to work (the SEND/RECEIVE round
trips in the PoC itself prove driver functionality on the patched kernel).

## kernel_refs
- sys/dev/misc/ipmi/ipmi_kcs.c:411-431 (unbounded BMC byte count)
- sys/dev/misc/ipmi/ipmi_kcs.c:432 (ir_replylen = i, unclamped)
- sys/dev/misc/ipmi/ipmi.c:448-468 (trusting consumer + copyout OOB read)
- sys/dev/misc/ipmi/ipmi.c:534-546 (fixed 1024-byte ir_reply allocation)
- sys/dev/misc/ipmi/ipmi.c:831-832 (operator-group device node)
- sys/dev/misc/ipmi/ipmi_smic.c:341 (twin)
- sys/sys/ipmi.h:35 (IPMI_MAX_RX 1024)

## 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.
