# CVE-2026-93796 — Root Cause Analysis: iwlwifi PCIe `iwl_pcie_rx_free()` double-free / use-after-free

## Summary

The Linux kernel iwlwifi PCIe transport frees its RX bookkeeping objects in
`iwl_pcie_rx_free()` (`drivers/net/wireless/intel/iwlwifi/pcie/gen1_2/rx.c`,
older trees: `pcie/rx.c`) but never invalidates the pointers on the still-live
`struct iwl_trans_pcie`. The function's own re-entry guard uses
`trans_pcie->rxq == NULL` as the "nothing allocated" sentinel, so after a first
free — performed on the nic-init error unwind when `iwl_pcie_tx_init()` fails
inside `iwl_pcie_nic_init()` — any later teardown or retry path
(interface down/up, driver unbind/rebind, reprobe, suspend/resume, error
recovery) that calls `iwl_pcie_rx_free()` again passes the guard and re-frees
`rx_pool`, `global_table`, `rxq` (kfree) and `alloc_page` (__free_pages),
producing a double-free, and dereferences the freed `rx_pool[]`/`rxq[]` arrays,
producing a slab use-after-free (CWE-416, with missing pointer invalidation
CWE-459).

## Impact

- Component: Linux kernel `iwlwifi` PCIe transport (`drivers/net/wireless/intel/iwlwifi/pcie/gen1_2/rx.c`).
- Affected versions: `< 6.12.111`; `6.13.0 – 6.18.53`; `6.19.0 – 7.1.x`.
- Fixed in: 6.12.111 (`6ec5bf430cf7466cb412e47f2761594092781fac`),
  6.18.53 (`3456c5bcc987aeb182e50e30c9b02be8420e9953`), upstream 7.2
  (`2c79d7a7b583050c9f58041465cb46fe3483ab5d`).
- Consequences: kernel memory corruption (double-free / slab use-after-free) in
  the WiFi driver teardown path — kernel crash (DoS) and a potentially
  exploitable heap corruption primitive on systems with Intel WiFi PCIe
  hardware. This run confirms memory corruption only; no code execution is
  claimed.

## Impact Parity

- Disclosed/claimed maximum impact: memory corruption (double-free / UAF) —
  KASAN-detectable; the advisory does not claim RCE.
- Reproduced impact this run: on the v6.18.52 KASAN+SLUB_DEBUG+DEBUG_OBJECTS
  kernel the second `iwl_pcie_rx_free()` produced (a) a KASAN
  `use-after-free` 8-byte read of the freed `rx_pool` in
  `iwl_pcie_free_rbs_pool` <- `iwl_pcie_rx_free` [iwlwifi], (b) a BAD_PAGE
  taint from the repeated `__free_pages(alloc_page)`, and (c) a kernel oops
  (NULL/wild dereference in `dma_free_attrs` <- `iwl_pcie_free_rxq_dma` <-
  `iwl_pcie_rx_free`) from walking the freed `rxq[]` state. The identical
  trigger on the fixed kernel completes cleanly 2/2 (negative control).
- Parity: `full` for the claimed memory-corruption impact class.
- Not demonstrated: code execution (out of scope per the advisory/ticket).

## Root Cause

`iwl_pcie_rx_free()` (v6.18.52, rx.c line 1204) ends with:

```c
	kfree(trans_pcie->rx_pool);
	kfree(trans_pcie->global_table);
	kfree(trans_pcie->rxq);

	if (trans_pcie->alloc_page)
		__free_pages(trans_pcie->alloc_page, trans_pcie->rx_page_order);
```

None of the four freed members are set to `NULL`, yet the function starts with:

```c
	if (!trans_pcie->rxq) {
		IWL_DEBUG_INFO(trans, "Free NULL rx context\n");
		return;
	}
```

The first call (nic-init unwind after a TX-init failure) frees everything but
leaves `rxq` non-NULL, defeating the sentinel. A second call on the same live
transport object walks the freed `rxq[]`/`rx_pool[]` arrays
(`iwl_pcie_free_rbs_pool()` reads `rx_pool[i].page`, the queue loop reads
`rxq->bd`/`rxq->napi.poll`) and then `kfree()`s the same three pointers again
and re-frees `alloc_page` — UAF reads plus double-frees.

The fix (upstream `2c79d7a7b583050c9f58041465cb46fe3483ab5d`, "wifi: iwlwifi:
pcie: null RX pointers after free", Emmanuel Grumbach, Intel) sets
`rx_pool`, `global_table`, `rxq` and `alloc_page` to `NULL` immediately after
freeing, so repeated cleanup and retry paths fail safe on the sentinel.

## Reproduction Steps

1. `bundle/repro/reproduction_steps.sh` (self-contained; see that file).
2. The script:
   - clones linux-stable `v6.18.52` (vulnerable) into the prepared project cache;
   - verifies `iwl_pcie_rx_free()` lacks the pointer-NULLing fix;
   - because QEMU cannot emulate an Intel WiFi PCIe device (explicitly
     anticipated by the ticket), patches a hardware-independent selftest trigger
     **into the real driver** (`pcie/gen1_2/rxfree-selftest.c` + a
     `rx_free_selftest=1` module-parameter hook in `iwl_drv_init()`): it builds
     RX state exactly as `iwl_pcie_rx_alloc()` does (kcalloc of
     `rxq`/`rx_pool`/`global_table`, `alloc_pages` for `alloc_page`, initialised
     RB-allocator work/lists) and then calls the **real, unmodified
     `iwl_pcie_rx_free()` twice** — first = nic-init unwind after
     `iwl_pcie_tx_init()` failure, second = later teardown/retry on the same
     live transport object;
   - builds a `defconfig`+KASAN(outline)+SLUB_DEBUG+DEBUG_OBJECTS kernel with
     `CONFIG_IWLWIFI=m`, producing a vulnerable `iwlwifi.ko`; then applies the
     stable fix hunk to `rx.c` and rebuilds a fixed `iwlwifi.ko` against the
     same kernel;
   - boots the kernel twice per build in QEMU/KVM with a busybox initramfs that
     `insmod`s the module with `rx_free_selftest=1`, capturing the serial log.
3. Expected evidence: vulnerable boots emit `BUG: KASAN: use-after-free` in
   `iwl_pcie_free_rbs_pool` <- `iwl_pcie_rx_free` (`[iwlwifi]`) followed by a
   BAD_PAGE taint and a fatal oops in `iwl_pcie_free_rxq_dma`; fixed boots
   print `SELFTEST-COMPLETE` with all four pointers NULLed and no KASAN splat.

## Evidence

- `bundle/logs/vm_vuln_attempt1.log`, `bundle/logs/vm_vuln_attempt2.log` —
  KASAN splats naming the RX teardown path (2/2 attempts).
- `bundle/logs/vm_fixed_attempt1.log`, `bundle/logs/vm_fixed_attempt2.log` —
  negative control, selftest completes cleanly (2/2 attempts).
- `bundle/logs/build_bzimage.log`, `bundle/logs/build_mod_vuln.log`,
  `bundle/logs/build_mod_fixed.log` — build transcripts.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest with
  artifact hashes and immutable target identity
  (`git:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git@8f3741e6feb045da5b406df0a80b42a1adfb289b` = v6.18.52).
- Environment: x86_64, QEMU/KVM (`-machine accel=kvm:tcg`), KASAN outline,
  SLUB_DEBUG, DEBUG_OBJECTS(_FREE/_TIMERS) enabled.

## Recommendations / Next Steps

- Backport/apply upstream `2c79d7a7b583050c9f58041465cb46fe3483ab5d`
  ("null RX pointers after free") — already in 6.12.111 / 6.18.53 / 7.2.
- Upgrade affected kernels to >= 6.12.111 / >= 6.18.53 / >= 7.2.
- Regression test: an in-tree KUnit or error-injection test that forces
  `iwl_pcie_tx_init()` failure and then re-runs teardown would lock the
  invariant "rx free is idempotent" in.

## Additional Notes

- Idempotency: the script is re-runnable; it restores `rx.c` to the vulnerable
  state, caches the bzImage and both module builds in the project cache, and
  re-executes fresh QEMU proofs on every run.
- Evidence scope: QEMU exposes no Intel WiFi PCIe device, so the trigger is a
  patched-in selftest that executes the **real** vulnerable function with
  faithful allocator state — matching the ticket's explicit fallback
  ("reproduce the unwind/retry sequence with a patched-in test trigger … state
  the scope honestly"). The double-free/UAF depends only on the pointer
  invalidation bug in `iwl_pcie_rx_free()`, not on hardware behavior: on real
  hardware the same two calls are reached via nic-init unwind + teardown/retry.
- Scope: memory corruption only; RCE is not claimed and was not attempted.
