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

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)

No comments yet.

PoC verification

Evidence pack

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

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): i counts 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) (user data_len up to 65535), copyout(kreq->ir_reply, ..., len - 1) out of a buffer of exactly IPMI_MAX_RX = 1024 bytes 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 the kmalloc chunk, readable by any process in group operator (/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):

  1. ipmi_smic.c: clamp i to req->ir_replybuflen before storing ir_replylen (fixes the hostile-BMC path at collection time);
  2. ipmi.c: defense-in-depth β€” clamp len to ir_replybuflen + 1 in 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

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

fixed - 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Sep 25 08:44:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

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.