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.
Recommended fix
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.
Related findings
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)
PoC verification
Evidence pack
findings/poc/DF-1654 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED: 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.
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.
No comments yet.