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

vmxnet3_rxq_eof() selects UNPOPULATED RX ring 1 from host rxcd->qid -> NULL-mbuf write -> guest kernel panic

Summary

The ring used for a completion is chosen by the host-supplied 10-bit rxcd->qid; DragonFly always runs with nrxqueues=1 (MSI-X unsupported) and leaves ring 1 unpopulated (all vrxb_m==NULL) on every default MTU config. One hostile descriptor {qid>=1, in-bounds rxd_idx, sop=1} yields m==NULL and the sop path writes m->m_pkthdr.rcvif/len through it - supervisor write at fixed near-NULL (0x18 observed), fatal trap 12. KASSERT guards compiled out on production kernels. Distinct from DF-1213 (in-bounds idx here) and DF-1214 (no cluster confusion). REPRODUCED ON THE GUEST KERNEL via a protocol-replica KLD injecting the hostile descriptor as the host would DMA it: 'Fatal trap 12, fault virtual address = 0x18, supervisor write, Stopped at vmxrep_sysctl+0x252: movl $0x40,0x18(%rbx)' (0x40 == hostile len); reproduced twice incl. clean rerun. Fix validated in-replica (no crash on same input). Malicious/compromised ESXi host kills a DragonFly guest with a single forged RX completion; driver compiled into GENERIC.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3779 Β· 10 files
FileTypeDescriptionSize
vmxrep.c β€” 17.5 KB view raw
Makefile β€” 73 B ↓ download
build.sh β€” 98 B view raw
run.sh β€” 129 B view raw
build.log β€” 5.4 KB view raw
panic.txt β€” 827 B view raw
fixrun.log β€” 548 B view raw
env.txt β€” 201 B view raw
fix.diff β€” 1.2 KB view raw
VERDICT.md β€” 3.8 KB ↓ raw
VERDICT.md
↓ download raw

DF-3779 VERDICT β€” REPRODUCED (mechanism, protocol-replica) β€” impact: panic

Bottom line

A single hostile RX completion descriptor with an in-bounds rxd_idx, sop=1, and qid >= nrxqueues makes vmxnet3_rxq_eof() select RX ring 1, which the driver leaves unpopulated (all vrxb_m == NULL) on every default configuration. The local m fetched at if_vmx.c:2223 stays NULL through the sop path and if_vmx.c:2265-2266 write through it β†’ supervisor write to a fixed near-NULL address (0x18 observed) β†’ fatal trap 12 β†’ guest kernel panic. The KASSERTs guarding the ring/btype invariants (if_vmx.c:2224, 2241-2249) are compiled out on production kernels.

Why source-certain

  • Ring selection from rxcd->qid with no validation: if_vmx.c:2217-2220 (same pattern in vmxnet3_rxq_discard_chain, if_vmx.c:2086-2089).
  • qid is a 10-bit host field (if_vmxreg.h:148); DragonFly never negotiates more than 1 RX queue (MSI-X unsupported on DFly: if_vmx.c:812-813 errors out, vmxnet3_check_multiqueue() if_vmx.c:587-589 forces 1/1) β€” so any qid != 0 selects ring 1.
  • Ring 1 unpopulated at default MTU: if_vmx.c:2563-2581 (populate = 1 whenever frame_size <= MCLBYTES(2048) + MJUMPAGESIZE(4096), i.e. every MTU <= 6126); ring 1's rxbuf array is only ever zero-filled (vmxnet3_init_rxq, if_vmx.c:1047-1048 M_ZERO).
  • vmxnet3_newbuf() at if_vmx.c:2257 refills rxbuf[idx] after m was fetched β€” the NULL local is dereferenced regardless.
  • The completion ring, the descriptors and update_rxhead all live in host-writable shared DMA memory (the driver itself reads host-written stats back from the same page: if_vmx.c:3684-3753).

How it was proven on the guest

Protocol-replica KLD (vmxrep.c) β€” copies the exact ring structures and the verbatim sop-path code of vmxnet3_rxq_eof, with the hostile descriptor injected as the host would DMA it. Run on DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (production kernel, no INVARIANTS):

vmxrep3779: m == NULL (ring 1 unpopulated); if_vmx.c:2265 ... next
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x18
fault code            = supervisor write data, page not present
Stopped at vmxrep_sysctl+0x252: movl $0x40,0x18(%rbx)      # 0x40 = the rxcd->len

(panic.txt; reproduced twice β€” initial run + clean rerun after reset.)

The store instruction is the replicated m->m_pkthdr.len = m->m_len = length (0x40 == the hostile len=64; m in %rbx == 0).

Exploitability

Fixed-address near-NULL write (address = offsetof(m_pkthdr) constants, value partially host-influenced via length). No controllable pointer target β†’ panic-only on this arch (no SMEP/SMAP/KASLR irrelevant here). Within the malicious-host threat model (compromised ESXi backend, nested-hypervisor attack, or a buggy host offloading to ring 1 by mistake) this is a one-descriptor guest kill.

Fix validation

Full patched-kernel validation is not possible on this guest (no vmxnet3 device to attach the real driver). The fix logic was validated with the same replica: test 901 (fixrun.log) drops the completion when the selected ring is unpopulated β€” no crash, guest alive: vmxrep901(fixed rx): unpopulated ring selected - completion dropped, no NULL deref, guest alive

fix.diff adds the equivalent NULL guard after the mbuf fetch in vmxnet3_rxq_eof() (and notes the same guard belongs in the discard path).

Kernel refs

sys/dev/virtual/vmware/vmxnet3/if_vmx.c:2217-2220, 2223, 2257, 2265-2266, 2563-2581, 1047-1048, 2086-2089; if_vmxreg.h:143-171.

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 reproduced→ patch + rebuild →patched clean

Patched-kernel validation impossible on this guest (no vmxnet3 PCI device; the real driver cannot attach). Fix LOGIC validated in-replica: fixed ring-selection path drops the completion on NULL mbuf under the identical hostile input - no crash, guest alive (fixrun.log: vmxrep901). fix.diff authors the guard for if_vmx.c:2223.

fixrun.log: 'vmxrep901(fixed rx): unpopulated ring selected - completion dropped, no NULL deref, guest alive'; baseline panic in panic.txt
↓ fix.diffper-fix-DF-3779

Confirmed kernel references

Detail

Exploit chain

host DMA -> rxcd{qid=1,rxd_idx=0(in-bounds),sop=1,eop=1,len=64} -> ring1 selected (if_vmx.c:2217-2220) -> m=NULL (if_vmx.c:2223, ring1 never populated at default MTU) -> KASSERTs compiled out -> m->m_pkthdr.rcvif=ifp (if_vmx.c:2265) -> fatal trap 12 write 0x18 -> guest panic

Evidence (decisive lines)

panic.txt: 'vmxrep3779: m == NULL ... Fatal trap 12 ... fault virtual address = 0x18 ... Stopped at vmxrep_sysctl+0x252: movl $0x40,0x18(%rbx)' (0x40 = hostile len=64). Reproduced twice incl. clean rerun after vm.sh reset with-src. fixrun.log: fixed logic (test 901) survives identical input.

PoC changes

Seed sketch replaced entirely: no PoC existed. Built protocol-replica KLD (vmxrep.c) copying the exact ring structs/gen protocol and the verbatim sop block; fixed a cosmetic %p-vs-string printf bug after the first run (fault evidence unaffected).

Verified recommended fix

NULL-guard the mbuf after the ring selection in vmxnet3_rxq_eof() and drop the completion (see fix.diff)

Verdict

One hostile RX completion (in-bounds rxd_idx, sop=1, qid>=nrxqueues) makes vmxnet3_rxq_eof() select the unpopulated ring 1 (default configs never populate it, if_vmx.c:2563-2581); the mbuf fetched at if_vmx.c:2223 is NULL and the sop path writes m->m_pkthdr.rcvif/len (if_vmx.c:2265-2266) through it. Proven on the guest kernel with a verbatim protocol-replica module: fatal trap 12, supervisor write at 0x18 (movl $0x40,0x18(%rbx)) - the rxcd->len store. Production kernels compile out the guarding KASSERTs. Panic/DoS in the malicious-host threat model; the vmx driver is compiled into GENERIC.