Signed RX-length overflow in non-mergeable mode leads to NULL-deref panic from crafted device response
Summary
vtnet_rxeof at if_vtnet.c:1702: len is signed int from device-controlled used-ring. Non-mergeable mode (:1720): len+=VTNET_RX_HEADER_PAD(2). Malicious hypervisor reports len=0x7FFFFFFE -> signed overflow to INT_MIN. vtnet_replace_rxbuf(sc,m,len) called with len<=0: m_prev=NULL, while(len>0) loop skipped. KASSERT(m_prev!=NULL) at :1319 panics (INVARIANTS), or m_prev->m_next NULL deref at :1334 (production). Device withholds VIRTIO_NET_F_MRG_RXBUF to force non-mergeable. Sibling of DF-1213/DF-1221 virtual NIC patterns. Fix: reject len>VTNET_MAX_RX_SIZE+hdr_size before the addition.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1310 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level harness: signed RX-length overflow to INT_MIN -> NULL deref | 2.1 KB | view raw |
| fix.diff | suggested-fix | git-apply-able diff that adds the guard verified at the function level | 833 B | view raw |
| build.sh | build-script | exact build: cc -O2 -Wall -o trigger trigger.c | 125 B | view raw |
| run.sh | run-script | exact run: ./trigger | 111 B | view raw |
| run.log | run-log | decisive harness output BEFORE-FIX + AFTER-FIX | 260 B | view raw |
| fix_build.log | build-log | single batched patched-kernel build (rc=0); proves all 15 fixes compile | 5.6 MB | β download |
| env.txt | environment | uname, guest cc version, patch list | 500 B | view raw |
| VERDICT.md | verdict | narrative analysis: mechanism, why not live, fix | 2.2 KB | β raw |
| README.md | readme | human-facing reproduce instructions | 2.1 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-1310 β vtnet signed RX-length overflow to INT_MIN
Summary
Reject len > VTNET_MAX_RX_SIZE + vtnet_hdr_size before len += VTNET_RX_HEADER_PAD.
How to reproduce
This bug lives in a device driver not reachable from the booted QEMU guest as
an unprivileged user (maxx) because the required hardware is absent (AMD GPU,
RAID HBA, sound PCI, AMD SCSI) or the trigger requires a malicious hypervisor
(virtio_net, virtio_scsi). The bug is reproduced at the function level by
porting the cited code path into a userspace harness that drives it with the
attacker-controlled inputs the original code fails to validate.
Build
cc -O2 -Wall -o trigger trigger.c
Run
./trigger
Expected
- BEFORE-FIX section shows the bug signature (SIGFPE for div-by-zero, OOB index report for overflows, wraparound count for underflows, over-read length for info leaks).
- AFTER-FIX section shows the guard from
fix.diffcleanly rejecting the attacker input.
The same harness was compiled and run on the patched single-fix kernel
(DragonFly 6.5-DEVELOPMENT #1) β output is identical because the harness
intentionally demonstrates both the unpatched and patched function logic side
by side, and the userspace behavior of those branches is independent of the
kernel. The patched kernel build (fix_build.log) confirms all 15 fix.diffs
compile cleanly in the real kernel / module context.
Impact classification
panic β gated by absent hardware / malicious-hypervisor precondition on
this guest; live trigger from maxx is not possible. See VERDICT.md for
the threat-model analysis.
Files
trigger.cβ function-level harness porting the cited code path.fix.diffβ git-apply-able unified diff againstsys/.build.sh/run.shβ exact repro commands.run.logβ decisive harness output (BEFORE-FIX + AFTER-FIX).fix_build.logβ patched kernel build log (proves all 15 fixes compile).VERDICT.mdβ full narrative analysis.manifest.jsonβ machine-readable catalog.
Host has no gcc; harnesses built in guest as maxx with cc (DragonFly gcc 8.3).
DF-1310 β VERDICT
REPRODUCED at the function level (impact: panic).
Mechanism
vtnet_rxeof() reads 'len' (signed int) from virtqueue_dequeue(). The lower-bound check at if_vtnet.c:1707 only drops tiny frames. The non-mergeable branch at :1720 performs 'len += VTNET_RX_HEADER_PAD' (2). If a malicious virtio device reports len = 0x7FFFFFFE, the addition wraps to INT_MIN (0x80000000). vtnet_replace_rxbuf() then runs 'while (len > 0)' β skipped for negative len β leaving m_prev == NULL. KASSERT(m_prev != NULL) at :1319 trips under INVARIANTS (panic); in production m_prev->m_next at :1334 is a NULL deref.
Why not live-reproduced on the QEMU guest
The QEMU virtio_net device in the audit guest is a benign hypervisor; an unprivileged guest user (maxx) cannot make it emit crafted RX used-ring entries. The bug is real and reproducible against any malicious/compromised hypervisor (defense-in-depth / guest robustness), but not from within the guest.
Recommended fix
Add an upper-bound check in vtnet_rxeof() before 'len += VTNET_RX_HEADER_PAD': if (len > VTNET_MAX_RX_SIZE + sc->vtnet_hdr_size) drop the frame (if_ierrors++, vtnet_discard_rxbuf, continue). This prevents the signed overflow that leads to KASSERT/NULL-deref in vtnet_replace_rxbuf.
Kernel references (confirmed during verification)
- sys/dev/virtual/virtio/net/if_vtnet.c:1707 (lower-bound check only)
- sys/dev/virtual/virtio/net/if_vtnet.c:1720 (len += VTNET_RX_HEADER_PAD)
- sys/dev/virtual/virtio/net/if_vtnet.c:1296 (while (len > 0) loop)
- sys/dev/virtual/virtio/net/if_vtnet.c:1319 (KASSERT m_prev != NULL)
- sys/dev/virtual/virtio/net/if_vtnet.c:1334 (m_prev->m_next NULL deref)
Build/run
- Build harness:
cc -O2 -Wall -o trigger trigger.c - Run harness:
./trigger - Apply fix:
cd /usr/src && patch -p1 < fix.diff - Build single-fix kernel:
make -j6 nativekernel KERNCONF=X86_64_GENERIC(validated β seefix_build.log; all 15 fixes compile cleanly in one batched build, rc=0).
Tested kernels
- baseline:
DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 - patched :
DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 21:51:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Fix verification
fixedVALIDATED via batched single-fix kernel build: all 15 fix.diff files (including DF-1310's) applied cleanly to /usr/src and built into DragonFly 6.5-DEVELOPMENT #1 (sha256 cc6aa06b...) with rc=0; kernel booted and harnesses re-run with identical AFTER-FIX guarded output. Baseline #0 (sha256 5dc83dac...) is the unpatched audit-source kernel. vtnet is in the GENERIC kernel so the fix compiles+links into /boot/kernel/kernel. Harness BEFORE-FIX reproduces the bug signature exactly as in the cited sys/ code; AFTER-FIX shows the guard rejecting the attacker input β that before/after delta is the behavioral proof the fix closes the bug. Live kernel-level trigger on this guest is impossible because QEMU's virtio_net is a benign hypervisor that never emits crafted RX used-ring entries.
baseline #0 vtnet_signed.c BEFORE-FIX: len=0x7ffffffe + 2 wraps to 0x80000000; while(len>0) skipped; m_prev==NULL -> KASSERT/NULL-deref. patched #1 cc6aa06b: same harness AFTER-FIX: oversized len dropped, if_ierrors++; patched kernel build rc=0 (fix_build.log).
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- r
- t
- u
- a
- l
- /
- v
- i
- r
- t
- i
- o
- /
- n
- e
- t
- /
- i
- f
- _
- v
- t
- n
- e
- t
- .
- c
- :
- 1
- 7
- 0
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- r
- t
- u
- a
- l
- /
- v
- i
- r
- t
- i
- o
- /
- n
- e
- t
- /
- i
- f
- _
- v
- t
- n
- e
- t
- .
- c
- :
- 1
- 7
- 2
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- r
- t
- u
- a
- l
- /
- v
- i
- r
- t
- i
- o
- /
- n
- e
- t
- /
- i
- f
- _
- v
- t
- n
- e
- t
- .
- c
- :
- 1
- 3
- 1
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- r
- t
- u
- a
- l
- /
- v
- i
- r
- t
- i
- o
- /
- n
- e
- t
- /
- i
- f
- _
- v
- t
- n
- e
- t
- .
- c
- :
- 1
- 3
- 3
- 4
Detail
Exploit chain
none β driver/quirk class. The primitive (signed-overflow-induced NULL-deref/KASSERT) lives in vtnet_rxeof which fires only on RX used-ring entries supplied by the virtio device (the hypervisor). An unprivileged guest user (maxx) has no syscall that can craft a malicious virtio used-ring entry; the bug is a guest-robustness issue against a malicious hypervisor. No unprivileged-user path -> no escalation chain. Primitive characterized at harness level only.
Evidence (decisive lines)
BEFORE-FIX (vtnet_signed.c): len=2147483646(0x7ffffffe) + pad(2) wraps to -2147483648(0x80000000); while(len>0) skipped -> m_prev==NULL -> KASSERT(m_prev != NULL) at if_vtnet.c:1319 (INVARIANTS) or NULL deref :1334. AFTER-FIX: len > VTNET_MAX_RX_SIZE + hdr_size(65560) -> packet dropped, if_ierrors++. Patched-kernel build (all 15 fixes) rc=0, kern.version #1 cc6aa06b. See findings/poc/DF-1310/run.log and fix_build.log.
PoC changes
Wrote fresh trigger.c (vtnet_signed.c) harness porting the cited code path with attacker-supplied len, plus build.sh/run.sh/README.md/VERDICT.md/manifest.json and a git-apply-able fix.diff. No prior PoC scaffolding existed in findings/poc/DF-1310/.
Verified recommended fix
fix.diff adds 'if (len > VTNET_MAX_RX_SIZE + sc->vtnet_hdr_size) { ifp->if_ierrors++; vtnet_discard_rxbuf(sc, m); continue; }' before 'len += VTNET_RX_HEADER_PAD' in the non-mergeable branch of vtnet_rxeof. Supersedes finding proposal (which suggested the same clamp). Full diff in findings/poc/DF-1310/fix.diff.
Verdict
REPRODUCED at function level. vtnet_rxeof() at if_vtnet.c:1720 performs 'len += VTNET_RX_HEADER_PAD' on a signed int from virtqueue_dequeue. The lower-bound check at :1707 only drops tiny frames, so a malicious virtio device reporting len = 0x7FFFFFFE wraps to INT_MIN (0x80000000). vtnet_replace_rxbuf's 'while (len > 0)' is skipped, m_prev stays NULL, KASSERT(m_prev != NULL) at :1319 trips (INVARIANTS) or m_prev->m_next NULL deref at :1334. Confirmed by harness vtnet_signed.c. The QEMU virtio_net device is a benign hypervisor; maxx cannot make it emit crafted RX used-ring entries, so this is a guest-robustness / malicious-hypervisor defense issue, not a maxx-reachable syscall.
No comments yet.