ipmi_smic: BMC-controlled reply byte count stored unclamped into ir_replylen β kernel-heap OOB read (info leak) via IPMICTL_RECEIVE_MSG(_TRUNC) copyout (DF-4777 twin)
Summary
The SMIC transport has the EXACT DF-4777 shape: i counts every streamed byte, storage clamped but ir_replylen=i unclamped - the driver even prints 'Read short: 1024 buffer, 16000 actual' and returns SUCCESS. The shared ipmi.c sink remains unclamped in-tree. VERIFIED end-to-end on guest: verbatim-source producer harness (ir_replylen=16000/1024 buffer) + real /dev/ipmi0 sink 3/3 runs leaking 6.6-7.2KB kernel heap (slab pointers + neighbor markers) to an unpriv operator user; clamp-at-both-ends fix validated on a rebuilt kernel (0 foreign bytes, IPMI functional).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4948 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 6.4 KB | β raw | |
| smicshock.c | β | 8.5 KB | view raw | |
| mock/ipmi.h | β | 415 B | view raw | |
| mock/ipmivars.h | β | 4.0 KB | view raw | |
| Makefile | β | 322 B | β download | |
| bumpko.c | β | 3.9 KB | view raw | |
| leak.c | β | 6.5 KB | view raw | |
| build.sh | β | 288 B | view raw | |
| run.sh | β | 303 B | view raw | |
| build.log | β | 7.0 KB | view raw | |
| run.log | β | 2.9 KB | view raw | |
| run.2.log | β | 261 B | view raw | |
| run.3.log | β | 261 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_build.log | β | 298 B | view raw | |
| fix_run.log | β | 1.6 KB | view raw | |
| leak_sample.txt | β | 1.1 KB | view raw | |
| env.txt | β | 702 B | view raw | |
| dmesg.txt | β | 14.5 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-4948 β SMIC twin of the DF-4777 unclamped-reply-length heap over-read
File: sys/dev/misc/ipmi/ipmi_smic.c Β· Class: CWE-125 OOB read β
kernel-heap info leak (CWE-909) Β· Severity: Medium (kernleak) Β·
Verdict: REPRODUCED (3/3 sink runs + live producer proof) and FIX
VALIDATED on a rebuilt kernel.
The bug
smic_polled_request() counts every reply data byte the BMC streams
(ipmi_smic.c:320-336); storage into req->ir_reply[] is clamped
(ipmi_smic.c:325) but the stored count is not:
i = 0;
while (state == 1) { /* until BMC says RD_END */
state = smic_read_byte(sc, &data);
...
if (i < req->ir_replybuflen)
req->ir_reply[i] = data; /* clamped store */
i++; /* unclamped count */
}
if (!smic_read_end(sc))
return (0);
req->ir_replylen = i; /* ipmi_smic.c:341 β BMC-controlled */
The shared consumer ipmi_ioctl() (ipmi.c:448-468) trusts ir_replylen:
len = ir_replylen + 1; len = min(user data_len, len); copyout(ir_reply,
..., len - 1) β over a buffer that is always exactly IPMI_MAX_RX = 1024
bytes for ioctl requests (ipmi.c:401-403, 534-546). A hostile BMC that
streams > 1024 data bytes before RD_END turns every
IPMICTL_RECEIVE_MSG(_TRUNC) into a kernel-heap disclosure of up to 64 KB
to any process in group operator (/dev/ipmi0 0660 root:operator,
ipmi.c:831-832). Exact twin of DF-4777 (ipmi_kcs.c:432), which called out
this line; DF-4777's fix was validation-only and never applied to sys/.
How to reproduce
Phase 1 (producer, any boot): smicshock.ko compiles the verbatim
/usr/src/sys/dev/misc/ipmi/ipmi_smic.c (copied by build.sh, never edited)
with -DLOCAL_MODULE against mock <ipmivars.h> whose only substitution
is INB/OUTB β a scripted hostile-BMC SMIC register emulator (protocol-legal
transactions; 16000 reply data bytes; last byte staged RD_END).
# host
dfbsd-qemu/vm.sh run_root 'sh /root/smicshock/build.sh' # BUILD-OK
dfbsd-qemu/vm.sh run_root 'kldload /root/smicshock/smicshock.ko; kldunload smicshock'
# expected: req->ir_replylen = 16000, ir_replybuflen = 1024, BUG REPRODUCED
Phase 2 (sink, end-to-end; QEMU needs the IPMI sim for a real /dev/ipmi0):
dfbsd-qemu/vm.sh down
QEMU_EXTRA="-device ipmi-bmc-sim,id=bmc0 -device isa-ipmi-kcs,bmc=bmc0" dfbsd-qemu/vm.sh up
# guest root: kldload ipmi && pw groupmod operator -m maxx
# kldload /root/bumpko/bumpko.ko ; cc -O2 -o /tmp/leak /root/smicshock/leak.c
# guest maxx (background): /tmp/leak
# guest root: sysctl hw.bumpreplylen=16000 # arm the end state the SMIC transport produced
# expected: RECEIVE ok: data_len = 16001; ~6.6-7.2k nonzero kernel bytes past
# offset 1024 (first at 0x042e); kernel pointers 0xfffff8008dcfxxxx;
# hundreds of "DF4777MARK-" fragments from live neighbor chunks.
Phase 3 (fix validation): patch -p1 < fix.diff in the guest /usr/src,
make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel,
reboot, repeat phases 1+2 β ir_replylen = 1024 (clamped) and
msg.data_len = 1025, nonzero beyond 1024: 0.
Artifacts
| file | what |
|---|---|
smicshock.c, mock/, Makefile |
producer harness (verbatim code-under-test + scripted BMC) |
bumpko.c, leak.c |
sink arming + unprivileged readout (reused verbatim from DF-4777's pack) |
build.sh, run.sh |
exact build/run commands |
build.log, run.log, run.2.log, run.3.log |
full logs, baseline |
fix.diff, fix_build.log, fix_run.log |
fix + validated kernel build/run |
leak_sample.txt, env.txt, dmesg.txt |
disclosed bytes, environment |
VERDICT.md, manifest.json, verdict.json |
narrative + machine verdicts |
DF-4948 β VERDICT
REPRODUCED β kernel-heap out-of-bounds READ (info leak), producer half proven live against the verbatim SMIC transport source, sink half proven end-to-end 3/3 runs with recognizable kernel-memory content on the stock kernel, and the one-file fix validated live on a rebuilt kernel (over-read eliminated, device still functional).
Root cause
sys/dev/misc/ipmi/ipmi_smic.c β smic_polled_request():
- reply collection loop (ipmi_smic.c:320-336):
icounts every data byte the BMC streams before signaling RD_END. Storage is bounded (if (i < req->ir_replybuflen)at :325) but the count is not. req->ir_replylen = i;(ipmi_smic.c:341) records the BMC-controlled total; the driver even detects the condition and proceeds anyway ("SMIC: Read short: 1024 buffer, 16000 actual", :346-350).- The shared consumer
ipmi_ioctl()IPMICTL_RECEIVE_MSG(_TRUNC) (sys/dev/misc/ipmi/ipmi.c:448-468) trusts it:len = ir_replylen + 1,len = min(recv->msg.data_len, len)(userdata_lenup to 65535),copyout(kreq->ir_reply, ..., len - 1)out of a buffer of exactlyIPMI_MAX_RX = 1024bytes for every ioctl request (ipmi.c:401-403, 534-546; sys/sys/ipmi.h:35). A BMC streaming > 1024 data bytes (trivial for a hostile controller β each byte is one SMIC port transaction) turns every RECEIVE ioctl into a heap disclosure of up to 64 KB past thekmallocchunk, readable by any process in groupoperator(/dev/ipmi0 0660 root:operator, ipmi.c:831-832).
This is the exact twin of DF-4777 (ipmi_kcs.c:411-432, KCS backend). The
DF-4777 verification called this line out ("The SMIC backend has the
identical defect (ipmi_smic.c:341) β same fix") and its fix.diff carried a
SMIC hunk, but that fix was validation-only β sys/ is a read-only audit
target β so both the KCS and SMIC producers and the ipmi.c sink remain
unclamped in the tree (confirmed: ipmi_smic.c:341 and ipmi.c:448 today).
Attack class: CWE-125 (OOB read) across the hostile-management-controller trust boundary; impact is adjacent-kernel-memory disclosure (info leak), same as DF-4777.
How it was reproduced
Producer (new for this finding β the SMIC transport itself)
QEMU has no SMIC interface emulation (only KCS/BT), so the transport half
was verified with smicshock.ko: a root-loaded harness that compiles the
verbatim /usr/src/sys/dev/misc/ipmi/ipmi_smic.c (copied unmodified
by build.sh, -DLOCAL_MODULE) into the module, substituting only INB/OUTB
(via mock <ipmivars.h>) with a scripted hostile-BMC SMIC register
emulator that executes each protocol step exactly as the silicon would:
WR_START/WR_NEXT/WR_END accepted, SC status codes returned, reply
netfn/command echoed, then 16000 data bytes streamed with the last
staged RD_END. (The emulator also accepts any control write in the read
phase as RD_NEXT, which is what tolerates the driver's CC/SC mixup at
ipmi_smic.c:202 β DF-4949.)
Baseline (clean-source snapshot, stock kernel, run.log phase 1):
smicshock: SMIC: Read short: 1024 buffer, 16000 actual <- driver sees it smicshock: smic_polled_request() = 1 <- returns success smicshock: req->ir_replylen = 16000 <- unclamped (bug) smicshock: req->ir_replybuflen = 1024
Sink (end-to-end, shared consumer, unmodified kernel)
With a real /dev/ipmi0 (QEMU ipmi-bmc-sim + isa-ipmi-kcs),
bumpko.ko (DF-4777's arming module, reused verbatim) sets
ir_replylen = 16000 on a completed request under IPMI_LOCK β byte-for-
byte the end state the hostile SMIC transport produced above β and the
unprivileged readout (maxx, uid 1001, group operator) runs the stock
RECEIVE path. 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 (1-byte-ish) reply nonzero in [1024,16000): 6624-7167 (first at 0x042e, all runs) marker fragments beyond the victim BMC data: 392-424
Disclosed content (leak_sample.txt): 0xfffff8008dcf.... kernel heap
(slab-zone linkage of the adjacent 2048-class chunk) beginning 70 bytes
past the victim chunk end β matching the allocation geometry β plus
hundreds of "DF4777MARK-" fragments from live neighboring allocations
(~940 nonzero bytes per 1024-byte stride in the block census).
Honest scope: info leak, not a write; uid=0 is not reachable from this bug alone. The disclosed slab-adjacent contents (kernel pointers, neighbor allocation data) feed a separate exploitation chain; on this guest (no KASLR) the pointers directly identify heap geometry.
Fix validation (done)
fix.diff (2 hunks, patch -p1 clean on the guest's /usr/src):
ipmi_smic.c: clampitoreq->ir_replybuflenbefore storingir_replylen(fixes the hostile-BMC path at collection time);ipmi.c: defense-in-depth β clamplentoir_replybuflen + 1in the RECEIVE path (protects against any producer of an inflated length).
Build: make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel
(completed Fri Sep 25 08:48:48 UTC 2026, fix_build.log), rebooted into
DragonFly 6.5-DEVELOPMENT #1: Fri Sep 25 08:44:00 UTC 2026.
- Producer re-run on the patched source (fix_run.log):
req->ir_replylen = 1024β clamped; transaction still succeeds. - Sink re-run with identical arming:
msg.data_len = 1025,nonzero in [1024,1024): 0,marker fragments: 0β over-read gone; the SEND/RECEIVE round trips of the readout program itself prove IPMI still functions on the patched kernel.
kernel_refs
- sys/dev/misc/ipmi/ipmi_smic.c:320-336 (unbounded BMC byte count)
- sys/dev/misc/ipmi/ipmi_smic.c:341 (ir_replylen = i, unclamped)
- sys/dev/misc/ipmi/ipmi_smic.c:346-350 (driver detects short buffer, proceeds)
- sys/dev/misc/ipmi/ipmi.c:448-468 (trusting consumer + copyout OOB read)
- sys/dev/misc/ipmi/ipmi.c:401-403,534-546 (fixed 1024-byte ir_reply alloc)
- sys/dev/misc/ipmi/ipmi.c:831-832 (operator-group device node)
- sys/sys/ipmi.h:35 (IPMI_MAX_RX 1024)
- sys/dev/misc/ipmi/ipmi_kcs.c:411-432 (DF-4777 twin, KCS)
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
fixedfixed - with fix.diff applied (ipmi_smic.c clamp + ipmi.c sink clamp), nativekernel rebuilt and rebooted, the producer stores ir_replylen=1024 (clamped, transaction succeeds) and the armed sink returns msg.data_len=1025 with zero bytes beyond the 1024-byte buffer; IPMI traffic over the KCS sim still round-trips on the patched kernel.
fix_run.log (producer: 'clamped (ir_replylen <= ir_replybuflen)' + req->ir_replylen = 1024; sink: 'RECEIVE ok: msg.data_len = 1025', 'nonzero in [1024,1024): 0', 'marker fragments: 0'), fix_build.log ('>>> Kernel install for X86_64_GENERIC completed on Fri Sep 25 08:48:48 UTC 2026'), baseline in run.log/run.2.log/run.3.log
Confirmed kernel references
Detail
Exploit chain
hostile/compromised BMC (or BMC-transport confusion) streams >1024 SMIC reply data bytes before RD_END -> smic_polled_request records ir_replylen=N>1024 (ipmi_smic.c:341) -> request completes normally -> any operator-group process issues IPMICTL_RECEIVE_MSG_TRUNC with msg.data_len=65535 -> ipmi.c:448-468 copyout reads N bytes from the 1024-byte M_IPMI chunk -> up to 64KB adjacent kernel heap (pointers + neighbor allocation contents) disclosed per request, repeatable.
Evidence (decisive lines)
run.log (producer BUG line + full sink run 1 with hexdump of kernel pointers at 0x042e+), run.2.log/run.3.log (runs 2-3: 7167/7160 foreign bytes), leak_sample.txt (pointer values + geometry), fix_run.log (patched kernel: clamped 1024 / 1025 / zero), fix_build.log, VERDICT.md narrative
PoC changes
Authored new producer harness (smicshock.c + mock headers): no seed existed for the SMIC file. Emulator iteration: first build dropped the SMIC_CC_SMS_RD_END case (smic_read_end correctly rejected the bogus status); after adding it the transaction completes. bsd.kmod.mk resets CFLAGS (line 98) so -DLOCAL_MODULE/-Imock must be appended after the include; SRCS needs device_if.h/bus_if.h. Sink half reuses DF-4777's bumpko.c/leak.c verbatim.
Verified recommended fix
Clamp i to req->ir_replybuflen before req->ir_replylen = i at ipmi_smic.c:341 (and clamp len to ir_replybuflen+1 in ipmi.c RECEIVE as defense-in-depth; cf. DF-4777 for the KCS twin).
Verdict
REPRODUCED. smic_polled_request() stores the BMC-controlled reply byte count unclamped into req->ir_replylen (ipmi_smic.c:341) while the write into ir_reply[] is clamped (:325) - the exact DF-4777 shape in the SMIC backend, still unfixed in the tree (DF-4777's fix was validation-only). The producer half was proven live by compiling the VERBATIM ipmi_smic.c into the smicshock.ko harness against a scripted hostile-BMC SMIC register emulator: with a 16000-byte streamed reply it stores ir_replylen=16000 for ir_replybuflen=1024, returns success, and even logs 'Read short: 1024 buffer, 16000 actual'. The shared consumer (ipmi.c:448-468, unmodified stock kernel) then copies out min(user_len, 16001) bytes from the fixed 1024-byte IPMI_MAX_RX chunk: 3/3 end-to-end runs disclose 6624-7167 nonzero kernel-heap bytes past offset 1024 (first foreign byte 0x042e in every run), including slab-linkage pointers (0xfffff8008dcfxxxx) and 392-424 fragments of live neighboring allocations, to an unprivileged operator-group process. Info leak only - no write primitive; uid=0 not reachable from this bug alone. fix.diff (SMIC clamp + ipmi.c sink clamp) validated on a rebuilt+reinstalled kernel: ir_replylen clamped to 1024, copyout capped at 1025 bytes, zero foreign bytes, IPMI device still functional.
No comments yet.