Bloquer les erreurs mémoire avec Address Sanitizer sur un Mac cloud

Bloquer les erreurs mémoire avec Address Sanitizer sur un Mac cloud

Un projet iOS combinant Swift, Objective-C et des dépendances C/C++ peut réussir tous ses tests habituels tout en déclenchant, sur un chemin d’exécution rarement emprunté, un dépassement de tableau, une double libération ou un accès après libération. Une fois la tâche transférée sur un Mac cloud, ne vous contentez pas de relancer les tests existants : ajoutez plutôt un pipeline Address Sanitizer (ASan) qui fait immédiatement échouer le build en cas d’accès mémoire invalide et conserve un bundle de résultats permettant de reproduire le problème.

Définir les erreurs que la barrière doit détecter

ASan détecte principalement les dépassements de limites sur la pile et le tas, les accès après libération, les doubles libérations ainsi que certains accès via des pointeurs invalides. Il est particulièrement adapté aux couches de pont Objective-C, aux bibliothèques de traitement d’images, aux abstractions de bases de données et aux interfaces C écrites à la main. Le code Swift pur peut également en bénéficier, mais une exécution réussie ne garantit pas l’absence de problèmes mémoire : les branches non parcourues par les tests ne sont pas vérifiées, tandis que les fuites et les accès concurrents nécessitent d’autres outils.

Commencez par sélectionner un ensemble de tests stables et sans interaction humaine comme point d’entrée de la barrière. Évitez d’y intégrer d’emblée tous les tests d’interface. Les parseurs principaux, la couche de cache et les interfaces entre langages sont prioritaires.

Niveau de test Fréquence d’exécution Traitement en cas d’échec
Tests unitaires principaux À chaque demande de fusion Bloquer la fusion
Tests d’intégration Branche principale ou demande de fusion Bloquer la publication
Tests UI complets Tâche planifiée ou phase de version candidate Créer un ticket et reproduire le problème

L’intérêt d’un Sanitizer ne se limite pas à faire échouer une tâche. Il permet aussi de reproduire la même erreur avec la même entrée et de conserver la pile d’appels, le nom du test et l’environnement de build.

Créer un Scheme et des répertoires dédiés

Dans Xcode, dupliquez le Scheme de test existant, nommez-le App-ASan, puis définissez-le comme Shared. Ne modifiez pas directement le Scheme utilisé quotidiennement par les développeurs, au risque de mélanger les paramètres du débogage local, des mesures de performances et de la CI. Le Scheme doit être versionné à l’emplacement suivant :

App.xcodeproj/xcshareddata/xcschemes/App-ASan.xcscheme

Utilisez un répertoire DerivedData distinct pour chaque tâche. Vous éviterez ainsi que des fichiers objets issus d’un build standard soient réutilisés par erreur avec ASan, ainsi que les conflits entre deux tâches parallèles sur l’index et les artefacts intermédiaires. Enregistrez également les bundles de résultats dans des répertoires séparés par identifiant de tâche, au lieu d’écraser systématiquement un fichier fixe.

Fixer la destination de test

Commencez par afficher les simulateurs disponibles :

xcrun simctl list devices available

Dans la CI, il est préférable de fixer le type d’appareil et la version majeure du système, tout en autorisant la version corrective à suivre les runtimes installés. Si l’équipe doit valider plusieurs versions du système, créez une matrice de tâches plutôt que de dépendre d’une sélection automatique dans une seule commande.

Exécuter la vérification avec xcodebuild

Le script ci-dessous utilise un répertoire de build et un bundle de résultats dédiés. Adaptez l’espace de travail, le Scheme et le nom du simulateur aux valeurs réelles du projet.

#!/bin/zsh
set -u

ROOT="${PWD}"
OUT="${ROOT}/artifacts/asan"
DERIVED="${ROOT}/.ci/DerivedData-ASan"

rm -rf "${OUT}" "${DERIVED}"
mkdir -p "${OUT}"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App-ASan \
  -configuration Debug \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "${DERIVED}" \
  -resultBundlePath "${OUT}/App-ASan.xcresult" \
  -enableAddressSanitizer YES \
  CODE_SIGNING_ALLOWED=NO \
  >"${OUT}/xcodebuild.log" 2>&1

status=$?
exit ${status}

N’ajoutez pas || true à la commande de test pour la forcer à réussir. Si la plateforme de CI doit post-traiter les journaux, enregistrez d’abord le code de sortie, terminez l’archivage, puis renvoyez ce code sans le modifier. CODE_SIGNING_ALLOWED=NO convient aux tests sur simulateur. Les tâches exécutées sur un appareil réel ou produisant une archive doivent utiliser une configuration de signature distincte et ne peuvent pas reprendre ce réglage tel quel.

ASan augmente la consommation de mémoire et la durée d’exécution. Le nombre de tâches simultanées sur une même machine doit donc être inférieur à celui des tests unitaires standards. Si un processus est interrompu par le système, réduisez d’abord la concurrence avant de conclure à un défaut de l’application testée.

Conserver des preuves d’échec transmissibles

En cas d’échec, conservez au minimum xcodebuild.log et le fichier .xcresult. Le premier facilite la recherche des rapports du Sanitizer ; le second contient la hiérarchie des tests, les pièces jointes associées aux échecs et les journaux d’activité. Avant l’archivage, vous pouvez effectuer un contrôle d’intégrité :

test -s artifacts/asan/xcodebuild.log
test -d artifacts/asan/App-ASan.xcresult
xcrun xcresulttool get test-results summary \
  --path artifacts/asan/App-ASan.xcresult \
  > artifacts/asan/summary.json

Les sous-commandes de xcresulttool peuvent varier selon la version de Xcode. Les appels à cet outil doivent donc être maintenus avec la version de Xcode correspondante. Si une commande devient incompatible, ne supprimez pas pour autant le bundle de résultats d’origine.

Lors de l’analyse du rapport, recherchez d’abord ERROR: AddressSanitizer, puis consignez le type d’erreur, la première frame appartenant au code de l’application, le test déclencheur et les données d’entrée. Les premières frames issues des bibliothèques système indiquent souvent seulement l’endroit où le crash s’est produit ; l’écriture ayant corrompu la mémoire peut être bien antérieure. Si l’erreur ne se reproduit pas de manière fiable, exécutez d’abord le test concerné en boucle et désactivez l’ordre aléatoire des tests afin d’éviter de modifier plusieurs variables à la fois.

Exécuter les tests par niveaux et éviter les pièges courants

La barrière quotidienne peut se limiter à l’ensemble des tests à haut risque, tandis que la branche principale exécute la suite ASan complète. Les étiquettes de test ou les Test Plans sont plus faciles à maintenir que des commandes assemblées à partir de noms de fichiers. Lors de l’ajout d’une interface C, d’opérations sur les pointeurs ou d’un traitement non sécurisé des tampons, ajoutez simultanément les tests concernés à l’ensemble rapide.

Trois erreurs sont courantes. La première consiste à réutiliser le cache d’un build standard, ce qui rend l’instrumentation incomplète. La deuxième consiste à téléverser uniquement le journal texte sans conserver le bundle de résultats. La troisième consiste à traiter un échec ASan comme un test instable et à simplement le relancer. Une nouvelle exécution peut aider à vérifier la reproductibilité, mais le premier rapport doit toujours être conservé. Même si plusieurs tentatives suivantes réussissent, un accès invalide déjà détecté ne doit pas être automatiquement accepté.

Enfin, définissez clairement les critères d’acceptation : le processus de test doit renvoyer un code de sortie nul, le bundle de résultats doit exister, le journal ne doit contenir aucune erreur ASan et la tâche ne doit pas avoir été interrompue par un délai d’expiration ou une pression excessive sur les ressources. La barrière évalue ainsi le résultat complet de l’exécution, et non une simple ligne de résumé donnant l’impression que les tests ont réussi.

Questions fréquentes

Faut-il lancer Address Sanitizer à chaque commit ?

Les tests unitaires ciblés et les modules sensibles peuvent tourner à chaque modification. Les longues suites UI conviennent mieux aux demandes de fusion, à la branche principale ou à une tâche planifiée.

Un contrôle réussi garantit-il l'absence de problème mémoire ?

Non. Seuls les chemins exécutés et certaines catégories d'accès invalides sont couverts. Il faut aussi rechercher les fuites, les courses de données et valider sur appareil réel.

Mac mini physique dédié

Choisir la durée de location d’un Mac dans le cloud selon la durée de la tâche

Deux configurations M4 sont disponibles à la location à la journée, à la semaine, au mois ou au trimestre. Les nœuds et leur disponibilité réelle sont indiqués en temps réel dans la console.

Choisir une configuration et commander