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)
PoC verification
Evidence pack
findings/poc/DF-3642 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- Address accumulation (4779):
flash_linear_addr += (j * sector_size);sits inside thedo { ... } while (++count < ICH_FLASH_CYCLE_REPEAT_COUNT)retry loop instead of the outerfor (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). - Retry budget never reset per sector (4798 / 4701):
countis a function-scope variable; the 10-retry budget is shared across alljiterations, so later sectors can silently get zero retries. - Unconditional success (4801): when the retry budget is exhausted via the
flcerrpath, thedo/whilesimply falls through and the function returnsE1000_SUCCESSeven though the last erase failed; the caller then programs dwords into an un-erased bank (the subsequent write failures cascade throughretry_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 β DF-3642
Status: not_reproduced (runtime) β defect certain (source), upstream-inherited.
What I did
- 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). - Proved the three code defects from the source (
e1000_ich8lan.c:4779,:4701+:4798,:4801) β full extracted lines intrace.log. - 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). - 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. - 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 -lshows i440FX bridge devices 0x1237/0x7000/0x7010/0x7113, none are e1000 LAN).em_attachnever 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_testableNot 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)
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).
No comments yet.