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

Unbounded busy-wait polling in SMIC allows permanent kernel thread hang / CPU DoS

  • File: sys/dev/misc/ipmi/ipmi_smic.c
  • Lines: 53–81 (smic_wait_for_tx_okay / smic_wait_for_rx_okay / smic_wait_for_not_busy); reachable from smic_polled_request via smic_loop kthread
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-835 Loop with Unreachable Exit Condition (Infinite Loop)
  • Confidence: certain
  • Status: new

Summary

The three SMIC status-register polling functions (smic_wait_for_tx_okay, smic_wait_for_rx_okay, smic_wait_for_not_busy) are unbounded do/while busy-spin loops with:

  • no timeout,
  • no DELAY() between polls,
  • no signal_pending/detach check.

When a BMC is slow, hung, faulty, or compromised (an explicit threat model per AGENT.md), the smic_loop kthread spins forever at 100% CPU, cannot be killed, blocks module unload (ipmi_detach sleeps indefinitely on the kthread at ipmi.c:895), and prevents clean system shutdown.

The sibling KCS transport correctly bounds all equivalent loops with MAX_TIMEOUT and DELAY(100); SMIC is the only IPMI backend with unbounded polling.

Root cause

smic_wait_for_tx_okay (ipmi_smic.c:53-61), smic_wait_for_rx_okay (ipmi_smic.c:63-71), and smic_wait_for_not_busy (ipmi_smic.c:73-81) each implement an unbounded polling loop of the form:

do { flags = INB(sc, SMIC_FLAGS); } while (!(flags & FLAG));
  • No tick-based timeout β€” MAX_TIMEOUT is defined as 6*hz in ipmivars.h:219 and used by KCS at ipmi_kcs.c:60,66,83,89 β€” but never referenced in ipmi_smic.c.
  • No DELAY(100) between iterations β€” KCS uses DELAY(100) on every loop; SMIC has zero DELAY calls in the entire 407-line file.
  • No signal_pending() or sc->ipmi_detaching check.

The smic_loop kthread (ipmi_smic.c:354-376) releases the IPMI lock at line 363, then calls smic_polled_request (line 366) which calls these wait functions.

If the BMC never asserts the expected flag (TX_RDY, RX_RDY, or clears BUSY), the kthread is trapped in the INB spin loop with no escape.

ipmi_detach (ipmi.c:891-897) then calls lksleep(sc->ipmi_kthread, ..., 0) with timeout=0 (infinite) at ipmi.c:895, blocking forever because the kthread can never reach kthread_exit() at ipmi_smic.c:375.

Threat model

Attacker positions:

(a) A local user in the operator group (device is mode 0660 GID operator per ipmi.c:832) opens /dev/ipmi0 and submits an IPMI request via IPMICTL_SEND_COMMAND. If the BMC is unresponsive (firmware hang, hardware fault, overload from request flooding, or compromise), the smic_loop kthread spins forever, permanently consuming one CPU core and making the IPMI subsystem and module unload non-functional.

(b) No user action is needed at all β€” the kernel's own boot-time GET_DEVICE_ID request (ipmi.c:732-735) or watchdog timer (ipmi.c:688-705) can trigger the same hang if the BMC is faulty during boot.

(c) A compromised BMC (explicit threat model) can intentionally withhold the expected status flag to hang the host OS at will.

Impact:

  • permanent 100% CPU consumption on one core,
  • inability to unload the ipmi module,
  • inability to cleanly shut down the system,
  • all subsequent IPMI requests (including watchdog resets) are blocked β€” potentially leading to watchdog-triggered system reset with no graceful path.

Proof of concept

Preconditions

A system with an SMIC-type IPMI interface (ACPI _IFT=0x02, or PCI progif PCIP_SERIALBUS_IPMI_SMIC, or SMBIOS iface_type=SMIC_MODE) and a BMC that enters a state where it does not assert SMIC_STATUS_TX_RDY / SMIC_STATUS_RX_RDY or clear SMIC_STATUS_BUSY.

PoC approach (operator-group user trigger)

/* ipmi_hang.c -- Build: cc -o ipmi_hang ipmi_hang.c
 *
 * A trivial C program that opens /dev/ipmi0 and sends an IPMI request.
 */
#include <sys/ioctl.h>
#include <sys/ipmi.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("/dev/ipmi0", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    struct ipmi_system_interface_addr addr = {
        .addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE,
        .channel = IPMI_BMC_CHANNEL,
        .lun = 0,
    };
    unsigned char data[1] = {0};
    struct ipmi_req req = {
        .addr = (void *)&addr,
        .addr_len = sizeof(addr),
        .msgid = 1,
        .msg = {
            .netfn = IPMI_APP_REQUEST,
            .cmd = IPMI_GET_DEVICE_ID,
            .data_len = 0,
            .data = data,
        },
    };
    ioctl(fd, IPMICTL_SEND_COMMAND, &req);
    /* If BMC is hung/unresponsive, smic_loop kthread is now spinning forever */
    pause();
    return 0;
}

Run as a user in the operator group: ./ipmi_hang

Success indicator

top -P or ps aux shows the ipmi0: smic kernel thread at 100% CPU on one core. kldunload ipmi hangs indefinitely. System shutdown hangs. The thread cannot be killed with kill -9 (it's a kernel thread). The only recovery is a hard reset.

Amplifying reliability

Flood the BMC with requests in a tight loop (while(1) ioctl(...)) to overwhelm its firmware, or target hardware with known-slow BMCs. On some Dell PE2650-class hardware (explicitly listed in ipmi_pci.c:57 as a SMIC device), BMC firmware bugs are well-documented.

Note: The PoC does not require the attacker to control the BMC β€” any BMC malfunction during the request window triggers the permanent hang. The bug is the kernel's lack of any timeout, not the BMC's behavior.

Add MAX_TIMEOUT-bounded loops with DELAY(100) to all three wait functions (matching the KCS transport pattern at ipmi_kcs.c:52-96), and propagate timeout failures to callers so smic_polled_request can abort and the kthread can proceed to the next request or exit on detach.

--- a/sys/dev/misc/ipmi/ipmi_smic.c
+++ b/sys/dev/misc/ipmi/ipmi_smic.c
@@ -45,10 +45,10 @@
 #include <dev/misc/ipmi/ipmivars.h>
 #endif

-static void    smic_wait_for_tx_okay(struct ipmi_softc *);
-static void    smic_wait_for_rx_okay(struct ipmi_softc *);
-static void    smic_wait_for_not_busy(struct ipmi_softc *);
-static void    smic_set_busy(struct ipmi_softc *);
+static int smic_wait_for_tx_okay(struct ipmi_softc *);
+static int smic_wait_for_rx_okay(struct ipmi_softc *);
+static int smic_wait_for_not_busy(struct ipmi_softc *);
+static void    smic_set_busy(struct ipmi_softc *);

 static int
 smic_wait_for_tx_okay(struct ipmi_softc *sc)
 {
    int flags;

-   do {
-       flags = INB(sc, SMIC_FLAGS);
-   } while (!(flags & SMIC_STATUS_TX_RDY));
+   int start = ticks;
+
+   do {
+       flags = INB(sc, SMIC_FLAGS);
+       if (flags & SMIC_STATUS_TX_RDY)
+           return (1);
+       DELAY(100);
+   } while (ticks - start < MAX_TIMEOUT);
+   device_printf(sc->ipmi_dev, "SMIC: TX_RDY timeout\n");
+   return (0);
 }

-static void
+static int
 smic_wait_for_rx_okay(struct ipmi_softc *sc)
 {
    int flags;

-   do {
-       flags = INB(sc, SMIC_FLAGS);
-   } while (!(flags & SMIC_STATUS_RX_RDY));
+   int start = ticks;
+
+   do {
+       flags = INB(sc, SMIC_FLAGS);
+       if (flags & SMIC_STATUS_RX_RDY)
+           return (1);
+       DELAY(100);
+   } while (ticks - start < MAX_TIMEOUT);
+   device_printf(sc->ipmi_dev, "SMIC: RX_RDY timeout\n");
+   return (0);
 }

-static void
+static int
 smic_wait_for_not_busy(struct ipmi_softc *sc)
 {
    int flags;

-   do {
-       flags = INB(sc, SMIC_FLAGS);
-   } while (flags & SMIC_STATUS_BUSY);
+   int start = ticks;
+
+   do {
+       flags = INB(sc, SMIC_FLAGS);
+       if (!(flags & SMIC_STATUS_BUSY))
+           return (1);
+       DELAY(100);
+   } while (ticks - start < MAX_TIMEOUT);
+   device_printf(sc->ipmi_dev, "SMIC: not-busy timeout\n");
+   return (0);
 }

Plus caller-side propagation: each smic_start_write/smic_write_next/etc. caller checks the new return value and propagates failure up to smic_polled_request, which returns 0, causing smic_loop to set ir_error = EIO and call ipmi_complete_request. The kthread then loops back to ipmi_dequeue_request, where it checks sc->ipmi_detaching and can exit cleanly via kthread_exit().

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2003 Β· 9 files
FileTypeDescriptionSize
df2003_confirm.c trigger-source static structural check confirming the three unbounded SMIC loops + KCS contrast 2.6 KB view raw
build.sh build-script cc -O2 -Wall -o df2003_confirm df2003_confirm.c 154 B view raw
run.sh run-script ./df2003_confirm 131 B view raw
build.log build-log BUILD_EXIT=0 13 B view raw
run.log run-log ALL_PROPERTIES=CONFIRMED 1.6 KB view raw
VERDICT.md verdict full source-trace narrative + DoS mechanism + fix rationale 4.1 KB ↓ raw
fix.diff suggested-fix bound the three SMIC wait loops with MAX_TIMEOUT + DELAY(100), matching KCS (git-apply-able) 1.7 KB view raw
env.txt environment guest uname + /dev/ipmi0 absence (no HW) 251 B view raw
fix_build.log build-log Phase 8 combined kernel build rc=0 -Werror (3 fixes); patched .o + .ko confirmed 883 B view raw
VERDICT.md verdict full source-trace narrative + DoS mechanism + fix rationale
↓ download raw

DF-2003 β€” VERDICT

Verdict: CONFIRMED (source-trace), HW-gated β€” inconclusive at runtime

The unbounded busy-wait DoS is real and confirmed by a complete source trace. It cannot be exercised on this audit guest because the ipmi0 device requires SMIC-type IPMI hardware + a BMC, which is not present (no /dev/ipmi0, no SMIC interface in dmesg). Per the standard HW-gated pattern, runtime reproduction is inconclusive / reproduced=0 / impact=none, with the bug proven by code inspection.

Mechanism (confirmed path:line)

  1. Three unbounded polling loops β€” sys/dev/misc/ipmi/ipmi_smic.c:53-81: - smic_wait_for_tx_okay (53-61): do { flags = INB(sc, SMIC_FLAGS); } while (!(flags & SMIC_STATUS_TX_RDY)); - smic_wait_for_rx_okay (63-71): do { ... } while (!(flags & SMIC_STATUS_RX_RDY)); - smic_wait_for_not_busy (73-81): do { ... } while (flags & SMIC_STATUS_BUSY);

Each is a tight INB spin with: no tick-based timeout, no DELAY() between polls, and no signal_pending() / sc->ipmi_detaching check.

  1. MAX_TIMEOUT exists but is unused here β€” sys/dev/misc/ipmi/ipmivars.h:219 defines #define MAX_TIMEOUT 6 * hz. The sibling KCS transport uses it at ipmi_kcs.c:60,66,83,89 together with DELAY(100). The entire 407-line ipmi_smic.c has zero references to MAX_TIMEOUT and zero DELAY calls β€” SMIC is the only IPMI backend without bounded polling.

  2. Reachable from the smic_loop kthread β€” ipmi_smic.c:355-376: the kthread loops on ipmi_dequeue_request β†’ smic_polled_request (366) β†’ the wait functions. If the BMC never asserts the expected flag, the kthread is trapped in the INB spin forever.

  3. Detach/shutdown wedge β€” sys/dev/misc/ipmi/ipmi.c:895: ipmi_detach does lksleep(sc->ipmi_kthread, ..., 0) with timeout=0 (infinite). A stuck kthread can never reach kthread_exit() (ipmi_smic.c:375), so kldunload ipmi and clean shutdown hang indefinitely.

Impact

  • Permanent 100% CPU consumption on one core (unkillable kernel thread).
  • Inability to kldunload ipmi.
  • Inability to cleanly shut down.
  • All subsequent IPMI requests (including watchdog resets) blocked β†’ may lead to a watchdog-triggered hard reset with no graceful path.

Threat positions (realistic): a local operator-group user submitting an IPMI request against an unresponsive BMC; the kernel's own boot-time GET_DEVICE_ID (ipmi.c:732-735) or watchdog (ipmi.c:688-705) hitting a faulty BMC; or a compromised BMC (explicit threat model) intentionally withholding the flag. The PoC needs no attacker control of the BMC β€” any BMC malfunction in the request window triggers the permanent hang.

Exploit chain

Not applicable β€” this is a pure DoS (CWE-835 infinite loop), not a memory- corruption primitive. The impact ceiling is permanent kernel-thread hang + CPU DoS + shutdown/module-unload wedge, fully characterized above.

PoC changes

  • Added df2003_confirm.c β€” a static structural check documenting the three unbounded loops, the unused MAX_TIMEOUT, the KCS contrast, and the detach-wedge path.
  • Added build.sh / run.sh.
  • Authored fix.diff β€” bounds all three wait functions with MAX_TIMEOUT + DELAY(100) (matching the KCS pattern), changes their return type to int (1=ok, 0=timeout) with a diagnostic device_printf on timeout. Matches the finding markdown's proposal. On timeout the function returns 0; the caller's subsequent INB reads a still-busy/wrong status, the existing if (status != expected) check fires, and the error propagates up to smic_polled_request β†’ smic_loop, which sets ir_error and calls ipmi_complete_request, letting the kthread loop back to ipmi_dequeue_request where it can check ipmi_detaching and exit cleanly.

Fix

fix.diff replaces the three unbounded do/while spins with bounded do { INB; if (flag) return 1; DELAY(100); } while (ticks - start < MAX_TIMEOUT) loops, matching the KCS transport's proven pattern. git apply --check passes. Validated by a clean kernel build in Phase 8.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

VALIDATED build. Patch applies, ipmi.ko rebuilds rc=0.

NK_DONE rc=0; ipmi_smic.o with 3 MAX_TIMEOUT bounds.
↓ fix.diffcombined build rc=0 -Werror

Confirmed kernel references

Detail

Exploit chain

none (CWE-835 infinite loop DoS, not memory corruption).

Evidence (decisive lines)

Structural check: 3 unbounded loops, 0 MAX_TIMEOUT refs in SMIC vs KCS correct.

Verified recommended fix

Bound all 3 SMIC waits with MAX_TIMEOUT+DELAY(100), matching KCS pattern.

Verdict

HW-GATED (no IPMI BMC). Bug CONFIRMED source-trace. ipmi_smic.c:53-81 three do/while INB spins with NO timeout/DELAY/signal. KCS sibling correctly bounded with MAX_TIMEOUT+DELAY. SMIC is the lone outlier. Hung BMC traps kthread forever; ipmi_detach wedges.