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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4108 Β· 4 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_encap sets arg.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 testing txstat & VGE_TDSTS_OWN from vge_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_testable
baseline no→ patch + rebuild →patched clean

One-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
↓ fix.diffper-fix-DF-4108

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).