MtkWlanNative.kext is an IO80211Controller subclass (plus an IO80211InfraProtocol interface) over the MtkWlan core,
so macOS treats the MT7925 as Wi-Fi: the System Settings pane, the menu bar, scan and join from the UI. The core keeps
the radio, the firmware, the join machines and WPA2; the front end translates between Apple's private Wi-Fi family and
the core's C ops table (../MtkWlan/MtkCoreApi.h, version 3). Plan: ~/.claude/plans/memoized-spinning-rossum.md.
Design reference: ga403wm-hackintosh kexts/itlmt7925Native (BSD-3-Clause-Clear), which carried traffic on Sequoia 24H32.
Status 2026-10-17: stage 0 done (tooling, headers, a kext that builds and passes the slot and link checks), nothing loaded.
| File | What |
|---|---|
abi/<build>/*.h, iface_stubs.inc, abi.json |
generated by tools/kcabi/genhdr.py from abi/native-wifi/<build>/: slot-exact class declarations, the 200 request-handler stubs, the sizes for the runtime guard. Never edited by hand. |
MtkNativeAbi.hpp |
the runtime guard: kernel version string must contain an allow-listed xnu tag, every guarded class size must match OSMetaClass::getClassSize(); else probe() declines and the core starts its Ethernet front end |
MtkWlanNative.{hpp,cpp} |
the controller: work queue before the base start, CoreCapture logger and fault reporter, media dictionary, the request allow-list (isCommandProhibited), the static answers (versions, capabilities, power, country), interface creation |
MtkWlanNativeIf.{hpp,cpp} |
the interface: role 1, one pool + four Tx submission + Tx/Rx completion queues registered with registerInfraEthernetInterface, the data path bridge, link state to the Family |
MtkWlanNativeReq.cpp |
request handlers with Apple's payload layouts (channel, rate, RSSI, op mode, supported channels, ...) and the generated stubs for everything else |
MtkNativeTrace.{hpp,cpp} |
MtkNativeTrace / MtkNativeTraceTail / MtkNativeLast registry properties (IOLog does not reach the unified log here) and MtkNativeCalls (which requests and slots userland reaches) |
corecapture_min.h, skywalk_min.h |
the exported factories of the opaque CoreCapture and IOSkywalk classes the kext calls |
build-mac.sh |
regenerate headers, compile, link, allow-list from abi.json, then the checks: kext bundle, no C++ runtime, genhdr.py check-native (both classes against Apple's concrete classes), linkcheck.py against the running kernel collection |
| Where | Key | Meaning |
|---|---|---|
Info.plist personality |
MtkNativeEnabled |
false: probe() declines (boot-arg `mtknative=0 |
Info.plist personality |
MtkNativeInterface |
true: the controller also creates the interface and takes the data path (stage B+); false: controller only (stage A). Boot-arg `mtknativeif=0 |
Info.plist personality |
MtkNativeAbi.Builds |
the allow-list: Build, Xnu tag, Sizes per guarded class; one entry per verified build, written by build-mac.sh / refresh.py |
core Info.plist |
MtkFrontEnd |
ethernet (default) or native: with native the core waits for this kext and falls back to MtkWlanEth after a decline or 15 s. Boot-arg mtkfe= overrides |
The two must agree for stage B and later: MtkFrontEnd native and MtkNativeInterface true. Stage A is MtkFrontEnd
ethernet and MtkNativeInterface false (Wi-Fi unchanged through en1, the controller loads beside it).
getWorkLoop()answers the core's loop untilIO80211Controller::starthas returned, then theIO80211WorkQueuecreated before it (IONetworkController::startotherwise callsml_thread_policyon the queue's NULL thread; the Family's Glue later checks that the BSD client's gate sits on the queue).- The core calls back with its gate held; the callbacks only copy (RX into a 512-frame ring) or flag (link, tx space,
scan done, join result). A 2 ms timer on the work queue drains the ring into pool packets, completes sent packets,
and every 10 ms applies the flags (
pollControl). Calls into the core (tx, set_enabled, set_mode, later scan / join) go throughcoreLoop->runAction. So the work queue gate may wait for the core gate, never the reverse.
ioreg -r -c MtkWlanNative -l -w0 | grep -E 'MtkNative|IO80211' (trace, calls, build, rejected count),
ioreg -r -c MtkWlanNativeIf -l -w0 | grep MtkNative (rx/tx counters, datapath, radio link, trace of the interface),
ioreg -r -c MtkWlan -l -w0 | grep -E 'MtkFrontEnd|MtkNative' (what the core saw: wanted, active, fallback, declined),
kmutil showloaded | grep -i mtk, networksetup -listallhardwareports, system_profiler SPAirPortDataType.
~/.venvs/legion/bin/python -I tools/kcabi/refresh.py on the updated system: see docs/native-wifi.md.
Slot 470 setMacAddress(mloAddrArray&) takes the first six bytes of the array and hands them to the core through
mtk_core_ops_v4::set_mac (gatedSetMac on the core loop); the Family applies the address to the BSD interface only
on success. A v3 core (MtkWlan 0.10.0) still binds (callbacks version 3) and the call answers not-ready. Trace lines
if:setMacAddress xx:xx:xx:xx:xx:xx -> rc; MtkMacChanges / MtkMacChangeRefused on the core. See docs/native-wifi.md.
setWCL_SCAN_REQ (slot 603) starts the core's scan (gatedScan, cache answered when the core refuses) and records the
requested SSID filter; serviceScan (10 ms poll) delivers on the core's scan_done, at once for a refused scan, or after
25 s: gatedSnapBss copies the cached table under the core gate, buildBeacon writes the 25G241 message (header 0x44,
flags at +0x40) and postEvent posts 0xc9 per BSS and 0xed. setWCL_SCAN_ABORT (598) posts 0xed. Property MtkNativeScan,
traces scan:request, scan:delivered; MtkNativeCalls P:c9 / P:ed count the posts.
N-139 showed stage C never ran: MtkNativeCalls had I:1b9=32 and no S:25b. On 25G241 every
apple80211setWCL_* / getWCL_* dispatcher first calls isCommandProhibited(<ioctl code>) on the interface (vtable
word 0xcc8) and only on 0 casts to IO80211InfraProtocol and calls the handler slot. commandAllowed() in
MtkWlanNative.cpp listed the stage A/B codes only, so the scan request got ENOTSUP 32 times. 0.3.1 admits 0x1b9
(WCL_SCAN_REQ) and 0x1b3 (WCL_SCAN_ABORT). Each later handler needs its code admitted too (table in
docs/native-wifi.md, "WCL ioctl codes").
setWCL_ASSOCIATE (slot 604) turns the apple80211AssocCandidates into a core join (SSID length +0x1c, SSID +0x20; an
apple80211_key at +0x40: key length +0x44, cipher +0x48, key +0x50, all confirmed in AppleBCMWLANCore::setWCL_ASSOCIATE
and AppleBCMWLANJoinAdapter::performJoin on 25G241; key length 0 = open, cipher 6..9 with 32 bytes = PMK). The result goes
back as 0xd8 link status (16 bytes, only this takes WCLNetManager out of LINK_DOWN), 0x4e, 0xd3 (0x1c), 0xd4 (0x14), 0xd5
(0xa4); the join manager checks exactly those sizes. getWCL_BSS_INFO (526) answers the associated BSS from the core's
cache as a beacon message, getWCL_EXTENDED_BSS_INFO (533) the 0x214-byte 25G241 layout (rates +0, HT/VHT/HE MCS +0xbc/
+0xcc/+0xd4, MLO context +0xdc, WPA/RSN element +0x113). setWCL_LEAVE_NETWORK (592) and setWCL_JOIN_ABORT (601) call the
core's disconnect; getWCL_LOW_LATENCY_INFO (525) answers 0 (a nonzero answer aborts every join); 468, 606, 602, 607,
605, 600, 594, 628, 635 and 527 are accepted and counted. A link the core drops is reported with 0xd8 down (reason 1); a
link the core restores by itself is announced again. bindCore now sets the front-end mode without
MTK_FE_KEEP_AUTOOPEN: macOS drives every join (the guard asks for it with networksetup -setairportnetwork at stages
D/E), mtkwifi join answers busy. Property MtkNativeJoin; traces assoc:*, wcl:*. The allowlist admits the WCL codes
0x1ab 0x1ba 0x1b6 0x1a9 0x1b7 0x1b1 0x1cc 0x1b2 0x1bc 0x1c6 0x1be 0x1f6 0x1bb 0x1b5 0x1ac 0x1eb. Sequence and event
layouts follow ga403wm itlmt7925Native (BSD-3-Clause-Clear), re-read against the 25G241 handlers.
N-141 showed the sequence the 25G241 Family runs for a join request from airportd: a directed scan, "Will associate"
with a per-network private MAC, GET 0x1ab WCL_LOW_LATENCY_INFO, SET 0x170 SET_MAC_ADDRESS (the private address;
IO80211SkywalkInterface::setSET_MAC_ADDRESS, Family code that ends in our slot 470 setMacAddress), then SET 0x1ba
ASSOCIATE. 0x170 was refused by the allowlist, so ASSOCIATE failed with 58 and airportd retried forever (I-064). 0.4.1
admits 0x170 and 0x22d (DBRG_ENTROPY, slot 662, new on 25G241; the bytes are accepted and dropped). setMacAddress
answers NotPermitted when the firmware refuses an address: WCLJoinManager tolerates success and NotPermitted only.
Stage C is proven end to end by the same log: airportd parsed the 48 scan results (security, channel, phy, rssi).
N-142 ran the whole join from macOS: 0x170 (slot 470, the private address applied to the firmware), ASSOCIATE, the five
events, then the Family's link-up requests, all answered; macOS saved the network and auto-joined it on the next boots.
What kept apps offline was the captive portal (Boingo, WISPr): the private address is a new client to it, so configd ranked
en1 Never until the Captive Network Assistant is passed (I-065), a user matter as on a real Mac. 0.4.2 only single-sources
the version string (MTK_NATIVE_VERSION, used by the MtkNativeVersion property and getDRIVER_VERSION, which the Family
shows as "Firmware Version"; 0.4.1 still said 0.1.0 there).
N-143 panicked in CCDataPipe::enqueueBlob when the Family completed a fault report (I-066): the data pipe's notify timer
is created in CCDataPipe::start, and the factories (CCPipe::withOwnerNameCapacity, CCStream::withPipeAndName) only
alloc and init. On 25G241 nothing else starts a pipe that no catalogue personality matches (ours was "!registered,
!matched" in the registry). 0.4.3 calls IOService::start(owner) on the fault data pipe, its stream and the log pipe right
after creating them (the pipe then creates its timer on its work loop, publishes the CCPipe resource and registers
itself), traced as ctl:fault pipe start=1. The pointer cast is safe: both classes have IOService as their only base.
N-144: starting the CoreCapture objects before the interface bound the core took the native start past the core's 15 s
front-end fallback (FE_FALLBACK_TICKS), so the Ethernet front end won. 0.4.4 starts them after mk:startWithInfo ok.
N-145 passed stage E (native interface, SSID, real default route after the captive portal), but the Family left and
re-joined every 15 to 60 s (I-067): it asked for channels 52, 11, 108 in turn and the core always joined its own pick
because setWCL_ASSOCIATE zeroed the BSSID. 0.4.5 reads the candidate list (WCLJoinRequest::addAssocCandidates:
u32 count at +0x214, entries of 18 bytes from +0x21c, BSSID at +6, u16 rssi at +12) and passes the first candidate's
BSSID to the core. Published as watchthelight-labs/MtkWlanNative, release 0.4.5 (pre-release until N-146).
0.4.6 and 0.4.7 (N-147, N-148): the candidate BSSID offset, and beacons the Family wants to hear about
N-146 showed the candidate's BSSID at +0x21c (0.4.5 read +0x222); 0.4.6 joins the right AP (N-147). The Family still
left about 60 s after every join: corecapture logged the fault "missed beacons timeout", raised by
WCLNetManager::handleMissedBeacons (reportFault, then leaveNetworkCommand; airportd records BeaconLoss). Its input
is event 0xaf, WCLNetManager::updateBeaconCounter: it adds the u32 at payload +0x54 to a counter and sets a flag that
the periodic check clears; nothing arrives, the fault fires. 0.4.7 posts 0xaf (0x58 bytes, count at +0x54) once a second
while the link is up, as long as the core's cache entry for the associated BSS was refreshed in the last 3 s (the core
updates it on every beacon or probe response). MtkNativeJoin shows beaconreports.
0.4.8 posted the beacon counter (0xaf) while the core holds the link; N-149 still lost the link with BeaconLoss 60 s after
each join. WCLNetManager::assocTimerAction (every 5 s, setTimeoutMS(5000)) forces a BSS info refresh every 60 s, runs
checkTrafficCounters (0x1b2) and calls handleMissedBeacons when the net manager's timestamps +0x150 and +0x158 are both
older than 60001 ms; the first call reports the CoreCapture fault "missed beacons timeout", the next one leaves. Both
timestamps are refreshed only by handleLqmUpdate, event 0x27 with exactly 0x1dc bytes (see the comment in
MtkWlanNativeReq.cpp for the fields). The beacon counter 0xaf feeds a different counter. 0.4.9 posts 0x27 once a second
next to 0xaf: valid flag, RSSI, a growing counter pair at +0x14/+0x1c, frames received at +0x24, beacons at +0x28.
Ten quiet minutes on N-150: one join, zero leaves, zero link drops, one 0x27 and one 0xaf per second, lease, unscoped
default route, internet. Released as 0.4.9 (latest) on mt7925-hackintosh-wifi-native; OCEFI promoted to the USB stick.
Left for later: MtkNativeJoin is refreshed only on a join (beaconreports shows 0 between joins), MtkNativeInterface
on the controller stays No, Country Code XZ, WPA2-PSK through the menu untested on air, no roaming (the Family asks for
one AP and gets it), sleep, AWDL.