Mergeable-RX sub-buffer negative device length installs a negative-m_len mbuf chain into the network stack (vtnet_rxeof_merged signed clamp)
Summary
virtqueue_dequeue stores the device-controlled used-ring length through a uint32_t* while vtnet_rxeof_merged owns an int len. The clamp 'if (m->m_len < len) len = m->m_len' is a signed compare: a device length >= 0x80000000 arrives as INT_MIN, skips the clamp, and is written as m->m_len and folded into pkthdr.len. The corrupted chain is delivered to if_input BEFORE any validation - ether_input taps BPF first. Downstream m_copydata computes count as negative then bcopy's (unsigned)0x80000000 bytes -> kernel smash; bpf_mcopy walks into the sub-mbuf copying stale cluster bytes to listeners. Mergeable mode is the live config of the audit guest's own console NIC. VERIFIED: function-level replica with exact types - device len 0x80000000 -> m_len=-2147483648, pkthdr.len=-2147483622, bcopy count (unsigned)0x80000000; after-fix arm shows clamped values. NOT DF-1310 (different mode/site/primitive).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3958 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level replica of vtnet_rxeof_merged signed clamp + downstream consumers | 5.8 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o trigger trigger.c | 65 B | view raw |
| run.sh | run-script | ./trigger | 89 B | view raw |
| build.log | build-log | guest compile output (BUILD_OK) | 9 B | view raw |
| run.log | run-log | baseline-kernel decisive run: negative m_len + 2GiB bcopy count | 1.1 KB | view raw |
| run.2.log | run-log | patched-kernel re-run incl. AFTER-FIX arm | 1.1 KB | view raw |
| fix.diff | suggested-fix | unsigned clamp in vtnet_rxeof_merged (git-apply-able) | 728 B | view raw |
| fix_build.log | build-log | make nativekernel with fix applied, -Werror, rc=0 | 5.3 KB | view raw |
| fixapply2.log | apply-log | patch -p1 output, all hunks applied | 1.3 KB | view raw |
| env.txt | environment | guest uname, cc version | 213 B | view raw |
| dmesg.txt | dmesg | vtnet0 attach on virtio_pci0, 3 MSI-X vectors | 320 B | view raw |
| VERDICT.md | verdict | full narrative | 4.0 KB | β raw |
| README.md | readme | reproduce instructions | 1.9 KB | β raw |
DF-3958 β vtnet mergeable-RX sub-buffer negative device length
vtnet_rxeof_merged() clamps the device-reported sub-buffer length with a
SIGNED compare; a used-ring length >= 0x80000000 arrives as a negative int,
skips the clamp, and is stored as m->m_len β an mbuf with a negative length
plus a corrupted m_pkthdr.len is chained and handed to if_input() before
any length validation.
Reproduce (on the DragonFly guest)
./build.sh # cc -O2 -Wall -o trigger trigger.c ./run.sh # prints BEFORE-FIX corruption signature + AFTER-FIX clamp
Expected (see run.log / run.2.log): with device length 0x80000000 the
BEFORE-FIX path yields m2->m_len = -2147483648,
m_head.m_pkthdr.len = -2147483622; the m_copydata consumer replica derives
a bcopy count of (unsigned)0x80000000 bytes. AFTER-FIX (unsigned clamp)
yields m2->m_len = 2048, pkthdr.len = 2074.
Verified
- Function-level harness built and run on the audit QEMU guest
(
DragonFly 6.5-DEVELOPMENT), both on the baseline kernel and on a kernel built withfix.diff(kernel#1 Thu Sep 17 05:23:00 UTC 2026). - fix.diff applies cleanly to
/usr/src(patch -p1, all hunks), kernel builds with-Werrorrc=0 (fix_build.log), boots, and vtnet0 (the guest's own console NIC) stays UP/RUNNING through a heavy ssh session β the fixed clamp path is exercised by every packet of the session. - The guest's benign QEMU device never emits insane used-ring lengths, so the corruption itself is demonstrated at the function level (same standard as DF-1310), not by crashing the live console NIC.
Trigger prerequisite
A malicious/compromised virtio host (or virtio device passed through from a
compromised host) that negotiates VIRTIO_NET_F_MRG_RXBUF and writes
used.ring[].len >= 0x80000000 for a mergeable sub-buffer. QEMU's own
virtio-net emulation always writes sane lengths, which is why the audit
guest cannot trigger it from inside.
DF-3958 β VERDICT
REPRODUCED at the function level (impact: panic), confidence: certain.
Root cause
virtqueue_dequeue() stores the device-controlled used-ring element length
through a uint32_t *lenp (sys/dev/virtual/virtio/virtio/virtqueue.c:544-561,
store at :560-561). vtnet_rxeof_merged() owns an int len and passes
&len to it (sys/dev/virtual/virtio/net/if_vtnet.c:1642,1649). For every
mergeable sub-buffer the driver then clamps and installs that length:
if (m->m_len < len) /* if_vtnet.c:1663 β SIGNED compare */
len = m->m_len;
m->m_len = len; /* if_vtnet.c:1666 β m_len := device len */
...
m_head->m_pkthdr.len += len; /* if_vtnet.c:1669 */
m_tail->m_next = m; /* if_vtnet.c:1670 β chained */
A device length >= 0x80000000 becomes a negative int; m->m_len < len
(2048 < INT_MIN) is false, so the clamp is skipped and m->m_len is set
negative. The chain is handed to ifp->if_input() at if_vtnet.c:1777.
This is NOT covered by DF-1310 (non-mergeable first buffer, len += VTNET_RX_HEADER_PAD
wrap, NULL m_prev deref in vtnet_replace_rxbuf). Here:
- the first-buffer guards do not protect sub-buffers: the
len < hdr_size + ETHER_HDR_LENdrop (if_vtnet.c:1707) and thevtnet_replace_rxbuf()walk (if_vtnet.c:1727) run only on the FIRST buffer of the merged frame; - the negative length installs directly into a live mbuf and the packet header instead of NULLing a local.
Impact
m2->m_lennegative andm_head->m_pkthdr.lennegative (proven by the harness:run.log, sweep section).- The corrupted chain reaches consumers BEFORE any length validation:
ether_input_oncpu()taps bridge/carp/ng_ether/BPF first (sys/net/if_ethersubr.c:1212-1290). m_copydata()on such a chain computescount = min(m->m_len - off, len)β negative β(unsigned)0x80000000βbcopyof 2 GiB (sys/kern/uipc_mbuf.c:1686-1695) β kernel smash / panic.bpf_mcopy()(sys/net/bpf.c:1275-1287) walks into the sub-mbuf and copies stale cluster bytes into listener buffers (minor info exposure to a BPF listener).- Plain IP/IPv6 demux drops the frame on
m_pkthdr.len < hdrchecks, so the practical ceiling is DoS/panic plus the BPF stale-byte exposure β consistent with the Medium severity of its non-mergeable twin DF-1310.
Why function-level (not live-panicked)
The audit guest's virtio-net device is QEMU's own benign emulation; it always writes sane used-ring lengths. Crashing the guest's console NIC to prove the primitive was deemed destructive without adding evidence beyond the arithmetic β same standard used for DF-1310.
Fix validation
fix.diff(unsigned clampif ((uint32_t)len > (uint32_t)m->m_len)) applies cleanly to/usr/src(fixapply2.log).- Kernel rebuilt
make -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0,-Werror(fix_build.log); installed and booted (#1 Thu Sep 17 05:23:00 UTC 2026); vtnet0 UP/RUNNING while the whole verification session ran over it (patched_boot.txt) β regression-clean. - Harness AFTER-FIX arm shows
m2->m_len = 2048,pkthdr.len = 2074(run.2.log, on the patched kernel).
Kernel references
- sys/dev/virtual/virtio/net/if_vtnet.c:1642,1649 (int len vs uint32_t store)
- sys/dev/virtual/virtio/net/if_vtnet.c:1663-1671 (signed clamp β negative m_len)
- sys/dev/virtual/virtio/net/if_vtnet.c:1707,1727 (first-buffer-only guards)
- sys/dev/virtual/virtio/net/if_vtnet.c:1777 (if_input of corrupted chain)
- sys/dev/virtual/virtio/virtio/virtqueue.c:544-561 (device len store)
- sys/kern/uipc_mbuf.c:1686-1695 (m_copydata huge bcopy consumer)
- sys/net/bpf.c:1275-1287 (bpf_mcopy stale-byte walk)
- sys/net/if_ethersubr.c:1212-1290 (pre-validation taps)
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
fixedfix.diff (unsigned clamp) applies cleanly; kernel rebuilt with -Werror rc=0; booted into patched kernel #1 (Sep 17 05:23); vtnet0 UP/RUNNING through the entire ssh session (the fixed clamp path carries the console traffic); harness AFTER-FIX arm shows the corrupted-mbuf arithmetic is gone (m2->m_len=2048, pkthdr.len=2074). The live device cannot emit the malicious length, so patched-side 'bad behavior absent' is proven at function level + regression-free live operation.
fixapply2.log (patch -p1 ok); fix_build.log (nativekernel rc=0, -Werror); patched_boot.txt (kernel #1 booted, vtnet0 RUNNING, stats clean); run.2.log (AFTER-FIX arm on patched kernel)
Confirmed kernel references
- sys/dev/virtual/virtio/net/if_vtnet.c:1642
- sys/dev/virtual/virtio/net/if_vtnet.c:1663
- sys/dev/virtual/virtio/net/if_vtnet.c:1666
- sys/dev/virtual/virtio/net/if_vtnet.c:1669
- sys/dev/virtual/virtio/net/if_vtnet.c:1777
- sys/dev/virtual/virtio/virtio/virtqueue.c:544
- sys/kern/uipc_mbuf.c:1686
- sys/net/bpf.c:1275
- sys/net/if_ethersubr.c:1212
Detail
Exploit chain
malicious/compromised virtio host negotiates VIRTIO_NET_F_MRG_RXBUF (always offered/negotiated on the audit guest) -> device writes used.ring[].len=0x80000000 for a mergeable sub-buffer -> vtnet_rxeof_merged installs m_len=INT_MIN and negative pkthdr.len -> chain reaches bridge/carp/ng_ether/BPF taps before demux -> m_copydata-style consumers bcopy with (unsigned)0x80000000 count -> panic/kernel smash; bpf_mcopy walks into the sub-mbuf copying stale cluster bytes to listeners
Evidence (decisive lines)
run.log lines 'm2->m_len = -2147483648' and 'bcopy of 2147483648 bytes'; run.2.log AFTER-FIX 'm2->m_len=2048 ... pkthdr.len=2074' on the patched kernel; fix_build.log rc=0; fixapply2.log all hunks; VERDICT.md for the full trace
PoC changes
n/a (first-generation evidence pack; harness written to mirror the kernel statements and types exactly, including the uint32_t/int store)
Verified recommended fix
In vtnet_rxeof_merged(), clamp the device length unsigned: if ((uint32_t)len > (uint32_t)m->m_len) len = m->m_len;
Verdict
REPRODUCED at the function level on the audit guest: vtnet_rxeof_merged() clamps the device-controlled mergeable sub-buffer length with a signed compare (if_vtnet.c:1663), so a used-ring length >= 0x80000000 (stored via uint32_t* by virtqueue_dequeue, virtqueue.c:560) arrives as INT_MIN, skips the clamp, and is installed as m->m_len and folded into m_pkthdr.len; the corrupted chain is delivered to if_input (if_vtnet.c:1777) before any length validation. Downstream m_copydata computes an unsigned 2GiB bcopy count (uipc_mbuf.c:1689) => kernel smash/panic; bpf_mcopy exposes stale cluster bytes. Distinct from DF-1310 (non-mergeable first buffer NULL-deref): different mode, different site, different primitive. Live QEMU device is benign, so corruption proven by the exact-arithmetic harness (run.log), house standard of DF-1310. Fix (unsigned clamp) applied, kernel rebuilt -Werror rc=0, booted, vtnet0 (console NIC) UP/RUNNING through the whole session; harness AFTER-FIX arm shows sane chain.
No comments yet.