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

NULL-deref panic in iwn_ampdu_tx_done(): TX_DONE on an aggregation queue with no bound session (sc->qid2tap[qid]==NULL)

Summary

iwn4965/5000_tx_done route any firmware TX_DONE whose qid >= firstaggqueue into iwn_ampdu_tx_done, which immediately dereferences sc->qid2tap[qid] (tap->txa_tid, tap->txa_ni). qid2tap[qid] is NULL outside an active TX aggregation session (cleared at teardown), so a stale or forged TX_DONE for an aggregation queue NULL-derefs -> kernel panic. Sibling of DF-1122 in the TX_DONE path, which pass 1 did not cover. Malicious/compromised firmware (the driver's stated trust boundary) or a narrow teardown race. Untested (needs device+hostile fw); source-certain analysis + fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3579 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
fix.diff β€” 505 B view raw
verdict.json β€” 2.3 KB view raw
manifest.json β€” 549 B view raw

DF-3579 β€” NULL-deref panic in TX_DONE path: iwn_ampdu_tx_done() dereferences sc->qid2tap[qid] without NULL check

Root cause (source-verified)

iwn4965_tx_done() / iwn5000_tx_done() dispatch on a firmware-supplied queue id:

qid = desc->qid & 0xf;                              /* if_iwn.c:3587 / 3617 */
...
if (qid >= sc->firstaggqueue) {
        iwn_ampdu_tx_done(sc, qid, desc->idx, stat->nframes,
            stat->ackfailcnt, &stat->status);        /* if_iwn.c:3601 / 3636 */

iwn_ampdu_tx_done() then does:

tap = sc->qid2tap[qid];                             /* if_iwn.c:3765 */
tid = tap->txa_tid;                                 /* if_iwn.c:3766 β€” NULL deref */
wn = (void *)tap->txa_ni;                           /* if_iwn.c:3767 */

sc->qid2tap[qid] is non-NULL only while a TX aggregation session is bound to that queue (set in iwn_addba_request, if_iwn.c:7440; cleared in iwn_addba_response failure path if_iwn.c:7463, iwn_ampdu_tx_stop if_iwn.c:7532, and from iwn_rx_compressed_ba/iwn_ampdu_tx_done themselves at if_iwn.c:3303/3874). Nothing re-validates the pointer when a TX_DONE for an aggregation queue arrives after the session was torn down (or if the firmware emits TX_DONE for a queue the driver never aggregated).

Same family as DF-1122 (Compressed-BA path), but in the TX_DONE path, which pass 1 did not cover.

Impact

Kernel NULL-deref panic. Attacker = malicious/buggy firmware (the driver's explicit trust boundary per the audit threat model), or a narrow teardown race where the firmware emits a late TX_DONE after iwn_ampdu_tx_stop() clears qid2tap[qid] (that path requires txq[qid].queued == 0, so in-flight completions are normally exhausted β€” hence likely, not certain, for benign firmware).

Status

untested β€” requires an iwn device plus malicious/compromised firmware or a won teardown race; the QEMU audit guest is virtio-only (no Intel WiFi PCI device). Source-level analysis only. No PoC authored (a userland model of a NULL deref proves nothing beyond the source).

Fix (sketch β€” see fix.diff)

tap = sc->qid2tap[qid];
if (tap == NULL)
        return;                 /* stale/unsolicited TX_DONE for agg queue */
VERDICT.md
↓ download raw

DF-3579 β€” VERDICT

Status: untested. No runtime reproduction possible: the bug needs an iwn device and either malicious firmware or an aggregation-teardown race; the audit guest is virtio-only with no Intel WiFi hardware.

Analysis (source-certain, trigger-speculative)

The dispatch chain is: - iwn_notif_intr() (if_iwn.c:3934-3937) β†’ ops->tx_done - iwn4965_tx_done() if_iwn.c:3587-3606 / iwn5000_tx_done() if_iwn.c:3617-3641: qid = desc->qid & 0xf (bounded 0..15 β€” in range for both 16- and 20-queue variants, so this index is safe), then for qid >= sc->firstaggqueue β†’ iwn_ampdu_tx_done(sc, qid, ...). - iwn_ampdu_tx_done() if_iwn.c:3765-3768 dereferences sc->qid2tap[qid] (tap->txa_tid, tap->txa_ni) with no NULL check.

qid2tap[qid] lifetimes: set only in iwn_addba_request (if_iwn.c:7440); cleared in iwn_addba_response failure (7463), iwn_ampdu_tx_stop (7532), iwn_rx_compressed_ba (3303) and iwn_ampdu_tx_done (3874) β€” after which the queue is a plain queue again, yet a firmware TX_DONE stamped with that qid (>= firstaggqueue) would still be routed into iwn_ampdu_tx_done and NULL-deref. With benign firmware this requires a stale completion racing the teardown (the stop path waits for queued == 0, so the window is narrow); with hostile/compromised firmware (the driver's stated trust boundary, same model as DF-1122) it is a trivially forgeable notification.

Consequences: NULL pointer read at tap->txa_tid β†’ kernel panic (DoS). No memory-corruption primitive (the deref is a read of a NULL-adjacent fixed offset).

Confidence: likely (code path certain; trigger needs malicious firmware or a race that benign firmware makes unlikely).

Recommendation

Apply fix.diff (bound + NULL check at the top of iwn_ampdu_tx_done). The same guard pattern should be applied to iwn_rx_compressed_ba as part of the DF-1122 fix.

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

fix.diff authored (bound + NULL guard) but not validated: no runtime reproduction exists on this guest (no iwn hardware).

findings/poc/DF-3579/fix.diff
↓ fix.diffper-fix-DF-3579

Confirmed kernel references

Detail

Exploit chain

malicious/buggy firmware emits TX_DONE with qid in [firstaggqueue, 15] while no aggregation session is bound -> iwn_ampdu_tx_done reads tap->txa_tid through NULL -> kernel panic (DoS).

Evidence (decisive lines)

VERDICT.md (full source trace), fix.diff (guard), README.md.

PoC changes

none β€” no PoC authored; a userland model of a NULL deref adds nothing over the source trace, and the in-kernel path needs absent hardware.

Verified recommended fix

Add 'if (qid >= sc->ntxqs || sc->qid2tap[qid] == NULL) return;' at the top of iwn_ampdu_tx_done (fix.diff).

Verdict

NULL-deref in iwn_ampdu_tx_done(): iwn4965_tx_done/iwn5000_tx_done route any TX_DONE whose firmware qid >= firstaggqueue into iwn_ampdu_tx_done, which dereferences sc->qid2tap[qid] (if_iwn.c:3765-3768) without a NULL check; the pointer is NULL outside an active TX aggregation session (cleared on teardown at if_iwn.c:7463/7532/3303/3874). A stale or forged TX_DONE for an aggregation queue panics the kernel. Untestable on the virtio-only audit guest (no iwn hardware); trigger requires malicious firmware (driver's stated trust boundary, same as DF-1122) or a narrow teardown race with benign firmware β€” hence likely, not certain.