macOS TCC SIGKILL : Description d'usage manquante dans Info.plist
Problème
Une app macOS plante avec EXC_CRASH (SIGKILL) dès qu'elle appelle une API protégée par les règles de confidentialité. Il n'y a aucune chance de gérer l'erreur, aucune invite de permission, aucune réponse de refus — juste une terminaison immédiate du processus par le système d'exploitation.
Le plantage est causé par l'application de TCC sur macOS : si votre app se lie à ou appelle une API qui accède à une ressource protégée par les règles de confidentialité et que votre Info.plist ne contient PAS la clé de description d'usage correspondante, le système d'exploitation tue le processus pour violation de confidentialité.
Contexte / Conditions de déclenchement
Le rapport de crash (~/Library/Logs/DiagnosticReports/<App>-*.ips) contient tous ces éléments :
"exception": { "type": "EXC_CRASH", "signal": "SIGKILL" }"termination": { "namespace": "TCC", ... }- La chaîne des détails de terminaison contient : « This app has crashed because it attempted to access privacy-sensitive data without a usage description »
- La pile d'appels du thread planté inclut
TCC.__TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__ - Les frames en dessous sont généralement
TCC.__TCCAccessRequest_block_invoke,libxpc.dylib._xpc_connection_reply_callout, les mécanismes de dispatch queue
La chaîne des détails de terminaison vous indique exactement quelle clé manque — lisez-la.
Tableau de correspondance des clés Info.plist
| Ressource | Clé Info.plist | Entitlement (apps en sandbox) |
|---|---|---|
| Caméra | NSCameraUsageDescription |
com.apple.security.device.camera |
| Microphone | NSMicrophoneUsageDescription |
com.apple.security.device.audio-input |
| Photos (lecture/écriture) | NSPhotoLibraryUsageDescription |
com.apple.security.personal-information.photos-library |
| Photos (ajout uniquement) | NSPhotoLibraryAddUsageDescription |
com.apple.security.photos.library.add-only |
| Contacts | NSContactsUsageDescription |
com.apple.security.personal-information.addressbook |
| Calendrier | NSCalendarsUsageDescription |
com.apple.security.personal-information.calendars |
| Rappels | NSRemindersUsageDescription |
com.apple.security.personal-information.reminders |
| Localisation | NSLocationUsageDescription (+ variantes WhenInUse / Always) |
com.apple.security.personal-information.location |
| Reconnaissance vocale | NSSpeechRecognitionUsageDescription |
— |
| Dossier Bureau | NSDesktopFolderUsageDescription |
com.apple.security.files.user-selected.read-only etc. |
| Dossier Documents | NSDocumentsFolderUsageDescription |
— |
| Dossier Téléchargements | NSDownloadsFolderUsageDescription |
com.apple.security.files.downloads.read-write |
Le piège des Photos (critique)
NSPhotoLibraryAddUsageDescription n'est pas un substitut à NSPhotoLibraryUsageDescription. Ils couvrent des portées de permission différentes :
- Ajout uniquement (
NSPhotoLibraryAddUsageDescription+com.apple.security.photos.library.add-only) : suffit pour les API qui enregistrent uniquement un asset, commePHAssetCreationRequestdepuis un fichier, ouGal.putVideo(path)sans album. - Accès complet (
NSPhotoLibraryUsageDescription) : requis pour toute lecture, toute requête, toute recherche/création d'album, et notamment pourGal.putVideo(path, album: 'SomeAlbum')— car « mettre dans l'album X » doit lire ou créer l'album, ce qui est une opération au niveau de la bibliothèque.
Symptôme en cas d'erreur : l'enregistrement sans album fonctionne, l'enregistrement avec album plante.
Solution
- Lisez les détails de terminaison du rapport de crash — il nomme la clé exacte.
- Ajoutez la clé à Info.plist de votre app :
- Flutter macOS :
mobile/macos/Runner/Info.plist - macOS natif :
<target>/Info.plistou le paramètre de buildINFOPLIST_FILEde la cible
- Flutter macOS :
- Si l'app est en sandbox (
com.apple.security.app-sandbox= true en release), ajoutez également l'entitlement correspondant à la fois àRunner/DebugProfile.entitlementsetRunner/Release.entitlements. - Reconstruisez. Les changements de Info.plist ne se rechargent pas à chaud dans Flutter — une compilation complète
flutter build macos --debugest requise. - Relancez l'app.
Exemple de patch
App Flutter macOS plantant sur gal.putVideo(path, album: 'MyAlbum') :
<!-- mobile/macos/Runner/Info.plist -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>MyApp needs access to save videos to your Photos library.</string>
<!-- AJOUTEZ CECI : -->
<key>NSPhotoLibraryUsageDescription</key>
<string>MyApp needs access to your Photos library to organize videos into albums.</string>
Puis :
cd mobile
flutter build macos --debug
open build/macos/Build/Products/Debug/MyApp.app
Vérification
- Aucun plantage sur l'action qui plantait auparavant.
- macOS affiche une invite de permission la première fois (si le statut est
.notDetermined). - Vérifiez le répertoire des nouveaux rapports de crash — aucun fichier
.ipsne devrait apparaître après l'action :ls -t ~/Library/Logs/DiagnosticReports/<AppName>-*.ips 2>/dev/null | head -3 - Vérifiez que le bundle .app compilé contient la clé (Info.plist est copié lors de la compilation, mais vérifiez pour être sûr) :
grep -A1 NSPhotoLibrary build/macos/Build/Products/Debug/MyApp.app/Contents/Info.plist log show --last 2m --predicate 'subsystem == "com.apple.TCC"'devrait afficher une autorisation pour le bundle ID de l'app au lieu d'un plantage.
Diagnostic à partir du rapport de crash
Une commande rapide pour extraire les informations clés d'un rapport de crash .ips :
python3 -c "
import json, sys
with open(sys.argv[1]) as f:
f.readline() # skip header JSON line
d = json.loads(f.read())
print('Signal:', d.get('exception', {}).get('signal'))
print('Namespace:', d.get('termination', {}).get('namespace'))
print('Details:', d.get('termination', {}).get('details'))
" ~/Library/Logs/DiagnosticReports/MyApp-*.ips
Si namespace == 'TCC' et les détails mentionnent « usage description », cette compétence s'applique.
Notes
- Pourquoi SIGKILL et pas un refus gracieux ? Apple a décidé que les violations de confidentialité sont une défaillance de politique du développeur, pas une condition d'exécution à gérer. Le système d'exploitation termine le processus pour qu'il n'y ait aucun moyen pour une app de « réessayer quand même » ou de logger des contournements. Il n'y a pas d'API pour capturer ceci.
- Cela s'applique même aux chemins de code que vous n'appelez pas. Si un framework lié ou un plugin touche une ressource protégée en votre nom (par exemple un plugin média qui sonde Photos au démarrage), vous avez besoin de la description d'usage même si votre propre code ne touche jamais Photos. C'est pourquoi la clé doit être ajoutée chaque fois que vous incluez un plugin qui pourrait accéder à la ressource.
- Le rechargement à chaud ne vous sauvera pas. Le rechargement à chaud et le redémarrage à chaud de Flutter ne mettent pas à jour Info.plist dans le bundle en cours d'exécution. Vous devez reconstruire complètement.
- iOS vs macOS : Les mêmes clés existent sur iOS, mais sur iOS une clé manquante entraîne généralement un refus silencieux plutôt qu'un SIGKILL sur les anciennes versions d'iOS. Les versions récentes d'iOS s'alignent sur macOS et vont planter. Dans tous les cas, définissez toujours la clé.
- La distinction « Ajout » vs Photos complet a été resserrée dans macOS 14/15/26. Les versions antérieures de macOS vous permettaient de faire certaines opérations d'album avec l'ajout uniquement ; les versions récentes ne le permettent pas. Si une app qui fonctionnait sur une macOS antérieure commence à planter après une mise à niveau de macOS, vérifiez ceci en premier.
- Connexe mais différent :
flutter-macos-tcc-responsible-process-camera-denialcouvre TCC refusant l'accès à la caméra quand l'attribution du processus responsable est mauvaise (pas de plantage, juste un refus silencieux). C'est un mode de défaillance TCC différent du SIGKILL couvert ici.
Références
- Apple : Requesting access to protected resources
- Apple :
NSPhotoLibraryUsageDescription - Apple :
NSPhotoLibraryAddUsageDescription - Forums Apple Developer : recherche « TCC privacy violation SIGKILL Info.plist »
log show --predicate 'subsystem == "com.apple.TCC"'pour les décisions TCC en temps réel.