Codifica sicura con concorrenza in Swift 4

Nel mio precedente articolo sulla codifica sicura in Swift, ho discusso le vulnerabilità di sicurezza di base in Swift come gli attacchi di iniezione. Mentre gli attacchi per iniezione sono comuni, ci sono altri modi in cui la tua app può essere compromessa. Un tipo comune ma a volte trascurato di vulnerabilità sono le condizioni di gara. 

Swift 4 introduce Accesso esclusivo alla memoria, che consiste in un insieme di regole per impedire l'accesso alla stessa area di memoria nello stesso momento. Ad esempio, il dentro fuori argomento in Swift dice a un metodo che può cambiare il valore del parametro all'interno del metodo.

func changeMe (_ x: inout MyObject, andChange y: inout MyObject) 

Ma cosa succede se passiamo nella stessa variabile per cambiare allo stesso tempo?

changeMe (& myObject, andChange: & myObject) // ???

Swift 4 ha apportato miglioramenti che impediscono la compilazione. Ma mentre Swift può trovare questi ovvi scenari in fase di compilazione, è difficile, soprattutto per motivi di prestazioni, trovare problemi di accesso alla memoria nel codice concorrente e la maggior parte delle vulnerabilità di sicurezza esiste sotto forma di condizioni di competizione.

Condizioni di gara

Non appena hai più di un thread che deve scrivere sugli stessi dati nello stesso momento, può verificarsi una condizione di competizione. Le condizioni di gara causano la corruzione dei dati. Per questi tipi di attacchi, le vulnerabilità sono in genere più sottili e gli exploit più creativi. Ad esempio, potrebbe esserci la possibilità di modificare una risorsa condivisa per modificare il flusso del codice di sicurezza che si verifica su un altro thread o, nel caso dello stato di autenticazione, un utente malintenzionato potrebbe essere in grado di sfruttare un intervallo di tempo tra il momento del controllo e il tempo di utilizzo di una bandiera.

Il modo per evitare le condizioni di gara è sincronizzare i dati. Sincronizzare i dati di solito significa "bloccarli" in modo che solo una discussione possa accedere a quella parte del codice alla volta (si dice che sia un mutex per l'esclusione reciproca). Mentre puoi farlo esplicitamente usando il NSLock classe, c'è la possibilità di perdere posti in cui il codice dovrebbe essere stato sincronizzato. Tenere traccia delle serrature e se sono già bloccate o meno può essere difficile.

Grand Central Dispatch

Invece di utilizzare i blocchi primitivi, è possibile utilizzare Grand Central Dispatch (GCD), la moderna API di concorrenza di Apple progettata per prestazioni e sicurezza. Non hai bisogno di pensare alle serrature da solo; fa il lavoro per te dietro le quinte. 

DispatchQueue.global (qos: .background) .async // coda concorrente, condivisa dal sistema // esegue il lavoro a lungo in background qui // ... DispatchQueue.main.async // serial queue // Aggiorna l'interfaccia utente - mostra i risultati tornano sul thread principale

Come puoi vedere, è un'API piuttosto semplice, quindi utilizza GCD come prima scelta quando progetti la tua app per la concorrenza.

I controlli di sicurezza di runtime di Swift non possono essere eseguiti attraverso i thread GCD perché creano un notevole calo di prestazioni. La soluzione è utilizzare lo strumento Thread Sanitizer se si lavora con più thread. Lo strumento Thread Sanitizer è ottimo per trovare problemi che potresti non trovare mai guardando il codice da solo. Può essere abilitato andando a Prodotto> Schema> Modifica schema> Diagnostica, e controllando il Thread Sanitizer opzione.

Se il design della tua app ti fa lavorare con più thread, è un altro modo per proteggersi dai problemi di sicurezza della concorrenza prova a progettare le tue lezioni in modo che non siano bloccate in modo che nessun codice di sincronizzazione sia necessario in primo luogo. Ciò richiede un po 'di riflessione sul design della tua interfaccia e può anche essere considerato un'arte separata in sé e per sé!

Il Checker principale della discussione

È importante ricordare che il danneggiamento dei dati può verificarsi anche se si eseguono aggiornamenti dell'interfaccia utente su qualsiasi thread diverso dal thread principale (qualsiasi altro thread viene definito thread in background). 

A volte non è nemmeno ovvio che tu sia su un thread in background. Per esempio, NSURLSession'S delegateQueue, quando impostato su zero, per impostazione predefinita richiamerà su un thread in background. Se esegui aggiornamenti dell'interfaccia utente o scrivi sui tuoi dati in quel blocco, ci sono buone possibilità per le condizioni di gara. (Risolvi questo problema avvolgendo gli aggiornamenti dell'interfaccia utente in DispatchQueue.main.async o passare dentro OperationQueue.main come coda dei delegati). 

Nuovo in Xcode 9 e abilitato di default è il Main Thread Checker (Prodotto> Schema> Modifica schema> Diagnostica> Verifica API di runtime> Controllo thread principale). Se il tuo codice non è sincronizzato, i problemi verranno visualizzati nel Problemi di runtime nel pannello di navigazione sinistro di Xcode, quindi prestalo attenzione durante il test della tua app. 

Per codificare per sicurezza, eventuali callback o gestori di completamento scritti devono essere documentati se restituiscono o meno sul thread principale. Ancora meglio, segui il nuovo design API di Apple che ti consente di passare a completionQueue nel metodo in modo da poter decidere in modo chiaro e vedere su quale thread ritorna il blocco di completamento.

Un esempio del mondo reale

Basta parlare! Facciamo un esempio.

classe Transaction // ... class Transactions private var lastTransaction: Transaction? func addTransaction (_ source: Transaction) // ... lastTransaction = source // Primo thread transaction.addTransaction (transazione) // Secondo thread transactions.addTransaction (transazione)

Qui non abbiamo sincronizzazione, ma più di un thread accede ai dati allo stesso tempo. La cosa buona di Thread Sanitizer è che rileverà un caso come questo. Il modo moderno GCD per risolvere questo problema è quello di associare i dati con una coda di invio seriale.

Transazioni di classe private var lastTransaction: Transaction? private var queue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") func addTransaction (_ source: Transaction) queue.async // ... self.lastTransaction = source

Ora il codice è sincronizzato con .async bloccare. Potresti chiederti quando scegliere .async e quando usare .sincronizzare. Puoi usare .async quando la tua app non ha bisogno di aspettare fino al termine dell'operazione all'interno del blocco. Potrebbe essere meglio spiegato con un esempio.

let queue = DispatchQueue (label: "com.myCompany.myApp.bankQueue") var transactionID: [String] = ["00001", "00002"] // Primo thread queue.async transactionIDs.append ("00003") // non fornisce alcun output quindi non c'è bisogno di aspettare che finisca // Un altro thread queue.sync if transactionIDs.contains ("00001") // ... Bisogna aspettare qui! stampa ("Transazione già completata")

In questo esempio, il thread che richiede l'array di transazioni se contiene una transazione specifica fornisce output, quindi è necessario attendere. L'altro thread non esegue alcuna azione dopo l'aggiunta all'array delle transazioni, quindi non è necessario attendere il completamento del blocco.

Questi blocchi sync e async possono essere racchiusi in metodi che restituiscono i dati interni, come i metodi getter.

ottieni return queue.sync transactionID

Scattering GCD blocca tutte le aree del tuo codice che accedono ai dati condivisi non è una buona pratica in quanto è più difficile tenere traccia di tutti i luoghi che devono essere sincronizzati. È molto meglio provare e mantenere tutte queste funzionalità in un unico posto. 

Un buon design utilizzando i metodi di accesso è un modo per risolvere questo problema. L'utilizzo dei metodi getter e setter e l'utilizzo di questi metodi solo per accedere ai dati significa che è possibile eseguire la sincronizzazione in un'unica posizione. Questo evita di dover aggiornare molte parti del tuo codice se stai modificando o refactoring l'area GCD del tuo codice.

Structs

Mentre le singole proprietà memorizzate possono essere sincronizzate in una classe, la modifica delle proprietà su una struttura interesserà effettivamente l'intera struttura. Swift 4 ora include la protezione per i metodi che mutano le strutture. 

Diamo prima un'occhiata a come si presenta una corruzione di struct (chiamata "Swift access race").

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

I due metodi nell'esempio cambiano le proprietà memorizzate, quindi sono contrassegnati mutanti. Diciamo che il thread 1 chiama inizio() e chiama il thread 2 finire(). Anche se inizio() solo cambiamenti id e finire() solo cambiamenti timestamp, è ancora una gara di accesso. Mentre normalmente è meglio bloccare i metodi di accesso interni, questo non si applica alle strutture poiché l'intera struttura deve essere esclusiva. 

Una soluzione è di cambiare la struttura in una classe quando si implementa il codice concorrente. Se hai avuto bisogno della struttura per qualche motivo, potresti, in questo esempio, creare un Banca classe che memorizza Transazione struct. Quindi i chiamanti delle strutture all'interno della classe possono essere sincronizzati. 

Ecco un esempio:

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

Controllo di accesso

Sarebbe inutile avere tutta questa protezione quando la tua interfaccia espone un oggetto mutante o un UnsafeMutablePointer ai dati condivisi, perché ora qualsiasi utente della tua classe può fare quello che vuole con i dati senza la protezione di GCD. Invece, restituire le copie ai dati nel getter. La progettazione accurata dell'interfaccia e l'incapsulamento dei dati sono importanti, soprattutto quando si progettano programmi concorrenti, per assicurarsi che i dati condivisi siano realmente protetti.

Assicurarsi che le variabili sincronizzate siano contrassegnate privato, al contrario di Aperto o pubblico, che consentirebbe ai membri di qualsiasi file sorgente di accedervi. Un cambiamento interessante in Swift 4 è che il privato l'estensione del livello di accesso è stata ampliata per essere disponibile nelle estensioni. In precedenza poteva essere utilizzato solo all'interno della dichiarazione allegata, ma in Swift 4, a privato è possibile accedere a una variabile in un'estensione, purché l'estensione di tale dichiarazione sia contenuta nello stesso file sorgente.

Non solo le variabili sono a rischio di danneggiamento dei dati, ma anche i file. Utilizzare il FileManager La classe Foundation, che è thread-safe, e controlla i flag dei risultati delle sue operazioni sui file prima di continuare nel codice.

Interfaccia con Objective-C

Molti oggetti Objective-C hanno una controparte mutabile raffigurata dal loro titolo. NSStringLa versione mutabile è nominata NSMutableString, NSArrayLo è NSMutableArray, e così via. Oltre al fatto che questi oggetti possono essere mutati al di fuori della sincronizzazione, anche i tipi di puntatore provenienti da Objective-C sovvertono gli optionals Swift. C'è una buona possibilità che tu possa aspettarti un oggetto in Swift, ma da Objective-C viene restituito come zero. 

Se l'app si arresta in modo anomalo, fornisce informazioni preziose sulla logica interna. In questo caso, potrebbe essere che l'input dell'utente non sia stato controllato correttamente e che l'area del flusso dell'app sia degna di essere vista per provare e sfruttare.

La soluzione qui è di aggiornare il codice Objective-C per includere annotazioni nullability. In questo caso possiamo fare un piccolo diversivo in quanto questo consiglio vale per l'interoperabilità sicura in generale, sia tra Swift e Objective-C che tra altri due linguaggi di programmazione. 

Prefigura le tue variabili Objective-C con annullabile quando nil può essere restituito, e non nullo quando non dovrebbe.

- (NSString non Null *) myStringFromString: (nullable NSString *) stringa;

Puoi anche aggiungere annullabile e non nullo alla lista degli attributi delle proprietà Objective-C.

@property (nullable, atomic, strong) data di NSDate *;

Lo strumento Static Analyzer in Xcode è sempre stato ottimo per trovare bug di Objective-C. Ora con le annotazioni nullability, in Xcode 9 è possibile utilizzare l'Analizzatore statico sul codice Objective-C e troverà incoerenze di nullability nel file. Fallo andando a Prodotto> Esegui azione> Analizza.

Mentre è abilitato di default, puoi anche controllare i controlli di nullability in LLVM con -Wnullability * bandiere.

I controlli di nullità sono utili per trovare problemi in fase di compilazione, ma non trovano problemi di runtime. Ad esempio, a volte assumiamo in una parte del nostro codice che un valore facoltativo esisterà sempre e userà lo scostamento della forza ! su di esso. Questo è un facoltativo implicito da scartare, ma non c'è davvero alcuna garanzia che esisterà sempre. Dopotutto, se fosse contrassegnato come facoltativo, è probabile che a un certo punto non sia più disponibile. Pertanto, è una buona idea evitare di forzare lo scartare con !. Invece, una soluzione elegante è quella di verificare in fase di esecuzione in questo modo:

guardia lascia dog = animal.dog () else // gestisci questo caso return // continua ... 

Per aiutarti ulteriormente, c'è una nuova funzionalità aggiunta in Xcode 9 per eseguire i controlli di nullability in fase di esecuzione. Fa parte di Undefined Behavior Sanitizer e, sebbene non sia abilitato per impostazione predefinita, è possibile attivarlo andando a Impostazioni di compilazione> Sanitizer del comportamento non definito e impostazione per Abilita i controlli di annotazione di nullità.

leggibilità

È buona norma scrivere i metodi con una sola voce e un punto di uscita. Non solo è buono per la leggibilità, ma anche per il supporto avanzato al multithreading. 

Diciamo che una classe è stata progettata senza concorrenza in mente. Successivamente i requisiti sono cambiati in modo che ora supporti il .serratura() e .sbloccare() metodi di NSLock. Quando arriva il momento di posizionare dei blocchi attorno a parti del tuo codice, potresti dover riscrivere molti dei tuoi metodi solo per essere sicuri dei thread. È facile perdere un ritorno nascosto nel mezzo di un metodo che in seguito avrebbe dovuto bloccare il tuo NSLock esempio, che può quindi causare una condizione di competizione. Inoltre, dichiarazioni come ritorno non sbloccherà automaticamente il lucchetto. Un'altra parte del codice che presume che il blocco sia sbloccato e tenta di bloccarlo di nuovo bloccherà l'app (l'applicazione si bloccherà e alla fine verrà interrotta dal sistema). I crash possono anche essere vulnerabilità di sicurezza nel codice multithreaded se i file di lavoro temporanei non vengono mai ripuliti prima della chiusura del thread. Se il tuo codice ha questa struttura:

se x se si restituisce true else return false ... return false

Puoi invece memorizzare il booleano, aggiornarlo lungo la strada e quindi restituirlo alla fine del metodo. Quindi il codice di sincronizzazione può essere facilmente integrato nel metodo senza molto lavoro.

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

Il .sbloccare() il metodo deve essere chiamato dallo stesso thread che ha chiamato .serratura(),  altrimenti si traduce in un comportamento indefinito.

analisi

Spesso, trovare e correggere vulnerabilità nel codice concorrente si riduce alla ricerca di bug. Quando trovi un bug, è come tenere uno specchio fino a te: una grande opportunità di apprendimento. Se ti sei dimenticato di sincronizzare in un punto, è probabile che lo stesso errore sia altrove nel codice. Prendersi il tempo di controllare il resto del codice per lo stesso errore quando si incontra un bug è un modo molto efficace per prevenire le vulnerabilità di sicurezza che continuerebbero ad apparire più e più volte nelle future versioni delle app. 

In effetti, molti dei recenti jailbreak di iOS sono stati causati da ripetuti errori di codifica trovati nell'IAOK di Apple. Una volta che conosci lo stile dello sviluppatore, puoi controllare altre parti del codice per bug simili.

La ricerca dei bug è una buona motivazione per il riutilizzo del codice. Sapendo che hai risolto un problema in un posto e non devi andare a cercare tutte le stesse occorrenze nel codice copia / incolla può essere un grande sollievo.

Le condizioni di gara possono essere complicate da trovare durante i test, perché la memoria potrebbe essere danneggiata solo nella "maniera giusta" per vedere il problema, e a volte i problemi compaiono molto tempo dopo nell'esecuzione dell'app. 

Quando esegui dei test, copri tutto il codice. Passare attraverso ogni flusso e caso e testare ogni riga di codice almeno una volta. A volte aiuta a inserire dati casuali (fuzzing degli input), o sceglie valori estremi nella speranza di trovare un caso limite che non sarebbe ovvio guardando il codice o usando l'app in un modo normale. Questo, insieme ai nuovi strumenti Xcode disponibili, può fare molto per prevenire le vulnerabilità della sicurezza. Mentre nessun codice è sicuro al 100%, seguendo una routine, come test funzionali iniziali, test unitari, test di sistema, stress e test di regressione, sarà davvero vantaggioso.

Oltre al debug della tua app, una cosa diversa per la configurazione del rilascio (la configurazione per le app pubblicate nel negozio) è che sono incluse le ottimizzazioni del codice. Ad esempio, ciò che il compilatore pensa è che un'operazione inutilizzata può essere ottimizzata, oppure una variabile potrebbe non restare più a lungo del necessario in un blocco concorrente. Per la tua app pubblicata, il tuo codice è effettivamente cambiato o diverso da quello che hai testato. Ciò significa che i bug possono essere introdotti che esistono solo una volta rilasciata la tua app. 

Se non stai utilizzando una configurazione di prova, assicurati di testare la tua app in modalità di rilascio navigando verso Prodotto> Schema> Modifica schema. Selezionare Correre dalla lista a sinistra e nella Informazioni riquadro a destra, cambia Costruisci la configurazione a pubblicazione. Anche se è buona norma coprire l'intera app in questa modalità, sappi che a causa delle ottimizzazioni, i breakpoint e il debugger non si comportano come previsto. Ad esempio, le descrizioni delle variabili potrebbero non essere disponibili anche se il codice è in esecuzione correttamente.

Conclusione

In questo post, abbiamo esaminato le condizioni di gara e come evitarle codificando in modo sicuro e utilizzando strumenti come il Thread Sanitizer. Abbiamo anche parlato di Exclusive Access to Memory, che è una grande aggiunta a Swift 4. Assicurati che sia impostato su Applicazione completa nel Impostazioni di compilazione> Accesso esclusivo alla memoria

Ricorda che queste forzature sono attive solo per la modalità di debug e, se stai ancora utilizzando Swift 3.2, molti degli enforcements discussi si presentano sotto forma di avvertimenti. Quindi prendi sul serio gli avvertimenti, o meglio ancora, sfrutta tutte le nuove funzionalità disponibili adottando Swift 4 oggi!

E mentre sei qui, controlla alcuni dei miei altri post sulla codifica sicura per iOS e Swift!