Files
QualcommWireless/docs/ISSUES_AND_POSTMORTEMS.md
Erwan 0809b01856 fix(wifi): prevent CPU triple fault and instant reboot via CCLogStream layout conforming FakeLogStream
### Breakthroughs & Rétro-ingénierie
- Analyse forensique du redémarrage instantané ("coupure de courant" sans log) survenu au commit 5357ca6 :
  1. Le désassemblage de `CCLogStream::shouldLog` (0xffffff80032ff9e6) dans `BootKernelExtensions.kc` a révélé un déréférencement en cascade non protégé : `movq 0x90(%rdi), %rax` puis `movq 0x58(%rax), %rax`.
  2. `fLogger` retournant un `OSString*` de seulement 32 octets (0x20), l'accès à +0x90 lisait une adresse arbitraire du tas kernel dont le déréférencement en Ring 0 déclenchait un #PF/#GP transformé en Triple Fault CPU immédiat (reset matériel sans panic handler).
  3. Parallèlement, `IO80211RNGAgent::withOptions` et `IO80211ControllerMonitor::withControllerAndProvider` requièrent impérativement un pointeur non-nul et invoquent `logger->retain()` (*0x20(%rax)).
  4. La résolution consiste en une classe `FakeLogStream : public OSObject` respectant au millimètre l'ABI Apple : un pointeur `p1` placé à l'offset strict +0x90 (validé par `static_assert`) pointant vers une zone mémoire `fP1Mem` dont l'offset +0x58 pointe vers `fP2Mem` initialisé à zéro. `shouldLog` et `log*` retournent ainsi false sans aucun déréférencement illégal et sans risquer d'écrire sur un pipe non configuré.

### Modifications apportées
- `QualcommWifi/QualcommWifi.cpp` :
  - Définition et enregistrement IOKit de `FakeFaultReporter` et `FakeLogStream` avec méthodes usine `create()`.
  - Initialisation de `fFaultReporter = FakeFaultReporter::create()` et `fLogger = FakeLogStream::create()` dans `init()`.
  - Implémentation locale de `QualcommWifi::getActionFramePoolCapacity()` (slot 440) renvoyant 0x100 (256 frames).
  - Suppression de l'écriture directe `kIOPCIConfigCommand` sur le bus PCIe AMD pendant `start()` afin d'éviter tout conflit de bridge ou transaction d'arbitrage invalidée.
  - Nettoyage du buffer statique non-thread-safe dans `logNvramStage`.
- `QualcommWifi/QualcommWifi.hpp` :
  - Déclaration de `virtual UInt32 getActionFramePoolCapacity(void) override`.
- `build_wifi.sh` :
  - Ajout du flag `-Wno-invalid-offsetof` pour compilation propre de l'assertion ABI.
- `docs/ISSUES_AND_POSTMORTEMS.md` :
  - Documentation détaillée du post-mortem INC-10 expliquant le mécanisme de triple fault et la solution apportée.

### Validation
- Compilation réussie avec 0 erreur.
- Vérification de la vtable `__ZTV12QualcommWifi` : taille exacte de 3728 octets (0xe90, 466 slots) conforme à OpenCore et Apple `IO80211Controller`.
- Validation de conformité des symboles et dépendances à 100% via `kmutil libraries`.
- Déploiement du kext compilé dans `/Volumes/EFI/EFI/OC/Kexts/QualcommWifi.kext`.
- Activation confirmée dans `/Volumes/EFI/EFI/OC/config.plist` (`Enabled = True`).
2026-10-17 23:14:09 +02:00

14 KiB

Historique & Post-Mortems des Problèmes Rencontrés

Ce document répertorie de manière exhaustive l'ensemble des anomalies, blocages, panics et bootloops rencontrés lors du développement du pilote natif Wi-Fi Qualcomm WCN7850 (QualcommWifi), avec leurs analyses techniques de cause racine et leurs résolutions vérifiées.


Sommaire des Incidents

ID Incident / Symptôme Composant Touché Cause Racine Statut
INC-01 Erreur d'édition de liens : symboles de réservation IONetworkController non résolus Linker / KPI macOS Méthodes virtuelles de réserve non exportées par le noyau Tahoe Résolu
INC-02 Crash précoce OpenCore : padslot mismatch ABI / Vtable Vtable IO80211Controller allongée à 466 slots sous Darwin 25 Résolu
INC-03 Échec silencieux de démarrage : IO80211Controller::start fails WorkQueue / IOKit IONetworkController::start requiert une IO80211WorkQueue Résolu
INC-04 Bootloop de 5 secondes : réinitialisation matérielle brutale du PC Kernel Panic Apple panic("No ivars->_faultReporter") dans findAndAttachToFaultReporter Résolu
INC-05 Échec d'allocation interne : Failed to allocate RNG Agent RNG / Logger IO80211RNGAgent requiert un logger non-nul lors de l'initialisation Résolu
INC-06 Désynchronisation du cycle d'initialisation au boot Cycle Kexts (Info.plist) OSBundleRequired positionné à Root au lieu de Network-Root Résolu
INC-07 Blocage du chargement firmware AMSS lors du téléversement BHIe PCIe ASPM Désynchronisation BAR0 causée par les transitions d'énergie L0s/L1 Résolu
INC-08 Corruptions mémoires & gel sur plateforme AMD avec > 4 Go RAM Moteur DMA 36-bit Pointeur DMA physique tronqué au-delà de 64 Go Résolu
INC-09 Impossibilité de lire les rapports de panique (redémarrage instantané) OpenCore / NVRAM Paramètres de débogage du noyau par défaut provoquant un reboot Résolu

Détail des Post-Mortems

INC-01 : Erreur d'édition de liens sur les réservations IONetworkController

  • Symptôme : Lors de la compilation et de l'édition de liens du binaire Mach-O Kext, le compilateur Clang échouait avec des erreurs Undefined symbols for architecture x86_64 : _ZN19IONetworkController29_RESERVEDIONetworkController2Ev jusqu'à 7Ev.
  • Cause Racine : Dans le Kernel Collection BootKernelExtensions.kc de macOS Tahoe, Apple conserve les slots virtuels de réservation pour préserver l'ABI C++, mais n'exporte pas leurs symboles dans les tables d'exportation globales (libkern/KPI).
  • Résolution : Définition de stubs d'export locaux C dans QualcommWifi.cpp :
    extern "C" {
        void _ZN19IONetworkController29_RESERVEDIONetworkController2Ev(void) {}
        void _ZN19IONetworkController29_RESERVEDIONetworkController3Ev(void) {}
        void _ZN19IONetworkController29_RESERVEDIONetworkController4Ev(void) {}
        void _ZN19IONetworkController29_RESERVEDIONetworkController5Ev(void) {}
        void _ZN19IONetworkController29_RESERVEDIONetworkController6Ev(void) {}
        void _ZN19IONetworkController29_RESERVEDIONetworkController7Ev(void) {}
    }
    

INC-02 : Crash OpenCore padslot mismatch sur la Vtable

  • Symptôme : Le Kext provoquait un plantage immédiat au chargement par OpenCore ou un KP dans Vtables.c.
  • Cause Racine : OpenCore valide strictement la taille de la table de fonctions virtuelles de chaque classe dérivée. Sous macOS Sonoma/Sequoia (Darwin 24), la vtable d'IO80211Controller comptait 448 entrées. Sous macOS Tahoe 26.x (Darwin 25), elle a été portée à 466 entrées (taille binaire de 0xe90 = 3728 octets). Tout écart dimensionnel invalide la correspondance des slots mémoires.
  • Résolution : Vérification systématique par script de la vtable __ZTV12QualcommWifi contre BootKernelExtensions.kc. Tous les slots jusqu'à l'offset 0xe90 sont rigoureusement alignés sur l'ABI Tahoe.

INC-03 : Échec de IO80211Controller::start sans WorkQueue

  • Symptôme : Le pilote démarrait dans init() et probe(), mais IO80211Controller::start affichait IO80211Controller::start fails et retournait false.
  • Cause Racine : L'analyse désassemblée de IO80211Controller::start (0xffffff8002291f30) a révélé qu'à l'offset +0x41, le code teste le retour de IONetworkController::start(provider). Cette dernière exige impérativement un gestionnaire de queue de travail valide pour instancier ses boucles d'événements.
  • Résolution : Implémentation des surcharges de création de la file de travail dans QualcommWifi.cpp :
    bool QualcommWifi::createWorkQueue(void) {
        if (!fWorkQueue) {
            fWorkQueue = IO80211WorkQueue::workQueue();
        }
        return (fWorkQueue != nullptr);
    }
    IO80211WorkQueue* QualcommWifi::getWorkQueue(void) const { return fWorkQueue; }
    bool QualcommWifi::createWorkLoop(void) { return createWorkQueue(); }
    IOWorkLoop* QualcommWifi::getWorkLoop(void) const { return getWorkQueue(); }
    

INC-04 : Le Bootloop de 5 Secondes - Kernel Panic _faultReporter

  • Symptôme : L'utilisateur subissait un redémarrage en boucle constant du Mac : OpenCore se lance, commence le chargement de macOS, puis au bout de 5 secondes environ, le PC s'éteint et redémarre instantanément sans message visible.
  • Cause Racine (Breakthrough Majeur) : Désassemblage complet du flux de IO80211Controller::start à l'adresse 0xffffff8002291f30 dans BootKernelExtensions.kc :
    1. À +0x272, le pilote invoque findAndAttachToFaultReporter() (0xffffff8002292628).
    2. À +0x13e, cette routine appelle la méthode virtuelle this->getFaultReporterFromDriver() (callq *0xd80(%rax), vtable slot 434).
    3. Notre code renvoyait initialement nullptr.
    4. À l'adresse 0xffffff8002292737 (+0x227), findAndAttachToFaultReporter contient un appel direct et inconditionnel à _panic :
      panic("\"No ivars->_faultReporter\" @%s:%d", "IO80211Controller.cpp", 2877)
      
    5. Faute d'argument empêchant le redémarrage, macOS déclenchait immédiatement un hardware reset.
  • Résolution :
    1. Instanciation dans init() d'un objet OSObject valide :
      fFaultReporter = OSString::withCString("QualcommFaultReporter");
      
    2. Surcharge de getFaultReporterFromDriver() retournant fFaultReporter.
    3. Libération propre dans free() via OSSafeReleaseNULL(fFaultReporter).
    4. Dans le noyau, findAndAttachToFaultReporter() constate que le pointeur est non-nul, stocke l'adresse dans ivars->_faultReporter, incrémente la référence via obj->retain() (*0x20(%rax)), et aucun panic n'est émis.

INC-05 : Échec d'allocation IO80211RNGAgent / getLogger()

  • Symptôme : Échec dans IO80211Controller::start à l'étape d'initialisation du générateur aléatoire Wi-Fi avec le log : Failed to allocate RNG Agent.
  • Cause Racine : L'inspection binaire de IO80211RNGAgent::withOptions() (0xffffff80022dd55c) a montré qu'à l'offset +0xe6, la fonction appelle this->getLogger() (vtable slot 426, *0xd40(%rax)). Si ce pointeur est nul, elle saute directement en erreur (+0x12f), détruit le descripteur et renvoie NULL.
  • Résolution : Instanciation dans init() d'un objet fLogger = OSString::withCString("QualcommLogger"), et implémentation de getLogger() retournant (void*)fLogger. RNGAgent effectue son appel logger->retain() et s'initialise avec succès.

INC-06 : Mésalignement du Cycle Kext OSBundleRequired

  • Symptôme : Tentative de matching du pilote PCI trop précoce lors de la séquence de boot, provoquant des échecs d'allocation de services réseau.
  • Cause Racine : Dans Info.plist, OSBundleRequired avait été temporairement passé à Root. Or, IO80211Family.kext d'Apple spécifie impérativement Network-Root. Déclarer Root force le chargement du pilote avant que la pile des protocoles réseau et des sockets du noyau ne soit prête.
  • Résolution : Rétablissement strict de :
    <key>OSBundleRequired</key>
    <string>Network-Root</string>
    

INC-07 : Blocage du Téléversement BHIe par ASPM PCIe

  • Symptôme : Timeout lors du transfert de l'image AMSS complète (~6 Mo) via BHIe, le statut BHI restant figé en boucle.
  • Cause Racine : La gestion d'énergie active du bus PCIe (Active State Power Management - ASPM) tente de basculer la liaison en veille L0s ou L1 pendant les transferts DMA volumineux, corrompant les accès aux registres MMIO de notification de sonnette (ATH12K_BHIE_TXVECDB).
  • Résolution : Désactivation dynamique d'ASPM avant tout transfert firmware :
    UInt8 pciCapOffset = 0;
    if (fPciDevice->findPCICapability(kIOPCIPCIExpressCapability, &pciCapOffset) && pciCapOffset != 0) {
        UInt16 linkCtrl = fPciDevice->configRead16(pciCapOffset + 0x10);
        linkCtrl &= ~0x3; // Désactive L0s et L1
        fPciDevice->configWrite16(pciCapOffset + 0x10, linkCtrl);
    }
    

INC-08 : Échec et Corruption DMA sur Plateformes AMD 64-bit (> 4 Go RAM)

  • Symptôme : Gel ou reboot brutal lors de l'accès aux anneaux de commandes ou lors du démarrage du Primary Boot Loader.
  • Cause Racine : Le SoC WCN7850 possède une unité DMA adressable sur 36 bits maximum (plage physique 0x000000000 à 0xFFFFFF000). Sur les cartes mères équipées de 32 Go ou 64 Go de RAM, les allocations kernel classiques peuvent renvoyer des tampons au-dessus de 64 Go. Les bits de poids fort étaient tronqués par le matériel, écrivant dans des zones mémoires arbitraires du noyau.
  • Résolution : Toutes les allocations DMA contiguës sont contraintes par un masque physique 36 bits :
    #define ATH12K_DMA_MASK_36BIT_PAGE 0xFFFFFF000ULL
    IOBufferMemoryDescriptor::inTaskWithPhysicalMask(
        kernel_task,
        kIODirectionInOut | kIOMemoryPhysicallyContiguous,
        size,
        ATH12K_DMA_MASK_36BIT_PAGE
    );
    

INC-09 : Redémarrages Automatiques Invisibles sans Affichage de KP

  • Symptôme : En cas de panic du noyau, le PC redémarrait instantanément sans donner le temps à l'utilisateur de lire la pile d'appels ou la cause du crash.
  • Cause Racine : Par défaut, le noyau XNU sous OpenCore réinitialise le processeur en cas de panique non interceptée.
  • Résolution : Injection de panic=0 dans les boot-args OpenCore du fichier config.plist. En cas d'erreur critique, le noyau gèle l'affichage, permettant de lire textuellement le fichier source et la ligne fautive (ex: IO80211Controller.cpp:2877).

INC-10 : Triple Fault Processeur ("Coupure de Courant") lors de l'appel CCLogStream::shouldLog sur fLogger

  • Symptôme : Lors du démarrage d'OpenCore avec QualcommWifi.kext actif, le PC s'éteint et redémarre instantanément comme une coupure de courant brutale, sans panic log ni rapport d'erreur NVRAM.
  • Cause Racine (Breakthrough Fondamental) :
    1. Lors du commit 5357ca6, getLogger() avait été implémenté en renvoyant une instance d'OSString (OSString::withCString("QualcommLogger")).
    2. Lors de l'exécution de IO80211Controller::start() -> createIOReporters() -> addReporterLegend(), le noyau macOS invoque CCLogStream::shouldLog(this->getLogger(), level) (0xffffff80032ff9e6).
    3. Le désassemblage de CCLogStream::shouldLog a révélé :
      movq 0x90(%rdi), %rax   ; Lit l'offset +0x90 de l'objet passé en rdi (fLogger)
      movq 0x58(%rax), %rax   ; Déréférence ce pointeur en Ring 0 !
      cmpl 0x8(%rax), %esi    ; Compare le niveau de log
      setle %al
      retq
      
    4. L'objet OSString ne mesurant que 32 octets (0x20), l'accès à +0x90 pointait sur une adresse mémoire arbitraire ou non mappée dans le heap kernel. Le déréférencement immédiat de cette valeur via movq 0x58(%rax), %rax déclenchait une faute de page (#PF) ou une protection générale (#GP) non interceptée en mode superviseur (Ring 0), provoquant un Triple Fault CPU réinitialisant la carte mère instantanément.
    5. Parallèlement, IO80211RNGAgent::withOptions() (0xffffff80022dd55c) et IO80211ControllerMonitor::initWithControllerAndProvider() (0xffffff80023929d0) requièrent impérativement que getLogger() retourne un pointeur non-nul et appellent logger->retain() (*0x20(%rax)).
  • Résolution Technique :
    1. Conception d'une classe FakeLogStream : public OSObject respectant au millimètre l'ABI de CCLogStream :
      • Taille minimale supérieure à 0x98 octets avec p1 placé exactement à l'offset +0x90 (validé par static_assert(__builtin_offsetof(FakeLogStream, p1) == 0x90)).
      • p1 pointe vers une zone mémoire fP1Mem (0x1000 octets) dont l'offset +0x58 pointe vers fP2Mem (0x100 octets initialisés à 0).
      • CCLogStream::shouldLog et CCLogStream::log* lisent ainsi 0x90 -> 0x58 -> 0, constatent un niveau de log nul, et retournent proprement false sans tenter d'émettre sur un pipe inexistant.
      • FakeLogStream étant un OSObject, les appels retain() et release() effectués par RNGAgent s'exécutent avec une fidélité parfaite.
    2. Implémentation locale de QualcommWifi::getActionFramePoolCapacity() (slot 440) renvoyant 0x100 (256 frames).
    3. Suppression de l'écriture directe kIOPCIConfigCommand sur le bus PCIe AMD pour éviter toute déstabilisation du root port.