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
Open
AirportItlwm: reset the scan-result cursor on a new scan, and don't follow it into freed memory#1069gargamel778 wants to merge 2 commits into
gargamel778 wants to merge 2 commits into
Conversation
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
marked this pull request as ready for review
September 5, 2026 04:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
getSCAN_RESULT()walksic_treewith a cursor pair,fNextNodeToSend/fScanResultWrapping, that is only ever written insidegetSCAN_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:fScanResultWrappingis latched,setSCAN_REQ()refuses scan requests outright.getSCAN_RESULT()calls, after which the next call dereferences it and passes it toRB_NEXT().This PR resets the cursor on a scan request, and validates it against the tree before use.
Why — defect 1
ic_treeis ordered byieee80211_node_cmp()(ieee80211_node.c:3482), which ismemcmp(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:
AirportSTAIOCTL.cpp:1343(setSCAN_REQ) and:1381(setSCAN_REQ_MULTIPLE). The flag is set at:1449whenRB_NEXT()returns NULL, and cleared only at:1400, insidegetSCAN_RESULT(). A client that reads to the end and stops therefore leaves scan requests failing with EINVAL until some other client happens to callgetSCAN_RESULT().The cursor is not per-client on the controller paths. In
AirportItlwmit 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 instart()(fNetIf = new AirportItlwmSkywalkInterface,AirportItlwmV2.cpp:262). One cursor therefore serves every process issuing scan ioctls, and nothing serialises access to it — noIOLock, no command gate, no atomic anywhere in these handlers. OnlyItlNetworkUserClienthas a genuinely per-client cursor, being anIOUserClient.Why — defect 2
ieee80211_free_allnodes()walks the tree and frees each node viaieee80211_node_free()(ieee80211_node.c:1683), which isieee80211_node_cleanup()followed byfree(ni)— a real free, not a recycle list.fNextNodeToSendis never cleared at any free site, and no reference is taken on it (ieee80211_ref_node()appears zero times inAirportSTAIOCTL.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) andsetSCAN_REQ_MULTIPLE()(:1385) both callieee80211_begin_cache_bgscan(), which callsieee80211_free_allnodes(ic, 0)when the previous cache scan is more than five minutes old (ieee80211.c:147-149).ieee80211_clean_nodes()fromieee80211_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_ONLYis defined for every target inproject.pbxproj, andic_node_cache_timeoutis only armed forIEEE80211_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()clearfNextNodeToSendandfScanResultWrappinginstead 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 inic_tree(RB_FIND(...) == fNextNodeToSend) before using it, and restarts fromRB_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 atAirportSTAIOCTL.cpp:1400,1403,1449,AirportItlwmSkywalkInterface.cpp:993,996,1007andItlNetworkUserClient.cpp:372,375,399— every one insidegetSCAN_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
masteras a series and builds for the Ventura target.master.setSCAN_REQ()throughieee80211_begin_cache_bgscan()toieee80211_free_allnodes().AirportItlwm-Venturatarget, which is the build that loads on Darwin 25 here).RB_FIND()on everygetSCAN_RESULT()call is O(log n) per result. If there is a cheaper invalidation point at the free sites — clearing the cursor inieee80211_free_allnodes()/ieee80211_free_node()— that is likely the better fix and I would rather do it there.