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

ix(4): ixgbe_read_posted_mbx/ixgbe_write_posted_mbx dispatch to ops.read/ops.write with unvalidated size, bypassing the clamp/reject enforced only by the non-posted wrappers - latent OOB-to-MMIO and cross-VF-mailbox-spill footgun (twin of DF-4638 e1000_mbx); no in-tree caller (DF ix has no SR-IOV)

Summary

The posted wrappers skip the size discipline that lives only in the non-posted wrappers; the raw VF/PF loops trust size verbatim over the fixed 64-byte window (IXGBE_PFMBMEM(vf)=0x13000+64*vf). Deader than the igb twin: DF ix has no SR-IOV and zero in-tree callers of any mailbox op (only init_params), and PF timeout=0 additionally short-circuits read_posted. On the day SR-IOV/ixv lands with size>16: write path reads the caller's buffer OOB and writes MMIO into the NEXT VF's mailbox (cross-VM forgery); read path discloses cross-VF data. Mirror-the-discipline fix (git-apply-verified).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4645 Β· 7 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
static-proof.txt β€” 2.3 KB view raw
env.txt β€” 682 B view raw
fix.diff β€” 729 B view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 1.0 KB view raw

DF-4645 β€” ixgbe posted-mbx wrappers bypass size discipline (twin of DF-4638)

Severity: Info (latent / dead-in-tree) Β· bucket: memcorrupt Β· confidence: certain (static)

What

sys/dev/netif/ix/ixgbe_mbx.c has the exact twin of the e1000_mbx shape filed as DF-4638 (ig_hal):

  • ixgbe_read_mbx() (ixgbe_mbx.c:47-62) clamps size to mbx->size (lines 55-56) before dispatching to mbx->ops.read.
  • ixgbe_write_mbx() (ixgbe_mbx.c:73-88) rejects size > mbx->size (lines 80-83) before dispatching to mbx->ops.write.

But the posted wrappers, which are what real VF drivers call, dispatch directly to the raw ops with no size validation at all:

  • ixgbe_read_posted_mbx() (ixgbe_mbx.c:224-241) β†’ mbx->ops.read(hw, msg, size, mbx_id) at line 238 β€” no clamp.
  • ixgbe_write_posted_mbx() (ixgbe_mbx.c:253-273) β†’ mbx->ops.write(hw, msg, size, mbx_id) at line 266 β€” no reject.

The raw ops themselves do no validation either: ixgbe_write_mbx_vf (425-456, loop at 445-446), ixgbe_read_mbx_vf (467-493, loop at 482-483), ixgbe_write_mbx_pf (665-695, loop at 683-684), ixgbe_read_mbx_pf (708-733, loop at 722-723) all trust size verbatim. mbx->size is always IXGBE_VFMAILBOX_SIZE == 16 words / 64 bytes (ixgbe_mbx.h:40; set at ixgbe_mbx.c:510, 755).

Consequence if a caller ever passes size > 16

Write path (for (i = 0; i < size; i++) IXGBE_WRITE_REG_ARRAY(hw, IXGBE_PFMBMEM(vf), i, msg[i])): 1. kernel OOB read of the caller's msg[] buffer beyond size-words is not the issue (caller-sized) β€” but any caller passing a fixed 16-word buffer with a bigger size gets an OOB read of adjacent kernel memory written into device-visible MMIO; 2. IXGBE_PFMBMEM(vf) = 0x13000 + 64*vf (ixgbe_type.h:455) is a 64-byte window β€” words 16+ of the loop spill into the next VF's mailbox: cross-VF mailbox corruption / message forgery on any future SR-IOV use.

Read path: the loop reads past the 16-word window (next VF's mailbox / other registers) into the caller's buffer β€” cross-VF data disclosure.

Why Info and not higher

The DragonFly ix driver has no SR-IOV/VF support and no caller of any mailbox message op β€” the entire message engine is dead code in-tree:

  • if_ix.c touches the mbx subsystem exactly once: hw->mbx.ops. init_params(hw) (if_ix.c:535-536).
  • No ixv driver exists in the tree; grep for read_posted|write_posted over sys/ finds zero callers outside ixgbe_mbx.c itself (see static-proof.txt).
  • ixgbe_init_mbx_params_pf (741-770) is registered only by ixgbe_82598.c… actually only 82599/x540/x550 attach code (ixgbe_82599.c:388, ixgbe_x540.c:148); PF-side timeout = 0 (line 752) additionally makes ixgbe_poll_for_msg/ack return IXGBE_ERR_MBX immediately, so even a hypothetical read_posted caller would never reach ops.read on the PF side.

This matches DF-4638's Info classification for the ig_hal twin (there the VF path at least exists in igb; here it does not).

Not runnable β€” why there is no PoC

  • Guest has no ixgbe-class PCI device at all (virtio-only; see env.txt).
  • Even with hardware, no in-tree code path invokes the posted wrappers, so there is no trigger to point at; the finding is a latent API-contract defect for the day SR-IOV/VF support lands (or an out-of-tree/ported caller).
  • Verification performed instead: exhaustive static caller analysis (static-proof.txt) + hardware-absence capture (env.txt).

Fix

fix.diff β€” mirror the non-posted discipline inside the posted wrappers (clamp on read, reject on write). Never applied to sys/ (read-only audit target).

VERDICT.md
↓ download raw

VERDICT β€” DF-4645 (ixgbe posted-mbx size-discipline bypass, twin of DF-4638)

status: untested Β· reproduced: 0 Β· impact: none (latent) Β· confidence: certain

Bottom line

The twin defect exists verbatim: ixgbe_read_posted_mbx dispatches to mbx->ops.read with an unvalidated size (sys/dev/netif/ix/ixgbe_mbx.c:238), bypassing the clamp that exists only in ixgbe_read_mbx (ixgbe_mbx.c:55-56); ixgbe_write_posted_mbx dispatches to mbx->ops.write unvalidated (ixgbe_mbx.c:266), bypassing the reject that exists only in ixgbe_write_mbx (ixgbe_mbx.c:80-83). The raw ops (ixgbe_read_mbx_vf ixgbe_mbx.c:467-493, ixgbe_write_mbx_vf 425-456, ixgbe_read_mbx_pf 708-733, ixgbe_write_mbx_pf 665-695) trust size verbatim; every mailbox is a fixed 16-word/64-byte window (IXGBE_VFMAILBOX_SIZE, ixgbe_mbx.h:40; IXGBE_PFMBMEM(vf)=0x13000+64*vf, ixgbe_type.h:455). A future caller with size > 16 would (a) read msg[i] OOB from kernel memory into device MMIO on the write side and (b) spill register writes/reads past the 64-byte window into the next VF's mailbox β€” cross-VF corruption/disclosure.

Why untested (honest classification)

  1. No trigger path exists in-tree. Exhaustive grep over sys/ proves the only reference the ix driver makes to the mbx subsystem is hw->mbx.ops.init_params(hw) at if_ix.c:535-536; read_posted / write_posted / ixgbe_read_mbx / ixgbe_write_mbx have zero callers outside ixgbe_mbx.c (static-proof.txt, steps 1-2). DragonFly's ix driver has no SR-IOV/VF support at all (static-proof.txt, step 5: zero vf/iov matches in if_ix.c) β€” unlike igb (DF-4638), where a VF path at least exists.
  2. No hardware on the verification guest. pciconf -l shows a virtio-only PCI bus β€” no 82598/82599/X540/X550 device (env.txt). if_ix is compiled into the kernel ("module already loaded or in kernel") so the code is present but dormant: with no device, ixgbe_init_mbx_params_* never runs and no struct ixgbe_hw mbx ops are ever populated or invoked.
  3. Additional structural dead-end on the PF side: ixgbe_init_mbx_params_pf sets mbx->timeout = 0 (ixgbe_mbx.c:752), and ixgbe_poll_for_msg/ ixgbe_poll_for_ack return IXGBE_ERR_MBX immediately when timeout==0 (ixgbe_mbx.c:164-165, 196-197), so ixgbe_read_posted_mbx would fail at the poll step before ever dispatching ops.read β€” even a hypothetical PF caller cannot reach the unvalidated read today.

Because there is no reachable primitive on any current DragonFly system, this is filed Info / latent exactly like its ig_hal twin DF-4638 (which at least has a live VF code path). No user→root route exists to demonstrate; the finding documents the API-contract footgun for the day SR-IOV lands or a ported caller (e.g., an ixv driver) is added.

Verification that WAS done

  • Full-file read of ixgbe_mbx.c (770 lines) + ixgbe_mbx.h + relevant ixgbe_type.h register macros; every dispatch/size path traced (see README).
  • Exhaustive caller analysis persisted (static-proof.txt).
  • Guest environment captured (env.txt): DragonFly 6.5-DEVELOPMENT x86_64, kernel with if_ix compiled in, virtio-only PCI, no /dev/ast*, no ixgbe device. Guest left clean (no state changed beyond read-only probes).

Fix

fix.diff mirrors the non-posted discipline inside both posted wrappers (read: clamp to mbx->size; write: reject with IXGBE_ERR_MBX + ERROR_REPORT2). Authored against the read-only sys/ tree, never applied. A fix-validation kernel build was not performed: the bug has no trigger path in-tree (see above), so a patched kernel is observationally identical by any runnable test β€” fix validation would be theater. fix_status: not_applicable.

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

fix.diff mirrors non-posted discipline (clamp read, reject write) at the exact dispatch sites (ixgbe_mbx.c:238, 266); not kernel-build-validated because the bug has no in-tree trigger path and the guest has no ixgbe hardware β€” a patched kernel is observationally identical under any runnable test.

findings/poc/DF-4645/fix.diff; VERDICT.md 'Fix' section
↓ fix.diffper-fix-DF-4645

Confirmed kernel references

Detail

Evidence (decisive lines)

static-proof.txt (exhaustive zero-caller grep), env.txt (guest: no ixgbe device, if_ix compiled-in but dormant), VERDICT.md (full path:line trace), fix.diff (clamps read / rejects write inside posted wrappers)

PoC changes

no PoC exists to change: the finding is latent (no in-tree trigger path, no hardware on guest); verification was static caller analysis + guest hardware-absence capture instead

Verified recommended fix

Mirror the non-posted wrappers' size discipline inside ixgbe_read_posted_mbx (clamp size to mbx->size) and ixgbe_write_posted_mbx (reject size > mbx->size with IXGBE_ERR_MBX) β€” see fix.diff.

Verdict

Twin of DF-4638 confirmed statically: ixgbe_read_posted_mbx (ixgbe_mbx.c:238) and ixgbe_write_posted_mbx (ixgbe_mbx.c:266) dispatch to the raw mbx ops with unvalidated size, bypassing the clamp (ixgbe_mbx.c:55-56) and reject (ixgbe_mbx.c:80-83) enforced only by the non-posted wrappers; the raw VF/PF ops trust size verbatim over a fixed 16-word/64-byte mailbox window, so a future size>16 caller gets kernel-OOB-read-to-MMIO on write and cross-VF mailbox spill on both directions. Not runnable today: the DF ix driver has no SR-IOV/VF support and zero in-tree callers of any mailbox message op (only if_ix.c:535 init_params), and the verification guest has no ixgbe-class PCI device (virtio-only). Filed Info/latent exactly like the ig_hal twin. Guest probed read-only and left clean.