
La sandbox applicative : apprendre à vivre en cage
On ne peut pas ouvrir un fichier sans demander la permission. À chaque fois.
Le Mac App Store exige le sandboxing. Soumettez sans l’entitlement de sandbox et l’application est rejetée. La sandbox est le modèle de sécurité d’Apple pour les logiciels Mac distribués, et pour la plupart des applications de productivité, c’est un inconvénient modéré. Pour un analyseur de disque, c’est une contrainte de fond.
Le boulot de Renala, c’est de lire le système de fichiers. La sandbox répond : pas sans permission.
Ce que fait la sandbox
On peut se la représenter comme un conteneur, dans le même esprit que Docker, mais appliqué au niveau du système d’exploitation. Une application sandboxée reçoit sa propre tranche de système de fichiers sous ~/Library/Containers/<bundle-id>/Data/. Préférences, caches, fichiers de support applicatif : tout vit là-dedans, pas dans le vrai répertoire personnel.
Par défaut, une application sandboxée ne peut pas lire de fichiers arbitraires. Elle ne peut pas ouvrir /Users/vous/Documents/ sans que l’utilisateur accorde explicitement l’accès. Elle ne peut pas atteindre les volumes autres que le disque de démarrage. Certains répertoires système lui sont tout bonnement invisibles.
Le modèle de sécurité est solide. Une application sandboxée compromise ne peut pas lire vos clés SSH, accéder à vos mails, ni toucher aux données des autres applications. La contrepartie, c’est que les applications légitimes, analyseurs de disque compris, doivent se battre pour des capacités que les applications non sandboxées obtiennent sans effort. Un analyseur de disque qui ne peut pas lire le disque, ça ne sert à rien.
Les security-scoped bookmarks
Quand l’utilisateur sélectionne un répertoire via NSOpenPanel, l’application reçoit une URL avec un accès valide pour la session en cours. On ferme l’application, on la relance : l’URL n’est plus qu’une chaîne de caractères. L’accès a disparu.
Pour rouvrir le même répertoire après un redémarrage sans redemander la permission, il faut un security-scoped bookmark. Quand l’utilisateur accorde l’accès, on appelle url.bookmarkData(options: .withSecurityScope, ...) et on stocke le blob résultant. Au lancement suivant, URL(resolvingBookmarkData:) reconstruit l’URL, et url.startAccessingSecurityScopedResource() réactive la portée.
L’accès n’est pas gratuit. Il faut appeler stopAccessingSecurityScopedResource() une fois qu’on a terminé, faute de quoi la portée de sécurité fuit. Le système limite le nombre de portées pouvant être ouvertes simultanément.
Quand ça marche, le système de bookmarks est invisible. L’utilisateur accorde l’accès une seule fois et n’y pense plus. Don’t Make Me Think s’applique à l’expérience de sécurité autant qu’à la navigation. Une invite de permission qui surgit sans raison apparente est perçue comme un bogue, même quand c’est techniquement le comportement attendu.
flowchart TD
A["L'utilisateur choisit un répertoire<br/>dans NSOpenPanel"] -->|"accorde l'accès"| B["URL + portée de sécurité<br/>active pour la session"]
B -->|"url.bookmarkData(...)"| C["Blob du bookmark<br/>persisté sur disque"]
B -->|"startAccessingSecurityScopedResource"| D["Accès aux fichiers"]
D -->|"stopAccessingSecurityScopedResource"| E["Portée libérée"]
C -->|"prochain lancement"| F["URL(resolvingBookmarkData:)"]
F -->|"startAccessingSecurityScopedResource"| D
F -->|"périmé ou invalide"| G["Nouvelle autorisation<br/>boîte de dialogue"]
Le piège NSHomeDirectory
Tôt dans le développement, le code de détection des disques cloud vérifiait si un chemin commençait par le répertoire personnel de l’utilisateur. La logique utilisait NSHomeDirectory() pour obtenir ce chemin. Approche raisonnable, contexte inadéquat.
Dans une application sandboxée, NSHomeDirectory() renvoie le chemin du conteneur : ~/Library/Containers/com.renala.app/Data/. Pas /Users/vous/. Le vrai répertoire personnel est invisible pour cette API.
Toutes les vérifications de chemins cloud échouaient en silence. Les chemins iCloud commencent par /Users/vous/Library/Mobile Documents/. Les chemins Google Drive commencent par /Users/vous/Library/CloudStorage/. Aucun ne commence par le chemin du conteneur. Le code de détection cloud était un no-op pour tous les utilisateurs. Pas de plantage, pas d’erreur, pas de log. Faux de bout en bout, sans un bruit. Le code passait tous les tests que j’avais écrits, parce que j’avais écrit les tests dans le même mauvais environnement.
La correction : ne plus utiliser NSHomeDirectory() du tout. Vérifier si le chemin contient "/Library/CloudStorage/" ou "/Library/Mobile Documents/". Recherche de sous-chaîne, pas correspondance de préfixe. Le vrai chemin contiendra ces segments quoi que le répertoire personnel renvoie comme préfixe.
L’entitlement lecture-écriture
L’entitlement de sandbox pour l’accès aux fichiers propose deux modes : lecture seule et lecture-écriture. Un analyseur de disque ne fait que lire des fichiers. Lecture seule est le choix évident. Empreinte plus légère, posture de sécurité plus propre.
Et puis est arrivé « Placer dans la Corbeille ».
NSWorkspace.recycle(_:completionHandler:) déplace des fichiers vers la Corbeille. Il exige un accès en écriture. Un entitlement en lecture seule bloque l’appel en silence. Rien ne bouge, aucune erreur.
Le compromis : demander l’entitlement lecture-écriture. C’est plus large que ce qui est strictement nécessaire pour l’analyse, mais un analyseur de disque qui se contente de montrer le problème sans laisser l’utilisateur le résoudre, c’est un outil de diagnostic sans remède. Personne n’en veut.
graph TD
A["Entitlement App Sandbox<br/>com.apple.security.app-sandbox"] --> B["Entitlement lecture-écriture<br/>com.apple.security.files.user-selected.read-write<br/>(requis pour Placer dans la Corbeille)"]
A --> C["Entitlement fichiers sélectionnés<br/>com.apple.security.files.user-selected.read-only<br/>(accès temporaire par session)"]
B --> D["Entitlement bookmarks<br/>com.apple.security.files.bookmarks.app-scope<br/>(accès persistant entre lancements)"]
C --> D
Volumes externes
Analyser un SSD externe, une clé USB ou un volume Time Machine requiert un accès au-delà du disque de démarrage. La sandbox ne l’accorde pas par défaut.
L’entitlement com.apple.security.files.all-volumes étend l’accès à tous les volumes montés. Sans lui, NSOpenPanel permet toujours d’accorder l’accès à un volume externe spécifique pour la session, mais l’expérience est morcelée et les permissions ne persistent pas proprement d’un lancement à l’autre.
Pour un outil dont la raison d’être est « aidez-moi à comprendre ce qu’il y a sur tous mes disques », bloquer les volumes externes par défaut serait une limitation sévère.
Le retour d’un ami
Un ami, designer graphique et spécialiste UX, a testé l’application sur son Mac Intel. Son retour écrit contenait ceci : « J’ai l’impression qu’elle n’analyse pas les fichiers cachés. Il faudrait peut-être ajouter une option ? Par ailleurs, on ne peut même pas cibler un dossier caché directement pour l’analyser. »
Il avait raison sur les deux points. Le scanner ignore les fichiers cachés par défaut (un comportement courant dans l’énumération de fichiers sous macOS) ; .Trash, .config et les répertoires similaires sont donc absents des résultats. La sandbox aggrave le problème : le sélecteur de fichiers standard ne montre pas les éléments cachés, si bien qu’un utilisateur qui voudrait analyser un dossier caché n’a aucun moyen d’y naviguer via l’interface.
Ajouter un bouton « afficher les fichiers cachés » au scanner est simple. Permettre de cibler un dossier caché via le sélecteur de fichiers exige de configurer le sélecteur explicitement, et la plupart des utilisateurs ne s’attendraient pas à en avoir besoin. Ce sont des limitations connues. Le retour a confirmé qu’elles ne passent pas inaperçues : un designer les a repérées dès la première session.
Vivre avec les contraintes
Pour la plupart des applications, la sandbox est un surcoût mineur. Pour un analyseur de disque, elle impose une attention explicite aux permissions, au cycle de vie des bookmarks et à la gestion des chemins.
Les bogues qu’elle engendre sont du pire genre : silencieux. NSHomeDirectory() qui renvoie le chemin du conteneur ne provoquera pas de plantage. Ne génèrera pas d’erreur dans les logs. Il fera simplement échouer la détection cloud pour tous les utilisateurs, pendant que le code a l’air parfaitement correct.
La distinction du chemin de conteneur, le cycle de vie des bookmarks, l’exigence de lecture-écriture pour la corbeille : chacun de ces points est documenté quelque part dans la documentation développeur d’Apple. Éparpillés entre différentes sections, jamais signalés comme pièges. On les découvre comme on découvre toutes les mines d’une plateforme : en marchant dessus.
L’App Store exige la sandbox. Les détails d’implémentation sont laissés en exercice au lecteur.
Références
- Apple: App Sandbox overview: Concepts, container layout, and entitlement reference.
- Apple: startAccessingSecurityScopedResource: Security-scoped bookmark activation and scope lifecycle.
- WWDC 2012: The OS X App Sandbox: Original deep dive into sandbox design, containers, and entitlements.
- Apple: Entitlements reference: Full list of available entitlement keys and their effects.