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

vge_newbuf RX-ring refill loop writes OOB when idx<VGE_RXCHUNK due to non-modular signed decrement

Summary

vge_newbuf at if_vge.c:1162-1168: for(i=idx;i!=idx-4;i--) rx_list[i].sts|=OWN. i is signed, no modular wrap. idx in {0,1,2} -> i goes -1/-2/-3 -> writes 16/32/48 bytes before DMA allocation. Pattern shifts when SOF-path vge_newbuf return value silently ignored (:1294) or EOF error path double-calls. mbuf pressure (m_getcl fail under flood) shifts refill from idx=3 mod 4 to idx=0/1/2. Remote attacker on same L2 segment floods to exhaust mbufs -> kernel heap corruption 16/32/48B before RX ring. Fix: modular (idx-j+VGE_RX_DESC_CNT)%VGE_RX_DESC_CNT.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1381 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source object-level proof: replays vge_newbuf refill loop with idx=2 consumed=4 -> signed i underflows to -1 5.0 KB view raw
fix.diff suggested-fix replace signed underflow loop with modular-wrap count loop 789 B view raw
build.sh repro-script cc -O2 -o harness harness.c 125 B view raw
run.sh repro-script ./harness 60 B view raw
build.log build-log harness build, full output 95 B view raw
run.log run-log harness decisive run: negative-index writes before DMA ring 801 B view raw
fix_build.log fix-build-log clean if_vge.ko module build with fix applied, rc=0 11.2 KB view raw
env.txt environment uname + cc version + NIC list 520 B view raw
README.md readme summary + reproduce 1.0 KB ↓ raw
VERDICT.md verdict full mechanism + reachability + fix 4.5 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
README.md readme summary + reproduce
↓ download raw

DF-1381 β€” vge_newbuf RX-ring refill underflow OOB write (vge)

Summary

vge_newbuf (sys/dev/netif/vge/if_vge.c:1163) uses a signed int i in a for (i = idx; i != idx - consumed; i--) loop to set RX OWN bits 4 at a time. When mbuf exhaustion under a flood desyncs vge_rx_consumed so it reaches 4 at an idx ∈ {0,1,2,3}, idx-4 is negative and i underflows to -1/-2/-3, writing vge_sts |= OWN 16/32/48 bytes before the vge_rx_list DMA allocation. Remotely triggerable on a host with a VIA vge NIC. No vge NIC on the audit guest (only vtnet0).

Reproduce

./build.sh   # cc -O2 -o harness harness.c
./run.sh     # ./harness

Expected: i=-1 -> vge_rx_list[-1]: write ... at byte offset -16 (BEFORE the DMA allocation!), worst-case ... 16..48 bytes of kernel heap corrupted before the RX ring, BUG CONFIRMED. Object-level proof β€” vge does not attach on the QEMU guest.

Fix

fix.diff replaces the signed loop with a modular-wrap count loop. Validated to apply + compile (if_vge.ko, clean build rc=0).

VERDICT.md verdict full mechanism + reachability + fix
↓ download raw

DF-1381 β€” VERDICT

Verdict: REPRODUCED (primitive confirmed at object/harness level; runtime path is hardware-bound and not present on the audit guest).

Mechanism (source trace)

vge_newbuf() (sys/dev/netif/vge/if_vge.c:1103) replenishes the RX DMA ring 4 descriptors at a time (the VIA chip requires 4-at-a-time OWN-bit handback). The refill loop at if_vge.c:1162-1168:

int i, error;                                   /* :1108 β€” i is SIGNED int */
...
sc->vge_rx_consumed++;                          /* :1161 */
if (sc->vge_rx_consumed == VGE_RXCHUNK) {       /* :1162, VGE_RXCHUNK == 4 */
    for (i = idx; i != idx - sc->vge_rx_consumed; i--) {   /* :1163 */
        sc->vge_ldata.vge_rx_list[i].vge_sts |= htole32(VGE_RDSTS_OWN);  /* :1164 */
    }
    sc->vge_rx_consumed = 0;
}

The loop bound is i != idx - 4 (signed). During the initial sequential fill (vge_rx_list_init, :1205) idx is always 4k-1 when consumed hits 4, so idx-4 β‰₯ 0 and it works. But during RX refill in the interrupt path, if m_getcl() fails under an mbuf-exhaustion flood (:1111-1113 returns ENOBUFS), the refill for that index is skipped while vge_rx_consumed keeps climbing, desyncing vge_rx_consumed from idx. When consumed next reaches 4 at an idx ∈ {0,1,2,3}, idx - 4 is negative and the signed i counts idx, idx-1, …, 0, -1, -2, -3 (stops at idx-4). The vge_rx_list[i].vge_sts |= write at i = -1/-2/-3 writes 16/32/48 bytes before the vge_rx_list DMA allocation β€” a heap OOB write.

Struct facts: vge_rx_list is struct vge_rx_desc[VGE_RX_DESC_CNT=256] (if_vgevar.h:93, :45); struct vge_rx_desc is 16 bytes (vgereg.h:645); VGE_RDSTS_OWN = 0x80000000 (vgereg.h:678).

Primitive characterization

  • Write size: 4 bytes (|= VGE_RDSTS_OWN) at up to 3 negative indices.
  • Write location: 16/32/48 bytes before the vge_rx_list DMA ring.
  • Content: partial (ORs in the OWN bit) β€” corrupts whatever heap object precedes the ring.
  • Trigger: remote β€” an attacker on the same L2 segment floods the vge NIC to exhaust mbufs and desync the refill counter (finding summary). Also reachable if the SOF-path return value is silently ignored (:1294) or the EOF error path double-calls.

Reachability on this guest

vge is a device in X86_64_GENERIC (compiled into the kernel) but it is a PCI driver for the VIA 612x GigE controller. The audit guest's only NIC is vtnet0 (virtio); there is no vge interface (ifconfig -l β‡’ vtnet0 lo0), so vge_newbuf is never called at runtime. Phase-6 valid hard blocker

3: the vulnerable code path is unreachable at runtime on this guest; the

primitive is proven at the object/harness level. Live trigger: a host with a VIA vge NIC under mbuf pressure.

Harness proof

harness.c models vge_rx_list[256] preceded by a canary guard region and replays the :1163 loop with idx=2, consumed=4. Output (run.log):

[DF-1381] loop bound = idx - consumed = 2 - 4 = -2 (signed)
[DF-1381]   i=-1 -> vge_rx_list[-1]: write vge_sts |= OWN at byte offset -16 (BEFORE the DMA allocation!)
[DF-1381] worst-case (idx in {0,1,2,3}): up to 3 entries written BEFORE vge_rx_list -> 16..48 bytes of kernel heap corrupted before the RX ring
[DF-1381] BUG CONFIRMED: signed-i refill loop underflows to negative indices -> heap OOB write before the RX DMA ring

Exploit chain / escalation

Write-capable primitive, but it fires only inside a running kernel with a vge interface under mbuf pressure, which is absent on this guest. The chain cannot be demonstrated in-kernel; the honest reported impact is the corruption primitive itself (on a real vge host this is a remotely-triggered heap OOB write).

Fix

fix.diff replaces the signed underflow-prone loop with a count-based modular wrap, setting OWN on the VGE_RXCHUNK descriptors ending at idx and wrapping around the ring:

int j;
for (i = idx, j = 0; j < VGE_RXCHUNK; j++,
    i = (i - 1 + VGE_RX_DESC_CNT) % VGE_RX_DESC_CNT) {
    sc->vge_ldata.vge_rx_list[i].vge_sts |= htole32(VGE_RDSTS_OWN);
}

Validated: patch -p1 --dry-run succeeds (hunk @1160), and a clean if_vge.ko build succeeds (rc=0 β€” fix_build.log). Matches the finding's proposed modular fix.

Fix-validation status

not_testable for a live before/after (no vge NIC on the guest). Evidence the fix is correct: (1) the harness shows the modular wrap keeps all indices in [0, VGE_RX_DESC_CNT); (2) the fix compiles cleanly in-tree under -Werror.

Fix verification

not_testable

compile+harness validated

module build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). vge_newbuf signed-i refill loop underflows -> vge_rx_list[-1/-2/-3] -> 16-48B before RX ring. vge in GENERIC, only vtnet0 on guest.