Swift 4中的并发安全编码

在上一篇有关Swift中安全编码的文章中 ,我讨论了Swift中的基本安全漏洞,例如注入攻击。 尽管注入攻击很常见,但是还有其他方法可以破坏您的应用程序。 竞赛条件是一种常见但有时被忽视的漏洞。

Swift 4引入了对内存的独占访问权限 ,该规则由一组规则组成,以防止同时访问同一区域的内存。 例如,Swift中的inout参数告诉方法可以更改方法内部参数的值。

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

但是,如果我们传递相同的变量来同时进行更改,会发生什么呢?

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

Swift 4进行了改进,以防止其编译。 但是,尽管Swift可以在编译时找到这些明显的场景,但尤其是出于性能原因,很难在并发代码中找到内存访问问题,并且大多数安全漏洞都以竞争条件的形式存在。

比赛条件

一旦有多个线程需要同时写入相同的数据,就会发生竞争状态。 竞争条件导致数据损坏。 对于这些类型的攻击,漏洞通常更微妙-漏洞利用更有创造力。 例如,可能有能力更改共享资源,以更改发生在另一个线程上的安全代码流,或者在身份验证状态下,攻击者可能能够利用检查时间之间的时间间隔以及使用标志的时间 。

避免竞争情况的方法是同步数据。 同步数据通常意味着“锁定”数据,以便一次只有一个线程可以访问该部分代码(称为互斥体,用于互斥)。 虽然您可以使用NSLock类显式地执行此操作,但是有可能会丢失应该同步代码的位置。 跟踪锁以及它们是否已被锁定可能很困难。

大中央派遣

除了使用原始锁,您还可以使用Grand Central Dispatch(GCD),这是Apple为提高性能和安全性而设计的现代并发API。 您无需自己考虑锁; 它在幕后为您完成工作。

DispatchQueue.global(qos: .background).async  //concurrent queue, shared by system
{
    //do long running work in the background here
    //...
    
    DispatchQueue.main.async //serial queue
    {
        //Update the UI - show the results back on the main thread
    }
}

如您所见,这是一个非常简单的API,因此在设计应用程序并发时将GCD作为首选。

Swift的运行时安全性检查无法跨GCD线程执行,因为它会严重影响性能。 解决方案是,如果要使用多个线程,则使用Thread Sanitizer工具。 线程清理工具非常适合通过自己查看代码来发现您可能永远找不到的问题。 可以通过转到“ 产品”>“方案”>“编辑方案”>“诊断 ”并选中“ 线程清除程序”选项来启用它。

如果应用程序的设计使您可以使用多个线程,那么另一种保护自己免受并发安全性问题的方法是, 尝试将您的类设计为无锁,这样一开始就不需要同步代码。 这需要对界面设计进行一些实际的思考,甚至​​可以将其本身视为一种独立的艺术!

主线程检查器

值得一提的是,如果您在除主线程之外的任何其他线程上进行UI更新(任何其他线程都称为后台线程),也会发生数据损坏。

有时,您甚至不在后台线程上。 例如, NSURLSessiondelegateQueue NSURLSession设置为nil ,默认情况下将在后台线程上回调。 如果您在该块中执行UI更新或写入数据,则极有可能出现竞争状况。 (通过将UI更新包装在DispatchQueue.main.async {}或通过将OperationQueue.main作为委托队列传递来解决此问题。)

Xcode 9中的新增功能是默认情况下启用的主线程检查器(“ 产品”>“方案”>“编辑方案”>“诊断”>“运行时API检查”>“主线程检查器” )。 如果您的代码未同步,问题将显示在Xcode左窗格导航器的“ 运行时问题 ”中,因此在测试应用程序时请注意它。

为了安全起见,无论您是否在主线程上返回,都应记录您编写的任何回调或完成处理程序。 更好的是,遵循Apple的更新的API设计,该设计使您可以在方法中传递completionQueue ,以便您可以清楚地确定并查看完成块返回的线程。

真实的例子

聊够了! 让我们来看一个例子。

class Transaction
{
    //...
}

class Transactions
{
    private var lastTransaction : Transaction?
    
    func addTransaction(_ source : Transaction)
    {
        //...
        lastTransaction = source
    }
}

//First thread
transactions.addTransaction(transaction)
        
//Second thread
transactions.addTransaction(transaction)

在这里,我们没有同步,但是有多个线程同时访问数据。 Thread Sanitizer的好处是它将检测到这种情况。 解决此问题的现代GCD方法是将您的数据与串行调度队列关联。

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

现在,代码已与.async块同步。 您可能想知道何时选择.async以及何时使用.sync 。 当您的应用不需要等待块内的操作完成时,可以使用.async 。 用一个例子可能会更好地解释。

let queue = DispatchQueue(label: "com.myCompany.myApp.bankQueue")
var transactionIDs : [String] = ["00001", "00002"]
        
//First thread
queue.async
{
    transactionIDs.append("00003")
}
//not providing any output so don't need to wait for it to finish
        
//Another thread
queue.sync
{
    if transactionIDs.contains("00001") //...Need to wait here!
    {
        print("Transaction already completed")
    }
}

在此示例中,询问事务数组是否包含特定事务的线程将提供输出,因此需要等待。 另一个线程在追加到事务数组之后不执行任何操作,因此它不需要等到该块完成。

这些同步块和异步块可以包装在返回内部数据的方法中,例如getter方法。

get
{
    return queue.sync { transactionID }
}

在您访问共享数据的代码的所有区域中散布GCD块并不是一个好习惯,因为很难跟踪所有需要同步的位置。 最好将所有这些功能都保留在一个地方。

使用访问器方法进行良好的设计是解决此问题的一种方法。 使用getter和setter方法并且仅使用这些方法来访问数据意味着您可以在一个地方进行同步。 如果您要更改或重构代码的GCD区域,这可以避免更新代码的许多部分。

结构

虽然可以在一个类中同步单个存储的属性,但是更改结构上的属性实际上会影响整个结构。 现在,Swift 4提供了对使结构变异的方法的保护。

首先,让我们看一下结构损坏(称为“快速访问竞赛”)的样子。

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
    }
}

示例中的两种方法更改了存储的属性,因此将其标记为mutating 。 假设线程1调用begin() ,线程2调用finish() 。 即使begin()仅更改idfinish()仅更改timestamp ,它仍然是访问竞赛。 通常,最好将访问器方法锁定在内部,但这不适用于结构,因为整个结构都需要互斥。

一种解决方案是在实现并发代码时将结构更改为类。 如果出于某种原因需要该结构,则在此示例中,您可以创建一个存储Transaction结构的Bank类。 然后,可以同步类内部结构的调用者。

这是一个例子:

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

访问控制

当您的接口向共享数据公开一个变异对象或UnsafeMutablePointer时,拥有所有这些保护将是毫无意义的,因为现在,您的任何UnsafeMutablePointer用户都可以在没有GCD保护的情况下对数据进行任何操作。 而是将副本返回到getter中的数据。 仔细的接口设计和数据封装非常重要,尤其是在设计并发程序时,以确保共享数据得到真正的保护。

确保同步变量标记为private ,而不是openpublic ,这将允许任何源文件中的成员访问它。 Swift 4中一个有趣的变化是private访问级别范围已扩展为可在扩展中使用。 以前,它只能在封闭的声明中使用,但是在Swift 4中,可以在扩展名中访问private变量,只要该声明的扩展名在同一源文件中即可。

变量不仅面临数据损坏的风险,而且文件也面临风险。 使用FileManager Foundation类是线程安全的,并在继续执行代码之前检查其文件操作的结果标志。

与Objective-C的接口

许多Objective-C对象的标题都有其对应的可变对象。 NSString的可变版本名为NSMutableStringNSArray的名称为NSMutableArray ,依此类推。 除了可以在同步之外对这些对象进行突变这一事​​实之外,来自Objective-C的指针类型也颠覆了Swift可选对象。 您很有可能在Swift中期望有一个对象,但是从Objective-C中它返回为nil。

如果应用程序崩溃,则可以深入了解内部逻辑。 在这种情况下,可能是没有正确检查用户输入,并且值得尝试尝试利用应用程序流程的区域。

此处的解决方案是更新您的Objective-C代码以包括可空性注释。 我们可以在这里稍作改动,因为该建议通常适用于安全互操作性,无论是在Swift和Objective-C之间还是在其他两种编程语言之间。

如果可以返回nil,则在Objective-C变量前添加nullable ,否则不应该使用nonnull

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

您还可以将nullablenonnull添加到Objective-C属性的属性列表。

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

Xcode中的Static Analyzer工具一直非常适合查找Objective-C错误。 现在带有可空性注释,在Xcode 9中,您可以在Objective-C代码上使用静态分析器,它将在文件中发现可空性不一致。 通过导航至产品>执行操作>分析执行此操作

默认情况下启用该功能后,您还可以使用-Wnullability*标志控制LLVM中的可空性检查。

可空性检查对于在编译时发现问题很有用,但它们找不到运行时问题。 例如,有时我们在代码的一部分中假设一个可选值将始终存在,并使用unwrap强制! 在上面。 这是一个隐式解包的可选内容,但实际上并不能保证它会一直存在。 毕竟,如果将其标记为可选,则在某些时候可能为零。 因此,最好避免用! 。 相反,一种优雅的解决方案是在运行时进行如下检查:

guard let dog = animal.dog() else
{
    //handle this case
    return
}
//continue...

为了进一步帮助您,Xcode 9中添加了一项新功能,可以在运行时执行可空性检查。 它是Undefined Behavior Sanitizer的一部分,虽然默认情况下未启用,但您可以通过以下方法来启用它:转到Build Settings> Undefined Behavior Sanitizer,然后为Enable Nullability Annotation Checks设置Yes

可读性

最好只用一个入口和一个出口点编写方法。 这不仅对可读性有好处,而且对高级多线程支持也有好处。

假设某个类在设计时没有考虑并发性。 后来,需求发生了变化,因此它现在必须支持NSLock.lock().unlock()方法。 当需要在代码的各个部分周围放置锁时,可能只是为了线程安全而需要重写许多方法。 很容易错过隐藏在方法中间的return ,该return稍后应该锁定您的NSLock实例,这可能会导致竞争状态。 同样,诸如return语句也不会自动解除锁定。 代码的另一部分假定锁定已解锁,然后尝试再次锁定,将使应用程序死锁(应用程序将冻结并最终被系统终止)。 如果从未在线程终止前清除临时工作文件,则崩溃也可能是多线程代码中的安全漏洞。 如果您的代码具有以下结构:

if x
    if y
		return true
	else
		return false
...
return false

相反,您可以存储布尔值,然后进行更新,然后在方法末尾将其返回。 然后,无需太多工作即可轻松将同步代码包装在该方法中。

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

.unlock()方法必须在同一个线程调用被称为.lock()否则会导致不确定的行为。

测验

通常,在并发代码中查找和修复漏洞可归结为漏洞搜寻。 当您发现错误时,就像在自己身上举起镜子一样,这是一个很好的学习机会。 如果您忘记在一个地方进行同步,则可能是同一错误出现在代码的其他地方。 花一些时间在遇到错误时检查其余代码是否存在相同的错误,这是防止安全漏洞的一种非常有效的方法,该漏洞将在以后的应用程序版本中不断出现。

实际上,最近的许多iOS越狱都是由于Apple的IOKit中反复出现编码错误而引起的。 一旦知道了开发人员的风格,就可以检查代码的其他部分是否存在类似的错误。

发现错误是代码重用的良好动力。 知道自己将问题解决在一个地方,而不必去复制/粘贴代码中查找所有相同的事件,这可以让您大为欣慰。

在测试过程中,查找竞争条件可能很复杂,因为可能必须以“正确的方式”破坏内存才能看到问题,有时问题会在应用程序的执行中出现很长时间。

在测试时,请覆盖所有代码。 遍历每个流程和案例,并至少对每一行代码进行一次测试。 有时,它有助于输入随机数据(使输入模糊),或选择极高的值,以期找到一种边缘情况,这种情况在看代码或以常规方式使用应用程序时不会很明显。 这与可用的新Xcode工具一起可以在防止安全漏洞方面大有帮助。 尽管没有代码是100%安全的,但遵循例程(例如早期功能测试,单元测试,系统测试,压力和回归测试)将真正奏效。

除了调试您的应用程序外,发布配置(商店中发布的应用程序的配置)与发布配置不同的一件事是还包括代码优化。 例如,编译器认为未使用的操作可以得到优化,或者变量的停留时间可能不会超过并发块中所需的时间。 对于已发布的应用程序,您的代码实际上已更改,或与您测试的代码不同。 这意味着可以引入仅在发布应用程序后才存在的错误。

如果您没有使用测试配置,请通过导航到Product> Scheme> Edit Scheme确保在发布模式下测试您的应用。 从左侧列表中选择Run ,然后在右侧的Info窗格中,将Build Configuration更改为Release 。 在这种模式下覆盖整个应用程序虽然很好,但是请注意,由于进行了优化,因此断点和调试器的行为将不符合预期。 例如,即使代码正确执行,变量描述也可能不可用。

结论

在这篇文章中,我们研究了竞争条件以及如何通过安全编码和使用诸如Thread Sanitizer之类的工具来避免竞争条件。 我们还讨论了对内存的独占访问,这是对Swift 4的重要补充。确保在“ 构建设置”>“对内存的独占访问”中将其设置为“ 完全强制执行

请记住,这些强制措施仅适用于调试模式,如果您仍在使用Swift 3.2,则讨论的许多强制措施仅以警告的形式出现。 因此,请认真对待警告,或者更好的是,立即采用Swift 4,充分利用所有可用的新功能!

翻译自: https://code.tutsplus.com/articles/secure-coding-in-swift-4-with-concurrency--cms-29917

你可能感兴趣的:(java,python,多线程,人工智能,面试)