ipmi_ssif: BMC-accumulated reply length stored unclamped into ir_replylen β kernel-heap OOB read (info leak) via IPMICTL_RECEIVE_MSG(_TRUNC) copyout (DF-4777/DF-4948 shape, third transport)
Summary
The payload copy is clamped but the accumulated total is not - with fully SMBus-legal per-block counts of 32 (distinct from DF-2025/2026's illegal values), a hostile BMC sends >33 continuation blocks and rides DFly's inverted 0xff break condition to drive ir_replylen past 1024. VERIFIED end-to-end: verbatim-source producer (16023/1024) + real /dev/ipmi0 sink 3/3 (7159-7190 kernel-heap bytes past the chunk, slab pointers, 426 neighbor markers, uid-1001 operator user); both-ends clamp fix validated on a rebuilt kernel (0 bytes beyond, IPMI functional). Completes the transport trio: kcs DF-4777, smic DF-4948, ssif DF-4978.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4978 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 9.6 KB | β raw | |
| ssifshock.c | β | 8.3 KB | view raw | |
| ipmivars.h | β | 3.0 KB | view raw | |
| bus/smbus/smbconf.h | β | 1.2 KB | view raw | |
| dev/smbus/smb/smb.h | β | 212 B | view raw | |
| smbus_if.h | β | 255 B | view raw | |
| Makefile | β | 461 B | β download | |
| bumpko.c | β | 3.9 KB | view raw | |
| leak.c | β | 6.5 KB | view raw | |
| build.sh | β | 110 B | view raw | |
| run.sh | β | 386 B | view raw | |
| build.log | β | 5.8 KB | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 1.4 KB | view raw | |
| run.3.log | β | 1.4 KB | view raw | |
| run.control.log | β | 617 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_build.log | β | 5.7 MB | β download | |
| fix_run.log | β | 1.1 KB | view raw | |
| leak_sample.txt | β | 378 B | view raw | |
| env.txt | β | 251 B | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-4978 β SSIF twin of the DF-4777/DF-4948 unclamped-reply-length heap over-read
File: sys/dev/misc/ipmi/ipmi_ssif.c Β· Class: CWE-125 OOB read β
kernel-heap info leak (CWE-909) Β· Severity: Medium (kernleak) Β·
Verdict: REPRODUCED (verbatim-source producer 16023/1024 + real
/dev/ipmi0 sink 3/3 runs leaking ~7.2 KB kernel heap to an unpriv
operator user) and FIX VALIDATED on a rebuilt kernel (0 bytes past
1024, device functional).
The bug
ssif_polled_request() copies reply payload into req->ir_reply[]
clamped to ir_replybuflen (ipmi_ssif.c:241-242, 253, 291-293) but
accumulates the BMC-controlled total unclamped β len += count - 1
per READ_CONT block (ipmi_ssif.c:294), single-part len = count - 3
(ipmi_ssif.c:240) β and stores it verbatim:
req->ir_replylen = len; /* ipmi_ssif.c:304 */
The shared consumer ipmi_ioctl() (ipmi.c:448-468) trusts it:
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,
sys/sys/ipmi.h:35). Every per-block count in the PoC is exactly 32
(fully SMBus-legal), so this is neither DF-2025 (count > 32 stack
over-read of ssif_buf) nor DF-2026 (unterminated loop). Exact twin of
DF-4777 (ipmi_kcs.c:432) and DF-4948 (ipmi_smic.c:341) β the third
transport, unfixed.
Note: DragonFly's multi-part loop termination is inverted relative
to its own comment, the SSIF spec, and FreeBSD β if (ssif_buf[0] !=
0xff) break; (ipmi_ssif.c:297) β so numbered blocks terminate and
0xff-marked blocks continue. The hostile script therefore sends 515
0xff-marked full 32-byte continuation blocks and terminates with the
block number the driver expects.
How to reproduce
Phase 1 (producer, any boot): ssifshock.ko compiles the verbatim
/usr/src/sys/dev/misc/ipmi/ipmi_ssif.c (copied by build.sh, never
edited) with mock transport headers whose only substitution is the
SMBus layer (mock bus/smbus/smbconf.h redirects smbus_bread/bwrite
to a scripted hostile-BMC emulator).
# host
dfbsd-qemu/vm.sh run_root 'sh /root/ssifshock/build.sh' # BUILD-OK
dfbsd-qemu/vm.sh run_root 'kldload /root/ssifshock/ssifshock.ko'
# expected dmesg: "BUG DF-4978 REPRODUCED: unclamped ir_replylen=16023
# over 1024-byte buffer (14999 bytes past end)"
Phase 2 (sink, end-to-end; QEMU needs the IPMI sim for /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: sh /root/ssifshock/build.sh; kldload ipmi
# pw groupmod operator -m maxx; kldload /root/bumpko/bumpko.ko
# guest maxx (background): /tmp/leak
# guest root: sysctl hw.bumpreplylen=16023 # the SSIF-produced end state
# expected: RECEIVE ok: data_len = 16024; ~7.2k nonzero kernel bytes past
# offset 1024 (first at 0x042e); kernel pointers 0xfffff800...;
# ~426 "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 β producer prints ir_replylen = 1024
(clamped) and the sink returns msg.data_len = 1025, nonzero beyond
1024: 0 with the IPMI device fully functional.
Artifacts
| file | what |
|---|---|
ssifshock.c, ipmivars.h, bus/smbus/smbconf.h, dev/smbus/smb/smb.h, smbus_if.h, 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 |
FULL producer build + decisive run (baseline) |
run.2.log, run.3.log |
sink stress runs (3/3 determinism) |
run.control.log |
control run (legal length 200 β 0 bytes beyond) |
fix.diff, fix_build.log, fix_run.log |
fix + full 37k-line validated kernel build + patched-kernel runs |
leak_sample.txt, env.txt |
disclosed bytes, guest environment |
VERDICT.md, manifest.json, verdict.json |
narrative + machine verdicts |
DF-4978 β VERDICT
REPRODUCED β kernel-heap out-of-bounds READ (info leak) through the
SSIF transport's unclamped reply-length accounting, to an operator-group
userspace process: verbatim-source producer run 1/1 on the live kernel
(ir_replylen = 16023 over a 1024-byte buffer), real-/dev/ipmi0 sink
3/3 runs with ~7.2 KB of recognizable kernel-memory content past the
chunk, control run clean, and the two-file clamp fix validated live on
a rebuilt kernel (over-read eliminated, device still functional).
Root cause
sys/dev/misc/ipmi/ipmi_ssif.c β ssif_polled_request():
- multi-part READ_CONT loop (ipmi_ssif.c:257-300): storage into
req->ir_reply[]is bounded (if (len < req->ir_replybuflen)+min(req->ir_replybuflen - len, count - 1)at :291-293) but the accumulator is not:len += count - 1at :294 runs for every block, forever, with no relation toir_replybuflen. - single-part path:
len = count - 3at :240 also ignoresir_replybuflen(bounded only by BMC-reportedcount). req->ir_replylen = len;(ipmi_ssif.c:304) records the BMC-controlled total verbatim β including the size_tβint truncation hazard for pathological accumulation.
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 hostile/compromised BMC (or an SMBus device impersonating the BMC
slave address) that answers a multi-part read with more than ~33
continuation blocks drives ir_replylen past 1024; every subsequent
RECEIVE ioctl then discloses up to 64 KB of adjacent kernel heap to any
process in group operator (/dev/ipmi0 0660 root:operator,
ipmi.c:831-832). This is the exact shape of DF-4777 (ipmi_kcs.c:432,
REPRODUCED + fix live-validated) and DF-4948 (ipmi_smic.c:341,
REPRODUCED 3/3 + fix validated on rebuilt kernel) β the third and last
transport, SSIF, same shared sink, both ends still unclamped in-tree.
Distinctness from the known SSIF findings (pass 1):
- DF-2025 =
count > 32per-block over-read of the 32-byte stack buffer via bcopy. Here every count is exactly 32; nothing outsidessif_buf[]is ever read by the transport. - DF-2026 = unterminated multi-part loop. Here the transaction terminates normally (one numbered block) and the request completes with success.
- DF-2024 = multi-part WRITE underflow (send path). This is the receive path.
DragonFly-specific quirk discovered during verification
The loop break condition at ipmi_ssif.c:297-298 is inverted relative to its own comment ("0xff marks the last block", :273), the IPMI SSIF spec, and FreeBSD's original:
/* If this was the last block we are done. */
if (ssif_buf[0] != 0xff)
break;
i.e. a numbered block terminates the transaction and a 0xff-marked block continues it. Consequences: (a) replies from spec-compliant BMCs are silently truncated to two parts (first block + first numbered middle block) β a functional correctness bug, not memory-unsafety, so not filed separately; (b) the hostile continuation blocks in this PoC carry 0xff and the terminator carries the expected block number β exactly what the first producer run demonstrated (the script that assumed spec semantics returned ir_replylen=58 after one block; the corrected script returns 16023).
How it was reproduced
Phase 1 β producer (verbatim transport, live kernel). The stock
QEMU ipmi-bmc-sim cannot attach to an SMBus/SSIF frontend at all (and
caps messages at 255 bytes), so the transport half is exercised by
compiling the VERBATIM /usr/src/sys/dev/misc/ipmi/ipmi_ssif.c (sha256
e4f05587774a67f75a8b980eeab9756056627605f3a1337c02e52421949996d7 on
the baseline kernel) into the ssifshock.ko harness with mock
transport headers whose only substitution is the SMBus layer: mock
bus/smbus/smbconf.h redirects smbus_bread/bwrite to a scripted
hostile BMC (protocol-legal transactions; multi-part READ_START; 515
0xff-marked continuation blocks of exactly 32 bytes; one numbered
terminating block). Every line of transport logic that runs is the
shipped kernel source, unmodified. Observed (run.log):
ssifshock: ssif_polled_request() = 1
ssifshock: emu middle blocks served = 515
ssifshock: req->ir_replylen = 16023 (BMC-controlled total, ipmi_ssif.c:304)
ssifshock: req->ir_replybuflen = 1024
BUG DF-4978 REPRODUCED: unclamped ir_replylen=16023 over 1024-byte buffer
(14999 bytes past end) -- heap OOB read at ipmi.c:448-468 copyout follows
Phase 2 β sink (real /dev/ipmi0, stock kernel, unprivileged user).
Guest booted with the QEMU IPMI simulator (KCS frontend for a real
ipmi0; SSIF cannot be emulated, so the end-state the SSIF transport
produced β ir_replylen=16023 on a completed request β is armed on the
head of the calling fd's completed queue by bumpko.ko, hot-patching
ipmi0's dev_ops.d_ioctl, then calling 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; the readout
process is unprivileged (maxx, uid 1001, groups maxx+operator). Heap
grooming identical to DF-4777 (30 same-size-class M_IPMI chunks filled
with a recognizable pattern, oldest 20 freed so the victim reuses a
chunk below live patterned chunks).
Observed, 3/3 independent runs (run.2.log, run.3.log, run.log):
RECEIVE ok: msg.data_len = 16024 (compcode + 16023 reply bytes) nonzero in [0,1024): 1 <- the BMC's real (1-byte-ish) reply nonzero in [1024,16023): 7159/7183/7190 (first at 0x042e, all runs) marker fragments beyond the victim BMC data: 426 (all runs)
Leaked content at the first foreign offset (leak_sample.txt):
0xfffff8008dcfb8cf-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, plus 426 occurrences of the
"DF4777MARK-" pattern from live neighboring kernel allocations β the
~940-nonzero-bytes-per-2048-stride census matches the allocation
geometry exactly.
Control (run.control.log): arming a legal reply length (200 < 1024)
yields data_len = 201 and zero bytes beyond the real reply β the
over-read manifests only when the transport-reported length exceeds the
buffer, i.e. exactly the hostile-SSIF condition. (An earlier control
attempt showed stale-arm contamination; the recorded control was run
with the arm sysctl explicitly reset to 0 first.)
Fix validation (mandatory: memory-corruption-class finding)
fix.diff clamps at both ends, mirroring the DF-4777/DF-4948 validated pattern:
- transport:
if (len > req->ir_replybuflen) len = req->ir_replybuflen;beforereq->ir_replylen = len;(ipmi_ssif.c:304); - sink:
if (kreq->ir_replybuflen != 0 && len > kreq->ir_replybuflen + 1) len = kreq->ir_replybuflen + 1;in ipmi.c:448 (defense in depth for every transport, present and future).
Applied to the guest's /usr/src, kernel rebuilt
(make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel,
37,637-line log in fix_build.log, kernel #1 Fri Sep 25 14:44:16 UTC
2026) and rebooted. Re-run of the exact PoC against the patched
kernel (fix_run.log):
- producer (now compiling the patched verbatim source, sha256
8cab9271...): same 515-block hostile script, transport still succeeds
(
ssif_polled_request() = 1) butir_replylen = 1024β clamped; - sink:
bumpkoarms the identical 16023 end-state on the real completed request (dmesg confirms the dev_ops patch), and the unprivileged RECEIVE now returnsmsg.data_len = 1025withnonzero in [1024,1024): 0andmarker fragments: 0; all IPMI ioctls still succeed (device functional).
Baseline vs patched:
| baseline | patched | |
|---|---|---|
| producer ir_replylen | 16023 (> 1024 buffer) | 1024 (clamped) |
| sink copyout length | 16023 bytes | 1024 bytes |
| nonzero bytes past offset 1024 | 7159 / 7183 / 7190 | 0 |
| kernel-pointer + marker disclosure | yes (0x042e+, 426 frags) | none |
| IPMI ioctls functional | yes | yes |
Impact, honestly scoped
The primitive is an info leak, not a write: uid=0 is not reachable from this bug alone. Precondition: a hostile/compromised BMC (or SMBus device at the BMC slave address) on an SSIF-type interface, plus local operator-group membership (0660 /dev/ipmi0) β the same trust boundary already accepted for DF-4777/DF-4948/DF-2025/DF-2026. Value: repeated, length-controlled disclosure of kernel heap adjacent to a 2048-class chunk (slab linkage pointers β KASLR-defeating kernel addresses, plus neighboring allocation contents). Severity Medium / bucket kernleak, consistent with the validated twins.
Aggravating note (not independently exploitable): ir_replylen is
int while len is size_t (ipmivars.h:49); after ~2^32 bytes of
accumulation (β138M blocks, chained on DF-2026's uncapped loop) the
stored count goes negative, and ipmi.c:461-468's len = min(data_len,
len) then computes len - 1 < 0, passing a huge size_t to copyout.
Fixing the clamp at :304 eliminates this truncation path as well.
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 (transport clamp at ipmi_ssif.c:304 + sink clamp at ipmi.c:448) applied to the guest /usr/src, kernel rebuilt (make -j6 nativekernel KERNCONF=X86_64_GENERIC, 37637-line fix_build.log) and booted: producer now reports ir_replylen=1024 (clamped) for the identical 515-block hostile script, and the armed 16023 end-state on the real /dev/ipmi0 yields msg.data_len=1025 with ZERO nonzero bytes beyond 1024 and zero marker fragments; all IPMI ioctls still succeed (device functional). Baseline vs patched delta is conclusive.
fix_run.log (patched-kernel producer + sink output), fix_build.log (full kernel rebuild), VERDICT.md 'Fix validation' section with baseline-vs-patched table
Confirmed kernel references
Detail
Exploit chain
hostile/compromised BMC (or SMBus device at the BMC slave address) on an SSIF interface -> multi-part read with 515 legal 32-byte 0xff continuation blocks + numbered terminator -> ir_replylen=16023 on a completed request -> unprivileged operator-group user issues IPMICTL_RECEIVE_MSG(_TRUNC) with a large data_len -> ipmi.c copyout reads 14999 bytes past the 1024-byte ir_reply heap chunk -> kernel heap disclosure (slab linkage pointers -> kernel-address info; neighboring allocation contents). Info-leak primitive only: no write, no uid=0 from this bug alone.
Evidence (decisive lines)
run.log (producer: BUG DF-4978 REPRODUCED, ir_replylen=16023/1024); run.2.log + run.3.log + run.log (sink 3/3: data_len=16024, 7159/7183/7190 nonzero past 1024, first 0x042e, 426 markers); run.control.log (legal arm: 0 bytes beyond); leak_sample.txt (0xfffff8008dcfxxxx kernel pointers at ir_reply[1070]+); fix_run.log (patched kernel: ir_replylen=1024 clamped, data_len=1025, 0 beyond, ioctls functional); fix_build.log (37637-line validated kernel build); VERDICT.md for the full narrative incl. the inverted 0xff break condition at ipmi_ssif.c:297.
PoC changes
Seed sketch (none existed -- new pass-2 finding) was authored from scratch modeled on the validated DF-4777/DF-4948 methodology: (1) mock headers live in the module dir because bsd.kmod.mk puts -I. ahead of -Idragonfly, shadowing the real bus/smbus/smbconf.h; (2) first producer run returned ir_replylen=58 because DragonFly's multi-part break condition is INVERTED (breaks on non-0xff at ipmi_ssif.c:297, unlike FreeBSD) -- the hostile script was corrected to send 0xff-marked continuation blocks terminated by one numbered block; (3) bumpko.c/leak.c reused verbatim from DF-4777; (4) control run initially raced a stale sysctl arm value -- redone with explicit disarm first.
Verified recommended fix
Clamp the stored total to the reply buffer at ipmi_ssif.c:304 and clamp again in the shared ipmi.c sink (len = min(len, ir_replybuflen + 1)) -- exactly fix.diff, live-validated on a rebuilt kernel.
Verdict
REPRODUCED and FIX VALIDATED. The verbatim SSIF transport (ipmi_ssif.c compiled unmodified into the harness) stores the BMC-accumulated multi-part reply total unclamped into req->ir_replylen (ipmi_ssif.c:304, accumulation at :294, single-part :240) -- a protocol-legal hostile BMC sending >33 full 32-byte continuation blocks yields ir_replylen=16023 over the fixed 1024-byte ioctl reply buffer. The shared stock-kernel sink (ipmi.c:448-468) then copyouts 16023 bytes from that buffer: 3/3 end-to-end runs on a real /dev/ipmi0 disclosed 7159/7183/7190 nonzero kernel-heap bytes past offset 1024 (kernel slab pointers at ir_reply[1070]+, 426 recognizable neighbor-chunk markers) to an unprivileged operator-group user; control run with a legal length leaked nothing. Two-line clamp fix (transport + sink) validated on a rebuilt kernel: producer clamps to 1024, sink returns 1025 with zero bytes beyond, IPMI device fully functional. Third transport carrying the DF-4777 (kcs) / DF-4948 (smic) shape.
No comments yet.