Sichere Codierung mit Parallelität in Swift 4

In meinem vorherigen Artikel über sicheres Codieren in Swift habe ich grundlegende Sicherheitslücken in Swift wie Injektionsangriffe diskutiert. Während Injektionsangriffe üblich sind, kann Ihre App auf andere Weise beeinträchtigt werden. Eine häufige, aber manchmal übersehene Art der Anfälligkeit sind Rennbedingungen. 

Swift 4 führt ein Exklusiver Zugriff auf den Speicher, Diese besteht aus einer Reihe von Regeln, um zu verhindern, dass auf denselben Speicherbereich gleichzeitig zugegriffen wird. Zum Beispiel die inout Das Argument in Swift teilt einer Methode mit, dass sie den Wert des Parameters innerhalb der Methode ändern kann.

func changeMe (_ x: inout MyObject undChange y: inout MyObject) 

Aber was passiert, wenn wir dieselbe Variable übergeben, um sich gleichzeitig zu ändern??

changeMe (& myObject undChange: & myObject) // ???

Swift 4 hat Verbesserungen vorgenommen, die das Kompilieren verhindern. Während Swift diese offensichtlichen Szenarien zwar zur Kompilierzeit finden kann, ist es aus Performancegründen schwierig, Speicherprobleme im gleichzeitigen Code zu finden, und die meisten Sicherheitslücken bestehen in Form von Race-Bedingungen.

Rennbedingungen

Sobald Sie mehr als einen Thread haben, der gleichzeitig in dieselben Daten schreiben muss, kann eine Race-Bedingung auftreten. Rennbedingungen verursachen Datenverfälschung. Für diese Arten von Angriffen sind die Sicherheitsanfälligkeiten in der Regel subtiler und die Exploits kreativer. So kann es beispielsweise möglich sein, eine gemeinsam genutzte Ressource zu ändern, um den Fluss des Sicherheitscodes in einem anderen Thread zu ändern, oder im Fall des Authentifizierungsstatus kann ein Angreifer eine Zeitspanne zwischen den Überprüfungszeiten ausnutzen und die Zeit der Verwendung einer Flagge.

Um Rennbedingungen zu vermeiden, müssen Sie die Daten synchronisieren. Das Synchronisieren von Daten bedeutet in der Regel, sie zu "sperren", sodass jeweils nur ein Thread auf diesen Teil des Codes zugreifen kann (der Mutex heißt - zum gegenseitigen Ausschluss). Sie können dies zwar explizit mit der NSLock In dieser Klasse besteht die Möglichkeit, Orte zu verpassen, an denen der Code hätte synchronisiert werden sollen. Das Verfolgen der Schlösser und ob sie bereits gesperrt sind oder nicht, kann schwierig sein.

Grand Central Dispatch

Anstelle der Verwendung primitiver Sperren können Sie die moderne Parallelitäts-API von Grand Central Dispatch (GCD) -Apple verwenden, die auf Leistung und Sicherheit ausgelegt ist. Sie müssen nicht selbst über die Schlösser nachdenken. Es erledigt die Arbeit für Sie hinter den Kulissen. 

DispatchQueue.global (qos: .background) .async // gleichzeitige Warteschlange, die vom System // gemeinsam genutzt wird, // führt im Hintergrund lange laufende Arbeit aus. //… DispatchQueue.main.async // serielle Warteschlange // Aktualisieren Sie die UI - show die Ergebnisse wieder im Hauptthread

Wie Sie sehen, ist dies eine ziemlich einfache API. Verwenden Sie daher GCD als erste Wahl, wenn Sie Ihre App für Parallelität entwerfen.

Die Laufzeitsicherheitsüberprüfungen von Swift können nicht für GCD-Threads ausgeführt werden, da dies einen erheblichen Performance-Erfolg verursacht. Die Lösung ist die Verwendung des Thread-Desinfektionsprogramms, wenn Sie mit mehreren Threads arbeiten. Das Thread-Sanitizer-Tool eignet sich hervorragend zum Auffinden von Problemen, die Sie möglicherweise nicht finden, wenn Sie sich den Code selbst ansehen. Sie können es aktivieren, indem Sie zu gehen Produkt> Schema> Bearbeitungsschema> Diagnose, und das überprüfen Thread-Desinfektionsmittel Möglichkeit.

Wenn Sie beim Design Ihrer App mit mehreren Threads arbeiten müssen, können Sie sich vor den Sicherheitsproblemen der Parallelität schützen Versuchen Sie, Ihre Klassen so zu gestalten, dass sie nicht gesperrt sind damit ist überhaupt kein Synchronisationscode notwendig. Dies erfordert einige Überlegungen zum Design Ihrer Benutzeroberfläche und kann sogar als eigenständige Kunst betrachtet werden!

Der Haupt-Thread-Checker

Es ist wichtig zu erwähnen, dass Daten beschädigt werden können, wenn Sie Aktualisierungen der Benutzeroberfläche für einen anderen Thread als den Haupt-Thread vornehmen (jeder andere Thread wird als Hintergrund-Thread bezeichnet).. 

Manchmal ist es nicht einmal offensichtlich, dass Sie sich in einem Hintergrundthread befinden. Zum Beispiel, NSURLSession's DelegateQueue, wenn eingestellt auf Null, wird standardmäßig in einem Hintergrundthread zurückgerufen. Wenn Sie UI-Aktualisierungen durchführen oder Ihre Daten in diesem Block schreiben, besteht eine gute Chance für die Rennbedingungen. (Beheben Sie dies, indem Sie die Aktualisierungen der Benutzeroberfläche in einbetten DispatchQueue.main.async oder übergeben OperationQueue.main als Delegatenwarteschlange.) 

Neu in Xcode 9 und standardmäßig aktiviert ist der Main Thread Checker (Produkt> Schema> Bearbeitungsschema> Diagnose> Laufzeit-API-Überprüfung> Main Thread Checker). Wenn Ihr Code nicht synchronisiert ist, werden Probleme im angezeigt Laufzeitprobleme im linken Fensterbereich von Xcode. Achten Sie daher beim Testen Ihrer App darauf. 

Zum Schutz der Sicherheit sollten alle von Ihnen geschriebenen Rückrufe oder Beendigungshandler dokumentiert werden, unabhängig davon, ob sie im Hauptthread zurückgegeben werden oder nicht. Besser noch, folgen Sie dem neueren API-Design von Apple, mit dem Sie eine Passage bestehen CompletionQueue In der Methode können Sie also eindeutig entscheiden, in welchem ​​Thread der Beendigungsblock zurückgegeben wird.

Ein reales Beispiel

Genug Gerede! Lassen Sie uns in ein Beispiel eintauchen.

Klasse Transaktion //… Klasse Transaktionen privat var lastTransaction: Transaktion? func addTransaction (_ source: Transaction) //… lastTransaction = source // Erster Thread transaction.addTransaction (Transaktion) // Zweiter Thread transaction.addTransaction (Transaktion)

Hier haben wir keine Synchronisation, aber mehr als ein Thread greift gleichzeitig auf die Daten zu. Das Gute an Thread Sanitizer ist, dass ein solcher Fall erkannt wird. Der moderne Weg der GCD besteht darin, Ihre Daten mit einer seriellen Versandwarteschlange zu verknüpfen.

class Transactions private var lastTransaction: Transaktion? private var queue = DispatchQueue (Label: "com.myCompany.myApp.bankQueue") func addTransaction (_ source: Transaction) queue.async //… self.lastTransaction = source

Nun ist der Code mit dem synchronisiert .asynchron Block. Sie fragen sich vielleicht, wann Sie sich entscheiden sollen .asynchron und wann verwenden .Sync. Sie können verwenden .asynchron Wenn Ihre App nicht warten muss, bis der Vorgang innerhalb des Blocks abgeschlossen ist. Es könnte besser mit einem Beispiel erklärt werden.

let queue = DispatchQueue (Label: "com.myCompany.myApp.bankQueue") var transactionIDs: [String] = ["00001", "00002"] // Erster Thread queue.async transactionIDs.append ("00003") // keine Ausgabe, also müssen Sie nicht warten, bis sie beendet ist // ein anderer Thread queue.sync if transactionIDs.contains ("00001") // ... hier müssen Sie warten! print ("Transaktion bereits abgeschlossen")

In diesem Beispiel stellt der Thread, der das Transaktionsarray fragt, ob es eine bestimmte Transaktion enthält, eine Ausgabe bereit, sodass es warten muss. Der andere Thread führt nach dem Anhängen an das Transaktionsarray keine Aktion aus, sodass er nicht warten muss, bis der Block abgeschlossen ist.

Diese Synchronisations- und Asynchronblöcke können in Methoden eingeschlossen werden, die Ihre internen Daten zurückgeben, beispielsweise Getter-Methoden.

get return queue.sync transactionID

Die Streuung von GCD-Blöcken in allen Bereichen Ihres Codes, die auf gemeinsam genutzte Daten zugreifen, ist nicht empfehlenswert, da es schwieriger ist, alle zu synchronisierenden Bereiche im Auge zu behalten. Es ist viel besser zu versuchen, all diese Funktionen an einem Ort aufzubewahren. 

Ein gutes Design mit Accessor-Methoden ist eine Möglichkeit, dieses Problem zu lösen. Wenn Sie Getter- und Setter-Methoden verwenden und nur diese Methoden verwenden, um auf die Daten zuzugreifen, können Sie sie an einem Ort synchronisieren. Dies vermeidet die Notwendigkeit, viele Teile Ihres Codes zu aktualisieren, wenn Sie den GCD-Bereich Ihres Codes ändern oder umgestalten.

Structs

Während einzelne gespeicherte Eigenschaften in einer Klasse synchronisiert werden können, wirkt sich das Ändern der Eigenschaften einer Struktur tatsächlich auf die gesamte Struktur aus. Swift 4 beinhaltet jetzt den Schutz für Methoden, die die Strukturen mutieren. 

Schauen wir uns zunächst an, wie eine Strukturkorruption ("Swift Access Race") aussieht.

struct Transaction private var id: UInt32 private var timestamp: Double //… mutating func begin () id = arc4random_uniform (101) // 0 - 100 //… mutating func finish () //… timestamp = NSDate ( ) .timeIntervalSince1970

Die beiden Methoden im Beispiel ändern die gespeicherten Eigenschaften, sodass sie markiert sind mutieren. Sagen wir, Thread-1-Aufrufe Start() und Thread 2 Aufrufe Fertig(). Selbst wenn Start() nur Änderungen Ich würde und Fertig() nur Änderungen Zeitstempel, Es ist immer noch ein Access Race. Normalerweise ist es besser, Accessor-Methoden zu sperren. Dies gilt jedoch nicht für Strukturen, da die gesamte Struktur exklusiv sein muss. 

Eine Lösung besteht darin, die Struktur in eine Klasse umzuwandeln, wenn Sie gleichzeitig den Code implementieren. Wenn Sie die Struktur aus irgendeinem Grund benötigen, können Sie in diesem Beispiel eine Bank Klasse, die speichert Transaktion structs. Dann können die Aufrufer der Strukturen innerhalb der Klasse synchronisiert werden. 

Hier ist ein Beispiel:

class Bank private var currentTransaction: Transaktion? private var queue: DispatchQueue = DispatchQueue (Label: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () //…

Zugangskontrolle

Es wäre sinnlos, all diesen Schutz zu haben, wenn Ihre Schnittstelle ein mutierendes Objekt oder ein UnsafeMutablePointer auf die gemeinsam genutzten Daten, da jetzt jeder Benutzer Ihrer Klasse mit den Daten alles tun kann, was er will, ohne den Schutz von GCD. Bringen Sie stattdessen Kopien zu den Daten im Getter zurück. Ein sorgfältiges Schnittstellendesign und die Verkapselung von Daten sind wichtig, insbesondere beim Entwurf gleichzeitiger Programme, um sicherzustellen, dass die gemeinsam genutzten Daten wirklich geschützt sind.

Stellen Sie sicher, dass die synchronisierten Variablen markiert sind Privatgelände, im Gegensatz zu öffnen oder Öffentlichkeit, Dadurch könnten Mitglieder aus jeder Quelldatei darauf zugreifen. Eine interessante Änderung in Swift 4 ist die Privatgelände Zugriffsebene wird erweitert, um in Erweiterungen verfügbar zu sein. Bisher konnte es nur innerhalb der beiliegenden Deklaration verwendet werden, aber in Swift 4 a Privatgelände Auf die Variable kann in einer Erweiterung zugegriffen werden, solange sich die Erweiterung dieser Deklaration in derselben Quelldatei befindet.

Variablen sind nicht nur für Datenkorruption gefährdet, sondern auch für Dateien. Verwenden Sie die Dateimanager Foundation-Klasse, die threadsicher ist, und überprüfen Sie die Ergebnisflags der Dateivorgänge, bevor Sie in Ihrem Code fortfahren.

Schnittstellen mit Objective-C

Viele Objective-C-Objekte haben ein veränderliches Pendant, das durch ihren Titel dargestellt wird. NSStringDie veränderliche Version heißt NSMutableString, NSArrayist's NSMutableArray, und so weiter. Abgesehen davon, dass diese Objekte außerhalb der Synchronisation mutiert werden können, unterdrücken Zeigertypen aus Objective-C auch die Swift-Optionals. Es gibt eine gute Chance, dass Sie ein Objekt in Swift erwarten könnten, aber von Objective-C wird es als Null zurückgegeben. 

Wenn die App abstürzt, gibt sie wertvolle Einblicke in die interne Logik. In diesem Fall könnte es sein, dass die Benutzereingaben nicht ordnungsgemäß überprüft wurden und der Bereich des App-Flusses einen Versuch wert ist.

Die Lösung hier ist, Ihren Objective-C-Code zu aktualisieren, um Annullierungen für die Nullfähigkeit aufzunehmen. Wir können hier eine leichte Ablenkung vornehmen, da dieser Hinweis für eine sichere Interoperabilität im Allgemeinen gilt, sei es zwischen Swift und Objective-C oder zwischen zwei anderen Programmiersprachen. 

Stellen Sie Ihre Objective-C-Variablen mit ein nullfähig wenn null zurückgegeben werden kann, und nicht wenn es nicht sollte.

- (Nonnull NSString *) myStringFromString: (nullfähige NSString *) Zeichenfolge;

Sie können auch hinzufügen nullfähig und nicht in die Attributliste der Objective-C-Eigenschaften.

@ property (nullable, atomic, strong) NSDate * Datum;

Das Static Analyzer-Tool in Xcode war schon immer großartig, um Objective-C-Fehler zu finden. Mit den Annotationen zur Nullfähigkeit können Sie in Xcode 9 den Static Analyzer für Ihren Objective-C-Code verwenden. In dieser Datei werden Inkonsistenzen für die Nullfähigkeit gefunden. Tun Sie dies, indem Sie zu navigieren Produkt> Aktion durchführen> Analyse.

Sie ist zwar standardmäßig aktiviert, Sie können aber auch die Überprüfung der Nullfähigkeit in LLVM mit steuern -Wankelbarkeit * Flaggen.

Nullfähigkeitsprüfungen eignen sich gut zum Finden von Problemen während der Kompilierzeit, aber keine Laufzeitprobleme. Manchmal gehen wir beispielsweise in einem Teil unseres Codes davon aus, dass ein optionaler Wert immer vorhanden ist, und verwenden das Force-Wrap ! darauf Dies ist ein implizit nicht verpacktes optionales optionales Element, aber es gibt keine Garantie dafür, dass es immer existiert. Wenn es als optional markiert wurde, ist es wahrscheinlich irgendwann gleich Null. Daher ist es ratsam, das Entpacken mit zu vermeiden !. Stattdessen ist eine elegante Lösung die Überprüfung zur Laufzeit wie folgt:

guard let dog = animal.dog () else // diesen Fall bearbeiten // weiter… 

Um Ihnen noch mehr zu helfen, wurde in Xcode 9 eine neue Funktion hinzugefügt, um die Überprüfung der Nullfähigkeit zur Laufzeit durchzuführen. Es ist Teil des Undefined Behavior Sanitizer. Wenn es standardmäßig nicht aktiviert ist, können Sie es aktivieren, indem Sie zu gehen Build-Einstellungen> Undefined Behavior Sanitizer und Einstellung Ja zum Aktivieren Sie die Annotationsprüfungen für die Nullfähigkeit.

Lesbarkeit

Es ist empfehlenswert, Ihre Methoden mit nur einem Einstiegs- und einem Exitpunkt zu schreiben. Dies ist nicht nur gut für die Lesbarkeit, sondern auch für die erweiterte Unterstützung von Multithreading. 

Angenommen, eine Klasse wurde ohne Parallelität entworfen. Später änderten sich die Anforderungen, so dass es jetzt das unterstützen muss .sperren() und .Freischalten() Methoden von NSLock. Wenn es an der Zeit ist, Sperren um Teile Ihres Codes zu setzen, müssen Sie möglicherweise viele Ihrer Methoden neu schreiben, um Thread-sicher zu sein. Es ist leicht zu übersehen Rückkehr versteckt in der Mitte einer Methode, die Sie später sperren sollte NSLock Instanz, die dann eine Racebedingung verursachen kann. Auch Aussagen wie Rückkehr entriegelt die Sperre nicht automatisch. Ein anderer Teil Ihres Codes, der davon ausgeht, dass die Sperre nicht gesperrt ist und erneut versucht, die Sperre aufzuheben, blockiert die App (die App wird einfrieren und wird möglicherweise vom System beendet). Abstürze können auch Sicherheitslücken im Multithread-Code darstellen, wenn temporäre Arbeitsdateien niemals bereinigt werden, bevor der Thread beendet wird. Wenn Ihr Code diese Struktur hat:

Wenn x, wenn y zurückkehrt, andernfalls zurückgeben

Sie können stattdessen das Boolean speichern, auf dem Weg aktualisieren und es am Ende der Methode zurückgeben. Der Synchronisationscode kann dann ohne großen Aufwand in die Methode eingeschlossen werden.

var success = false // <--- lock if x if y success = true… // < --- unlock return success

Das .Freischalten() Die Methode muss von demselben Thread aufgerufen werden, der aufgerufen wurde .sperren(),  Andernfalls führt dies zu undefiniertem Verhalten.

Testen

Das Auffinden und Beheben von Schwachstellen im gleichzeitigen Code ist häufig auf die Fehlersuche zurückzuführen. Wenn Sie einen Fehler finden, ist es, als würden Sie einen Spiegel für sich selbst halten - eine großartige Lernmöglichkeit. Wenn Sie vergessen haben, an einer Stelle zu synchronisieren, liegt der gleiche Fehler wahrscheinlich an anderer Stelle im Code. Wenn Sie sich die Zeit nehmen, den restlichen Code auf denselben Fehler zu überprüfen, wenn Sie einen Fehler entdecken, können Sie Sicherheitslücken auf effiziente Weise verhindern, die in zukünftigen App-Versionen immer wieder auftauchen. 

In der Tat waren viele der jüngsten iOS-Jailbreaks auf wiederholte Codierungsfehler in Apples IOKit zurückzuführen. Sobald Sie den Stil des Entwicklers kennen, können Sie andere Teile des Codes auf ähnliche Fehler überprüfen.

Fehlersuche ist eine gute Motivation für die Wiederverwendung von Code. Zu wissen, dass Sie ein Problem an einer Stelle behoben haben und nicht alle gleichen Vorkommnisse im Copy / Paste-Code finden müssen, kann eine große Erleichterung sein.

Während des Tests kann es schwierig sein, Rennbedingungen zu finden, da der Speicher möglicherweise auf die „richtige Art“ beschädigt werden muss, um das Problem zu erkennen, und manchmal treten die Probleme später bei der Ausführung der App auf. 

Wenn Sie testen, decken Sie Ihren gesamten Code ab. Durchlaufen Sie jeden Fluss und jeden Fall und testen Sie jede Codezeile mindestens einmal. Manchmal hilft es, Zufallsdaten einzugeben (Fuzzing der Eingaben) oder extreme Werte zu wählen, in der Hoffnung, einen Randfall zu finden, der nicht offensichtlich ist, wenn der Code betrachtet oder die App auf normale Weise verwendet wird. Dies kann zusammen mit den neuen Xcode-Tools dazu beitragen, Sicherheitslücken zu vermeiden. Obwohl kein Code zu 100% sicher ist, zahlt sich eine Routine, wie z. B. frühzeitige Funktionstests, Komponententests, Systemtests, Belastungs- und Regressionstests, aus.

Neben dem Debuggen Ihrer App unterscheidet sich die Versionskonfiguration (die Konfiguration für Apps, die im Store veröffentlicht wird) durch die Möglichkeit, dass Codeoptimierungen enthalten sind. Das, was der Compiler für eine ungenutzte Operation hält, kann beispielsweise optimiert werden, oder eine Variable bleibt möglicherweise nicht länger in einem gleichzeitigen Block als notwendig. Für Ihre veröffentlichte App wurde Ihr Code tatsächlich geändert oder unterscheidet sich von dem, den Sie getestet haben. Dies bedeutet, dass Fehler eingeführt werden können, die erst nach der Freigabe Ihrer App vorhanden sind. 

Wenn Sie keine Testkonfiguration verwenden, stellen Sie sicher, dass Sie Ihre App im Freigabemodus testen, indem Sie zu navigieren Produkt> Schema> Schema bearbeiten. Wählen Lauf aus der Liste links und in der Info Bereich rechts, ändern Konfiguration erstellen zu Veröffentlichung. Obwohl es gut ist, Ihre gesamte App in diesem Modus abzudecken, sollten Sie wissen, dass sich die Haltepunkte und der Debugger aufgrund von Optimierungen nicht wie erwartet verhalten. Beispielsweise sind Variablenbeschreibungen möglicherweise nicht verfügbar, obwohl der Code ordnungsgemäß ausgeführt wird.

Fazit

In diesem Beitrag haben wir uns mit den Rennbedingungen befasst und wie Sie diese vermeiden können, indem Sie sicher kodieren und Werkzeuge wie den Thread Sanitizer verwenden. Wir sprachen auch über den exklusiven Zugriff auf Speicher, der eine großartige Ergänzung zu Swift 4 darstellt Vollständige Durchsetzung im Build-Einstellungen> Exklusiver Zugriff auf Speicher

Beachten Sie, dass diese Erzwungen nur für den Debug-Modus aktiviert sind. Wenn Sie weiterhin Swift 3.2 verwenden, werden viele der besprochenen Erzwungen nur in Form von Warnungen angezeigt. Nehmen Sie also die Warnungen ernst, oder besser noch, nutzen Sie alle neuen Funktionen, die Swift 4 heute bietet!

Und während Sie hier sind, lesen Sie einige meiner anderen Beiträge zur sicheren Kodierung für iOS und Swift!