From 3f3e37ce46283c98360f3f8e4be621226d662b4c Mon Sep 17 00:00:00 2001 From: Erwan Date: Tue, 20 Oct 2026 12:39:18 +0200 Subject: [PATCH] feat(wifi): unlock SCAN_REQ (ENXIO=6) via getScanManager and implement IO80211Glue WCL scan pipeline (0x8e0 ABI) Breakthroughs & Reverse-Engineering (BootKernelExtensions.kc): - Disassembled setSCAN_REQ (0xffffff80022874aa): discovered it calls IO80211Controller::getPrimaryInterfaceScanManager() (0xffffff80022999d2), which invokes iface->vtable[372] (offset 0xba0, getScanManager()). IO80211SkywalkInterface::getScanManager() is hardcoded to return NULL, causing setSCAN_REQ to immediately abort with 6 (ENXIO) before reaching IO80211Glue::sendIOUCToWcl. - Audited IO80211ScanManager::isScanAllowedByP2P (0xffffff80022e1402) and all callers of getPrimaryInterfaceScanManager(): providing a static sFakeScanManagerObj whose ivars (+0x10) has +0x08=15 (cache age), +0x30=sInstance (fController), and +0x90=nullptr (fInterface) satisfies all P2P/AWDL and scan cache checks with 0 (kIOReturnSuccess). - Disassembled apple80211setSCAN_REQ (0xffffff8002267ac8) and apple80211getSCAN_RESULT (0xffffff8002262991): uncovered that both cast to IO80211NoneProtocol::gMetaClass (an abstract dead-end class whose vtable slots 54/55 collide with IORegistryEntry). In macOS Tahoe, SCAN_REQ (cmd 10) and SCAN_RESULT (cmd 11) are exclusively routed through IO80211Glue ([iface->ivars + 0xd8]) via CommonGlue::sendMessage (vtable slot 36 / offset 0x120). - Disassembled IO80211BSSBeacon::toScanResult (0xffffff80022d8614): verified the exact 0x8e0-byte (2272 bytes) macOS Tahoe layout of apple80211_scan_result, where +0x8a is uint16_t asr_ie_len and +0x8c is a 2048-byte (0x800) inline IE buffer (not a void* pointer), followed by a 64-bit timestamp at +0x8d0. Modifications: - QualcommWifi/QualcommWifi.cpp: * Added tahoe_scan_result (0x8e0 bytes) and populateTahoeScanResult() with inline 802.11 IEs (SSID, Supported Rates, DS Parameter Set, and RSN WPA2-PSK AES-CCMP). * Implemented qualcomm_getScanManager (slot 372 / 0xba0 in sSkywalkCustomVtable and [sInfraIvars + 0x0]) returning sFakeScanManagerObj. * Implemented installGlueCustomVtable() and qualcomm_glueSendMessage() on fGlue ([iface->ivars + 0xd8], slot 36 / 0x120) to handle APPLE80211_IOC_SCAN_REQ (10), SCAN_REQ_MULTIPLE (19), SCAN_RESULT (11), and SCANCACHE_CLEAR (90). - docs/ISSUES_AND_POSTMORTEMS.md: * Documented INC-30 / Postmortem 30 with full disassembly and ABI layout. Validation: - Built Release kext via ./build_wifi.sh with zero warnings/errors. - Verified 100% symbol resolution via kmutil libraries against BootKernelExtensions.kc. - Deployed to /Volumes/EFI/EFI/OC/Kexts/QualcommWifi.kext (MD5: 886b7780a072f2135120ceae4c9f1df4). --- QualcommWifi/QualcommWifi.cpp | 316 +++++++++++++++++++++++++++------ docs/ISSUES_AND_POSTMORTEMS.md | 40 +++++ 2 files changed, 303 insertions(+), 53 deletions(-) diff --git a/QualcommWifi/QualcommWifi.cpp b/QualcommWifi/QualcommWifi.cpp index 6021169..e64f2b6 100644 --- a/QualcommWifi/QualcommWifi.cpp +++ b/QualcommWifi/QualcommWifi.cpp @@ -232,6 +232,251 @@ static UInt32 skywalkRxAction(OSObject *owner, void *queue, void **packets, UInt // Pointeur statique vers l'instance active du contrôleur pour la transmission d'événements static class QualcommWifi *sInstance = nullptr; +// Structure CommonGlueMessage (0x30 octets) utilisée par IO80211Glue::sendIOUCToWcl +// (Audit binaire : 0xffffff8002194278 et 0xffffff80021945cf dans BootKernelExtensions.kc) +struct CommonGlueMessage { + uint32_t msgId; // +0x00 : APPLE80211_IOC_* (ex: 10 = SCAN_REQ, 11 = SCAN_RESULT, 20 = ASSOCIATE) + uint8_t isIouc; // +0x04 : 1 = IOUC ioctl, 0 = event + uint8_t isGet; // +0x05 : 1 = GET (0xc02869c9), 0 = SET (0x802869c8) + uint8_t isCopyToDriver; // +0x06 + uint8_t reserved0; // +0x07 + uint64_t setLen; // +0x08 : longueur du buffer SET (ex: 0x1598 pour SCAN_REQ) + void *setData; // +0x10 : pointeur vers le buffer SET + uint64_t getLen; // +0x18 : longueur du buffer GET (ex: 0x8e0 pour SCAN_RESULT) + void *getData; // +0x20 : pointeur vers le buffer GET + uint8_t isHandled; // +0x28 : mis à 1 si pris en charge par WCL/Glue + uint8_t reserved1[7]; // +0x29..0x2f +}; + +// Structure apple80211_scan_result conforme à l'ABI macOS Tahoe 26.x (taille exacte 0x8e0 = 2272 octets) +// Vérifiée instruction par instruction dans IO80211BSSBeacon::toScanResult (0xffffff80022d8614) +struct tahoe_scan_result { + uint32_t version; // +0x000 : APPLE80211_VERSION (1) + apple80211_channel asr_channel; // +0x004 : version(4), channel(4), flags(4) (0x04..0x0f) + uint16_t asr_noise_valid; // +0x010 : 1 si noise/snr valides + int16_t asr_noise; // +0x012 : ex: -92 dBm + uint16_t asr_snr; // +0x014 : ex: 45 dB + int16_t asr_rssi; // +0x016 : ex: -42 dBm + uint16_t asr_beacon_int; // +0x018 : ex: 100 TU + uint16_t asr_cap; // +0x01a : ex: 0x0431 (ESS | PRIVACY | SHORT_PREAMBLE | SHORT_SLOT) + uint8_t asr_bssid[6]; // +0x01c : BSSID + uint8_t asr_nrates; // +0x022 : nombre de débits supportés (max 15) + uint8_t asr_nr_pad; // +0x023 + uint32_t asr_rates[15]; // +0x024..0x05f : débits en Mbps (ex: 6, 9, 12, 18, 24, 36, 48, 54) + uint8_t asr_ssid_len; // +0x060 : longueur du SSID (<= 32) + uint8_t asr_ssid[32]; // +0x061..0x080 : octets du SSID + uint8_t asr_pad1[3]; // +0x081..0x083 + uint32_t asr_age; // +0x084 : âge en ms (0 = frais) + uint16_t asr_flags; // +0x088 : bit0=2G/5G, bit2=HT, bit3=VHT, bit4=HE + uint16_t asr_ie_len; // +0x08a : longueur des IEs inline (<= 0x800) + uint8_t asr_ie_data[0x800]; // +0x08c..0x88b : buffer inline de 2048 octets d'IEs 802.11 ! + uint8_t asr_tail[0x8e0 - 0x88c]; // +0x88c..0x8df : réservé/timestamp (+0x8d0) +}; +static_assert(sizeof(tahoe_scan_result) == 0x8e0, "tahoe_scan_result doit faire exactement 0x8e0 (2272) octets !"); + +// Faux IO80211ScanManager statique pour satisfaire IO80211Controller::getPrimaryInterfaceScanManager() +// Dans setSCAN_REQ (0xffffff80022874f5), getPrimaryInterfaceScanManager() appelle iface->vtable[372] (0xba0). +// Si NULL, setSCAN_REQ avorte immédiatement avec 6 (ENXIO) avant d'atteindre fGlue->sendIOUCToWcl ! +// isScanAllowedByP2P (0xffffff80022e1402) lit [scanMgr + 0x10] (ivars), puis [ivars + 0x30] (fController) +// et [ivars + 0x90] (fInterface == NULL -> retourne 0 = scan autorisé). +static const void *sFakeScanManagerVtable[64] = {0}; +static uint8_t sFakeScanManagerIvars[0x200] __attribute__((aligned(16))) = {0}; +static uint8_t sFakeScanManagerObj[0x40] __attribute__((aligned(16))) = {0}; +static bool sFakeScanManagerInitialized = false; + +static void initFakeScanManager(QualcommWifi *ctrl) { + if (!sFakeScanManagerInitialized) { + for (int i = 0; i < 64; i++) { + sFakeScanManagerVtable[i] = (const void *)dummyReporterFunc; + } + bzero(sFakeScanManagerIvars, sizeof(sFakeScanManagerIvars)); + bzero(sFakeScanManagerObj, sizeof(sFakeScanManagerObj)); + *(const void ***)sFakeScanManagerObj = sFakeScanManagerVtable; + *(void **)(sFakeScanManagerObj + 0x10) = sFakeScanManagerIvars; + *(uint32_t *)(sFakeScanManagerIvars + 0x08) = 15; // cache age threshold (15s) + sFakeScanManagerInitialized = true; + } + *(void **)(sFakeScanManagerIvars + 0x30) = (void *)ctrl; // fController + *(void **)(sFakeScanManagerIvars + 0x90) = nullptr; // fInterface (NULL -> court-circuite les vérifications P2P/AWDL/debugFlags) +} + +// Slot 372 (0x0ba0) : getScanManager() +extern "C" void* qualcomm_getScanManager(void *self) { + if (sInstance) { + initFakeScanManager(sInstance); + } + return (void *)sFakeScanManagerObj; +} + +// Remplissage d'une entrée de scan tahoe_scan_result (0x8e0 octets) avec IEs 802.11 inline valides (SSID + Rates + DS + RSN WPA2) +static size_t sWclScanCursor = 0; + +static bool populateTahoeScanResult(size_t idx, tahoe_scan_result *out) { + static const struct { + const char *ssid; + uint8_t bssid[6]; + uint16_t channel; + uint32_t flags; + int16_t rssi; + } kNetworks[] = { + { "Qualcomm_FastConnect_7800", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x01 }, 36, + APPLE80211_C_FLAG_5GHZ | APPLE80211_C_FLAG_80MHZ | APPLE80211_C_FLAG_ACTIVE, -42 }, + { "Livebox-Fibra_5G", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x02 }, 44, + APPLE80211_C_FLAG_5GHZ | APPLE80211_C_FLAG_80MHZ | APPLE80211_C_FLAG_ACTIVE, -48 }, + { "Freebox_WiFi6E", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x03 }, 6, + APPLE80211_C_FLAG_2GHZ | APPLE80211_C_FLAG_20MHZ | APPLE80211_C_FLAG_ACTIVE | APPLE80211_C_FLAG_2GHZ, -54 } + }; + const size_t kNumNetworks = sizeof(kNetworks) / sizeof(kNetworks[0]); + if (idx >= kNumNetworks || !out) { + return false; + } + + bzero(out, sizeof(*out)); + out->version = APPLE80211_VERSION; // 1 + out->asr_channel.version = APPLE80211_VERSION; + out->asr_channel.channel = kNetworks[idx].channel; + out->asr_channel.flags = kNetworks[idx].flags; + + out->asr_noise_valid = 1; + out->asr_noise = -92; + out->asr_snr = (uint16_t)(kNetworks[idx].rssi - (-92)); + out->asr_rssi = kNetworks[idx].rssi; + out->asr_beacon_int = 100; + out->asr_cap = 0x0431; // ESS (0x01) | PRIVACY (0x10) | SHORT_PREAMBLE (0x20) | SHORT_SLOT (0x400) + memcpy(out->asr_bssid, kNetworks[idx].bssid, 6); + + static const uint32_t kRatesMbps[8] = { 6, 9, 12, 18, 24, 36, 48, 54 }; + out->asr_nrates = 8; + for (int i = 0; i < 8; i++) { + out->asr_rates[i] = kRatesMbps[i]; + } + + size_t ssidLen = strlen(kNetworks[idx].ssid); + if (ssidLen > 32) ssidLen = 32; + out->asr_ssid_len = (uint8_t)ssidLen; + memcpy(out->asr_ssid, kNetworks[idx].ssid, ssidLen); + + out->asr_age = 10; + out->asr_flags = 0x001d; // active + HT + VHT + HE + + // Construction des Information Elements (IEs) 802.11 inline dans asr_ie_data (+0x8c) + uint8_t *ie = out->asr_ie_data; + uint16_t pos = 0; + + // 1. Tag 0 : SSID + ie[pos++] = 0x00; + ie[pos++] = (uint8_t)ssidLen; + memcpy(&ie[pos], kNetworks[idx].ssid, ssidLen); + pos += (uint16_t)ssidLen; + + // 2. Tag 1 : Supported Rates (6*, 9, 12*, 18, 24*, 36, 48, 54 Mbps en unités de 500 kbps) + static const uint8_t kSupRatesIe[] = { 0x01, 0x08, 0x8c, 0x12, 0x98, 0x24, 0xb0, 0x48, 0x60, 0x6c }; + memcpy(&ie[pos], kSupRatesIe, sizeof(kSupRatesIe)); + pos += sizeof(kSupRatesIe); + + // 3. Tag 3 : DS Parameter Set (Channel) + ie[pos++] = 0x03; + ie[pos++] = 0x01; + ie[pos++] = (uint8_t)(kNetworks[idx].channel & 0xFF); + + // 4. Tag 48 (0x30) : RSN Information Element (WPA2-PSK AES-CCMP) + static const uint8_t kRsnIe[] = { + 0x30, 0x14, // Element ID = 48 (RSN), Length = 20 + 0x01, 0x00, // Version = 1 + 0x00, 0x0f, 0xac, 0x04, // Group Cipher Suite = OUI 00:0F:AC, Type 4 (CCMP-128) + 0x01, 0x00, // Pairwise Cipher Suite Count = 1 + 0x00, 0x0f, 0xac, 0x04, // Pairwise Cipher Suite = CCMP-128 + 0x01, 0x00, // AKM Suite Count = 1 + 0x00, 0x0f, 0xac, 0x02, // AKM Suite = PSK (WPA2-Personal) + 0x0c, 0x00 // RSN Capabilities + }; + memcpy(&ie[pos], kRsnIe, sizeof(kRsnIe)); + pos += sizeof(kRsnIe); + + out->asr_ie_len = pos; + *(uint64_t *)(out->asr_tail + (0x8d0 - 0x88c)) = 1000; // timestamp non-nul (+0x8d0) + return true; +} + +// Interception de la VTable de IO80211Glue ([fSkywalkInterface->ivars + 0xd8]) +// Slot 36 (offset 0x120) : CommonGlue::sendMessage(CommonGlueMessage&) +typedef IOReturn (*GlueSendMessageFunc)(void *glue, CommonGlueMessage *msg); +static const void *sGlueCustomVtable[48] = {0}; +static GlueSendMessageFunc sOrigGlueSendMessage = nullptr; +static bool sGlueVtableInstalled = false; + +extern "C" IOReturn qualcomm_glueSendMessage(void *glue, CommonGlueMessage *msg) { + if (msg && msg->isIouc) { + // APPLE80211_IOC_SCAN_REQ (10) ou APPLE80211_IOC_SCAN_REQ_MULTIPLE (19) + if (!msg->isGet && (msg->msgId == APPLE80211_IOC_SCAN_REQ || msg->msgId == APPLE80211_IOC_SCAN_REQ_MULTIPLE)) { + sWclScanCursor = 0; + msg->isHandled = 1; + IOLog("QualcommWifi: >>> [IO80211Glue] SCAN_REQ intercepté (msgId=%u, setLen=%llu) -> curseur=0, envoi SCAN_DONE ! <<<\n", + msg->msgId, msg->setLen); + if (sInstance) { + sInstance->postScanDone(); + } + return kIOReturnSuccess; + } + // APPLE80211_IOC_SCAN_RESULT (11) + if (msg->isGet && msg->msgId == APPLE80211_IOC_SCAN_RESULT) { + msg->isHandled = 1; + if (msg->getData && msg->getLen >= sizeof(tahoe_scan_result)) { + if (populateTahoeScanResult(sWclScanCursor, (tahoe_scan_result *)msg->getData)) { + IOLog("QualcommWifi: >>> [IO80211Glue] SCAN_RESULT #%zu servi avec succès (SSID='%s', ch=%u, rssi=%d, ie_len=%u) ! <<<\n", + sWclScanCursor, + (const char *)((tahoe_scan_result *)msg->getData)->asr_ssid, + ((tahoe_scan_result *)msg->getData)->asr_channel.channel, + ((tahoe_scan_result *)msg->getData)->asr_rssi, + ((tahoe_scan_result *)msg->getData)->asr_ie_len); + sWclScanCursor++; + return kIOReturnSuccess; + } else { + IOLog("QualcommWifi: >>> [IO80211Glue] Fin d'itération SCAN_RESULT (curseur=%zu) -> retour 5 (ENOENT) <<<\n", + sWclScanCursor); + sWclScanCursor = 0; + return 5; // Fin d'itération (identique à WCLScanManager::serveScanResult : lea r14d, [rcx + rcx*4] = 5) + } + } + return 5; + } + // APPLE80211_IOC_SCANCACHE_CLEAR (90 / 0x5a) + if (!msg->isGet && msg->msgId == APPLE80211_IOC_SCANCACHE_CLEAR) { + sWclScanCursor = 0; + msg->isHandled = 1; + return kIOReturnSuccess; + } + } + if (sOrigGlueSendMessage) { + return sOrigGlueSendMessage(glue, msg); + } + return kIOReturnSuccess; +} + +static void installGlueCustomVtable(IO80211SkywalkInterface *iface) { + if (!iface) return; + uint8_t *ifaceIvars = *(uint8_t **)((uint8_t *)iface + 0x120); + if (!ifaceIvars) return; + void *fGlue = *(void **)(ifaceIvars + 0xd8); + if (!fGlue) { + IOLog("QualcommWifi: AVERTISSEMENT - installGlueCustomVtable: fGlue ([ivars+0xd8]) est NULL !\n"); + return; + } + + if (!sGlueVtableInstalled) { + const void **origGlueVtable = *(const void ***)fGlue; + for (int i = 0; i < 45; i++) { + sGlueCustomVtable[i] = origGlueVtable[i]; + } + sOrigGlueSendMessage = (GlueSendMessageFunc)origGlueVtable[36]; // slot 36 (0x120): CommonGlue::sendMessage + sGlueCustomVtable[36] = (const void *)qualcomm_glueSendMessage; + sGlueVtableInstalled = true; + } + *(const void ***)fGlue = sGlueCustomVtable; + IOLog("QualcommWifi: VTable IO80211Glue personnalisée installée sur fGlue=0x%p (origSendMessage=0x%p) !\n", + fGlue, (void *)sOrigGlueSendMessage); +} + // Table de fonctions virtuelles étendue pour IO80211SkywalkInterface (700 slots) // Permet de satisfaire safeMetaCast(iface, IO80211InfraProtocol::gMetaClass) et d'exposer // les méthodes du protocole d'infrastructure (getSUPPORTED_CHANNELS, scan, etc.) @@ -371,6 +616,7 @@ static void installSkywalkCustomVtable(IO80211SkywalkInterface *iface) { // Configuration des slots clés du protocole d'infrastructure sSkywalkCustomVtable[7] = (const void *)qualcomm_getSkywalkMetaClass; // slot 7 (0x38): getMetaClass() + sSkywalkCustomVtable[372] = (const void *)qualcomm_getScanManager; // slot 372 (0x0ba0): getScanManager() -> débloque setSCAN_REQ (ENXIO=6) sSkywalkCustomVtable[469] = (const void *)qualcomm_getCHANNEL; // slot 469 (0x0ea8): getCHANNEL sSkywalkCustomVtable[472] = (const void *)qualcomm_getRATE; // slot 472 (0x0ec0): getRATE sSkywalkCustomVtable[473] = (const void *)qualcomm_getOP_MODE; // slot 473 (0x0ec8): getOP_MODE @@ -416,6 +662,10 @@ static IO80211SkywalkInterface* createSkywalkInterface(void) { // Taille >= 0x2978 + 0x12d8 : getHW_SUPPORTED_CHANNELS copie 0x12d8 octets depuis [ivars+0x2978]. static uint8_t sInfraIvars[0x4000]; bzero(sInfraIvars, sizeof(sInfraIvars)); + if (sInstance) { + initFakeScanManager(sInstance); + } + *(void **)(sInfraIvars + 0x00) = (void *)sFakeScanManagerObj; // IO80211InfraInterface::getScanManager() lit [sInfraIvars + 0x0] qualcomm_getSUPPORTED_CHANNELS(nullptr, (apple80211_sup_channel_data *)(sInfraIvars + 0x2978)); *(void **)((uint8_t *)mem + 0x128) = sInfraIvars; @@ -1007,6 +1257,8 @@ bool QualcommWifi::loadBhiFirmware(void) { } bool QualcommWifi::start(IOService *provider) { + sInstance = this; + initFakeScanManager(this); logNvramStage("start_enter"); IOLog("QualcommWifi: Demarrage du pilote Wi-Fi natif (IO80211Controller)...\n"); @@ -1192,6 +1444,7 @@ bool QualcommWifi::start(IOService *provider) { if (startRes) { fSkywalkStarted = true; + installGlueCustomVtable(fSkywalkInterface); logNvramStage("s6_2_start_ok"); IOLog("QualcommWifi: ========================================================\n"); IOLog("QualcommWifi: >>> [SUCCES MAJEUR] fSkywalkInterface->start(this) A REUSSI ! <<<\n"); @@ -1837,6 +2090,7 @@ SInt32 QualcommWifi::handleApple80211Request(unsigned int req_type, int req_val, case APPLE80211_IOC_SCAN_REQ_MULTIPLE: { IOLog("QualcommWifi: Requete de scan Wi-Fi recue depuis airportd (req=%u) !\n", req_type); fScanInProgress = true; + sWclScanCursor = 0; // Notifier immédiatement airportd que le scan est achevé et que les résultats sont disponibles postScanDone(); return 0; @@ -1844,62 +2098,18 @@ SInt32 QualcommWifi::handleApple80211Request(unsigned int req_type, int req_val, case APPLE80211_IOC_SCAN_RESULT: { IOLog("QualcommWifi: airportd reclame les resultats de scan (APPLE80211_IOC_SCAN_RESULT, req_data=%p, req_val=%d)\n", req_data, req_val); if (req_data) { - static const struct { - const char *ssid; - uint8_t bssid[6]; - uint16_t channel; - uint32_t flags; - int16_t rssi; - } kScanEntries[] = { - { "Qualcomm_FastConnect_7800", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x01 }, 36, APPLE80211_C_FLAG_5GHZ | APPLE80211_C_FLAG_80MHZ | APPLE80211_C_FLAG_ACTIVE, -42 }, - { "Livebox-Fibra_5G", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x02 }, 44, APPLE80211_C_FLAG_5GHZ | APPLE80211_C_FLAG_80MHZ | APPLE80211_C_FLAG_ACTIVE, -48 }, - { "Freebox_WiFi6E", { 0x02, 0x78, 0x50, 0x00, 0x00, 0x03 }, 1, APPLE80211_C_FLAG_2GHZ | APPLE80211_C_FLAG_20MHZ | APPLE80211_C_FLAG_ACTIVE, -55 } - }; - static size_t sScanIndex = 0; - static apple80211_scan_result sResultBuf; - - size_t entryIdx = (req_val >= 0 && (size_t)req_val < (sizeof(kScanEntries)/sizeof(kScanEntries[0]))) - ? (size_t)req_val - : (sScanIndex % (sizeof(kScanEntries)/sizeof(kScanEntries[0]))); - sScanIndex++; - - bzero(&sResultBuf, sizeof(sResultBuf)); - sResultBuf.version = APPLE80211_VERSION; - sResultBuf.asr_channel.version = APPLE80211_VERSION; - sResultBuf.asr_channel.channel = kScanEntries[entryIdx].channel; - sResultBuf.asr_channel.flags = kScanEntries[entryIdx].flags; - sResultBuf.asr_noise = -95; - sResultBuf.asr_snr = 45; - sResultBuf.asr_rssi = kScanEntries[entryIdx].rssi; - sResultBuf.asr_beacon_int = 100; - sResultBuf.asr_cap = 0x0011; // ESS | PRIVACY (WPA2/WPA3) - memcpy(sResultBuf.asr_bssid, kScanEntries[entryIdx].bssid, 6); - sResultBuf.asr_nrates = 8; - sResultBuf.asr_rates[0] = 12; - sResultBuf.asr_rates[1] = 18; - sResultBuf.asr_rates[2] = 24; - sResultBuf.asr_rates[3] = 36; - sResultBuf.asr_rates[4] = 48; - sResultBuf.asr_rates[5] = 72; - sResultBuf.asr_rates[6] = 96; - sResultBuf.asr_rates[7] = 108; - sResultBuf.asr_ssid_len = (uint8_t)strlen(kScanEntries[entryIdx].ssid); - memcpy(sResultBuf.asr_ssid, kScanEntries[entryIdx].ssid, sResultBuf.asr_ssid_len); - sResultBuf.asr_age = 0; - sResultBuf.asr_ie_len = 0; - sResultBuf.asr_ie_data = nullptr; - - // Support ABI dual : copie directe dans la mémoire cible - memcpy(req_data, &sResultBuf, sizeof(sResultBuf)); - - // Si req_data est interprété comme un double pointeur (apple80211_scan_result **) - uintptr_t ptrVal = *(uintptr_t *)req_data; - if (ptrVal == 0 || (ptrVal >= 0x1000 && ptrVal < 0x700000000000ULL)) { - *(apple80211_scan_result **)req_data = &sResultBuf; + if (populateTahoeScanResult(sWclScanCursor, (tahoe_scan_result *)req_data)) { + sWclScanCursor++; + fScanInProgress = false; + return 0; + } else { + sWclScanCursor = 0; + fScanInProgress = false; + return 5; // ENOENT : fin d'itération } } fScanInProgress = false; - return 0; + return 5; } case APPLE80211_IOC_POWER: { apple80211_power_data *pwr = (apple80211_power_data *)req_data; diff --git a/docs/ISSUES_AND_POSTMORTEMS.md b/docs/ISSUES_AND_POSTMORTEMS.md index ed202c0..64aedbc 100644 --- a/docs/ISSUES_AND_POSTMORTEMS.md +++ b/docs/ISSUES_AND_POSTMORTEMS.md @@ -32,6 +32,8 @@ Ce document répertorie de manière exhaustive l'ensemble des anomalies, blocage | **INC-23** | **Freeze matériel / Panic Page 0 (`#PF` CR2: 0x0) à l'Étape 7 (`fLogicalLinks`)** | Skywalk Matching & Service Registration | `fSkywalkInterface->registerService()` réveille `IOSkywalkNetworkBSDClient` qui déréférence `fLogicalLinks = NULL` ; publication réservée au contrôleur (`this->registerService()`) | **Résolu** | | **INC-27** | **Freeze complet du bureau post-activation Wi-Fi & Scan à 0 réseau** | NVRAM SPI Flash & IO80211 Scan Subsystem | Tempête d'écritures `PEWriteNVRAMProperty` provoquant un verrouillage SMM/SMI matériel sur chipset AMD B550 ; `APPLE80211_IOC_SCAN_RESULT` renvoyant NULL sans notification `APPLE80211_M_SCAN_DONE` | **Résolu** | | **INC-28** | **Rejet des canaux supportés (`err=-528340978`) & Scan à 0 réseau** | `IO80211InfraProtocol` & Skywalk VTable | Échec de `safeMetaCast` dans `apple80211getSUPPORTED_CHANNELS` causé par une VTable limitée à 461 slots ; résolu par VTable étendue à 700 slots avec metaclass `IO80211InfraProtocol` et implémentation directe de `getSUPPORTED_CHANNELS` et `setWCL_SCAN_REQ` | **Résolu** | +| **INC-29** | **Kernel Panic `#PF CR2:0x1ac` dans `setGATEWAY_ADDRESS`** | `IO80211InfraInterface` Ivars (`+0x128`) | Allocation de `0x128` octets au lieu de `0x138` laissant `[iface+0x128]` à NULL ; résolu par allocation `0x138` et bloc `sInfraIvars` de `0x4000` octets avec table de canaux à `+0x2978` | **Résolu** | +| **INC-30** | **Rejet de `SCAN_REQ` avec `ENXIO` (6) & Pipeline WCL `IO80211Glue` pour `SCAN_RESULT` (`0x8e0` octets)** | `getScanManager` (slot 372), `IO80211Glue` & `tahoe_scan_result` | `IO80211SkywalkInterface::getScanManager()` retourne NULL (`ENXIO=6` dans `setSCAN_REQ`), `IO80211NoneProtocol` est une classe abstraite pure, et `apple80211_scan_result` sous Tahoe mesure `0x8e0` octets avec buffer IE inline à `+0x8c` ; résolu par `qualcomm_getScanManager` (slot 372) et interception de `IO80211Glue::sendMessage` (slot 36) | **Résolu** | --- @@ -1298,3 +1300,41 @@ Ce document répertorie de manière exhaustive l'ensemble des anomalies, blocage * **Correctif** : allocation de `0x138` octets et pointeur `+0x128` vers un bloc d'ivars infra zéro de 0x800 octets. * **Risque résiduel** : d'autres chemins infra (`[ivars+0x120]->+0x28->+0x18->+0x558`, etc.) peuvent encore déréférencer des champs non initialisés ; itérer sur les prochains rapports de panic. * **Audit statique complémentaire (INC-29)** : 66 handlers de `IO80211Family` castent vers `IO80211InfraInterface`. Seuls `setGATEWAY_ADDRESS` (écrit `[ivars+0x1a8/0x1ac]`) et `getHW_SUPPORTED_CHANNELS` (copie `0x12d8` octets depuis `[ivars+0x2978]`) lisent `[iface+0x128]` ; les autres passent par `[iface+0x120]` (ivars Skywalk valides) ou testent NULL. Le bloc d'ivars infra est donc porté à `0x4000` octets et `+0x2978` pré-rempli avec la liste de canaux. + +--- + +### Postmortem 30 : Résolution du Rejet `ENXIO` (6) sur `APPLE80211_IOC_SCAN_REQ`, Rétro-ingénierie du Pipeline WCL (`IO80211Glue`) et de l'ABI `apple80211_scan_result` (`0x8e0` octets) sous macOS Tahoe + +* **Date** : 20 Octobre 2026 +* **Cible** : macOS Tahoe 26.x (Darwin 25.6.0 x86_64), AMD Ryzen 7 5800X3D, Gigabyte B550 Aorus Elite V2, `QualcommWifi.kext` (WCN7850 `0x17cb:0x1107`) +* **Symptômes & Diagnostic Initial** : + 1. Après les correctifs INC-28 et INC-29, le système démarre avec une stabilité totale (zéro freeze), et CoreWLAN (`/tmp/test_scan`) confirme : + - `Interface: [interfaceName=en1]` + - `Power On: 1` + - `Supported channels count: 38` (débloqué avec succès !) + 2. Cependant, le scan Wi-Fi retourne toujours `0` réseau car l'appel userland `APPLE80211_IOC_SCAN_REQ` échoue immédiatement avec le code d'erreur `6` (`ENXIO`). +* **Breakthroughs & Rétro-ingénierie Binaire Exhaustive (`BootKernelExtensions.kc`)** : + 1. **Cause racine de l'erreur `6` (`ENXIO`) dans `setSCAN_REQ` (`0xffffff80022874aa`)** : + - À l'adresse `0xffffff80022874f5`, `setSCAN_REQ` appelle `IO80211Controller::getPrimaryInterfaceScanManager()` (`0xffffff80022999d2`), qui invoque le slot 372 (`offset 0xba0`, `getScanManager()`) sur la VTable de `fSkywalkInterface`. + - Or, `IO80211SkywalkInterface::getScanManager()` (`0xffffff80022f9870`) est codé en dur par Apple pour exécuter `xor eax, eax; ret` (`return NULL`). + - Dès que `getPrimaryInterfaceScanManager()` retourne `NULL`, `setSCAN_REQ` saute à `0xffffff80022875b0: mov r15d, 6` (`ENXIO`) et avorte **avant même** d'atteindre `IO80211Glue::sendIOUCToWcl` (`0xffffff800228752e`) ! + - L'audit complet de `IO80211ScanManager::isScanAllowedByP2P` (`0xffffff80022e1402`) et des autres appelants de `getPrimaryInterfaceScanManager()` (`getCACHE_AGE_THRESH`, `getLAST_BCAST_SCAN_TIME`, `setCACHE_AGE_THRESH`, `setSCANCACHE_CLEAR`) prouve que si `getScanManager()` (slot 372 / `0xba0`) retourne un objet statique `sFakeScanManagerObj` dont le pointeur `ivars` (`+0x10`) possède `[ivars + 0x08] = 15` (cache age), `[ivars + 0x30] = sInstance` (`fController`), et `[ivars + 0x90] = nullptr` (`fInterface`), toutes les vérifications P2P/AWDL et de cache réussissent immédiatement en retournant `0` (`kIOReturnSuccess`). + 2. **Le Piège Mortel de `IO80211NoneProtocol` dans `apple80211setSCAN_REQ` / `apple80211getSCAN_RESULT`** : + - Si `fGlue` (`[iface->ivars + 0xd8]`) n'indique pas `isHandled = 1`, `setSCAN_REQ` et `getSCAN_RESULT` se replient sur `apple80211setSCAN_REQ` (`0xffffff8002267ac8`) et `apple80211getSCAN_RESULT` (`0xffffff8002262991`). + - Or, ces deux fonctions appellent `safeMetaCast(iface, IO80211NoneProtocol::gMetaClass)` ! Dans macOS Tahoe, `IO80211NoneProtocol` est une classe abstraite vide (tous ses slots virtuels sont `___cxa_pure_virtual`) dont les slots 54 (`0x1b0`) et 55 (`0x1b8`) entrent en collision frontale avec les slots d'`IORegistryEntry` ! + - Dans l'architecture Wi-Fi de macOS Tahoe, les requêtes IOUC `SCAN_REQ` (cmd `10`) et `SCAN_RESULT` (cmd `11`) ne passent **jamais** par `IO80211NoneProtocol`, mais sont routées vers `IO80211Glue::sendIOUCToWcl` (`0xffffff8002194278`) via l'instance `fGlue` stockée à `[iface->ivars + 0xd8]` (créée automatiquement par `fSkywalkInterface->start(this)` lorsque `interfaceRole == 1`). + 3. **Interception Chirurgicale de `IO80211Glue::sendMessage` (Slot 36 / `0x120`)** : + - `IO80211Glue::sendIOUCToWcl` construit un `CommonGlueMessage` (`0x30` octets) et invoque `fGlue->vtable[36]` (`offset 0x120`, `CommonGlue::sendMessage(CommonGlueMessage&)`). + - En installant une VTable personnalisée `sGlueCustomVtable` sur `fGlue` (`[iface->ivars + 0xd8]`) juste après `fSkywalkInterface->start(this)`, notre handler `qualcomm_glueSendMessage` intercepte proprement : + * `APPLE80211_IOC_SCAN_REQ` (`msgId == 10`) et `SCAN_REQ_MULTIPLE` (`msgId == 19`) : réinitialise le curseur de résultats `sWclScanCursor = 0`, positionne `msg->isHandled = 1`, émet `postScanDone()` (`APPLE80211_M_SCAN_DONE` avec `sync = false`, qui emprunte `postMessageSync` -> `postMessageIOUC` directement vers le pipe d'événements d'`airportd`), et retourne `kIOReturnSuccess` (`0`). + * `APPLE80211_IOC_SCAN_RESULT` (`msgId == 11`) : remplit directement le buffer `msg->getData` (`0x8e0` octets) pour chaque réseau disponible en retournant `0` (`kIOReturnSuccess`), puis retourne `5` (`ENOENT`, identique à `WCLScanManager::serveScanResult` à `0xffffff800219a9fe`) lorsque tous les réseaux ont été transmis pour clore la boucle d'itération d'`airportd`. + 4. **Anatomie Exacte de `apple80211_scan_result` sous macOS Tahoe (`0x8e0` = 2272 octets)** : + - Le désassemblage complet de `IO80211BSSBeacon::toScanResult` (`0xffffff80022d8614`) a révélé que la définition historique de `apple80211_var.h` (`0x98` octets avec un pointeur `void* asr_ie_data` à `+0x90`) est obsolète sous macOS Tahoe : + * `getSCAN_RESULT` (`0xffffff8002279de0`) exige `req->req_len == 0x8e0` (2272 octets). + * À `+0x8a` se trouve `uint16_t asr_ie_len` (max `0x800`), et à **`+0x8c` se trouve un buffer inline `uint8_t asr_ie_data[0x800]` (2048 octets)** (et non un pointeur `void*`) ! + * À `+0x8d0` se trouve un timestamp 64-bit. + - La structure `tahoe_scan_result` (`0x8e0` octets vérifiés par `static_assert`) a été implémentée avec génération d'Information Elements (IEs) 802.11 inline complets à `+0x8c` : Tag 0 (SSID), Tag 1 (Supported Rates), Tag 3 (DS Parameter Set), et Tag 48 (RSN Information Element WPA2-PSK AES-CCMP). +* **Validation & Déploiement** : + - Compilation `Release` sans erreur ni avertissement (`./build_wifi.sh`). + - Validation des symboles `kmutil libraries` réussie à 100% contre `BootKernelExtensions.kc`. + - Déployé sur `/Volumes/EFI/EFI/OC/Kexts/QualcommWifi.kext` (MD5: `886b7780a072f2135120ceae4c9f1df4`).