Codificare securizată cu concurentă în Swift 4

În articolul meu precedent despre codificarea securizată în Swift, am discutat despre vulnerabilitățile de securitate de bază ale lui Swift, cum ar fi atacurile prin injectare. În timp ce atacurile prin injectare sunt frecvente, există alte moduri în care aplicația dvs. poate fi compromisă. Un fel de vulnerabilitate obișnuită, dar uneori trecătoare, este condițiile de rasă. 

Swift 4 introduce Acces exclusiv la memorie, care constă într-un set de reguli care să împiedice accesarea aceleiași zone de memorie în același timp. De exemplu, în afară argument în Swift spune o metodă pe care o poate modifica valoarea parametrului din interiorul metodei.

func changeMe (_ x: inout MyObject, șiModificare y: inout MyObject) 

Dar ceea ce se întâmplă dacă trecem în aceeași variabilă să se schimbe în același timp?

changeMe (& myObject, șiChange: & myObject) // ???

Swift 4 a făcut îmbunătățiri care împiedică compilarea acestuia. Dar, în timp ce Swift poate găsi aceste scenarii evidente la momentul compilării, este dificil, în special din motive de performanță, să găsească probleme de acces la memorie în codul concurent și majoritatea vulnerabilităților de securitate există sub forma condițiilor de rasă.

Condiții de rasă

De îndată ce aveți mai multe firuri care trebuie să scrie simultan aceleași date, poate apărea o condiție de rasă. Condițiile de rasă provoacă coruperea datelor. Pentru aceste tipuri de atacuri, vulnerabilitățile sunt de obicei mai subtile - iar exploatările sunt mai creative. De exemplu, ar putea exista posibilitatea de a modifica o resursă partajată pentru a schimba fluxul de cod de securitate care se întâmplă pe alt fir sau în cazul stării de autentificare, un atacator ar putea să profite de un decalaj de timp între momentul verificării și timpul de utilizare a unui steag.

Modul de a evita condițiile de rasă este sincronizarea datelor. Sincronizarea datelor înseamnă, de obicei, să o "blochezi" astfel încât numai un fir să poată accesa acea parte a codului la un moment dat (considerat a fi un mutex - pentru excluderea reciprocă). În timp ce puteți face acest lucru în mod explicit folosind NSLock , este posibil să pierdeți locurile unde codul ar fi trebuit sincronizat. Urmărirea blocărilor și dacă acestea sunt deja blocate sau nu pot fi dificile.

Grand Central Dispatch

În loc să utilizați încuietori primitivi, puteți folosi API-ul de distribuție Grand Central Dispatch (GCD) -Apple, conceput pentru performanță și securitate. Nu trebuie să te gândești singur la încuietori; lucrează pentru tine în spatele scenei. 

Dispozitivul QQueue.global (qos: .background) .async // coada corespondentă, împărtășită de sistem // efectuați o activitate lungă în fundal aici // ... Dispozitivul QQueue.main.async // queue serial // // Actualizați interfața UI rezultatele din nou pe firul principal

După cum puteți vedea, este destul de simplu API, deci utilizați GCD ca prima opțiune când proiectați aplicația pentru concurrency.

Verificările de securitate ale execuției Swift nu pot fi efectuate în firele GCD deoarece creează o lovitură de performanță semnificativă. Soluția este să folosiți instrumentul Thread Sanitizer dacă lucrați cu mai multe fire. Instrumentul Thread Sanitizer este foarte bun în găsirea unor probleme pe care nu le veți găsi niciodată prin căutarea codului însuși. Poate fi activat prin a merge la Produs> Schemă> Editați schema> Diagnostice, și verificarea Thread Sanitizer opțiune.

Dacă designul aplicației dvs. vă face să lucrați cu mai multe fire, este necesar să vă protejați de problemele legate de securitate ale concurenței încercați să creați clasele dvs. pentru a fi blocate astfel încât nu este necesar niciun cod de sincronizare. Acest lucru necesită o gândire reală despre proiectarea interfeței dvs. și poate fi chiar considerată o artă separată în sine și în sine!

Controlul principal al filetului

Este important de menționat că corupția datelor poate apărea și în cazul în care faceți actualizări UI pe orice alt fir decât firul principal (orice alt fir este denumit thread de fundal). 

Uneori nu e nici măcar evident că ești pe un fir de fundal. De exemplu, NSURLSession„s delegateQueue, când este setat la zero, în mod implicit va apela un fir de fundal. Dacă faceți actualizări ale UI sau scrieți datele dvs. în acel bloc, există o șansă bună pentru condițiile de curse. (Fixați acest lucru prin împachetarea actualizărilor UI în DispatchQueue.main.async sau treceți OperationQueue.main ca coadă de delegație.) 

Nou în Xcode 9 și activat în mod prestabilit este Controlul principal al threadului (Produs> Schemă> Editați schema> Diagnostice> Verificarea API-ului Runtime> Controlul principal al thread-ului). Dacă codul dvs. nu este sincronizat, problemele vor apărea în Probleme legate de timpul de execuție în panoul de navigare din stânga al Xcode, acordați atenție acestuia în timp ce testați aplicația. 

Pentru a codifica securitatea, orice apeluri de apel sau handlers de completare pe care le scrieți ar trebui să fie documentate dacă se întorc pe firul principal sau nu. Mai bine, urmați noul design API al Apple, care vă permite să treceți completionQueue în metoda, astfel încât să puteți decide în mod clar și să vedeți ce thread revine blocul de completare.

Un exemplu în lumea reală

Vorbiți destul! Să ne aruncăm într-un exemplu.

clasa Tranzacție // ... clasă Tranzacții private var lastTransaction: Transaction? func addTransaction (_ sursă: Transaction) // ... lastTransaction = source // Primul thread transactions.addTransaction (tranzacție) // Second thread transactions.addTransaction (transaction)

Aici nu avem sincronizare, dar mai mult de un fir accesează datele în același timp. Cel mai bun lucru despre Thread Sanitizer este că va detecta un caz ca acesta. Modul GCD modern pentru a remedia acest lucru este de a asocia datele dvs. cu o coadă de expediere în serie.

Tranzacții de clasă private var lastTransaction: Transaction? private var queue = DispatchQueue (etichetă: "com.myCompany.myApp.bankQueue") func addTransaction (_ sursă: Tranzacție) queue.async // ... self.lastTransaction = source

Acum codul este sincronizat cu .async bloc. S-ar putea să te întrebi când să alegi .async și când să utilizați .sincronizați. Poți să folosești .async când aplicația dvs. nu trebuie să aștepte până când operația din interiorul blocului nu este finalizată. Ar putea fi mai bine explicat printr-un exemplu.

(queue = DispatchQueue (etichetă: "com.myCompany.myApp.bankQueue") var ID: [String] = ["00001", "00002"] // Prima linie queue.async transactionIDs.append ("00003") // nu furnizează nici o ieșire, deci nu trebuie să așteptați ca aceasta să se termine // Un alt thread queue.sync dacă transactionIDs.contains ("00001") // ... Trebuie să așteptați aici! print ("Tranzacția a fost deja finalizată")

În acest exemplu, firul care întreabă matricea tranzacțiilor dacă conține o tranzacție specifică oferă o ieșire, deci trebuie să așteptați. Celălalt fir nu ia nicio măsură după ce se atașează la matricea de tranzacții, deci nu trebuie să așteptați până când blocul nu este finalizat.

Aceste blocuri de sincronizare și asincronizare pot fi înfășurate în metode care returnează datele interne, cum ar fi metodele getter.

obține return queue.sync transactionID

Împrăștierea GCD blochează toate zonele din codul dvs. care accesează datele partajate nu este o practică bună, deoarece este mai greu să țineți evidența tuturor locurilor care trebuie sincronizate. Este mult mai bine să încercați să păstrați toate aceste funcționalități într-un singur loc. 

Designul bun, folosind metodele accesorilor, este o modalitate de a rezolva această problemă. Utilizând metode getter și setter și folosirea acestor metode numai pentru a accesa datele înseamnă că puteți sincroniza într-un singur loc. Acest lucru evită necesitatea de a actualiza mai multe părți ale codului dvs. dacă schimbați sau refactorizați zona GCD a codului.

structs

Deși proprietățile stocate singure pot fi sincronizate într-o clasă, schimbarea proprietăților pe un struct va afecta de fapt întreaga structură. Swift 4 include acum protecție pentru metode care mută structurile. 

Să aruncăm o privire mai întâi la ceea ce arată structura corupției (numită "cursa de acces rapidă").

() id = arc4random_uniform (101) // 0 - 100 // ... mutating func fin () // ... timestamp = NSDate () ) .timeIntervalSince1970

Cele două metode din exemplu schimbă proprietățile stocate, astfel încât acestea sunt marcate mutant. Să spunem apelurile din firul 1 începe() și apelurile pentru firul 2 finalizarea(). Chiar dacă începe() doar modificări id și finalizarea() doar modificări timestamp-ul, este încă o cursă de acces. În timp ce în mod normal este mai bine să blochezi în interiorul metodei accessor, acest lucru nu se aplică structurilor deoarece întregul struct trebuie să fie exclusiv. 

O soluție este să modificați structul la o clasă atunci când implementați codul concurent. Dacă ai nevoie de struct pentru un anumit motiv, ai putea, în acest exemplu, să creezi un bancă clasa care stochează Tranzacţie structs. Apoi, apelanții structurilor din interiorul clasei pot fi sincronizați. 

Iată un exemplu:

clasa Bank private var currentTransaction: Transaction? privat vară de așteptare: DispatchQueue = DispatchQueue (etichetă: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () // ...

Controlul accesului

Ar fi inutil să ai toată această protecție atunci când interfața ta expune un obiect mutant sau un UnsafeMutablePointer la datele partajate, pentru că acum orice utilizator din clasa ta poate face orice vrea cu datele fără protecția GCD. În schimb, returnați copii la datele din getter. Designul de interfață atentă și încapsularea datelor sunt importante, în special atunci când se proiectează programe concurente, pentru a vă asigura că datele partajate sunt într-adevăr protejate.

Asigurați-vă că variabilele sincronizate sunt marcate privat, spre deosebire de deschis sau public, care ar permite membrilor din orice fișier sursă să le acceseze. O schimbare interesantă în Swift 4 este aceea că privat Nivelul de nivel de acces este extins pentru a fi disponibil în extensii. Anterior, aceasta putea fi utilizată numai în declarația anexată, dar în Swift 4, a privat poate fi accesată într-o extensie, atâta timp cât extensia acelei declarații se află în același fișier sursă.

Nu numai variabilele sunt expuse riscului de corupție a datelor, ci și de fișiere. Folosește Manager de fișiere Clasa de fundație, care este sigură pentru fire și verificați steagurile de rezultate ale operațiilor de fișier înainte de a continua în codul dvs..

Interfața cu obiectivul-C

Multe obiecte Obiectiv-C au o corespondență mutabilă descrisă de titlul lor. NSStringeste numit versiunea mutable NSMutableString, NSArrayeste NSMutableArray, si asa mai departe. Pe lângă faptul că aceste obiecte pot fi mutate în afara sincronizării, tipurile de indicatori provenind de la Obiectiv-C, de asemenea, subminează opțiunile Swift. Există o șansă bună să aștepți un obiect în Swift, dar din obiectivul C este returnat ca fiind zero. 

În cazul în care aplicația se blochează, aceasta oferă informații valoroase despre logica internă. În acest caz, ar putea fi faptul că datele introduse de utilizator nu au fost verificate corespunzător și că acea zonă a fluxului de aplicații merită privită pentru a încerca și a exploata.

Soluția de aici este să vă actualizați codul Obiectiv-C pentru a include adnotări de nulitate. Putem să facem o mică deviere aici, deoarece acest sfat se aplică în general interoperabilității în siguranță, fie între Swift și Obiectiv-C, fie între alte două limbi de programare. 

Prefață-ți variabilele Objective-C cu poate fi nulă când nu poate fi returnat niciunul și nenul când nu ar trebui.

- (non-null NSString *) myStringFromString: (Nullable NSString *) șir;

De asemenea, puteți adăuga poate fi nulă și nenul la lista de atribute ale obiectelor Obiectiv-C.

@property (nullable, atomic, puternic) NSDate * data;

Instrumentul Static Analyzer în Xcode a fost întotdeauna grozav pentru găsirea de bug-uri Obiectiv-C. Acum, cu adnotări de nullabilitate, în Xcode 9 puteți folosi Analizorul static pe codul Obiectiv-C și va găsi neconcordanțe de nullabilitate în fișierul dvs. Faceți acest lucru navigând la Produs> Efectuați acțiunea> Analizați.

În timp ce este activat în mod implicit, puteți controla, de asemenea, verificările de nullabilitate în LLVM cu -Wnullability * steaguri.

Verificările de nevalabilitate sunt bune pentru a găsi probleme la momentul compilării, dar nu găsesc probleme legate de runtime. De exemplu, uneori presupunem într-o parte a codului nostru că o valoare opțională va exista întotdeauna și va folosi forța de desfacere ! pe el. Acesta este un opțional implicit neîncărcat, dar nu există nicio garanție că acesta va exista întotdeauna. La urma urmei, dacă ar fi marcat opțional, probabil că va fi zero la un moment dat. Prin urmare, este o idee bună să evitați împachetarea cu forța !. În schimb, o soluție elegantă este de a verifica la runtime, cum ar fi:

garda lasa caine = animal.dog () altceva // sa manipuleze acest caz retur // continua ... 

Pentru a vă ajuta în continuare, există o nouă funcție adăugată în Xcode 9 pentru a efectua verificări de nullabilitate în timpul rulării. Face parte din Sanitizatorul nedefinit de comportament și, deși nu este activat în mod implicit, îl puteți activa accesând Setări de configurare> Sanitizare nedefinită a comportamentului și setarea da pentru Activați verificările de adnotare a nulității.

Diviziune

Este o practică bună să vă scrieți metodele cu un singur punct de intrare și un punct de ieșire. Nu numai că acest lucru este bun pentru lizibilitate, dar și pentru suportul multithreading avansat. 

Să presupunem că o clasă a fost proiectată fără concurrency. Ulterior, cerințele s-au schimbat, astfel încât trebuie să sprijine acum .blocare() și .deblocare () metode de NSLock. Când vine timpul să plasați încuietori în jurul unor părți ale codului dvs., este posibil să trebuiască să rescrieți o mulțime de metode doar pentru a vă proteja firul. E ușor să ratați a întoarcere ascunsă în mijlocul unei metode care mai târziu trebuia să vă blocheze NSLock exemplu, care poate provoca o condiție de rasă. De asemenea, declarații precum întoarcere nu va debloca automat blocarea. O altă parte a codului care presupune blocarea este deblocată și încearcă să blocheze din nou va bloca aplicația (aplicația va îngheța și eventual va fi terminată de sistem). Accidentele pot fi, de asemenea, vulnerabilități de securitate în codul multithreaded, dacă fișierele de lucru temporare nu sunt niciodată curățate înainte ca firul să se termine. Dacă codul dvs. are această structură:

dacă x dacă y se întoarce altfel return false ... return false

Puteți să salvați Booleanul, să-l actualizați de-a lungul drumului și apoi să îl returnați la sfârșitul metodei. Apoi, codul de sincronizare poate fi ușor înfășurat în metodă fără prea multă muncă.

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

.deblocare () metoda trebuie să fie chemată din același fir care a sunat .blocare(),  în caz contrar, rezultă un comportament nedefinit.

Testarea

Deseori, găsirea și remedierea vulnerabilităților în codul concurent se reduce la vânătoarea de bug-uri. Când găsești un bug, e ca și cum ai avea o oglindă până la tine - o mare oportunitate de învățare. Dacă ați uitat să sincronizați într-un singur loc, probabil că aceeași greșeală se află în altă parte a codului. Acordând timp pentru a verifica restul codului pentru aceeași greșeală când întâlniți un bug este o modalitate foarte eficientă de a preveni vulnerabilitățile de securitate care ar continua să apară mereu și repetat în versiuni viitoare de aplicații. 

De fapt, multe dintre cele mai recente jailbreaks iOS au fost din cauza greșelilor repetate de codificare găsite în IOKit de la Apple. Odată ce știți stilul dezvoltatorului, puteți verifica alte părți ale codului pentru erori similare.

Determinarea bug-urilor este o motivație bună pentru reutilizarea codului. Știind că ai rezolvat o problemă într-un singur loc și nu trebuie să te duci să găsești toate aceleași întâmplări în codul de copiere / lipire poate fi o mare ușurare.

Condițiile de rasă pot fi complicate pentru a fi găsite în timpul testelor, deoarece memoria ar putea fi coruptă doar în modul "corect" pentru a vedea problema și uneori problemele apar o lungă perioadă de timp mai târziu în execuția aplicației. 

Când testați, acoperiți tot codul. Treceți prin fiecare flux și caz și testați fiecare linie de cod cel puțin o dată. Uneori, vă ajută să introduceți date aleatorii (fuzzând intrările) sau să alegeți valori extreme în speranța de a găsi un caz de margine care nu ar fi evident dacă vă uitați la cod sau dacă folosiți aplicația într-un mod normal. Acest lucru, alături de noile instrumente Xcode disponibile, poate merge mult spre prevenirea vulnerabilităților de securitate. Deși niciun cod nu este 100% sigur, după o rutină, cum ar fi testele funcționale timpurii, testele unității, testele de sistem, testele de stres și de regresie, vor plăti cu adevărat.

Dincolo de depanarea aplicației dvs., un lucru diferit pentru configurația de lansare (configurația aplicațiilor publicate în magazin) este că includ optimizările de cod. De exemplu, ceea ce compilatorul crede că este o operație neutilizată poate fi optimizată, sau o variabilă nu poate rămâne mai mult decât este necesar într-un bloc simultan. Pentru aplicația dvs. publicată, codul dvs. este de fapt modificat sau diferit de cel pe care l-ați testat. Aceasta înseamnă că pot fi introduse erori care există numai după ce lansați aplicația. 

Dacă nu utilizați o configurație de testare, asigurați-vă că testați aplicația în modul de eliberare accesând navigarea la Produs> Schemă> Editare schemă. Selectați Alerga din lista din stânga și din Info panoul din dreapta, schimbare Construiți configurația la Eliberare. Deși este bine să acoperiți întreaga aplicație în acest mod, știți că, din cauza optimizărilor, punctele de întrerupere și depanatorul nu se vor comporta conform așteptărilor. De exemplu, este posibil ca descrierile variabilelor să nu fie disponibile chiar dacă codul se execută corect.

Concluzie

În acest post, am analizat condițiile de rasă și modul de a le evita prin codificarea în siguranță și folosind instrumente precum Thread Sanitizer. De asemenea, am vorbit despre accesul exclusiv la memorie, care este un plus extraordinar pentru Swift 4. Asigurați-vă că este setat Punerea în aplicare completă în Setări de configurare> Acces exclusiv la memorie

Amintiți-vă că aceste aplicații sunt doar pentru modul de depanare și dacă utilizați în continuare Swift 3.2, multe dintre măsurile enunțate au doar forma unor avertismente. Luați deci avertismentele în serios sau, mai bine, utilizați toate noile caracteristici disponibile prin adoptarea Swift 4 astăzi!

Și în timp ce sunteți aici, verificați câteva dintre celelalte postări ale mele despre codificarea securizată pentru iOS și Swift!


Cod