β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4777

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4777 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative incl. transport-substitution caveat + geometry
↓ download 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): 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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4777

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.