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

ig_hal: e1000_read_posted_mbx/e1000_write_posted_mbx dispatch to ops.read/ops.write with unvalidated size, bypassing the read-clamp/write-reject discipline enforced only by the non-posted wrappers β€” latent kernel-buffer overflow footgun on the live igb VF path

Summary

The size clamp/reject lives only in the non-posted wrappers; the posted variants call ops directly with the caller's size unvalidated - and one future caller with a message-derived size gets a kernel overflow whose content is PF-controlled mailbox words (running past caller buffers and the 16-word register window). Every in-tree caller passes <=16 with correctly sized buffers, so latent today; the API is live on igb VF attach. Mirror-the-sibling-checks fix, compile-validated on guest.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4638 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
fix.diff β€” 692 B view raw
env.txt β€” 1.1 KB view raw
build.log β€” 1.6 KB view raw
verdict.json β€” 3.2 KB view raw
manifest.json β€” 960 B view raw

DF-4638 β€” ig_hal: posted mailbox wrappers bypass the size clamps their non-posted siblings enforce

File: sys/dev/netif/ig_hal/e1000_mbx.c (pass 2, GLM 5.3)

What

  • e1000_read_mbx() (e1000_mbx.c:80-81) clamps size down to mbx->size before dispatching to ops.read β€” this is what prevents ops.read (the for (i = 0; i < size; i++) msg[i] = E1000_READ_REG_ARRAY(...) loop at e1000_mbx.c:736-737 / :512-513) from walking past the caller's buffer.
  • e1000_write_mbx() (e1000_mbx.c:105-106) rejects size > mbx->size.
  • The posted variants call ops.read/ops.write directly, with no size handling at all: e1000_read_posted_mbx() at e1000_mbx.c:260 and e1000_write_posted_mbx() at e1000_mbx.c:287.

Today every in-tree caller (all in e1000_vf.c: :283/:288/:334/:337/:385-388/:436/ :498/:501/:559) passes size ∈ {1,2,3,16} ≀ mbx->size = 16 (E1000_VFMAILBOX_SIZE, set at e1000_mbx.c:540/:766), with correctly sized stack buffers β€” so there is no demonstrable overflow on the current tree. The hazard is latent: the VF mailbox API is live code (the igb driver attaches to 82576_VF/I350_VF PCI IDs β€” if_igb.c:108/:123 β€” and dispatches through e1000_api.c:511/:514 to e1000_init_function_pointers_vf), and any future caller of the posted API with size > mbx->size gets a kernel stack/heap overflow filled with PF-controlled mailbox words, plus out-of-range E1000_VMBMEM register-array writes.

Severity: Info (defense-in-depth / API hardening). Confidence: certain (the missing checks are source facts; the impact statement is explicitly latent).

Reproduce

Nothing to run β€” this is a hardening gap. The fix was compile-validated on the audit guest against the running kernel's headers:

# build.log (guest session):
cc -fsyntax-only -D_KERNEL -I/usr/obj/usr/src/sys/X86_64_GENERIC \
   -I/usr/src/sys -I/usr/src/sys/dev/netif/ig_hal e1000_mbx_fixed.c   # FIXED_CC_RC=0

fix.diff mirrors the sibling checks into the posted wrappers (read clamps, write rejects) β€” git-apply-clean against sys/.

VERDICT.md
↓ download raw

DF-4638 VERDICT β€” posted mailbox wrappers bypass the size clamps

Status: untested (nothing runnable: no igb/SR-IOV hardware on the audit guest β€” env.txt; and there is no in-tree caller that overflows today, by design of the finding). Fix: compile-validated on guest (FIXED_CC_RC=0, BASE_CC_RC=0 β€” build.log).

Analysis

The non-posted wrappers implement the mailbox size discipline:

  • e1000_read_mbx (e1000_mbx.c:72-87): if (size > mbx->size) size = mbx->size; (:80-81) β€” protects the destination buffer, since the per-MAC ops.read implementations copy size words unconditionally (e1000_read_mbx_vf :512-513, e1000_read_mbx_pf :736-737).
  • e1000_write_mbx (e1000_mbx.c:98-112): if (size > mbx->size) ret_val = -E1000_ERR_MBX; (:105-106) β€” protects both the source buffer read (msg[i] in e1000_write_mbx_vf :476-477 / _pf :697-698) and the E1000_VMBMEM(_n) + i register-array window (16 words per VF, e1000_regs.h:589).

The posted wrappers dispatch around that discipline:

  • e1000_read_posted_mbx (:246-263): after e1000_poll_for_msg succeeds, calls mbx->ops.read(hw, msg, size, mbx_id) (:260) with the caller's size unmodified β€” no clamp.
  • e1000_write_posted_mbx (:275-294): checks !mbx->ops.write || !mbx->timeout (:283) then calls mbx->ops.write(hw, msg, size, mbx_id) (:287) β€” no size check.

Reachability today (why Info, not higher)

All in-tree callers of the posted API are in sys/dev/netif/ig_hal/e1000_vf.c: write/read 1 (:283/:288), 3 (:334/:337), 16 via e1000_write_msg_read_ack (:385-388, retmsg[E1000_VFMAILBOX_SIZE]), 16 (:436), 1 (:498/:501); plus one non-posted ops.read(hw, &in_msg, 1, 0) (:559). Every size ≀ mbx->size = 16 (e1000_mbx.h:62; installed at e1000_mbx.c:540/:766), every buffer correctly sized. No overflow is constructible on the current tree β€” the gap is a loaded footgun on a live API (igb's VF attach path: if_igb.c:108/:123 β†’ e1000_api.c:511/:514 β†’ e1000_vf.c:151 β†’ e1000_mbx.c:542-545), aimed at exactly the "trust the VF/PF message size" family. A future caller passing a size derived from a message field or a larger constant gets a kernel-memory overflow whose content is PF-controlled mailbox words.

Note the API inconsistency is also a correctness trap in the other direction: e1000_read_mbx silently truncates while e1000_write_mbx errors β€” the posted variants do neither.

Classes hunted and killed on this file (pass-2 negative result)

  • e1000_poll_for_msg/e1000_poll_for_ack (:181-234): countdown = mbx->timeout (0 or 2000, u32 in e1000_hw.h:983 β€” no sign issue); bounded 2000 Γ— 500 Β΅s; on exhaustion mbx->timeout latches 0 = permanent-fail-until-reset (conservative, not a bypass).
  • e1000_obtain_mbx_lock_vf/pf (:426-446/:644-668): bounded 10 Γ— 1 ms.
  • e1000_check_for_msg_pf/check_for_ack_pf (:585/:606): E1000_MBVFICR_VFREQ_VF1 << vf_number / E1000_MBVFICR_VFACK_VF1 << vf_number is UB for vf_number β‰₯ 32, but the PF ops have no in-tree caller (DFly igb has no PF-side SR-IOV) β€” dead code; would deserve a bound (vf_number = vf_number % 32) the day a PF driver appears.
  • e1000_init_mbx_params_pf (:755-784): missing break before default: is cosmetic (case body completes then falls into return E1000_SUCCESS); unsupported MACs leave ops NULL-zeroed and every dispatcher NULL-checks (:83, :108, :128, :148, :168, :253, :283).
  • e1000_read_v2p_mailbox/check_for_bit_vf (:322-351): shadow-register merge is idempotent; only reachable on VF dispatch where dev_spec.vf is the valid union member.
  • No fw/VF-controlled count reaches any loop bound in this file β€” all sizes are caller-supplied (that is precisely why the missing caller-side discipline matters).

Fix

fix.diff (git-apply-clean; compile-validated on the guest, build.log): mirror the sibling checks into the posted wrappers β€” read clamps to mbx->size, write rejects size > mbx->size.

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

compile-validated only: fix.diff applies cleanly (git apply --check) and cc -fsyntax-only passes against the running kernel's headers on the guest for both baseline and patched file (BASE_CC_RC=0/FIXED_CC_RC=0, build.log); runtime validation impossible on this virtio-only guest (no igb/VF device)

fix.diff; build.log
↓ fix.diffper-fix-DF-4638

Confirmed kernel references

Detail

Exploit chain

[]

Evidence (decisive lines)

VERDICT.md (clamp/reject vs direct-dispatch line cites; caller sweep; killed-class list); build.log (on-guest baseline+patched cc -fsyntax-only RC=0); env.txt (no e1000 hardware on guest); fix.diff (git-apply-clean)

PoC changes

n/a β€” no seeded PoC; the actionable artifact is fix.diff, compile-validated on the guest

Verified recommended fix

Mirror the sibling checks into the posted wrappers: clamp size to mbx->size in e1000_read_posted_mbx before ops.read, and reject size > mbx->size in e1000_write_posted_mbx (fix.diff).

Verdict

not_run as PoC by design: this is a hardening gap with no overflowing caller in the current tree (verified by exhaustive caller sweep β€” e1000_vf.c is the only user of the posted API; e1000_api.c dispatches VF ops for igb's 82576_VF/I350_VF attach path). The size discipline exists only in the non-posted wrappers (read clamp e1000_mbx.c:80-81, write reject :105-106); the posted wrappers call ops.read/ops.write directly (:260, :287) with the caller's size unvalidated. No igb/SR-IOV hardware on the guest (virtio-only, env.txt). fix.diff (mirror sibling checks) is git-apply-clean and compile-validated against X86_64_GENERIC headers on the guest (FIXED_CC_RC=0, build.log).