Skip to content

AirportItlwm: reset the scan-result cursor on a new scan, and don't follow it into freed memory - #1069

Open
gargamel778 wants to merge 2 commits into
OpenIntelWireless:masterfrom
gargamel778:scan-cursor-fixes
Open

gargamel778 wants to merge 2 commits into
OpenIntelWireless:masterfrom
gargamel778:scan-cursor-fixes

Conversation

@gargamel778

@gargamel778 gargamel778 commented Sep 5, 2026

Copy link
Copy Markdown

Summary

getSCAN_RESULT() walks ic_tree with a cursor pair, fNextNodeToSend / fScanResultWrapping, that is only ever written inside getSCAN_RESULT() itself. Nothing resets it when a new scan is requested, and nothing invalidates it when the nodes it points at are freed. That produces two defects:

  1. A new scan resumes the previous enumeration instead of starting one, so results are silently incomplete; and while fScanResultWrapping is latched, setSCAN_REQ() refuses scan requests outright.
  2. The node the cursor holds can be freed between two getSCAN_RESULT() calls, after which the next call dereferences it and passes it to RB_NEXT().

This PR resets the cursor on a scan request, and validates it against the tree before use.

Why — defect 1

ic_tree is ordered by ieee80211_node_cmp() (ieee80211_node.c:3482), which is memcmp(ni_macaddr, IEEE80211_ADDR_LEN), i.e. BSSID order. A client that enumerates part of the list and stops leaves the cursor parked mid-tree, so the next enumeration begins at an arbitrary BSSID and omits every lower-addressed BSS until something drains the list to the wrap.

The latch compounds it. Both entry points open with the same guard:

if (fScanResultWrapping)
    return 22;

AirportSTAIOCTL.cpp:1343 (setSCAN_REQ) and :1381 (setSCAN_REQ_MULTIPLE). The flag is set at :1449 when RB_NEXT() returns NULL, and cleared only at :1400, inside getSCAN_RESULT(). A client that reads to the end and stops therefore leaves scan requests failing with EINVAL until some other client happens to call getSCAN_RESULT().

The cursor is not per-client on the controller paths. In AirportItlwm it is an ivar of the controller (AirportItlwm.hpp:259-260); in the Skywalk build it is an ivar of the single interface the controller allocates in start() (fNetIf = new AirportItlwmSkywalkInterface, AirportItlwmV2.cpp:262). One cursor therefore serves every process issuing scan ioctls, and nothing serialises access to it — no IOLock, no command gate, no atomic anywhere in these handlers. Only ItlNetworkUserClient has a genuinely per-client cursor, being an IOUserClient.

Why — defect 2

ieee80211_free_allnodes() walks the tree and frees each node via ieee80211_node_free() (ieee80211_node.c:1683), which is ieee80211_node_cleanup() followed by free(ni) — a real free, not a recycle list. fNextNodeToSend is never cleared at any free site, and no reference is taken on it (ieee80211_ref_node() appears zero times in AirportSTAIOCTL.cpp), so refcounting in the cleaners does not protect it.

The reachable path is the scan ioctl itself, not the node-cache timeout: setSCAN_REQ() (:1354) and setSCAN_REQ_MULTIPLE() (:1385) both call ieee80211_begin_cache_bgscan(), which calls ieee80211_free_allnodes(ic, 0) when the previous cache scan is more than five minutes old (ieee80211.c:147-149).

ieee80211_clean_nodes() from ieee80211_node_cache_timeout() is not a path here, and I mention it because my first write-up wrongly named it: that timeout sits inside #ifndef IEEE80211_STA_ONLY, IEEE80211_STA_ONLY is defined for every target in project.pbxproj, and ic_node_cache_timeout is only armed for IEEE80211_C_HOSTAP / IEEE80211_C_IBSS, neither of which iwm (mac80211.cpp:4787) or iwx (ItlIwx.cpp:13045) sets.

What changed

AirportSTAIOCTL.cpp, AirportItlwmSkywalkInterface.cpp, ItlNetworkUserClient.cpp:

  • setSCAN_REQ() / setSCAN_REQ_MULTIPLE() clear fNextNodeToSend and fScanResultWrapping instead of returning 22 while the flag is latched. A scan request starts a new enumeration, so the previous cursor is dropped rather than treated as a reason to refuse.
  • getSCAN_RESULT() checks the cursor is still in ic_tree (RB_FIND(...) == fNextNodeToSend) before using it, and restarts from RB_MIN() if it is not.

All three call sites carry the same cursor logic, so both changes are applied to each.

Verification

Read against master (53c51c2). Cursor writes appear at AirportSTAIOCTL.cpp:1400,1403,1449, AirportItlwmSkywalkInterface.cpp:993,996,1007 and ItlNetworkUserClient.cpp:372,375,399 — every one inside getSCAN_RESULT(), none in either scan-request handler.

Behaviour that prompted this: incomplete and erratic scan results on macOS Tahoe 26.6.2 with an Intel 7265, which cleared up with the cursor reset in place.

Test plan

  • Applies cleanly to master as a series and builds for the Ventura target.
  • Cursor write sites audited across all three call sites against pristine master.
  • Free path traced from setSCAN_REQ() through ieee80211_begin_cache_bgscan() to ieee80211_free_allnodes().
  • Runtime-tested on Intel 7265 / macOS Tahoe 26.6.2 / x86_64 (AirportItlwm-Ventura target, which is the build that loads on Darwin 25 here).
  • Use-after-free not observed firing. Defect 2 rests on code reading — no panic log, no KASAN, no instrumented build. A reviewer able to run a sanitised build, or to hold a cursor across a >5-minute idle and then issue a scan, could confirm or kill it.
  • Scan-completeness result is corroboration, not a controlled A/B. Other changes were in flight on the test machine; I did not isolate this patch.
  • Reviewer opinion wanted on the shape of the fix: RB_FIND() on every getSCAN_RESULT() call is O(log n) per result. If there is a cheaper invalidation point at the free sites — clearing the cursor in ieee80211_free_allnodes() / ieee80211_free_node() — that is likely the better fix and I would rather do it there.

fNextNodeToSend / fScanResultWrapping are an ic_tree cursor advanced only
inside getSCAN_RESULT().  Nothing resets them when a scan starts, so a
client that enumerates part of the list and stops leaves the cursor
parked mid-tree.  ic_tree is ordered by ieee80211_node_cmp(), i.e. by
BSSID, so the next enumeration begins at an arbitrary BSSID and silently
omits every lower-addressed BSS until someone drains the list to the
wrap.

The cursor is shared, not per-client, on both controller paths: in
AirportItlwm it is an instance variable of the controller itself, and in
the Skywalk build it lives on the single interface object the controller
allocates in start() (fNetIf = new AirportItlwmSkywalkInterface).  Either
way one cursor serves every process that issues scan ioctls, and nothing
serialises access to it -- there is no lock, command gate or atomic
anywhere in these handlers.  Only ItlNetworkUserClient, being an
IOUserClient, gets a genuinely per-client cursor.

Additionally, "if (fScanResultWrapping) return 22" rejected the scan
request outright while the flag was latched.  That guard is in both
setSCAN_REQ and setSCAN_REQ_MULTIPLE, and the flag is cleared only by
getSCAN_RESULT(), so a client that stops reading after the final result
wedges scan requests for every process on the system until some other
client happens to call getSCAN_RESULT() again.  Reset the cursor instead
of refusing.
…ache

ieee80211_free_allnodes() walks ic_tree and frees every node through
ieee80211_free_node(), which does RB_REMOVE(ieee80211_tree, &ic->ic_tree,
ni) and then genuinely frees the node -- ic_node_free is
ieee80211_node_free(), which is ieee80211_node_cleanup() followed by
free(), not a recycle list.  Nothing invalidates fNextNodeToSend, so a
cursor left parked between two getSCAN_RESULT() calls can point at freed
memory, which the next call reads and passes to RB_NEXT().

The reachable path is the scan request itself, not the node-cache
timeout: setSCAN_REQ() and setSCAN_REQ_MULTIPLE() both call
ieee80211_begin_cache_bgscan(), which calls ieee80211_free_allnodes(ic, 0)
whenever the previous cache scan is more than five minutes old.  So the
very ioctl that begins a new enumeration can free the nodes the cursor
still refers to.  (ieee80211_clean_nodes() from
ieee80211_node_cache_timeout() cannot reach this in these builds: that
timeout is inside #ifndef IEEE80211_STA_ONLY, IEEE80211_STA_ONLY is
defined for every target, and ic_node_cache_timeout is only armed for
IEEE80211_C_HOSTAP / IEEE80211_C_IBSS, which neither iwm nor iwx sets.)

Check that the cursor is still in the tree before using it, and restart
the enumeration from RB_MIN() if it is not.
@gargamel778
gargamel778 marked this pull request as ready for review September 5, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant