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

iwm phy_db: latent heap OOB read in channel_id_to_txp() β€” entry size never checked before reading txp_chg->max_channel_idx (path currently dead in-tree)

Summary

channel_id_to_txp reads the 6-byte packed header from every TXP group entry after only a NULL check - set_section accepts lengths 0..5 and kmalloc(0) returns non-NULL in DFly, so a short entry reads up to 6 bytes past the object. Dead in-tree (only CFG/NCH call the get_section chain, never CHG - exhaustive call-graph proof) but becomes a live hostile-fw OOB read the moment the CHG lookup is wired as in newer iwlwifi. size>=sizeof(header) skip fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4817 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
verdict.json β€” 2.5 KB view raw
fix.diff β€” 460 B view raw
manifest.json β€” 1.0 KB view raw

DF-4817 β€” iwm phy_db: latent heap OOB read in channel_id_to_txp() β€”

entry length never checked before reading txp_chg->max_channel_idx

File

sys/dev/netif/iwm/if_iwm_phy_db.c (pass 2, GLM 5.3 re-audit)

Class

CWE-125 (out-of-bounds read), latent / currently unreachable in-tree (hardening finding, Info severity).

The defect

channel_id_to_txp() (if_iwm_phy_db.c:372-393) walks the TXP channel-group entries:

for (i = 0; i < phy_db->n_group_txp; i++) {
        txp_chg = (void *)phy_db->calib_ch_group_txp[i].data;
        if (!txp_chg)
                return 0xff;                      /* NULL check only   */
        if (le16toh(txp_chg->max_channel_idx) >= ch_index)   /* OOB read */
                return i;
}

It validates only that data != NULL, never that calib_ch_group_txp[i].size >= sizeof(struct iwm_phy_db_chg_txp) (6 bytes: uint32_t space; uint16_t max_channel_idx; β€” if_iwmreg.h:2298-2301, packed). A firmware that sent a CHG_TXP section with length in [0..5] produces a kmalloc(size) entry (kmalloc(0) is a legal allocation in DragonFly β€” kern_slaballoc.c handles the degenerate size==0 case and returns a non-NULL pointer) whose max_channel_idx read runs past the allocation by up to 6 bytes. The NULL check passes because the pointer is non-NULL.

Why it is latent (reachability analysis)

channel_id_to_txp() is static and has exactly one caller: iwm_phy_db_get_section_data() (if_iwm_phy_db.c:410), invoked when type == IWM_PHY_DB_CALIB_CHG_TXP. iwm_phy_db_get_section_data() itself is static with exactly two call sites β€” if_iwm_phy_db.c:506 (IWM_PHY_DB_CFG) and :523 (IWM_PHY_DB_CALIB_NCH), both in iwm_send_phy_db_data(). Neither passes a CHG type, so the TXP lookup path (including this read) is dead code in this tree today. It becomes live the moment anyone wires iwm_phy_db_get_section_data() for channel-group types (e.g. porting channel-selection flows from newer iwlwifi, where this helper is used for per-channel TXP retrieval).

Build / run

No PoC possible: hardware-gated (see DF-4816 pack β€” guest has no Intel Wireless device) and the buggy path is unreachable in the current tree. Filed as Info: hardening/latent memory-safety defect with a one-line fix.

Fix

See fix.diff β€” size-check each entry before reading the header.

VERDICT.md
↓ download raw

DF-4817 β€” VERDICT

Status: untested (latent defect, unreachable in-tree; hardware absent from audit guest) β€” code-level proof certain by inspection

Proof of the defect (static)

  1. struct iwm_phy_db_chg_txp is 6 bytes packed (uint32_t space; uint16_t max_channel_idx; β€” sys/dev/netif/iwm/if_iwmreg.h:2298-2301). max_channel_idx sits at offset 4.
  2. Entries are populated solely by iwm_phy_db_set_section() (if_iwm_phy_db.c:312-321): entry->data = kmalloc(size) with size = firmware-controlled phy_db_notif->length (uint16, used verbatim β€” only the source bounds are DF-1898's problem; the destination is exactly size bytes), and entry->size = size. A length of 0..5 is accepted and yields a valid non-NULL allocation smaller than the 6-byte header (kmalloc(0) returns a valid pointer in DragonFly: kern_slaballoc.c "Handle the degenerate size == 0 case").
  3. channel_id_to_txp() (if_iwm_phy_db.c:381-390) reads txp_chg->max_channel_idx from every entry after checking only !txp_chg β€” an allocation of 0..5 bytes passes the NULL check and the 2-byte load at offset 4 reads out of bounds (up to 6 bytes past the object).

Reachability (why Info, not Low/Medium)

  • Call graph, exhaustively (grep over the whole tree): channel_id_to_txp ← iwm_phy_db_get_section_data (if_iwm_phy_db.c:410, only caller) ← iwm_send_phy_db_data (if_iwm_phy_db.c:506 IWM_PHY_DB_CFG, :523 IWM_PHY_DB_CALIB_NCH, only call sites). No caller passes IWM_PHY_DB_CALIB_CHG_TXP/CHG_PAPD, so the defective loop cannot execute in this tree as shipped.
  • Both functions are static to if_iwm_phy_db.c β€” no external caller can reach them either.
  • Impact if ever wired up: hostile-firmware heap OOB read of ≀6 bytes per entry, value compared against a channel index β€” a low-value oracle; not a write primitive, not controllable beyond fw.

Why no runtime test

Same hardware constraint as DF-4816 (guest pciconf -l: no Intel Wireless 7265/8000+ device; no iwm module), compounded by the path being dead code today. No syscall-reachable injection point exists.

Fix validation

fix.diff adds the missing entry-size check (skip entries smaller than sizeof(struct iwm_phy_db_chg_txp)). Not build/run-testable on the guest for the reasons above β€” fix_status = not_testable. Diff is trivially compile-safe (type already visible at that point via if_iwmreg.h).

Bottom line

Real but latent memory-safety defect (missing length validation before a fixed-offset struct read) on a path that is currently dead in-tree; one-line fix removes it permanently. Filed at Info severity so it is on record before anyone revives the CHG lookup path.

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 adds the missing entry-size check. Not testable on the audit guest: no iwm hardware, and the defective loop is unreachable in the current tree (no caller passes a CHG type).

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

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (static proof: struct layout, population path, exhaustive call graph); README.md; fix.diff (entry size check)

PoC changes

No seed PoC; no PoC possible (hardware-gated + dead code path).

Verified recommended fix

In channel_id_to_txp, skip entries whose size is smaller than sizeof(struct iwm_phy_db_chg_txp) before reading max_channel_idx

Verdict

Latent heap out-of-bounds read (Info severity, hardening). channel_id_to_txp() reads txp_chg->max_channel_idx (offset 4, 2 bytes) from every TXP group entry after checking only entry->data != NULL (if_iwm_phy_db.c:381-390); iwm_phy_db_set_section() accepts firmware lengths of 0..5 bytes, producing allocations smaller than the 6-byte struct iwm_phy_db_chg_txp header (if_iwmreg.h:2298-2301), so the read runs up to 6 bytes past the object (kmalloc(0) returns a valid non-NULL pointer in DragonFly). Currently unreachable: channel_id_to_txp's only caller iwm_phy_db_get_section_data is invoked only with IWM_PHY_DB_CFG (if_iwm_phy_db.c:506) and IWM_PHY_DB_CALIB_NCH (:523) - never a channel-group type - and both functions are static to this file. The defect becomes a live hostile-firmware OOB read the moment the CHG lookup path is wired up (as in newer iwlwifi). No runtime test possible: guest lacks Intel Wireless hardware (verified) and the path is dead code in-tree.