ipmi_kcs: BMC-controlled reply byte count stored unclamped into ir_replylen β kernel-heap OOB read (info leak) via IPMICTL_RECEIVE_MSG(_TRUNC) copyout
Summary
Storage is bounded at ir_replybuflen but the count is recorded verbatim; ir_replybuflen is always IPMI_MAX_RX=1024 while ipmi_ioctl computes len=ir_replylen+1 and copyouts that many bytes - a hostile/compromised BMC streaming >2KB discloses up to 64KB of adjacent kernel heap (slab-linkage pointers verified) to an operator-group process via /dev/ipmi0. Distinct from DF-1865 (the completing over-count case). LIVE on guest: 3/3 runs, 7136 nonzero bytes beyond offset 1024, kernel pointers at chunk+2050, control run clean; clamp fix validated live (patched kernel returns 1025/0 overread, driver functional). SMIC twin at ipmi_smic.c:341.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4777 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak.c | trigger | unprivileged groom+victim+RECEIVE PoC (operator group) | 6.5 KB | view raw |
| bumpko.c | trigger | root KLD substituting the hostile-BMC transport end-state (QEMU sim caps replies at 255) | 3.9 KB | view raw |
| build.sh | build | exact build commands | 134 B | view raw |
| run.sh | run | exact run sequence + expectations | 195 B | view raw |
| build.log | log | module build output | 103 B | view raw |
| run.log | log | baseline reproduction 1 (full output) | 1.4 KB | view raw |
| run.2.log | log | baseline reproduction 2 | 1.4 KB | view raw |
| run.3.log | log | baseline reproduction 3 | 1.4 KB | view raw |
| run.control.log | log | control: legal 200-byte reply -> no over-read | 617 B | view raw |
| leak_sample.txt | evidence | leaked kernel pointers + marker pattern, 3-run profile | 1.2 KB | view raw |
| fix.diff | suggested-fix | kcs+smic collection clamp + ipmi.c sink clamp (validated live) | 1.6 KB | view raw |
| fix_build.log | log | patched-kernel nativekernel build (BUILD-OK) | 5.2 MB | β download |
| fix_run.log | log | patched-kernel decisive run: data_len=1025, 0 beyond | 621 B | view raw |
| env.txt | env | guest environment (baseline kernel) | 655 B | view raw |
| VERDICT.md | verdict | full narrative incl. transport-substitution caveat + geometry | 6.2 KB | β raw |
| manifest.json | manifest | this file | 2.5 KB | view raw |
| verdict.json | verdict | machine verdict | 5.0 KB | view raw |
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):
icounts 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
kmallocchunk. Repeatable per request; length chosen by the BMC; readout by any process in groupoperator(/dev/ipmi00660 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):
ipmi_kcs.c: clampitoreq->ir_replybuflenbefore storingir_replylen(fixes the real hostile-BMC path at collection time);ipmi_smic.c: identical clamp for the twin SMIC backend;ipmi.c: defense-in-depth β clamplentoir_replybuflen + 1in 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.
Fix verification
fixedfix.diff applied to guest /usr/src, nativekernel+installkernel BUILD-OK, rebooted into #1 Wed Sep 23 15:29:40 UTC 2026; identical PoC+arming on the patched kernel returns data_len=1025 with ZERO bytes beyond the 1024-byte buffer (vs 16001/7136-nonzero/424-markers baseline), 2/2 runs, driver still functional. In the module-assisted flow the ipmi.c sink clamp is the active defense; the kcs/smic collection clamps cover the real hostile-BMC path.
fix.diff; fix_build.log (BUILD-OK); fix_run.log (data_len=1025, 0 beyond); VERDICT.md fix section
Confirmed kernel references
Detail
Exploit chain
hostile/compromised BMC streams >1024 KCS READ data bytes -> ir_replylen unclamped -> operator-group user RECEIVE_MSG_TRUNC with 65535-byte buffer -> copyout reads up to 65535 bytes from 1024-byte heap chunk -> adjacent-kernel-memory disclosure (slab pointers + neighbor allocation contents), repeatable per ioctl
Evidence (decisive lines)
run.log/run.2.log/run.3.log (3 baseline reproductions, block census + marker counts), leak_sample.txt (kernel pointers + pattern at first foreign offset 0x42e), run.control.log (legal reply: no over-read), fix_run.log (patched: data_len=1025, 0 beyond), fix_build.log (BUILD-OK), VERDICT.md (geometry + caveats)
PoC changes
leak.c self-contained: two fds (SEND purges only the sender's completed queue), groom phase (30 same-size-class patterned chunks, free oldest 20 so the victim reuses a chunk below live patterned neighbors), sysctl-poll arming, block census + marker-fragments analysis. bumpko.c KLD (new): devclass walk to ipmi_softc, hot-patch ipmi0 dev_ops.d_ioctl with a wrapper that sets head kreq->ir_replylen under IPMI_LOCK before the original ioctl - substitutes the hostile-BMC transport half that QEMU's 255-byte-capped sim cannot produce.
Verified recommended fix
Clamp the reply count to ir_replybuflen at collection (kcs+smic) and clamp len to ir_replybuflen+1 in the ipmi.c RECEIVE sink (fix.diff, validated live)
Verdict
REPRODUCED: kcs_polled_request() records the BMC-controlled reply byte count unclamped (ipmi_kcs.c:432, i counts every byte the BMC streams in READ state) while storage is bounded at ir_replybuflen=IPMI_MAX_RX=1024; ipmi_ioctl() IPMICTL_RECEIVE_MSG(_TRUNC) then copyouts ir_replylen+1 bytes from the fixed 1024-byte heap buffer (ipmi.c:448-468) -> kernel-heap OOB read of up to 64KB past the kmalloc chunk into an operator-group process. Deterministic 3/3 runs: data_len=16001, 7136+ nonzero bytes beyond offset 1024 including 0xfffff8008dcfc8c8-style slab-linkage kernel pointers (first at chunk+2050, exactly past the 2048-class chunk end) and 424 recognizable marker fragments from live neighbor chunks; control run with a legal <1024 reply shows zero over-read. Transport caveat (documented): the stock QEMU ipmi-bmc-sim caps BMC replies at MAX_IPMI_MSG_SIZE=255 (QEMU hw/ipmi/ipmi_kcs.c ipmi_kcs_handle_rsp), so the transport end-state ir_replylen>1024 - exactly what a hostile BMC produces - was emulated by a root-loaded harness module setting ir_replylen on the completed request; the entire sink (length math + copyout) ran in the unmodified stock kernel and the readout was the unprivileged operator-group user. Fix (clamp in kcs+smic at collection, clamp at ipmi.c sink) built, installed, rebooted: same PoC + same arming -> data_len=1025, zero bytes beyond the buffer, 2/2 runs; driver still functional. Info leak only - no write primitive, no uid=0 from this bug alone.
No comments yet.