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

ig_hal VF: uninitialized kernel-stack tail (up to 50 of 60 payload bytes) of the E1000_VF_SET_MULTICAST mailbox message transmitted verbatim to the PF (hypervisor)

Summary

msgbuf[16] is never zeroed; only msgbuf[0] and count*2 hash bytes are written, yet write_msg_read_ack transmits all 16 words to VMBMEM which the host PF reads - for the common <30-group case up to 50 of 60 payload bytes are uninitialized guest kernel stack disclosed to the host (guest->host kernleak, hostile-PF class). Upstream initializes the array. Demo: count=5 -> 50/60 uninit bytes; zero-init fix compile-validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4795 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
mcmsg_demo.c β€” 2.9 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 104 B view raw
build.log β€” 1.5 KB view raw
run.log β€” 1.7 KB view raw
env.txt β€” 1.4 KB view raw
fix.diff β€” 793 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.7 KB view raw

DF-4795 β€” ig_hal VF: uninitialized kernel-stack tail of the multicast mailbox message transmitted to the PF

File: sys/dev/netif/ig_hal/e1000_vf.c (pass 2) Function: e1000_update_mc_addr_list_vf() β€” lines 400-437

What happens

u32 msgbuf[E1000_VFMAILBOX_SIZE];          /* :403 β€” never initialized */
u16 *hash_list = (u16 *)&msgbuf[1];
...
for (i = 0; i < mc_addr_count; i++)        /* :429-434 β€” writes count*2 bytes
                                               starting at byte 4            */
        hash_list[i] = hash_value & 0x0FFF;

e1000_write_msg_read_ack(hw, msgbuf, E1000_VFMAILBOX_SIZE);  /* :436 β€” 16 words */

e1000_write_msg_read_ack (:380-389) forwards all E1000_VFMAILBOX_SIZE(16) words via write_posted, and e1000_write_mbx_vf (sys/dev/netif/ig_hal/e1000_mbx.c:476-477) copies all size words verbatim into the E1000_VMBMEM mailbox registers that the PF (hypervisor / host OS) reads. With mc_addr_count < 30, the words past the written hash entries β€” up to 50 of the 60 payload bytes for small counts, e.g. count=5 β†’ bytes 14..63 β€” are uninitialized kernel stack from the guest, disclosed to the host-side PF driver. (The |= at :422 also reads msgbuf[0] uninitialized before it is overwritten at :426 β€” dead value, no transmit.)

Upstream FreeBSD initializes the buffer: u32 msgbuf[E1000_VFMAILBOX_SIZE] = {}; (cgit sys/dev/e1000/e1000_vf.c).

Threat model: the classic guest→host boundary. The recipient is the PF driver in the more-privileged host, so this is a disclosure of guest kernel stack residue across a privilege boundary that hostile-PF/VF hardening (mxgpu/ena class) normally wants hermetic. Any unprivileged local user in a guest can trigger it by joining multicast groups on an igb VF interface (no privilege needed for group joins; igb_set_multi() → e1000_update_mc_addr_list → this function).

Reproduce (logic-level)

cc -Wall -O2 -o mcmsg_demo mcmsg_demo.c && ./mcmsg_demo

Decisive line (deterministic byte-initialization model of the exact write pattern):

mc_addr_count=  5: ... uninit stack bytes sent to PF: 50 of 60

Fix

fix.diff hunk 1 β€” u32 msgbuf[E1000_VFMAILBOX_SIZE] = { 0 }; (git-apply clean; compile-validated on the audit guest, build.log).

Runtime PoC status

untested β€” the transmit requires an SR-IOV igb VF (audit guest is virtio-only, see env.txt). The byte-level write/forward pattern is proven from source; see VERDICT.md.

VERDICT.md
↓ download raw

DF-4795 β€” VERDICT

Status: untested (byte-level transmit pattern proven from source; guest has no SR-IOV igb VF hardware). Severity: Low (kernleak bucket β€” guest kernel-stack disclosure to the host PF only). Confidence: certain.

Root cause (path:line)

sys/dev/netif/ig_hal/e1000_vf.c:403 β€” u32 msgbuf[E1000_VFMAILBOX_SIZE]; is never initialized in e1000_update_mc_addr_list_vf(). The function initializes only:

  • msgbuf[0] (:426-427), and
  • hash_list[0..mc_addr_count-1] = msgbuf bytes 4 .. 4+2*count-1 (:429-434), count clamped to 30.

It then sends the entire 16-word buffer: e1000_write_msg_read_ack(hw, msgbuf, E1000_VFMAILBOX_SIZE) (:436) β†’ mbx->ops.write_posted(hw, msg, size, 0) (:385) β†’ e1000_write_mbx_vf, which copies for (i = 0; i < size; i++) E1000_WRITE_REG_ARRAY(hw, E1000_VMBMEM(0), i, msg[i]); (sys/dev/netif/ig_hal/e1000_mbx.c:476-477) into the mailbox registers the PF reads. For count < 30 the tail (up to 50 of 60 payload bytes; count=5 β†’ bytes 14..63 = words 1(partial)..15) is uninitialized kernel stack transmitted to the host-side PF driver.

Secondary dead-read of the same array: msgbuf[0] |= … at :422 reads msgbuf[0] uninitialized (value discarded by :426) β€” folded into this finding, no separate transmit.

Reachability (live path)

Same chain as DF-4794: igb VF devices (if_igb.c:108,123) β†’ e1000_vfadapt* (e1000_api.c:392-397) β†’ VF op table (e1000_api.c:510) β†’ igb_set_multi() calls e1000_update_mc_addr_list with the joined-group count (if_igb.c:1304); group joins need no privilege. Every multicast update with fewer than 30 groups (the overwhelmingly common case β€” e.g. a plain IPv6 host with a handful of solicited-node groups) transmits uninitialized stack words to the PF.

Impact

Disclosure of guest kernel-stack residue across the guestβ†’host boundary (CWE-908 / CWE-457 family). Recipient is the host PF driver (more privileged than the guest); exposure beyond the PF driver depends on host behavior (driver logging / mailbox capture). No guest-internal privilege crossing; no remote exposure (mailbox is VF↔PF only). Severity Low; CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:N.

Evidence

  • run.log β€” mcmsg_demo (deterministic model of the exact write pattern) on the audit guest: mc_addr_count=5 β†’ uninit stack bytes sent to PF: 50 of 60; count=31/128 β†’ 0 (clamp covers the tail only at exactly 30).
  • Upstream divergence: FreeBSD current initializes u32 msgbuf[E1000_VFMAILBOX_SIZE] = {}; in e1000_update_mc_addr_list_vf (cgit.freebsd.org, fetched 2026-09-23).
  • build.log β€” fix compile-validation on the guest: BASE_CC_RC=0, FIXED_CC_RC=0; guest source verified bit-identical to the audited tree (sha256 c0cdce6b…).
  • env.txt β€” virtio-only guest, no e1000/VF device β‡’ runtime transmit not exercisable (status untested, attempts 0).

Pass-2 kill list for the rest of e1000_vf.c

See DF-4794/VERDICT.md (covers all mailbox sizes vs buffers, hash bounds, shift-loop termination, wait-loop bounds, NULL-op dispatch, perm_addr memcpy bounds). Sibling DF-4638 (posted-wrapper size validation) remains the canonical mbx footgun finding β€” no in-tree caller from this file overflows it (sizes 1/2/3/16 against correctly sized buffers).

Fix

fix.diff hunk 1: u32 msgbuf[E1000_VFMAILBOX_SIZE] = { 0 }; (git-apply clean; guest compile-validated). Hunk 2 belongs to DF-4794.

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 git apply --check clean; both hunks applied on guest copy; cc -fsyntax-only BASE_CC_RC=0 / FIXED_CC_RC=0 with only the pre-existing systm.h:216 warning (build.log); runtime fix-validation impossible on this virtio-only guest (no igb/VF device)

fix.diff; build.log; env.txt
↓ fix.diffper-fix-DF-4795

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (count=5 line: 'uninit stack bytes sent to PF: 50 of 60'); VERDICT.md (write-pattern proof path:line, reachability chain, upstream divergence); build.log (guest compile validation + source sha256 match); env.txt (no e1000 hardware on guest); fix.diff (git-apply-clean, hunk 1 = this finding)

PoC changes

no seeded PoC; authored mcmsg_demo.c (deterministic byte-initialization model of the exact write/forward pattern) + combined fix.diff

Verified recommended fix

Zero-initialize the message buffer: u32 msgbuf[E1000_VFMAILBOX_SIZE] = { 0 }; (fix.diff hunk 1)

Verdict

not runnable on the audit guest by hardware absence (virtio-only, env.txt); the byte-level write/forward pattern is certain from source: msgbuf[16] declared uninitialized at e1000_vf.c:403, only msgbuf[0] (:426-427) and count*2 bytes at offset 4 (:429-434) are written, yet e1000_write_msg_read_ack (:436 -> :385) forwards all E1000_VFMAILBOX_SIZE=16 words and e1000_write_mbx_vf copies all size words into VMBMEM (e1000_mbx.c:476-477) which the PF reads. FreeBSD current initializes the array (= {}), confirming upstream treats this as a bug. Path is live for igb VF devices (if_igb.c:108/123 -> e1000_api.c:510 -> igb_set_multi if_igb.c:1304); unprivileged users trigger it by joining multicast groups. Deterministic demo on guest (run.log): count=5 -> 50/60 bytes uninitialized. fix.diff hunk 1 (= { 0 }); compile-validated BASE_CC_RC=0/FIXED_CC_RC=0 (build.log), git apply --check clean.