# DF-4956 VERDICT — REPRODUCED (kernel memory leak)

## Finding

`ath_hal_v4kEepromAttach()` (sys/dev/netif/ath/ath_hal/ah_eeprom_v4k.c:282-399)
mallocs `HAL_EEPROM_v4k` at :311 (`ath_hal_malloc` → `kmalloc(M_ATH_HAL,
M_INTWAIT|M_ZERO)`, ah_osdep.c:118-124; **1152 bytes** on this build) and has
three post-malloc error returns that neither free the object nor install it:

* :324 `return HAL_EEREAD;` — mid-image EEPROM read failure (loop at :318-326)
* :366 `return HAL_EEBADSUM;` — XOR checksum != 0xffff (check at :363)
* :385 `return HAL_EEBADSUM;` — EEPROM version nibble != AR5416_EEP_VER (14)

The eeprom becomes reachable only at :390-396
(`AH_PRIVATE(ah)->ah_eeprom = ee`, `ah_eepromDetach = v4kEepromDetach`), after
every error return. The caller (ar9285_attach.c:246-248 `ecode != HAL_OK →
goto bad` → `ah->ah_detach(ah)` at :393 → ar5416Detach
(ar5416/ar5416_attach.c:490-508) → `ath_hal_eepromDetach` macro
(ah_internal.h:474-477)) is a **no-op** when `ah_eepromDetach` was never
installed → the 1152-byte block is permanently orphaned. This is the third and
final member of the eeprom-attach leak family: DF-4873 (ar9287, ~1KB per
block), DF-4913 (v14, 4800B, verified live 600 blocks/2.93MB), DF-1995
(ar5210 v1). Both prior verdicts predicted this file has the identical bug —
it does.

## Reproduction (2026-09-25, guest DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC)

KLD PoC `v4kleak.c` calls the real in-kernel `ath_hal_v4kEepromAttach()`
(ath_hal compiled into the guest kernel; symbol verified via
`nm /boot/kernel/kernel` → `ffffffff8034f1a0 T ath_hal_v4kEepromAttach`) with
a synthetic zeroed `struct ath_hal_private` whose `ah_eepromRead` callback
serves crafted v4k-variant EEPROM images (image starts at word offset
`owl_eep_start_loc` == 64, per ah_eeprom_v4k.h:34-39):

| mode            | trigger                                   | return       | orphans |
|-----------------|-------------------------------------------|--------------|---------|
| EEREAD@324      | read callback fails at image word 5       | HAL_EEREAD   | 200/200 |
| EEBADSUM@366    | XOR checksum != 0xffff                    | HAL_EEBADSUM | 200/200 |
| BADVER@385      | checksum-valid, version nibble 0          | HAL_EEBADSUM | 200/200 |
| control         | fully valid image (version 0xE001)        | HAL_OK       | 0 (detach frees) |

Result (`vmstat -m`, malloc type `ath_hal`):

* baseline: 0 blocks → **600 blocks / 675K** after trigger; **unchanged after
  `kldunload`** (permanent — no back-reference to the orphaned block exists).
* run 2 (run.2.log): 600 → **1.17K blocks / 1.32M** (leak accumulates per
  attach round; 1152 B × 1200 ≈ 1.32 MB ✓).
* control contributed 200 transient allocations that were all freed via the
  installed `v4kEepromDetach` (ah_eeprom_v4k.c:267-274) — in-use unchanged by
  control, proving the malloc↔detach pairing is balanced and only the three
  error paths orphan.

See run.log / run.2.log / console.log in this directory.

## Impact ceiling

Unbounded kernel-heap exhaustion: 1152 B per failed attach. Trigger content is
attacker-chosen on the untrusted-EEPROM model (corrupt/reprogrammed EEPROM or
hostile ar9285 "Kite" card); every attach attempt fails the same way.
Repeatable without reboot via hotplug / devd re-probe / driver reload cycles —
same model as DF-4873/DF-4913 (physical access or crafted hardware). Not
remotely triggerable, not a memory-safety corruption: **impact = kernel memory
leak (DoS by exhaustion), Low severity**.

## Why it is not worse

The orphaned block is zeroed at malloc (M_ZERO) and never handed to any
consumer after the error return — no UAF, no info leak (block never copied
out; the diag export path v4kEepromDiag at :131-144 only runs after a
*successful* install), no overflow. Pure memory loss.

## Pass-2 negatives worth recording (v4k file beyond the family leak)

* read loop :318-326 writes exactly `sizeof(struct ar5416eeprom_4k)` = 376 B
  (188 words) into the first member of the 1152-B object — compile-time bound,
  not attacker-enlargable. No OOB.
* `v4kEepromReadCTLInfo` :232-262: writes ≤ 12×8 entries into
  `ee_rdEdgesPower[96]` (max index 91); `ctlIndex[12]`/`ctlData[12]` indexed
  only under `i < AR5416_4K_NUM_CTLS`. Bounded.
* `v4kEepromGetSpurChan` :197-205: `ix` bound is HALASSERT-only in this
  function, but both callers loop `i < AR5416_EEPROM_MODAL_SPURS`
  (ar5416_attach.c:671-672, ar9280_attach.c:610-611) — no attacker-reachable
  OOB.
* diag export :131-144 exports device-read + M_ZERO-zeroed data only — no
  uninitialized kernel memory disclosure (M_ZERO at ah_osdep.c:121-124).
* `eepromSwap` :147-195: loops bounded by AR5416_4K_MAX_CHAINS=1 /
  AR5416_EEPROM_MODAL_SPURS=5.

## Fix

`fix.diff` in this directory: `ath_hal_free(ee)` before each of the three
error returns (mirrors the DF-4873/DF-4913 family fix; `git apply --check`
clean against the pristine tree). Fix validation would require rebuilding the
guest kernel (ath_hal is compiled in, module symbol shadowing does not apply)
— not performed: the finding is a pure leak (no corruption) for which fix
validation is not mandated, and the PoC control path demonstrates the
free-on-error is exactly the installed-detach free.

## PoC changes vs the initial seed

Adapted from the DF-4913 v14 harness (same KLD/sysctl pattern, moduledata_t
requirement, `-I/usr/obj/usr/src/sys/X86_64_GENERIC` include). One bug fixed
during bring-up: the control-path kprintf dropped its `NFAIL` argument
(-Werror format failure, build.log). v4k-specific deltas: include
ah_eeprom_v4k.h (owl_eep_start_loc 64, not 256), NWORDS from
`struct ar5416eeprom_4k` (188), error-return line numbers 324/366/385,
control version 0xE001 (AR5416_EEP_VER == 0xE, ah_eeprom_v14.h:44).

## 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.
