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

Unchecked PHY-read returns across the shared e1000 PHY layer: link-stall workaround branches on uninitialized kernel stack; power up/down write back a zeroed PHY_CONTROL after failed reads (DF-3643/DF-3671 family, e1000_phy.c instance)

Summary

e1000_link_stall_workaround_hv() drops read_reg()'s return and branches on uninitialized stack data; e1000_power_up/down_phy_copper() ignore read failures and write back a value that zeroes PHY_CONTROL except the power bit (error-path link clobber); e1000_determine_phy_address() drops e1000_get_phy_id()'s return; e1000_phy_init_script_igp3() drops every write return. When MDIC fails *data is never written, so decisions and register write-backs derive from uninitialized or zeroed state. VERIFIED: logic-replication harness with instrumented always-failing read shim - GCC's own -Wuninitialized fired on the guest build; stdout 'decision on UNINITIALIZED data' 3/3 deterministic. Primary site reached on 82578/PCH via cfg_on_link_up on every link-up. Transient MDIC failure makes the kernel branch on uninitialized stack and can clobber PHY_CONTROL, dropping/misconfiguring the link. No kernel-memory disclosure, no OOB - Info, matching the family precedent. Cable-length walks, poll loops, divisions, MDIC/SFP offsets all killed bounded this pass.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3762 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 5.7 KB ↓ raw
trigger.c β€” 2.7 KB view raw
build.sh β€” 126 B view raw
run.sh β€” 90 B view raw
build.log β€” 128 B view raw
run.log β€” 451 B view raw
run.2.log β€” 621 B view raw
env.txt β€” 953 B view raw
module_presence.log β€” 1.4 KB view raw
verdict.json β€” 3.0 KB view raw

DF-3762 β€” Unchecked PHY-read returns in e1000_phy.c (DF-3643/DF-3671 family)

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

What

The shared e1000 PHY layer consumed by em(4)/emx(4)/igb(4) makes configuration decisions β€” and writes registers back β€” based on PHY reads whose return values are dropped. When an MDIC/I2C read fails (-E1000_ERR_PHY), the callee returns without writing *data, and the caller branches on uninitialized kernel stack or writes a zeroed value back over live PHY configuration.

Cluster members (all in e1000_phy.c):

line function defect
3848 e1000_link_stall_workaround_hv read_reg(PHY_CONTROL, &data) unchecked; data (decl. 3840) is uninitialized stack; if (data & PHY_CONTROL_LB) at 3849 decides on garbage. Runtime-reachable: called from e1000_ich8lan.c:1755 (cfg_on_link_up) on 82578/PCH link-up.
3503 e1000_power_up_phy_copper void fn; read return dropped; on failure writes 0 & ~MII_CR_POWER_DOWN back to PHY_CONTROL, zeroing speed/duplex/AN config (link clobber on error path).
3521 e1000_power_down_phy_copper same pattern with MII_CR_POWER_DOWN set.
3075 e1000_determine_phy_address e1000_get_phy_id(hw); return dropped; stale/partial phy->id feeds PHY-type dispatch.
2918-2984 e1000_phy_init_script_igp3 all 30+ write_reg returns dropped.

Severity Info β€” matches the precedent classification of the same family in DF-3643 (e1000_ich8lan.c) and DF-3671 (ixgbe_common.c). No kernel memory is leaked to userspace and no OOB occurs; the impact is wrong control flow / PHY misconfiguration from uninitialized kernel stack plus an error-path link DoS. This is Intel shared code inherited upstream.

Build / Run (logic replication β€” no e1000 hardware needed)

./build.sh   # cc -O2 -Wall -o trigger trigger.c
./run.sh     # ./trigger ; exit 1 == uninitialized-data decision demonstrated

Built and run on the audit QEMU guest (host has no compiler) β€” see build.log / run.log. Note the compiler's own proof: after inlining the failing read shim, GCC emits warning: 'data' is used uninitialized in this function [-Wuninitialized] for the replicated decision.

Why not executed against the real kernel path

The guest is virtio-only: its only NIC is virtio_pci (class 0x020000, chip 0x10001af4) and its only GPU is virtio-gpu. em/emx/igb are statically registered in the guest kernel, but with no e1000 device the PHY-layer code under audit cannot run. See module_presence.log / env.txt.

Fix

Check every read_reg/get_phy_id/write_reg return; bail out on failure (see fix sketch in VERDICT.md).

VERDICT.md
↓ download raw

VERDICT β€” DF-3762

status: inconclusive (source-level: certain; hardware execution: impossible on this guest) β€” reproduced: 0 β€” impact: none demonstrated (severity class of the defect itself: Info).

The defect (source-traced)

sys/dev/netif/ig_hal/e1000_phy.c:

  • e1000_link_stall_workaround_hv() β€” u16 data; at :3840 is never initialized. At :3848 the code calls hw->phy.ops.read_reg(hw, PHY_CONTROL, &data) and drops the return value. For 82578/PCH parts ops.read_reg is e1000_read_phy_reg_hv (set in e1000_ich8lan.c e1000_init_phy_params_ich8lan) β†’ __e1000_read_phy_reg_hv β†’ e1000_read_phy_reg_mdic. The mdic reader returns -E1000_ERR_PHY on MDI timeout / error bit / offset mismatch (e1000_phy.c:322-335) without ever storing to *data. Control then reaches :3849 if (data & PHY_CONTROL_LB) β€” a branch on uninitialized kernel stack. Depending on garbage: the workaround is silently skipped, or a bogus msec_delay(200) + HV_MUX_DATA_CTRL PHY writes sequence runs (:3864-3874).
  • e1000_power_up_phy_copper() (:3498-3506) and e1000_power_down_phy_copper() (:3516-3525) are void; both do read-PHY_CONTROL β†’ modify β†’ write-back with the read return dropped (:3503, :3521). mii_reg is pre-initialized to 0, so a failed read yields a write-back that zeroes PHY_CONTROL except the power-down bit β€” destroying speed/duplex/autoneg config (error-path link DoS; the write-back-clobber twin of DF-3671's AN-advertisement clobber).
  • e1000_determine_phy_address() (:3062-3090) drops e1000_get_phy_id()'s return (:3075) and matches hw->phy.id against the ID table regardless.
  • e1000_phy_init_script_igp3() (:2912-2987) drops every write_reg return in the 30-step init script.

Runtime reachability of the primary site: e1000_ich8lan.c:1755 calls e1000_link_stall_workaround_hv(hw) from the 82578 cfg_on_link_up path β€” i.e. on every link-up on affected PCH hardware; a single failed MDIC transaction (PHY in reset, bus contention after semaphore handoff β€” the poll budget is only 640*3 iterations of 50 Β΅s) triggers it.

This is Intel shared code (FreeBSD ig_hal lineage); the same pattern was filed for the sibling files as DF-3643 and DF-3671 β€” this is the e1000_phy.c instance of that family, not previously reported for this file.

What was verified, and how

  1. Source-level (certain). Full read of the 4,260-line file; every read_reg/write_reg/get_phy_id call site audited (62 read sites); the three unchecked read sites at :3503/:3521/:3848 are the only ones that feed decisions or write-backs; error paths of the mdic/i2c readers confirmed to leave *data untouched. Constants verified in headers (e1000_phy.h, e1000_defines.h).
  2. Logic replication (host-side proof of the pattern). trigger.c replicates the exact call/decision structure with an instrumented always-failing read shim. Compiled on the audit guest with cc -O2 -Wall: GCC itself proved the defect β€” after inlining, it emitted warning: 'data' is used uninitialized in this function [-Wuninitialized] (run.log) β€” and execution shows the branch consuming never-written data. This proves the code logic, not the kernel instance (see below).
  3. In-kernel execution: impossible on this guest. The guest kernel statically registers pci/if_em and pci/if_igb, but the machine is virtio-only (NIC = virtio_pci 1af4:1000; GPU = virtio-gpu 1af4:1050). No e1000 PHY exists, so the audited path cannot execute. kldload of the matching modules fails registration (module pci/if_em already exists!), and there is no device for the driver to attach to (module_presence.log). Honest classification: inconclusive (cannot run on available hardware), not "not_reproduced" (nothing failed to happen that the code says should happen).

Incidental observation (out of scope, root-only, different file): loading i915.ko on this guest succeeds (DRM init runs, probe finds no Intel GPU), but kldunload i915 wedges in D-state (lwe) holding the module subsystem locks β€” a drm-port lifecycle bug, not in intel_runtime_pm.c, and not attacker-reachable (kldload/kldunload require root). Guest was reset clean after the run.

Exploit chain

None. The class (Info) admits no escalation: the uninitialized stack word influences only an internal branch; no kernel memory is copied to userspace and no length/index is derived from it.

Fix sketch

--- a/sys/dev/netif/ig_hal/e1000_phy.c
+++ b/sys/dev/netif/ig_hal/e1000_phy.c
@@ s32 e1000_link_stall_workaround_hv(struct e1000_hw *hw)
-   hw->phy.ops.read_reg(hw, PHY_CONTROL, &data);
-   if (data & PHY_CONTROL_LB)
+   ret_val = hw->phy.ops.read_reg(hw, PHY_CONTROL, &data);
+   if (ret_val)
        return E1000_SUCCESS;
+   if (data & PHY_CONTROL_LB)
+       return E1000_SUCCESS;

and analogously: make e1000_power_{up,down}_phy_copper return s32 and propagate the read failure instead of writing back; check e1000_get_phy_id()'s return in e1000_determine_phy_address(); check each write_reg in e1000_phy_init_script_igp3() (or aggregate). Fix validation: not_testable β€” requires e1000 hardware absent from the guest; the patch is mechanical (return-value plumbing identical to the DF-3643/DF-3671 upstream parity fixes).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Mechanical return-value plumbing (same shape as DF-3643/DF-3671 upstream parity fixes); cannot be kernel-build validated without e1000 hardware β€” guest is virtio-only.

fix sketch in VERDICT.md; no kernel build performed (no hardware to observe behavior on).
per-fix-DF-3762

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (compiler -Wuninitialized proof + decision-on-uninitialized-data output); VERDICT.md (full path:line trace); module_presence.log + env.txt (guest virtio-only, em/igb static, no HW); trigger.c (replication source)

PoC changes

Seed had no runnable PoC; authored trigger.c logic-replication (instrumented failing read shim mirroring e1000_read_phy_reg_mdic error path) because no e1000 hardware exists on the guest; built on the guest via base64 transport (host has no compiler).

Verified recommended fix

Check and propagate every phy read_reg/write_reg/get_phy_id return in e1000_phy.c (:3848, :3503, :3521, :3075, init-script) instead of branching/writing back on failure.

Verdict

Source-certain instance of the DF-3643/DF-3671 unchecked-PHY-read family in the shared e1000 PHY layer: e1000_phy.c:3848 drops read_reg()'s return and branches on uninitialized stack at :3849 (reachable via e1000_ich8lan.c:1755 cfg_on_link_up on 82578/PCH); :3503/:3521 write back over PHY_CONTROL after ignored read failures (error-path link clobber); :3075 drops get_phy_id()'s return; :2918-2984 drop all init-script write returns. Logic-replication harness compiled+run on the guest proves the uninitialized-decision pattern (GCC -Wuninitialized fires); in-kernel execution impossible on the virtio-only guest (no e1000 NIC), so classified inconclusive rather than not_reproduced. Info severity, no escalation path.