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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4978 Β· 24 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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 - 1 at :294 runs for every block, forever, with no relation to ir_replybuflen.
  • single-part path: len = count - 3 at :240 also ignores ir_replybuflen (bounded only by BMC-reported count).
  • 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 > 32 per-block over-read of the 32-byte stack buffer via bcopy. Here every count is exactly 32; nothing outside ssif_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:

  1. transport: if (len > req->ir_replybuflen) len = req->ir_replybuflen; before req->ir_replylen = len; (ipmi_ssif.c:304);
  2. 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) but ir_replylen = 1024 β€” clamped;
  • sink: bumpko arms the identical 16023 end-state on the real completed request (dmesg confirms the dev_ops patch), and the unprivileged RECEIVE now returns msg.data_len = 1025 with nonzero in [1024,1024): 0 and marker 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.