Sikker koding med samtidighet i Swift 4

I min tidligere artikkel om sikker koding i Swift diskuterte jeg grunnleggende sikkerhetsproblemer i Swift som injeksjonsangrep. Mens injeksjonsangrep er vanlige, er det andre måter appen din kan bli kompromittert med. En vanlig, men noen ganger oversett type sårbarhet er raseforhold. 

Swift 4 introduserer Eksklusiv tilgang til minne, som består av et sett med regler for å hindre at det samme minnesområdet blir åpnet samtidig. For eksempel, inout argument i Swift forteller en metode som kan endre verdien av parameteren inne i metoden.

func changeMe (_ x: inout MyObject, og Change y: inout MyObject) 

Men hva skjer hvis vi passerer i samme variabel for å skifte samtidig?

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

Swift 4 har gjort forbedringer som forhindrer dette i å kompilere. Men mens Swift kan finne disse åpenbare scenariene på kompileringstid, er det vanskelig, spesielt av ytelsesmessige årsaker, å finne problemer med minneadgang i samtidskode, og de fleste sikkerhetsproblemene finnes i form av raseforhold.

Race betingelser

Så snart du har mer enn en tråd som trenger å skrive til de samme dataene samtidig, kan det oppstå et løpevilkår. Race forhold forårsaker data korrupsjon. For disse typer angrep er sårbarhetene vanligvis mer subtile, og utnyttelsene er mer kreative. For eksempel kan det være mulig å endre en delt ressurs for å endre strømmen av sikkerhetskode som skjer på en annen tråd, eller når det gjelder godkjenningsstatus, kan en angriper muliggjøre en tidsgap mellom kontrolltidspunktet og tidspunktet for bruk av et flagg.

Måten å unngå raseforhold er å synkronisere dataene. Synkroniseringsdata betyr vanligvis å "låse" den slik at bare en tråd kan få tilgang til den delen av koden av gangen (sies å være en mutex-for gjensidig utestenging). Mens du kan gjøre dette eksplisitt ved hjelp av NSLock klasse, det er potensial til å savne steder der koden skulle ha blitt synkronisert. Å holde oversikt over låsene, og om de allerede er låst eller ikke, kan være vanskelige.

Grand Central Dispatch

I stedet for å bruke primitive låser, kan du bruke Grand Central Dispatch (GCD) -Apples moderne samtidighet API utviklet for ytelse og sikkerhet. Du trenger ikke å tenke på låsene selv; det gjør jobben for deg bak kulissene. 

DispatchQueue.global (qos: .background) .async // samtidig kø, delt av system // gjør det lange løpende arbeid i bakgrunnen her // ... DispatchQueue.main.async // seriekø // Oppdater UI-showet resultatene tilbake på hovedtråden

Som du kan se, er det ganske enkelt en API, så bruk GCD som ditt førstevalg når du designer appen din for samtidighet.

Swifts kjøretids sikkerhetskontroller kan ikke utføres på tvers av GCD-tråder fordi det skaper en betydelig ytelse hit. Løsningen er å bruke verktøyet Trådgjenoppretting hvis du jobber med flere tråder. The Thread Sanitizer verktøyet er flott å finne problemer du aldri finner ved å se på koden selv. Det kan aktiveres ved å gå til Produkt> Ordning> Rediger skjema> Diagnostikk, og sjekke Trådrensemaskin alternativ.

Hvis utformingen av appen din gjør at du arbeider med flere tråder, er en annen måte å beskytte deg mot sikkerhetsproblemer med samtidighet prøv å utforme klassene dine for å være låsefri slik at ingen synkroniseringskode er nødvendig i utgangspunktet. Dette krever noen ekte tanke om utformingen av grensesnittet, og kan til og med betraktes som en egen kunst i seg selv!

Hovedtranskontrolleren

Det er viktig å nevne at data korrupsjon også kan oppstå hvis du bruker UI-oppdateringer på en hvilken som helst annen tråd enn hovedtråden (noen annen tråd kalles en bakgrunnstråd). 

Noen ganger er det ikke engang åpenbart at du er på en bakgrunnstråd. For eksempel, NSURLSession's delegateQueue, når satt til nil, vil som standard ringe tilbake på en bakgrunnstråd. Hvis du bruker UI-oppdateringer eller skriver til dataene dine i den aktuelle blokken, er det en god sjanse for raseforhold. (Løs dette ved å pakke inn UI-oppdateringene i DispatchQueue.main.async eller pass inn OperationQueue.main som delegatskø.) 

Nytt i Xcode 9 og aktivert som standard er hovedtrådkontrollen (Produkt> Ordning> Rediger skjema> Diagnostikk> Runtime API-kontroll> Hovedtranskontrolleren). Hvis koden din ikke er synkronisert, vises problemer i Runtime problemer På venstre rute navigator av Xcode, så vær oppmerksom på det mens du tester appen din. 

For å kode for sikkerhet, må eventuelle tilbakeringinger eller ferdigstillingshåndterere som du skriver, dokumenteres om de kommer tilbake på hovedtråden eller ikke. Bedre ennå, følg Apples nyere API-design som lar deg passere en completionQueue i metoden slik at du tydelig kan bestemme og se hvilken tråd ferdigstillingsblokken returnerer på.

Et eksempel på ekte verden

Nok snakk! La oss dykke inn i et eksempel.

klassetransaksjon // ... klassetransaksjoner private var lastTransaction: Transaction? func addTransaction (_ source: Transaction) // ... lastTransaction = kilde // Første trådtransaksjoner.addTransaksjon (transaksjon) // Andre trådtransaksjoner.addTransaksjon (transaksjon)

Her har vi ingen synkronisering, men flere enn én tråd får tilgang til dataene samtidig. Det gode ved Thread Sanitizer er at det vil oppdage en sak som dette. Den moderne GCD-måten å fikse dette på er å knytte dataene dine med en seriell forsendelseskø.

klassetransaksjoner private var lastTransaction: Transaction? privat var kø = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") func addTransaction (_ kilde: Transaksjon) queue.async // ... self.lastTransaction = source

Nå er koden synkronisert med .async blokkere. Du lurer kanskje på når du skal velge .async og når du skal bruke .synkron. Du kan bruke .async når appen din ikke trenger å vente til operasjonen i blokken er ferdig. Det kan bli bedre forklart med et eksempel.

la kø = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") var transaksjonsID: [String] = ["00001", "00002"] // Første trådkø.async transactionIDs.append ("00003") // ikke gir noe output, så det må ikke vente på at det skal fullføres. // En annen trådkøe.sync hvis transactionIDs.contains ("00001") // ... Trenger du å vente her! print ("Transaksjonen er allerede fullført")

I dette eksemplet gir tråden som spør transaksjonsarrayen hvis den inneholder en bestemt transaksjon, utdata, så det må vente. Den andre tråden tar ingen tiltak etter å ha lagt til transaksjonsarrangementet, så det behøver ikke å vente til blokken er fullført.

Disse synkroniserings- og asynkblokkene kan pakkes inn i metoder som returnerer interne data, for eksempel getter-metoder.

få return queue.sync transactionID

Spredning GCD blokkerer over områdene av koden din, som gir tilgang til delt data, ikke er en god praksis, da det er vanskeligere å holde styr på alle stedene som må synkroniseres. Det er mye bedre å prøve og holde all denne funksjonaliteten på ett sted. 

God design med tilgangsmetoder er en måte å løse dette problemet på. Ved å bruke getter og setter-metoder, og bare ved å bruke disse metodene for å få tilgang til dataene, kan du synkronisere på ett sted. Dette unngår å måtte oppdatere mange deler av koden din hvis du endrer eller refactorerer GCD-området i koden din.

structs

Mens enkelt lagrede egenskaper kan synkroniseres i en klasse, vil endring av egenskaper på en struktur faktisk påvirke hele strukturen. Swift 4 inneholder nå beskyttelse for metoder som muterer strukturer. 

La oss først se på hva en strukturkorrupsjon (kalt "Swift Access Race") ser ut som.

struct Transaction privat var id: UInt32 privat var tidsstempel: Dobbel // ... muterende func begynnelse () id = arc4random_uniform (101) // 0 - 100 // ... muterende func finish () // ... timestamp = NSDate ) .timeIntervalSince1970

De to metodene i eksemplet endrer de lagrede egenskapene, så de er merket mutere. La oss si at tråd 1 ringer begynne() og tråd 2 ringer bli ferdig(). Selv om begynne() bare endringer id og bli ferdig() bare endringer tidsstempel, det er fortsatt et aksessløp. Selv om det vanligvis er bedre å låse innvendige tilgangsmetoder, gjelder dette ikke for strukturer som hele strukturen må være eksklusiv. 

En løsning er å endre strukturen til en klasse når du implementerer din samtidige kode. Hvis du trenger strukturen av en eller annen grunn, kan du i dette eksempelet lage en Bank klasse som lagrer Transaksjon structs. Deretter kan innringerne av structs inne i klassen synkroniseres. 

Her er et eksempel:

klasse Bank private var currentTransaction: Transaction? Private kø: DispatchQueue = DispatchQueue (etikett: "com.myCompany.myApp.bankQueue") func doTransaction () queue.sync currentTransaction? .begin () // ...

Adgangskontroll

Det ville være meningsløst å ha all denne beskyttelsen når grensesnittet ditt avslører et muterende objekt eller en UnsafeMutablePointer til de delte dataene, for nå kan enhver bruker av klassen din gjøre hva de vil ha med dataene uten beskyttelse av GCD. I stedet returnerer du kopier til dataene i getteren. Forsiktig grensesnittdesign og datainnkapsling er viktig, spesielt når du designer samtidige programmer, for å sikre at de delte dataene er virkelig beskyttet.

Kontroller at de synkroniserte variablene er merket privat, i motsetning til åpen eller offentlig, som vil tillate medlemmer fra hvilken som helst kildefil å få tilgang til den. En interessant endring i Swift 4 er at privat Tilgangsnivået er utvidet for å være tilgjengelig i utvidelser. Tidligere kunne den bare brukes i den vedlagte erklæringen, men i Swift 4, a privat variabel kan nås i en utvidelse, så lenge utvidelsen av denne erklæringen er i samme kildefil.

Ikke bare er variabler i fare for data korrupsjon, men også filer. Bruke Filbehandler Foundation-klassen, som er trådsikker, og kontroller resultatflaggene i filoperasjonen før du fortsetter i koden din.

Grensesnitt med mål-C

Mange Objective-C objekter har en mutable motpart avbildet av tittelen. NSStringDen mutable versjonen heter NSMutableString, NSArrays er NSMutableArray, og så videre. Foruten det faktum at disse objektene kan bli mutert utenfor synkroniseringen, underkaster pekertyper som kommer fra Objective-C også Swift-alternativene. Det er en god sjanse for at du kan forvente et objekt i Swift, men fra Objective-C returneres det som null. 

Hvis appen krasjer, gir den verdifull innsikt i den interne logikken. I dette tilfellet kan det være at brukerinngang ikke var korrekt sjekket, og det området av appstrømmen er verdt å se på for å prøve å utnytte.

Løsningen her er å oppdatere Objective-C-koden din for å inkludere ugyldighetsannonser. Vi kan ta en liten avledning her, da dette rådet gjelder sikker interoperabilitet generelt, enten mellom Swift og Objective-C eller mellom to andre programmeringsspråk. 

Forord dine mål-C-variabler med ha nullverdier når null kan returneres, og nonnull når det ikke burde.

- (nonnull NSString *) myStringFromString: (nullable NSString *) streng;

Du kan også legge til ha nullverdier og nonnull til attributtlisten over objektiv-C egenskaper.

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

Det Static Analyzer verktøyet i Xcode har alltid vært bra for å finne Objective-C bugs. Nå med nulleringsannonser, i Xcode 9 kan du bruke Static Analyzer på Objective-C-koden din, og det vil finne nullabilitetsforstyrrelser i filen din. Gjør dette ved å navigere til Produkt> Utfør handling> Analyser.

Selv om det er aktivert som standard, kan du også kontrollere nullitetskontrollene i LLVM med -Wnullability * flagg.

Nullability sjekker er bra for å finne problemer på kompileringstid, men de finner ikke runtime problemer. For eksempel antar vi noen ganger i en del av koden at en valgfri verdi alltid vil eksistere og bruk kraften til å pakke ut ! på den. Dette er en implisitt uåpnet valgfri, men det er ingen garanti for at den alltid vil eksistere. Tross alt, hvis det ble merket valgfritt, er det sannsynlig at det er null på et tidspunkt. Derfor er det en god ide å unngå kraftutpakning med !. I stedet er en elegant løsning å sjekke ved kjøretid slik som:

vakt la hunden = animal.dog () ellers // håndtere denne saken tilbake // fortsett ... 

For ytterligere å hjelpe deg, er det lagt til en ny funksjon i Xcode 9 for å utføre nullabilitetskontroller ved kjøring. Det er en del av Undefined Behavior Sanitizer, og mens den ikke er aktivert som standard, kan du aktivere den ved å gå til Bygg innstillinger> Udefinert oppførsel Sanitizer og innstilling Ja til Aktiver Nullability Annotation Checks.

lesbarhet

Det er god praksis å skrive metodene dine med bare én oppføring og ett utgangspunkt. Ikke bare er dette bra for lesbarhet, men også for avansert multithreading-støtte. 

La oss si at en klasse ble designet uten samtidighet i tankene. Senere endret kravene slik at den nå må støtte .låse() og .låse opp() metoder for NSLock. Når det kommer tid til å plassere låser rundt deler av koden din, må du kanskje skrive mange metoder for å være trådsikker. Det er lett å savne en komme tilbake skjult midt i en metode som senere skulle låse deg NSLock forekomst, som da kan forårsake en løpevilkår. Også uttalelser som komme tilbake vil ikke låse opp låsen automatisk. En annen del av koden din som utgjør låsen, er låst opp og forsøker å låse igjen, vil låse appen (appen vil fryses og til slutt bli avsluttet av systemet). Krasj kan også være sikkerhetsproblemer i multithreaded kode hvis midlertidige arbeidsfiler aldri blir ryddet før tråden slutter. Hvis koden din har denne strukturen:

hvis x hvis du returnerer sant ellers returnerer falsk ... returner falsk

Du kan i stedet lagre den boolske, oppdatere den underveis, og returnere den på slutten av metoden. Da kan synkroniseringskoden enkelt pakkes inn i metoden uten mye arbeid.

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

De .låse opp() Metoden må kalles fra samme tråd som kalt .låse(),  ellers resulterer det i udefinert oppførsel.

testing

Ofte finner og fastsetter sårbarheter i samtidig kode ned til feiljakt. Når du finner en feil, er det som å holde et speil opp til deg selv - en god læringsmulighet. Hvis du har glemt å synkronisere på ett sted, er det sannsynlig at den samme feilen er andre steder i koden. Tar deg tid til å sjekke resten av koden din for samme feil når du støter på en feil, er en svært effektiv måte å forhindre sikkerhetsproblemer som ville fortsette å vises igjen og igjen i fremtidige apputgivelser. 

Faktisk har mange av de siste iOS-jailbreakene vært på grunn av gjentatte kodingsfeil som finnes i Apples IOKit. Når du kjenner utviklerens stil, kan du sjekke andre deler av koden for lignende feil.

Feilsøking er god motivasjon for kodeutnyttelse. Å vite at du har løst et problem på ett sted og ikke trenger å finne alle de samme hendelsene i kopi / lime-kode kan være en stor lettelse.

Løpevilkårene kan være kompliserte å finne under test, fordi det kan hende at minne må være skadet på bare "riktig måte" for å se problemet, og noen ganger oppstår problemene lenge senere i appens utførelse.. 

Når du tester, dekke hele koden din. Gå gjennom hver strøm og sak og test hver linje av kode minst en gang. Noen ganger hjelper det med å legge inn tilfeldige data (fuzzing inngangene), eller velg ekstreme verdier i håp om å finne en kanten sak som ikke ville være åpenbart å se på koden eller bruke appen på vanlig måte. Dette, sammen med de nye Xcode-verktøyene som er tilgjengelige, kan gå langt for å hindre sikkerhetsproblemer. Selv om ingen kode er 100% sikker, vil det etter en rutine, som tidligere funksjonstester, enhetstester, systemtest, stress og regresjonstester, virkelig lønne seg.

Utover feilsøking av appen din, er en ting som er forskjellig for utgivelseskonfigurasjonen (konfigurasjonen for apper publisert i butikken) at kodeoptimaliseringer er inkludert. For eksempel, hva kompilatoren mener er en ubrukt operasjon kan bli optimalisert ut, eller en variabel kan ikke holde seg lenger enn nødvendig i en samtidig blokk. For din publiserte app, er koden din faktisk endret, eller forskjellig fra den du testet. Dette betyr at det kan innføres feil som bare eksisterer når du slipper appen din. 

Hvis du ikke bruker en testkonfigurasjon, må du kontrollere at du tester appen din i utgivelsesmodus ved å navigere til Produkt> Ordning> Rediger skjema. Å velge Løpe fra listen til venstre, og i info ruten til høyre, endre Bygg konfigurasjon til Utgivelse. Selv om det er bra å dekke hele appen din i denne modusen, vet du at på grunn av optimaliseringer, vil breakpoints og debugger ikke oppføre seg som forventet. For eksempel kan det hende at variable beskrivelser ikke er tilgjengelige selv om koden kjøres riktig.

Konklusjon

I dette innlegget så vi på raseforhold og hvordan du kan unngå dem ved å kode sikkert og bruke verktøy som Thread Sanitizer. Vi snakket også om Eksklusiv tilgang til minne, noe som er et flott tillegg til Swift 4. Pass på at den er satt til Full håndhevelse i Bygg innstillinger> Eksklusiv tilgang til minne

Husk at disse håndhevelsene bare er på for feilsøkingsmodus, og hvis du fremdeles bruker Swift 3.2, kommer mange av de diskuterte handlingene kun i form av advarsler. Så ta advarslene seriøst, eller enda bedre, bruk alle de nye funksjonene som er tilgjengelige ved å vedta Swift 4 i dag!

Og mens du er her, sjekk ut noen av mine andre innlegg på sikker koding for iOS og Swift!