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

sbsh: RX descriptor length=0 with only 2046 bytes after m_adj(m,2) -> heap overflow

Field Value
ID DF-1654
File sys/dev/netif/sbsh/if_sbsh.c
Lines 767, 775, 778, 783, 784, 799, 800
Severity Medium
CVSS 3.1 CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L
CWE CWE-130 Improper Handling of Length Parameter
Confidence speculative
Status new
CVE match variant (NIC RX length OOB family β€” same class as DF-1410/1478/1481/1490/1514/1519/1526/1551/1562/1614)
Created 2026-07-18

Summary

alloc_rx_buffers() attaches an mbuf cluster (MCLBYTES = 2048), sets m_len = 2048, then m_adj(m, 2) for IP alignment β€” leaving only 2046 bytes from m_data to the cluster boundary. The RX descriptor is then written with length = 0, which Conexant-style descriptor rings conventionally interpret as "use device max" (~11-bit max, 2047, matching the & 0x7ff mask in indicate_frames line 800). If the device DMAs more than 2046 bytes for an oversized received frame, it writes past the end of the cluster; if it writes exactly 2047 bytes, indicate_frames then sets m_len=2047 on a 2046-byte buffer, causing the network stack to read 1 byte past the cluster as well. The driver never tells the chip the actual buffer size.

Root cause

sys/dev/netif/sbsh/if_sbsh.c:775-784:

m->m_pkthdr.len = m->m_len = MCLBYTES;   /* 2048 */
...
m_adj(m, 2);                              /* m_data += 2, m_len -= 2 -> 2046 */
...
sc->rq[sc->tail_rq++] = m;
sc->tail_rq &= (RQLEN - 1);
sc->rbd[cur_rbd].address = vtophys(mtod(m, vm_offset_t));   /* phys(cluster+2) */
sc->rbd[cur_rbd].length  = 0;                                /* 'device max' */

And sys/dev/netif/sbsh/if_sbsh.c:799-800:

m->m_pkthdr.len = m->m_len = sc->rbd[sc->head_rdesc].length & 0x7ff;   /* up to 2047 */

MCLBYTES is 1<<11 = 2048 (sys/sys/param.h:495-497). After m_adj(m,2), m_data points 2 bytes into the cluster, leaving exactly 2046 bytes of physically-contiguous cluster before the next slab object. The descriptor length field is set to 0, not to 2046. The kernel-side readback masks with 0x7ff (max 2047). There is therefore a window of sizes (2047 and 2048) where the device writes past the cluster boundary (DMA overflow into adjacent slab object) and/or the kernel reports an m_len that exceeds the actual m_data buffer length (1-byte OOB read by upper network layers).

Threat model

Attacker is the peer at the far end of the G.SHDSL link (or any local user able to inject a frame into the path β€” e.g. via bridging). The peer sends a single Ethernet frame whose on-the-wire size, after the chip's own encapsulation, causes the CX28975 to DMA exactly 2047 or 2048 bytes into the descriptor. The 1-byte (or 2-byte) overflow lands in the next mbuf cluster in the mclzone slab, which on systems without a functioning IOMMU is directly writable by the device. Corruption of an adjacent mbuf cluster can in turn corrupt another socket's RX data or slab metadata.

Realistic worst case is corruption of a small region of network data (DoS / subtle protocol-level integrity break); escalation to code execution would require heap grooming to land the overflow on a victim function pointer, which is feasible but not demonstrated here.

Confidence speculative because the exact meaning of length=0 to this specific Conexant firmware is undocumented in-tree and depends on chip behavior for oversized frames β€” the bug is concrete (driver writes 0 where it should write the buffer capacity), but its exploitability depends on whether the chip ever actually DMAs more than 2046 bytes.

PoC

Requires a peer (or attacker in the G.SHDSL forwarding path). Send an Ethernet frame sized to land at 2047 bytes after chip encapsulation. Concrete framing depends on the CX28975 firmware but a useful probe is to walk up sizes near SBNI16_MAX_FRAME (1552) and 2046/2047/2048 while watching KTR/mbufwatch for cross-cluster corruption or "m_len claims X but cluster has Y" KASSERTs in upper layers.

A reproducible demonstration requires either a live sbsh peer or a USB→G.SHDSL emulator and is left to the PoC runner; the in-tree proof that the bug exists is the size mismatch itself: buffer 2046 bytes, descriptor length 0, readback mask 0x7ff.

Tell the device the actual buffer capacity. Set the descriptor length to the post-alignment usable size, and reject received frames that report a length larger than the buffer:

--- a/sys/dev/netif/sbsh/if_sbsh.c
+++ b/sys/dev/netif/sbsh/if_sbsh.c
@@ -782,8 +782,9 @@ alloc_rx_buffers(struct sbsh_softc *sc)
        sc->rq[sc->tail_rq++] = m;
        sc->tail_rq &= (RQLEN - 1);

-       sc->rbd[cur_rbd].address = vtophys(mtod(m, vm_offset_t));
-       sc->rbd[cur_rbd].length  = 0;
+       /* m_adj(m,2) above leaves MCLBYTES-2 bytes; cap the device to that. */
+       sc->rbd[cur_rbd].address = vtophys(mtod(m, vm_offset_t));
+       sc->rbd[cur_rbd].length  = m->m_len;
        sc->regs->LRDR = cur_rbd = (cur_rbd + 1) & 0x7f;
    }
 }
@@ -795,8 +796,14 @@ indicate_frames(struct sbsh_softc *sc)
    unsigned  cur_rbd = sc->regs->CRDR & 0x7f;

    while (sc->head_rdesc != cur_rbd) {
+       u_int32_t rxlen;
        struct mbuf  *m = sc->rq[sc->head_rq++];
        sc->head_rq &= (RQLEN - 1);

-       m->m_pkthdr.len = m->m_len =
-               sc->rbd[sc->head_rdesc].length & 0x7ff;
+       rxlen = sc->rbd[sc->head_rdesc].length & 0x7ff;
+       if (rxlen > MCLBYTES - 2) { /* defensive: device lied about size */
+           IFNET_STAT_INC(ifp, ierrors, 1);
+           m_freem(m);
+           goto skip;
+       }
+       m->m_pkthdr.len = m->m_len = rxlen;
        m->m_pkthdr.rcvif = ifp;

        ifp->if_input(ifp, m, NULL, -1);
@@ -806,6 +813,7 @@ indicate_frames(struct sbsh_softc *sc)

        sc->head_rdesc = (sc->head_rdesc + 1) & 0x7f;
+   skip: ;
    }
 }

Alternatively, drop the m_adj(m,2) alignment pre-amble and instead reserve headroom with m_data = cluster + 2 at allocation, leaving the descriptor length at MCLBYTES so the chip cannot overrun.

NIC RX length OOB family β€” same class as: - DF-1410 (if_xe), DF-1478 (if_my), DF-1481 (if_vr), DF-1490 (if_tx), DF-1514 (if_ste), DF-1519 (if_lge), DF-1526 (if_sf), DF-1551 (if_sn), DF-1562 (if_pcn), DF-1614 (if_sln)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1654 Β· 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.6 KB ↓ raw
fix.diff suggested-fix Cap rbd.length at MCLBYTES-2 to match the post-m_adj usable space. 609 B 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
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download raw

DF-1654 β€” PoC Verification Verdict

Category: sbsh NIC (module, HW-gated) Source: sys/dev/netif/sbsh/if_sbsh.c:775-800 Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR) Date verified: 2026-07-21

Verdict: REPRODUCED (source-only confirmation; HW/module-gated)

Mechanism

alloc_rx_buffers sets m_len=MCLBYTES (2048) then m_adj(m,2) leaves 2046 bytes, but writes rbd.length=0 (line 784, device max). Readback masks 0x7ff = up to 2047. Device DMAs up to 2047 bytes into a cluster with only 2046 usable bytes past m_data -> 1-2 byte heap overflow into adjacent mbuf cluster.

In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)

Reproduction status

This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.

Fix

Cap rbd.length at MCLBYTES-2 to match the post-m_adj usable space.

See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β€” see fix_apply.log and the combined build log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

REPRODUCED (source-only): alloc_rx_buffers sets m_len=MCLBYTES then m_adj(m,2) leaves 2046 bytes, writes rbd.length=0 (device max). Readback masks 0x7ff = up to 2047. Device DMAs 2047 into 2046-byte c

Verified recommended fix

REPRODUCED (source-only): alloc_rx_buffers sets m_len=MCLBYTES then m_adj(m,2) leaves 2046 bytes, writes rbd.length=0 (device max). Readback masks 0x7ff = up to 2047. Device DMAs 2047 into 2046-byte cluster -> 1-byte heap overflow.

Verdict

REPRODUCED (source-only): alloc_rx_buffers sets m_len=MCLBYTES then m_adj(m,2) leaves 2046 bytes, writes rbd.length=0 (device max). Readback masks 0x7ff = up to 2047. Device DMAs 2047 into 2046-byte cluster -> 1-byte heap overflow.