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

static channel-pointer in bwn_phy_lp_b2063_switch_channel defeats the not-found NULL check after the first call β€” unsupported channels silently re-program the previous channel's RF values and return success

Summary

'static const struct bwn_b206x_chan *bc = NULL' persists across calls: after one successful switch, a later switch to a channel absent from the chantable leaves bc at the previous entry, the 'if (bc == NULL) return EINVAL' check never fires, the radio is programmed with the old channel's data, and 0 is returned; the caller records plp_chan = the new (never tuned) channel. The sibling b2062 function uses the correct non-static local. Wrong-frequency operation is a regulatory/interference hazard, not memory corruption. Today's net80211-validated channel set keeps it latent. Drop 'static'.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3836 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
verdict.json β€” 3.1 KB view raw
fix.diff β€” 409 B view raw
env.txt β€” 1.1 KB view raw
manifest.json β€” 697 B view raw

DF-3836 β€” static channel-table pointer in bwn_phy_lp_b2063_switch_channel keeps the previous entry on lookup failure β†’ radio silently tuned to the wrong channel

  • File: sys/dev/netif/bwn/bwn/if_bwn_phy_lp.c (pass 2)
  • Class: CWE-457 (stale state via static local) β†’ wrong-frequency operation; success returned for an unsupported channel
  • Severity: Low (reachability constrained: callers pass net80211-validated channel numbers, all of which are present in the table today; the bug turns any future/edge miss from a clean EINVAL into silent mistuning)
  • Upstream-inherited: FreeBSD head has the identical static const struct bwn_b206x_chan *bc in bwn_phy_lp_b2063_switch_channel (their line 815); note their b2062 variant correctly uses a non-static local (their line 954), as does DragonFly's (line 926).

Build / Run / Expected

Hardware-gated (env.txt). Mental test, source-level:

call 1: bwn_phy_lp_b2063_switch_channel(mac, 36)   -> bc = &table[17], OK
call 2: bwn_phy_lp_b2063_switch_channel(mac, 9x)   -> 9x not in table; loop ends without
          match; bc STILL &table[17] from call 1 (static!) -> NULL check passes ->
          radio reprogrammed for channel 36's RF values, returns 0 (success)

Expected on vulnerable code: bwn_phy_lp_switch_channel then records plp->plp_chan = chan (the unhandled one) and writes it to BWN_CHANNEL β€” the stack believes it is on the new channel while the RF is on the old one (regulatory/interference hazard, association failure).

Root cause

if_bwn_phy_lp.c:792-808:

    static const struct bwn_b206x_chan *bc = NULL;   /* STATIC */
    ...
    for (i = 0; i < N(bwn_b2063_chantable); i++) {
            if (bwn_b2063_chantable[i].bc_chan == chan) {
                    bc = &bwn_b2063_chantable[i];
                    break;
            }
    }
    if (bc == NULL)
            return (EINVAL);

The static keyword makes bc persistent across calls, so the bc == NULL failure detection only works on the very first invocation. The sibling function bwn_phy_lp_b2062_switch_channel uses a plain (non-static) const struct bwn_b206x_chan *bc (line 926) β€” the intended form.

VERDICT.md
↓ download raw

VERDICT β€” DF-3836

status: inconclusive (source-certain, hardware-gated) β€” reproduced: 0 β€” guest impact: none.

Defect demonstration (source-level)

bwn_phy_lp_b2063_switch_channel β€” if_bwn_phy_lp.c:792-808:

line 794: static const struct bwn_b206x_chan *bc = NULL; ← persistent storage lines 801-806: linear search; bc assigned only on match lines 807-808: if (bc == NULL) return (EINVAL); ← only detects first-call miss

Call sequence: switch(36) succeeds β†’ bc = &bwn_b2063_chantable[17] forever. Any later switch(chan βˆ‰ table): loop ends unassigned, bc != NULL, RF is programmed with channel 36's bc_data[] (lines 810-921), function returns 0. Caller (bwn_phy_lp_switch_channel:567-581) stores plp->plp_chan = chan and writes BWN_CHANNEL β€” divergent state.

Control: the b2062 sibling at line 926 uses const struct bwn_b206x_chan *bc = NULL; (no static) β€” the intended pattern; its bc == NULL check is sound.

Practical reachability

Callers pass ieee80211_chan2ieee(ic, ic->ic_curchan) (if_bwn.c:1911β†’4434) or the literal 7 (if_bwn_phy_lp.c:477, 1763). All net80211-regdomain channels currently exposed are present in the 58-entry table, so the stale path needs a future channel-set expansion or a half/quarter channel β€” hence Low. No memory-safety consequence (bc points into valid const data); impact is wrong-frequency operation.

Guest

No Broadcom wireless PCI device (env.txt); bwn never attaches; not exercisable.

Fix

Delete static (fix.diff, matches the b2062 form and FreeBSD's own b2062 function). git apply --check passes. Upstream FreeBSD carries the same static in its b2063 function (their line 815) and should receive the same 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

Requires LP PHY hardware and an out-of-table channel; absent on the audit guest (env.txt). fix.diff is git-apply-able (verified).

fix.diff
↓ fix.diffper-fix-DF-3836

Confirmed kernel references

Detail

Exploit chain

none (correctness/regulatory): unsupported channel request -> silently stays on previous RF frequency while the stack's channel state says otherwise; may transmit on a regulated band the operator did not select.

Evidence (decisive lines)

README.md (static-vs-non-static comparison 794 vs 926; caller 567-581); env.txt; fix.diff (drop 'static', git-apply --check passes)

PoC changes

No PoC β€” would require LP PHY hardware and a channel outside the chantable.

Verified recommended fix

Remove 'static' from the bc local in bwn_phy_lp_b2063_switch_channel (fix.diff); mirror upstream.

Verdict

bwn_phy_lp_b2063_switch_channel declares its lookup result as a static local: 'static const struct bwn_b206x_chan bc = NULL;' (sys/dev/netif/bwn/bwn/if_bwn_phy_lp.c:794). After any successful call, bc permanently holds the last matched table entry; on a later call with a channel absent from bwn_b2063_chantable the search loop (801-806) leaves bc unchanged, the 'if (bc == NULL) return (EINVAL);' failure check (807-808) is defeated, and the function programs the PREVIOUS channel's RF values (810-921) and returns 0. The caller bwn_phy_lp_switch_channel (567-581) then records plp->plp_chan = chan and writes the bogus channel to BWN_CHANNEL β€” net80211 believes the radio moved while the RF stayed on the old frequency (regulatory/interference and association-failure hazard, not memory corruption). The intended non-static form is proven by the sibling bwn_phy_lp_b2062_switch_channel which uses a plain 'const struct bwn_b206x_chan bc = NULL;' (line 926). Source defect is certain; practical reachability today is constrained because all callers pass net80211-validated channel numbers that exist in the table (if_bwn.c:1911 chan2ieee / if_bwn.c:4434), so no guest reproduction is possible (no Broadcom hardware, env.txt) and real-world triggering needs a channel outside the 58-entry table (e.g. future half/quarter-channel or exotic-regdomain additions). Upstream FreeBSD has the identical static at their line 815.