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

e1000_erase_flash_bank_ich8lan: erase address accumulates across retries, retry budget never reset, and erase failure reported as success

Summary

In the ICH8/PCH flash bank erase used by every NVM commit, flash_linear_addr += (j * sector_size) sits inside the per-attempt do/while retry loop instead of the outer sector loop, so each flcerr retry erases a progressively higher wrong address; the function-scope retry counter 'count' is never reset between sectors; and exhausting the retry budget via the flcerr path falls through to an unconditional return E1000_SUCCESS, so the caller then programs dwords into an un-erased bank. Reached with NVM content alone at driver attach (VALID_CSUM bit clear triggers write+update -> erase). Byte-identical in FreeBSD HEAD - vendor-inherited, un-fixed upstream. Malicious/counterfeit PCIe GbE device, bit-corrupted NVM, or refashed card: attach-time NVM checksum repair issues erase commands against wrong flash sectors (drift up to tens of KB) and writes an un-erased bank - NVM image corruption / NIC config bricking. No kernel memory touched (FADDR device register; flash-controller region grants protect BIOS/ME regions), no local privilege or syscall needed. Diffed against FreeBSD stable/12+HEAD: every non-whitespace delta is an upstream addition DragonFly lacks; zero DragonFly-local logic edits.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3642 Β· 8 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
trace.sh β€” 1.2 KB view raw
trace.log β€” 2.4 KB view raw
guest_env.txt β€” 1.5 KB view raw
fix.diff β€” 1.1 KB view raw
verdict.json β€” 3.4 KB view raw
manifest.json β€” 1.1 KB view raw

DF-3642 β€” e1000_erase_flash_bank_ich8lan: flash address accumulates across erase retries; erase failures reported as success

What / where

sys/dev/netif/ig_hal/e1000_ich8lan.c:e1000_erase_flash_bank_ich8lan (lines 4692-4802), the ICH8/PCH hardware-sequencing flash bank erase used by every NVM (EEPROM-image) commit on em(4)-driven Intel GbE MACs (ICH8..I219). Three coupled defects in the retry loop:

  1. Address accumulation (4779): flash_linear_addr += (j * sector_size); sits inside the do { ... } while (++count < ICH_FLASH_CYCLE_REPEAT_COUNT) retry loop instead of the outer for (j ...) loop. Every retry after a flash-cycle error (flcerr β†’ continue) issues the next erase at a higher address: base + nΒ·jΒ·sector_size after n retries. With 256-byte sectors (iteration = 16) the drift reaches ~10Β·Ξ£jΒ·256 β‰ˆ up to tens of KB past the intended sector, i.e. erase commands against the wrong sectors of the flash (both NVM banks, and the tail of whatever the GbE flash-region grant covers).
  2. Retry budget never reset per sector (4798 / 4701): count is a function-scope variable; the 10-retry budget is shared across all j iterations, so later sectors can silently get zero retries.
  3. Unconditional success (4801): when the retry budget is exhausted via the flcerr path, the do/while simply falls through and the function returns E1000_SUCCESS even though the last erase failed; the caller then programs dwords into an un-erased bank (the subsequent write failures cascade through retry_write_flash_*).

How it is reached (no user syscall needed)

em_attach β†’ e1000_validate_nvm_checksum (if_em.c:781, retried at :787) β†’ hw->nvm.ops.validate = e1000_validate_nvm_checksum_ich8lan (e1000_ich8lan.c:4364) β†’ if the NVM image lacks the VALID_CSUM bit (:4395), it writes and updates the NVM (:4397-4400) β†’ e1000_update_nvm_checksum_ich8lan (:4214) β†’ e1000_erase_flash_bank_ich8lan(hw, bank) (:4246 / :4252).

So the erase engine runs at driver attach whenever the card's NVM content lacks the checksum-valid bit β€” a malicious/counterfeit PCIe GbE device, a bit-corrupted NVM, or a reflashed card is sufficient; no local privilege is required on the host.

Impact

Flash-content corruption outside the intended bank (wrong sectors erased β†’ invalid NVM image / bricked NIC config; bounded by the PCH flash-controller region grants, so the BIOS/ME regions stay protected on conforming parts), plus wasted write cycles into an un-erased bank. Availability/integrity of the device, not kernel memory. Severity: Low (hardware/NVM-content precondition).

Reproduce (source-level)

sh findings/poc/DF-3642/trace.sh        # regenerates trace.log with all cited lines
git apply --check findings/poc/DF-3642/fix.diff

Runtime reproduction requires physical Intel ICH8/PCH GbE MAC hardware whose flash reports cycle errors; the audit QEMU guest has only virtio NICs (see guest_env.txt), so the defect is proven at source level and confirmed byte-identical in FreeBSD HEAD (upstream-inherited).

Expected result after fix

Each erase command targets bank_base_addr + j*sector_size regardless of retry count; a sector whose retry budget is exhausted returns -E1000_ERR_NVM and aborts the commit instead of silently "succeeding".

VERDICT.md
↓ download raw

VERDICT β€” DF-3642

Status: not_reproduced (runtime) β€” defect certain (source), upstream-inherited.

What I did

  1. Read the full 6,117-line file and traced every caller chain into e1000_erase_flash_bank_ich8lan (see README.md chain: if_em.c:781/787 β†’ e1000_api.c:1270 β†’ e1000_ich8lan.c:4395-4403 β†’ :4214 β†’ :4246/4252).
  2. Proved the three code defects from the source (e1000_ich8lan.c:4779, :4701+:4798, :4801) β€” full extracted lines in trace.log.
  3. Verified DragonFly made no local edits here: diffed against FreeBSD stable/12 (/tmp/opencode/dfly_vs_fs12.diff) β€” all non-whitespace deltas are upstream additions DragonFly lacks (PCH tgp/adp/mtp/ptp cases, k0s config).
  4. Fetched FreeBSD HEAD (/tmp/opencode/fs_ich8lan.c, main branch 2026-09-15): the accumulation bug is still present at line 4931 β€” vendor-inherited and un-fixed upstream; a DragonFly-side fix is legitimately ours to contribute.
  5. Guest verification (guest_env.txt): em(4)/ig_hal is compiled into X86_64_GENERIC, but the guest has no Intel GbE MAC (virtio NIC only, pciconf -l shows i440FX bridge devices 0x1237/0x7000/0x7010/0x7113, none are e1000 LAN). em_attach never matches, so the flash-cycle code cannot execute on this guest. A hardware-in-the-loop rig (real I217/I218/I219 or an FPGA PCIe card emulating one with a flaky flash controller) would be required to observe the wrong-sector erase at runtime.

Why not false-positive

The control flow is unambiguous: the += executes on every pass of the inner do/while (including every continue retry), count is initialized once at declaration (:4701) and never reset inside the for (j ...) loop, and the only return paths before :4801 are (a) flash_cycle_init failure and (b) the !flcdone timeout β€” exhausting the budget via the flcerr/continue path falls through to return E1000_SUCCESS. No interpretation required.

Impact assessment (honest ceiling)

  • Not kernel memory corruption: all writes go to the device register FADDR; the flash controller enforces region grants, so the BIOS/ME regions are not reachable by the GbE master on conforming parts.
  • Realistic ceiling: NVM image corruption / NIC config bricking during an attach-time repair flow on hardware whose flash reports cycle errors (failing/counterfeit part), reachable from NVM content alone.
  • Precondition strength is why this is Low, not Medium: needs a specific physical device state, not a local user.

Fix

fix.diff (git-apply-able, git apply --check verified against the read-only tree): pin bank_base_addr once, recompute the address per attempt, reset the retry budget per sector, and propagate erase failure instead of returning success. Fix validation on hardware was not possible on this guest (fix_status: not_testable; baseline could not be reproduced either).

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

Not testable on this guest: baseline requires an Intel ICH8/PCH GbE MAC with a flash controller that reports cycle errors; the QEMU guest has none. fix.diff verified to apply cleanly against the read-only sys/ tree (git apply --check) and was reviewed against the erase state machine line-by-line.

findings/poc/DF-3642/fix.diff; git apply --check output recorded in VERDICT.md; guest_env.txt (hardware absent)
↓ fix.diffper-fix-DF-3642

Confirmed kernel references

Detail

Evidence (decisive lines)

trace.log (extracted cited lines incl. the accumulation and unconditional success); guest_env.txt (module-in-kernel + no e1000 hardware); fix.diff (git apply --check clean); VERDICT.md (full reasoning + upstream diff)

PoC changes

n/a - no seeded PoC; evidence pack built from scratch as a source-level trace + upstream-identity proof

Verified recommended fix

Pin the bank base address once and recompute flash_linear_addr = bank_base + j*sector_size per attempt; reset the retry counter per sector; propagate erase failure instead of returning E1000_SUCCESS.

Verdict

Source-certain defect, runtime-untestable on this guest. e1000_erase_flash_bank_ich8lan accumulates the flash erase address inside the per-attempt retry loop (e1000_ich8lan.c:4779), never resets the shared retry budget across sectors (:4701/:4798), and returns E1000_SUCCESS after retry exhaustion (:4801), so flash-cycle errors redirect erases to progressively wrong sectors and erase failures are reported as success; reached at driver attach from NVM content alone via em_attach -> e1000_validate_nvm_checksum -> update -> erase (if_em.c:781, e1000_ich8lan.c:4395-4403,4246/4252). Byte-identical in FreeBSD HEAD (vendor-inherited). The QEMU guest compiles em(4)/ig_hal into GENERIC but has no Intel GbE MAC (virtio only, pciconf verified), so no runtime trigger exists; hardware-in-the-loop required. No kernel memory impact - FADDR writes and flash-controller region grants bound the blast radius to NVM content (NIC config integrity/availability).