vge TX short-frame pad path writes one fragment past vge_frag[7] when a <60-byte frame arrives with 7 DMA segments (vge_encap maxsegs off-by-one corrupts the next descriptor's vge_sts/vge_ctl)
Summary
vge_encap sets maxsegs = VGE_TX_FRAGS (7) and the callback guard admits nseg==7; the short-frame pad then writes &d->vge_frag[7] - ring bytes +64..71, fully overwriting the NEXT descriptor's vge_sts with segs[0].ds_addr and two bytes of its vge_ctl. Corrupted in-flight descriptor status can desync txeof's OWN walk (reclaim/skip errors). Reachability needs a pathological <60-byte frame spanning 7 DMA segments - contrived; Low/likely. Reserve-one-slot fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4108 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| verdict.json | β | 2.0 KB | view raw | |
| fix.diff | β | 597 B | view raw |
DF-4108 β vge(4): TX short-frame pad path writes one fragment beyond vge_frag[7] when a sub-60-byte frame arrives with 7 DMA segments (maxsegs off-by-one)
File: sys/dev/netif/vge/if_vge.c (pass 2)
Class: intra-ring OOB write past the descriptor's fragment array
(CWE-787, bounded to the ring allocation) β next descriptor's
vge_sts/vge_ctl corruption.
Hardware gate: VT612x NIC (absent on the audit guest). Reachability
additionally requires a pathological TX mbuf shape; see confidence note.
The defect
vge_encap allows the full fragment array: if_vge.c:1622
arg.vge_maxsegs = VGE_TX_FRAGS; (= 7, if_vgereg.h:584,606 β the array is
vge_frag[VGE_TX_FRAGS]).
The map callback vge_dma_map_tx_desc (if_vge.c:690-706) fills frag[0..nseg-1]
(the guard at 676 rejects only nseg > 7), then for short frames:
if (ctx->vge_m0->m_pkthdr.len < VGE_MIN_FRAMELEN) { /* < 60 bytes */
f = &d->vge_frag[i]; /* i == nseg == 7 -> frag[7] OOB */
f->vge_buflen = ...; /* offset 70-71 of the descriptor */
f->vge_addrlo = segs[0].ds_addr; /* offset 64-67 */
f->vge_addrhi = ...; /* offset 68-69 */
struct vge_tx_desc = {u32 sts; u32 ctl; frag frag[7]} = 64 bytes, so
frag[7] lands at ring offset 64..71 = the next descriptor's vge_sts
(bytes 0-3, fully overwritten with segs[0].ds_addr) and vge_ctl (bytes 4-7).
If the next descriptor is chip-owned (in flight), its status/owner
accounting is corrupted β vge_txeof may then reclaim the wrong
descriptor / leak or double-free its mbuf (m_freem on a still-pending
slot frees an mbuf the chip is still reading).
Reachability caveat (why Low / likely)
A frame under 60 bytes spanning exactly 7+ DMA segments is not producible
by the ordinary TCP/IP output path (segment headers alone exceed 60 bytes
well before 7 segments); it requires pathological mbuf chopping (e.g. via
carefully constructed sendmsg/UDP with tiny buffers hitting coincident
page boundaries, or a BPF write, which is privileged). The bounds-contract
defect itself is certain: the pad write needs nseg+1 <= VGE_TX_FRAGS but
the guard admits nseg == VGE_TX_FRAGS.
Fix
Reserve one fragment for the pad: arg.vge_maxsegs = VGE_TX_FRAGS - 1;
(matching FreeBSD's later fix) β see fix.diff.
DF-4108 VERDICT
Status: untested (hardware-gated β no VT612x NIC on the audit guest; source-level defect certain, packet-shape reachability contrived β Low/likely) Impact: 8-byte OOB write past a TX descriptor's fragment array into the next descriptor (vge_sts/vge_ctl) β descriptor-accounting corruption; latent mbuf mishandling in vge_txeof if the victim descriptor is in flight.
Trace
- Array:
struct vge_tx_desc { uint32_t vge_sts; uint32_t vge_ctl; struct vge_tx_frag vge_frag[VGE_TX_FRAGS]; }(if_vgereg.h:600-607, VGE_TX_FRAGS=7 at :584). Descriptor stride 64 bytes (8 + 7*8). - Producer:
vge_encapsetsarg.vge_maxsegs = VGE_TX_FRAGS(if_vge.c:1622). - Callback guard:
if (nseg > ctx->vge_maxsegs) { fail }(if_vge.c:676) β admits nseg == 7. - Frag fill loop:
for (i = 0; i < nseg; i++) f = &d->vge_frag[i]; ...(690-695) β consumes all 7 slots at nseg==7. - Pad path for frames < VGE_MIN_FRAMELEN (60):
f = &d->vge_frag[i];with i == nseg == 7 (697-706) β writes addrlo (desc+64..67 = next descriptor's vge_sts, full overwrite), addrhi (+68..69 β next vge_ctl[0:2]), buflen (+70..71 β next vge_ctl[6:8]). - Consumer effect:
vge_txeof(1425-1444) walks considxβprodidx testingtxstat & VGE_TDSTS_OWNfromvge_tx_list[idx].vge_stsβ a corrupted sts on an in-flight descriptor can make the driver reclaim a descriptor the chip still owns (m_freem of a pending mbuf β NIC reads freed cluster) or skip one (mbuf leak / oactive stall). - Trigger shape: pkthdr.len < 60 with exactly 7 DMA segments β not generated by ordinary stack output (needs pathological mbuf chopping; BPF writes are privileged). Hence Low severity, confidence 'likely' (defect certain, reachability contrived).
Why untested
Guest has no VIA NIC (virtio only); no TX path executes. FreeBSD's later
driver history fixed this exact contract by reserving a fragment
(maxsegs = VGE_TX_FRAGS - 1 style), corroborating the defect.
Fix
arg.vge_maxsegs = VGE_TX_FRAGS - 1; in vge_encap so the pad fragment
always has a slot (fix.diff).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableOne-line fix guarantees the pad write stays inside vge_frag[0..6]; not exercisable on the guest (no VT612x hardware).
findings/poc/DF-4108/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md: descriptor layout math (stride 64, frag[7] at +64..71), the 676 guard analysis, and the vge_txeof consumer consequence.
PoC changes
No runnable PoC (hardware absent + pathological mbuf shape needed); pack is the trace + one-line reserve fix.
Verified recommended fix
Reserve one fragment for the pad entry: arg.vge_maxsegs = VGE_TX_FRAGS - 1 (fix.diff).
Verdict
Source-level defect certain, reachability contrived: vge_encap admits nseg == VGE_TX_FRAGS (7) via maxsegs at if_vge.c:1622, and vge_dma_map_tx_desc's short-frame pad then writes frag[7] (if_vge.c:697-706) - 8 bytes past the 64-byte descriptor into the ring's next vge_tx_desc.vge_sts/vge_ctl. If that descriptor is in flight, vge_txeof's OWN-bit walk (1425-1444) can reclaim/skip it wrongly (free an mbuf the NIC still reads). Triggering needs a <60-byte frame spanning 7 DMA segments - not produced by normal stack output; hence Low. Untestable on guest (no VT612x).
No comments yet.