Speicherfehler mit Address Sanitizer in der Cloud-Mac-CI stoppen

Speicherfehler mit Address Sanitizer in der Cloud-Mac-CI stoppen

Ein iOS-Projekt mit einer Mischung aus Swift-, Objective-C- und C/C++-Abhängigkeiten kann selbst dann auf selten ausgeführten Codepfaden Array-Grenzen überschreiten, Speicher doppelt freigeben oder nach der Freigabe weiterverwenden, wenn die regulären Tests wiederholt erfolgreich sind. Nach der Verlagerung der Aufgaben auf einen Cloud-Mac sollten daher nicht einfach nur die vorhandenen Tests erneut ausgeführt werden. Effektiver ist eine zusätzliche Address-Sanitizer-Pipeline (ASan), die den Build bei ungültigen Speicherzugriffen sofort abbricht und ein reproduzierbares Ergebnispaket aufbewahrt.

Zuerst festlegen, was das Gate erkennen soll

ASan erkennt hauptsächlich Zugriffe außerhalb der Heap- und Stack-Grenzen, Use-after-free, doppelte Freigaben sowie bestimmte Zugriffe über ungültige Zeiger. Besonders geeignet ist das Werkzeug für Objective-C-Bridge-Schichten, Bildverarbeitungsbibliotheken, Datenbank-Wrapper und manuell implementierte C-Schnittstellen. Auch reiner Swift-Code profitiert davon. Ein erfolgreicher Durchlauf bedeutet jedoch nicht, dass keine Speicherfehler vorhanden sind: Nicht ausgeführte Verzweigungen werden nicht geprüft, und für Speicherlecks oder Datenrennen sind zusätzliche Werkzeuge erforderlich.

Als Einstiegspunkt für das Gate empfiehlt sich zunächst eine stabile Auswahl automatisierter Tests, die keine manuelle Interaktion erfordern. Nicht gleich zu Beginn sollten sämtliche UI-Tests aufgenommen werden. Kernparser, Cache-Schichten und sprachübergreifende Schnittstellen haben die höchste Priorität.

Testebene Ausführungshäufigkeit Vorgehen bei Fehlern
Zentrale Unit-Tests Bei jedem Merge Request Zusammenführung blockieren
Integrationstests Im Hauptbranch oder bei Merge Requests Veröffentlichung blockieren
Vollständige UI-Tests Zeitgesteuert oder in der Release-Candidate-Phase Fehler erfassen und reproduzieren

Der Nutzen eines Sanitizers besteht nicht nur darin, einen Job fehlschlagen zu lassen. Entscheidend ist, dass derselbe Fehler mit derselben Eingabe erneut ausgelöst werden kann und dabei Aufrufstack, Testname und Build-Umgebung erhalten bleiben.

Eigenes Scheme und separate Verzeichnisse anlegen

Kopieren Sie in Xcode das vorhandene Test-Scheme, nennen Sie es App-ASan und markieren Sie es als Shared. Das von Entwicklern im Alltag verwendete Scheme sollte nicht direkt verändert werden, da sich sonst lokale Debugging-Einstellungen, Leistungsmessungen und CI-Parameter gegenseitig beeinflussen. Das Scheme muss unter folgendem Pfad eingecheckt werden:

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

Jeder Job sollte ein eigenes DerivedData-Verzeichnis verwenden. Dadurch wird verhindert, dass Objektdateien aus regulären Builds versehentlich für ASan verwendet werden oder zwei parallel laufende Jobs um Indizes und Zwischenprodukte konkurrieren. Auch die Ergebnispakete sollten nach Job-ID getrennt gespeichert werden, statt immer wieder dieselbe feste Datei zu überschreiben.

Testziel fest vorgeben

Zeigen Sie zunächst die aktuell verfügbaren Simulatoren an:

xcrun simctl list devices available

In der CI sollten Gerätetyp und Hauptversion des Betriebssystems fest vorgegeben werden, während sich die Patchversion entsprechend der installierten Runtime aktualisieren darf. Muss das Team mehrere Betriebssystemversionen prüfen, sollten dafür separate Matrix-Jobs eingerichtet werden. Eine automatische Auswahl innerhalb eines einzelnen Befehls ist zu vermeiden.

Prüfung mit xcodebuild ausführen

Das folgende Skript verwendet ein separates Build-Verzeichnis und ein eigenes Ergebnispaket. Workspace, Scheme und Simulatorname müssen an das jeweilige Projekt angepasst werden.

#!/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}

Hängen Sie nicht || true an den Testbefehl an, um bedingungslos einen erfolgreichen Status zu erzwingen. Muss die CI-Plattform die Protokolle nachbearbeiten, sollte zuerst der Exit-Code gespeichert und nach der Archivierung unverändert zurückgegeben werden. CODE_SIGNING_ALLOWED=NO eignet sich für Simulatortests. Jobs für reale Geräte oder Archive benötigen dagegen eine eigene Signaturkonfiguration und dürfen diese Einstellung nicht unverändert übernehmen.

ASan erhöht den Speicherbedarf und die Ausführungszeit. Deshalb sollte die Parallelität auf demselben Rechner niedriger angesetzt werden als bei regulären Unit-Tests. Wird ein Prozess vom System beendet, sollte zunächst die Parallelität reduziert werden, bevor von einem Fehler in der getesteten Anwendung ausgegangen wird.

Fehlernachweise für die Weitergabe sichern

Bei einem fehlgeschlagenen Job müssen mindestens xcodebuild.log und .xcresult gespeichert werden. Das Textprotokoll erleichtert die Suche nach Sanitizer-Berichten, während das Ergebnispaket die Testhierarchie, Fehleranhänge und Aktivitätsprotokolle enthält. Vor der Archivierung kann eine Integritätsprüfung ausgeführt werden:

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

Die Unterbefehle von xcresulttool können sich zwischen Xcode-Versionen ändern. Deshalb müssen die Werkzeugaufrufe zusammen mit der verwendeten Xcode-Version gepflegt werden. Ist ein Befehl nicht kompatibel, darf das ursprüngliche Ergebnispaket deswegen nicht gelöscht werden.

Suchen Sie bei der Analyse eines Berichts zunächst nach ERROR: AddressSanitizer. Erfassen Sie anschließend den Fehlertyp, den ersten Stack-Frame aus dem Anwendungscode, den auslösenden Test und die Eingabedaten. Die obersten Stack-Frames aus Systembibliotheken zeigen häufig nur, wo der Absturz aufgetreten ist. Der tatsächlich fehlerhafte Schreibzugriff kann deutlich früher stattgefunden haben. Lässt sich der Fehler nicht zuverlässig reproduzieren, sollte zunächst nur der betroffene Test wiederholt ausgeführt und eine zufällige Testreihenfolge deaktiviert werden, damit nicht mehrere Variablen gleichzeitig verändert werden.

Tests gestaffelt ausführen und typische Fehler vermeiden

Für das alltägliche Gate kann es genügen, nur die Tests mit hohem Risiko auszuführen. Im Hauptbranch folgt dann die vollständige ASan-Suite. Test-Tags oder Test Plans lassen sich leichter pflegen als Befehle, die anhand von Dateinamen zusammengesetzt werden. Werden neue C-Schnittstellen, Zeigerarithmetik oder unsichere Pufferoperationen eingeführt, müssen die zugehörigen Tests gleichzeitig in die schnelle Testauswahl aufgenommen werden.

Drei typische Fehler treten besonders häufig auf. Erstens werden reguläre Build-Caches wiederverwendet, wodurch die Instrumentierung unvollständig bleibt. Zweitens wird nur das Textprotokoll hochgeladen, nicht aber das Ergebnispaket. Drittens wird ein ASan-Fehler als sporadischer Testfehler behandelt und direkt erneut ausgeführt. Wiederholungen können die Stabilität des Fehlers bestätigen, doch der erste Bericht muss dennoch aufbewahrt werden. Auch mehrere erfolgreiche Wiederholungen dürfen einen bereits erkannten ungültigen Speicherzugriff nicht automatisch freigeben.

Abschließend müssen die Abnahmekriterien eindeutig formuliert sein: Der Testprozess endet mit dem Exit-Code null, das Ergebnispaket ist vorhanden, das Protokoll enthält keine ASan-Fehler und der Job wurde weder wegen einer Zeitüberschreitung noch aufgrund von Ressourcenknappheit beendet. So bewertet das Gate das vollständige Ausführungsergebnis und nicht nur eine einzelne, scheinbar erfolgreiche Testzusammenfassung.

Häufig gestellte Fragen

Sollte Address Sanitizer bei jedem Commit laufen?

Gezielte Unit-Tests und riskante Module können bei jeder Änderung geprüft werden. Lange UI-Suiten gehören eher in Merge-Requests, den Hauptzweig oder einen geplanten Lauf.

Beweist ein erfolgreicher Lauf, dass die App speichersicher ist?

Nein. Er deckt nur ausgeführte Pfade und bestimmte Fehlerklassen ab. Zusätzlich bleiben Lecksuche, Datenrennen-Prüfung und Tests auf echten Geräten erforderlich.

Dedizierter physischer Mac mini

Cloud-Mac-Mietdauer passend zur Aufgabendauer wählen

Zwei M4-Konfigurationen können tageweise, wöchentlich, monatlich oder vierteljährlich gemietet werden. Maßgeblich sind die im Dashboard in Echtzeit angezeigten Knoten und Verfügbarkeitsinformationen.

Konfiguration auswählen und bestellen