Säker kodning med samtidighet i Swift 4

I min tidigare artikel om säker kodning i Swift diskuterade jag grundläggande säkerhetsproblem i Swift som injektionsattacker. Medan injektionsattacker är vanliga, finns det andra sätt som din app kan äventyras. En vanlig men ibland förbisedd sårbarhet är rasförhållanden. 

Swift 4 introducerar Exklusiv åtkomst till minne, som består av en uppsättning regler för att förhindra att samma minnesområde öppnas samtidigt. Till exempel, in ut argument i Swift berättar en metod att det kan ändra parametervärdet inom metoden.

func changeMe (_x: inout MyObject, ochÄndra y: inout MyObject) 

Men vad händer om vi passerar i samma variabel för att ändra samtidigt?

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

Swift 4 har gjort förbättringar som förhindrar att detta sammanställs. Men medan Swift kan hitta dessa uppenbara scenarier vid kompileringstid, är det svårt, särskilt av prestationsskäl, att hitta problem med minnesåtkomst i samtidig kod och de flesta säkerhetssårbarheterna finns i form av rasförhållanden.

Race villkor

Så fort du har mer än en tråd som behöver skriva till samma data samtidigt kan ett tävlingsförhållande inträffa. Loppförhållandena orsakar datakorruption. För dessa typer av attacker är sårbarheterna vanligtvis mer subtila, och exploaterna är mer kreativa. Det kan till exempel vara möjligheten att ändra en gemensam resurs för att ändra flödet av säkerhetskoden som händer på en annan tråd, eller när det gäller autentiseringsstatus kan en angripare utnyttja en tidsgap mellan klockan och tiden för användning av en flagga.

Sättet att undvika löpförhållanden är att synkronisera data. Synkroniseringsdata betyder vanligtvis att "låsa" den så att endast en tråd kan komma åt den delen av koden åt gången (sägs vara en mutex-för gemensam uteslutning). Medan du kan göra detta explicit med hjälp av NSLock klass, kan man sakna platser där koden skulle ha synkroniserats. Att hålla koll på låsen och om de redan är låsta eller inte kan vara svåra.

Grand Central Dispatch

I stället för att använda primitiva lås kan du använda Grand Central Dispatch (GCD) -Apples moderna parallell API som är utformat för prestanda och säkerhet. Du behöver inte tänka på låsen själv; det fungerar för dig bakom kulisserna. 

DispatchQueue.global (qos: .background) .async // samtidig kö, delad av system // gör långt löpande arbete i bakgrunden här // ... DispatchQueue.main.async // seriekö // Uppdatera UI-showen resultaten återkommer på huvudgänget

Som du kan se är det ganska enkelt API, så använd GCD som ditt första val när du utformar din app för samtidighet.

Swifts körtidssäkerhetskontroller kan inte utföras över GCD-tråden eftersom det skapar en betydande prestationsfrekvens. Lösningen är att använda verktyget Trådhantering om du arbetar med flera trådar. Thread Sanitizer-verktyget är bra för att hitta problem som du aldrig hittar genom att titta på koden själv. Det kan aktiveras genom att gå till Produkt> Schema> Redigeringsschema> Diagnostik, och kontrollera Tråd Sanitizer alternativ.

Om designen av din app gör att du arbetar med flera trådar, är ett annat sätt att skydda dig från säkerhetsproblemen av sammankallighet att försök att designa dina klasser för att vara låsa fria så att ingen synkroniseringskod är nödvändig i första hand. Detta kräver viss verklig tanke om utformningen av ditt gränssnitt, och kan även betraktas som en separat konst i sig själv!

Huvudtrådscheckaren

Det är viktigt att nämna att data korruption kan också uppstå om du gör UI-uppdateringar på någon annan tråd än huvudtråden (någon annan tråd kallas en bakgrundsgänga). 

Ibland är det inte ens uppenbart att du är på en bakgrundsgänga. Till exempel, NSURLSession's delegateQueue, när den är inställd på noll, kommer som vanligt att ringa tillbaka på en bakgrundstråd. Om du gör UI-uppdateringar eller skriver till dina data i det blocket finns det en bra chans för racerförhållanden. (Fixa det här genom att paketera UI-uppdateringarna i DispatchQueue.main.async eller lämna in OperationQueue.main som delegatskö.) 

Ny i Xcode 9 och aktiverad som standard är Main Thread Checker (Produkt> Schema> Redigeringsschema> Diagnostik> Runtime API Checking> Main Thread Checker). Om din kod inte synkroniseras visas problem i Runtime Issues i den vänstra rutan navigatorn i Xcode, var uppmärksam på den när du testar din app. 

För att koda för säkerhet ska eventuella återuppringningar eller färdighetshanterare som du skriver dokumenteras om de återvänder på huvudtråden eller inte. Bättre än, följ Apples nyare API-design som låter dig passera en completionQueue i metoden så att du tydligt kan bestämma och se vilken tråd färdigställningsblocket återkommer på.

Ett verkligt exempel på världen

Noga prata! Låt oss dyka in i ett exempel.

klasstransaktioner // ... klasstransaktioner privat var lastTransaction: Transaktion? func addTransaction (_ källa: Transaktion) // ... lastTransaction = source // Första trådtransaktioner.addTransaktion (transaktion) // Andra trådtransaktioner.addTransaktion (transaktion)

Här har vi ingen synkronisering, men mer än en tråd får åtkomst till data samtidigt. Det fina med Thread Sanitizer är att det kommer att upptäcka ett sådant fall. Den moderna GCD-lösningen för att åtgärda detta är att associera dina data med en serieförsändarkö.

klasstransaktioner privat var lastTransaction: Transaktion? privat var kö = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") func addTransaction (_ källa: Transaktion) queue.async // ... self.lastTransaction = source

Nu synkroniseras koden med .async blockera. Du kanske undrar när du ska välja .async och när man ska använda .synkronisera. Du kan använda .async när din app inte behöver vänta tills operationen inuti blocket är klar. Det kan bli bättre förklarat med ett exempel.

låt kö = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") var transaktionsID: [String] = ["00001", "00002"] // Första trådkö.async transactionIDs.append ("00003") // inte ger någon produktion så behöver inte vänta på att den ska sluta // En annan trådkö.sync om transactionIDs.contains ("00001") // ... Behöver vänta här! print ("Transaktionen är redan klar")

I det här exemplet ger tråden som frågar transaktionsmatrisen om den innehåller en specifik transaktion output, så det måste vänta. Den andra tråden tar inga åtgärder efter att ha bifogats transaktionsmatrisen, så det behöver inte vänta tills blocket är klart.

Dessa synkroniserings- och asynkblock kan vikas in i metoder som returnerar din interna data, såsom getter-metoder.

få return queue.sync transactionID

Spridning GCD blockerar alla områden i din kod som åtkomst till delad data är inte en bra metod eftersom det är svårare att hålla reda på alla platser som behöver synkroniseras. Det är mycket bättre att försöka hålla all denna funktionalitet på ett ställe. 

Bra design med accessor metoder är ett sätt att lösa detta problem. Med hjälp av getter och setter-metoder och endast genom att använda dessa metoder för att komma åt data kan du synkronisera på ett ställe. Detta undviker att uppdatera många delar av din kod om du ändrar eller refactorerar GCD-området i din kod.

structs

Medan enskilda lagrade egenskaper kan synkroniseras i en klass, kommer ändring av egenskaper på en struktur faktiskt att påverka hela strukturen. Swift 4 innehåller nu skydd för metoder som muterar strukturerna. 

Låt oss först titta på vad en strukturkorruption (kallad "Swift Access Race") ser ut.

struct Transaction privat var id: UInt32 privat var tidsstämpel: Double // ... mutating func start () id = arc4random_uniform (101) // 0 - 100 // ... mutation func finish () // ... timestamp = NSDate ) .timeIntervalSince1970

De två metoderna i exemplet ändrar de lagrade egenskaperna, så de är markerade mutera. Låt oss säga att tråd 1 ringer Börja() och tråd 2 samtal Avsluta(). Även om Börja() ändras bara id och Avsluta() ändras bara tidsstämpel, Det är fortfarande en åtkomstlöpning. Medan det normalt är bättre att låsa inåtkomstmetoder, gäller det inte för strukturer eftersom hela strukturen måste vara exklusiv. 

En lösning är att ändra strukturen till en klass när du implementerar din samtidiga kod. Om du behövde strukten av någon anledning, kan du i detta exempel skapa en Bank klass som lagrar Transaktion structs. Då kan de som ringer in i klassen synkroniseras. 

Här är ett exempel:

klass Bank privat var currentTransaction: Transaction? privatkö: DispatchQueue = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () // ...

Åtkomstkontroll

Det skulle vara meningslöst att ha allt detta skydd när ditt gränssnitt exponerar ett mutationsobjekt eller en UnsafeMutablePointer till den delade dataen, för nu kan någon användare av din klass göra vad de vill med data utan att skydda GCD. Istället returnerar du kopior till data i getteren. Noggrann gränssnittsdesign och datainkapsling är viktiga, särskilt vid utformning av samtidiga program, för att säkerställa att den delade dataen är verkligen skyddad.

Se till att de synkroniserade variablerna är markerade privat, i motsats till öppna eller offentlig, vilket skulle tillåta medlemmar från någon källfil att få tillgång till den. En intressant förändring i Swift 4 är att privat Tillgångsnivån är utökat för att vara tillgängligt i tillägg. Tidigare kunde den endast användas inom den bifogade deklarationen, men i Swift 4, a privat variabel kan nås i en förlängning, så länge som förlängningen av den deklarationen finns i samma källfil.

Det finns inte bara risker för datakorruption utan även filer. Använd Filhanterare Foundation-klassen, som är trådsäker, och kontrollera resultatflaggarna i dess filoperationer innan du fortsätter i din kod.

Gränssnitt med mål-C

Många Objective-C-objekt har en muterbar motsvarighet som avbildas av deras titel. NSStrings mutable version heter NSMutableString, NSArrays är NSMutableArray, och så vidare. Förutom att dessa objekt kan muteras utanför synkroniseringen, underkänner pekartyper från Objective-C också Swift-alternativ. Det finns en bra chans att du kan vänta ett objekt i Swift, men från Objective-C returneras det som noll. 

Om appen kraschar ger den värdefull inblick i den interna logiken. I det här fallet kan det vara att användarinmatningen inte kontrollerats korrekt och det området av appflödet är värt att titta på för att försöka utnyttja.

Lösningen här är att uppdatera din Objective-C-kod för att inkludera annulleringsannonser. Vi kan göra en liten omställning här eftersom detta råd gäller för säker driftskompatibilitet i allmänhet, oavsett om det är mellan Swift och Objective-C eller mellan två andra programmeringsspråk. 

Förord ​​dina mål-C-variabler med null när noll kan returneras, och nonnull när det inte borde.

- (nonnull NSString *) myStringFromString: (nollställbar NSString *) sträng;

Du kan också lägga till null och nonnull till attributlistan med objektiv-C egenskaper.

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

Det statiska analysverktyget i Xcode har alltid varit bra för att hitta Objective-C-fel. Nu med nollställningsanmärkningar, i Xcode 9 kan du använda Static Analyzer på din Objective-C-kod och det kommer att hitta inkompatibiliteter i nuläget i din fil. Gör detta genom att navigera till Produkt> Utför åtgärd> Analysera.

Medan den är aktiverad som standard kan du också kontrollera nulllängningskontrollerna i LLVM med -Wnullability * flaggor.

Nullability kontroller är bra för att hitta problem vid kompileringstid, men de hittar inte runtime problem. Till exempel, ibland antar vi i en del av vår kod att ett valfritt värde alltid kommer att finnas och använd kraften omplocka ! på det. Detta är en implicit oöppnad valfri, men det finns ingen garanti för att det alltid kommer att finnas. När allt är märkt frivilligt, är det troligt att det är noll någon gång. Därför är det en bra idé att undvika att tvinga med sig !. Istället är en elegant lösning att kontrollera vid körning som så:

vakt låt hunden = animal.dog () annars // hantera detta fall tillbaka // fortsätt ... 

För att ytterligare hjälpa dig, läggs en ny funktion till i Xcode 9 för att utföra nullabilitetskontroller vid körning. Det är en del av Undefined Behavior Sanitizer, och medan den inte är aktiverad som standard kan du aktivera den genom att gå till Bygga inställningar> Odefinierad beteende Sanitizer och inställning Ja för Aktivera Nullability Annotation Checks.

Läsbarhet

Det är bra att skriva dina metoder med endast en post och en utgångspunkt. Inte bara är det bra för läsbarhet, men också för avancerat multithreading-stöd. 

Låt oss säga att en klass utformades utan samtidighet i åtanke. Senare ändrades kraven så att den nu måste stödja .låsa() och .låsa upp() metoder för NSLock. När det är dags att placera låser runt delar av koden, kan du behöva skriva om många av dina metoder för att vara trådsäker. Det är lätt att missa en lämna tillbaka gömd i mitten av en metod som senare skulle låsa din NSLock Exempel, som då kan orsaka ett tävlingsförhållande. Också uttalanden som lämna tillbaka kommer inte automatiskt låsa upp låset. En annan del av din kod som antar låsningen är upplåst och försöker låsa igen kommer att låsa upp appen (appen fryser och så småningom avslutas av systemet). Kraschar kan också vara säkerhetsproblem i multithreaded code om tillfälliga arbetsfiler aldrig rengörs innan tråden slutar. Om din kod har denna struktur:

om x om y returnerar sant annat returnerar falskt ... returnera falskt

Du kan istället lagra den booleska, uppdatera den längs vägen och sedan returnera den i slutet av metoden. Då kan synkroniseringskoden enkelt lindas i metoden utan mycket arbete.

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

De .låsa upp() Metoden måste kallas från samma tråd som kallas .låsa(),  annars leder det till odefinierat beteende.

Testning

Ofta är det svårt att hitta och fixa sårbarheter i samtidig kod. När du hittar en bugg, är det som att hålla en spegel upp till dig själv - en bra inlärningsmöjlighet. Om du glömde att synkronisera på ett ställe är det troligt att samma misstag finns någon annanstans i koden. Om du tar tid att kolla resten av koden för samma misstag när du stöter på ett fel är ett mycket effektivt sätt att förebygga säkerhetsproblem som skulle fortsätta att visas om och om igen i framtida appmeddelanden. 

Faktum är att många av de senaste iOS-jailbreaks har varit på grund av upprepade kodningsfel som hittades i Apples IOKit. När du väl vet utvecklarens stil kan du kolla andra delar av koden för liknande buggar.

Fel upptäckt är bra motivation för kodåteranvändning. Att veta att du fixade ett problem på ett ställe och inte behöver hitta alla samma händelser i kopiera / klistra in kan vara en stor lättnad.

Loppförhållandena kan vara komplicerade att hitta under testning eftersom minnet kanske måste vara skadat på bara "rätt sätt" för att se problemet, och ibland uppstår problemen en lång tid senare i appens körning. 

När du testar täcker all din kod. Gå igenom varje flöde och fall och testa varje kodlinje minst en gång. Ibland bidrar det till att mata in slumpmässiga data (fuzzing ingångarna) eller välja extrema värden i hopp om att hitta ett kantfall som inte skulle vara uppenbart när man tittar på koden eller använder appen på normalt sätt. Detta, tillsammans med de nya Xcode-verktygen som finns tillgängliga, kan gå långt för att förebygga säkerhetsproblem. Medan ingen kod är 100% säker, kommer det att följa en rutin, till exempel tidiga funktionstester, enhetstester, systemtest, stress och regressionstest, verkligen betala.

Utöver att felsöka din app är en sak som skiljer sig från konfigurationen för utlösning (konfigurationen för appar som publiceras i butiken) att kodoptimeringar ingår. Till exempel, vad kompilatorn tycker är en oanvänd operation kan optimeras ut, eller en variabel kanske inte stannar längre än nödvändigt i ett samtidigt block. För din publicerade app är din kod faktiskt ändrad, eller annorlunda än den du testade. Det innebär att det kan introduceras fel som bara finns när du släpper ut din app. 

Om du inte använder en testkonfiguration, se till att du testar din app i släppläge genom att navigera till Produkt> Schema> Redigeringsschema. Välj Springa från listan till vänster och i Info rutan till höger ändras Bygg konfiguration till Släpp. Även om det är bra att täcka hela appen i det här läget, vet du att det på grund av optimeringar inte uppspelningspunkter och debugger inte uppträder som förväntat. Till exempel kan variabla beskrivningar inte vara tillgängliga även om koden exekveras korrekt.

Slutsats

I det här inlägget tittade vi på tävlingsförhållanden och hur man undviker dem genom att koda säkert och använda verktyg som Thread Sanitizer. Vi pratade också om exklusiv åtkomst till minne, vilket är ett bra komplement till Swift 4. Se till att det är inställt på Full verkställighet i Bygg inställningar> Exklusiv åtkomst till minne

Kom ihåg att dessa tillämpningar endast är aktiverade för debug-läge, och om du fortfarande använder Swift 3.2, kommer många av de diskuterade diskussionerna endast i form av varningar. Så ta varningarna allvarligt, eller ännu bättre, utnyttja alla nya funktioner som finns tillgängliga genom att anta Swift 4 idag!

Och medan du är här, kolla in några av mina andra inlägg på säker kodning för iOS och Swift!