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)
PoC verification
Evidence pack
findings/poc/DF-1381 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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).
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_listDMA 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_testablecompile+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.
No comments yet.