Swift 5.10 引入了严格并发检查,通过 @Sendable 闭包和 Actor 隔离在编译期防止数据竞争。然而,这些安全保证几乎完全依赖于编译器的静态分析,运行时缺乏强制机制。攻击者可利用 UnsafeMutablePointer 将非 Sendable 类型强制转换为 @Sendable 闭包,并将其跨 Actor 传递;在另一个 Actor 上通过 withUnsafeCurrentTask 执行任意代码,破坏原 Actor 的状态,实现数据竞争和类型混淆。更隐蔽的是,利用 TaskLocal 传递裸指针,或通过 GCD 直接调度绕过 @MainActor 隔离,可在服务器端 Swift 应用(如 Vapor)中实现持久化的隐蔽后门。
Sendable与 Actor 隔离
Sendable协议的类型安全承诺
Sendable 是 Swift 并发模型的核心协议之一,用于标记可以安全地跨并发域传递的类型。值类型(如 Int、String)、无内部共享状态的 final class 以及显式标记为 @Sendable 的闭包均符合 Sendable。编译器会检查所有跨 Actor 或 Task 传递的值的类型,如果非 Sendable 类型被错误传递,则发出严格并发检查的编译错误或警告。
在底层,@Sendable 闭包会被编译器转换为捕获不可变副本或同步原语保护的上下文,但这一转换完全发生在 SIL(Swift Intermediate Language)层,最终生成的 LLVM IR 中并没有额外的运行时检查。也就是说,一旦绕过编译器的静态检查,运行时将不加区别地执行任何闭包。
Actor 隔离的执行语义
Actor 是一种引用类型,通过串行执行队列保护其内部状态。所有对 Actor 可变状态的访问都必须通过 await 异步调用或在 Actor 内部的同步代码中进行。编译器负责保证这一约束:在 Actor 外部,无法直接访问其存储属性或调用未标记 nonisolated 的方法。
运行时实现上,Actor 携带一个内部的 DistributedActor 标识和串行执行器。当任务需要在 Actor 上执行时,会被调度到该执行器上排队。但 Actor 的执行器本身是可替换的,且 GCD 等旧并发 API 可以绕过这一调度机制,直接将任务提交到任意队列执行。这构成了 Actor 隔离的第一道裂缝。
classNonSendable{
var value: Int = 0
}
let ns = NonSendable()
let closure: () -> Void = {
ns.value += 1// 捕获非 Sendable 对象
}
// 伪装成 @Sendable 闭包
let sendableClosure: @Sendable () -> Void = unsafeBitCast(closure, to: (@Sendable () -> Void).self)
由于 unsafeBitCast 被标记为 @discardableResult 且本身是 Swift 标准库的操作,编译器不会对其产生并发警告。随后,攻击者可将此伪装的闭包传递给一个 Actor 的方法,该方法接受 @Sendable 闭包参数并在内部执行。因为闭包的 @Sendable 标签已在类型系统中被伪造,编译器放行,而闭包实际捕获的非 Sendable 状态将在 Actor 的执行上下文中被修改,破坏 Actor 的状态隔离。
跨 Actor 状态破坏示例
定义两个 Actor:Bank 管理账户余额,Malicious 在内部利用伪装的闭包篡改 Bank 的私有状态。
actor Bank {
privatevar balance: Int = 1000
funcperformTransaction(_ operation: @Sendable () -> Void) {
operation() // 在 Actor 上执行闭包
}
funcgetBalance() -> Int { balance }
}
actor Malicious {
let bank: Bank
let ns: NonSendable
init(bank: Bank, ns: NonSendable) {
self.bank = bank
self.ns = ns
}
funcattack() async {
let closure: () -> Void = {
self.ns.value = -9999// 捕获的 ns 将在 Bank 上被修改
}
// 绕过并发检查
let sendableClosure: @Sendable () -> Void = unsafeBitCast(closure, to: (@Sendable () -> Void).self)
await bank.performTransaction(sendableClosure)
}
}
执行攻击后,ns.value 被修改,而修改发生的位置是在 Bank 的串行执行上下文中,打破了 Bank 对其内部隔离的假设。如果 ns 同时被多个线程访问,就会产生数据竞争。在更复杂的场景中,攻击者可以传递捕获了 bank 自身引用的闭包,在 Bank 内部直接访问 bank.balance(因为此时已在 Actor 上下文),从而绕过 private 访问控制。
利用TaskLocal传递裸指针
TaskLocal 是 Swift 用于在线程上下文中隐式传递值的机制。攻击者可以创建一个 TaskLocal<UnsafeMutablePointer<NonSendable>> 并写入一个指向非 Sendable 对象的指针,然后在另一个 Actor 中读取该本地值,从而跨域传递裸指针。此过程中不涉及任何 Sendable 检查,因为 UnsafeMutablePointer 本身是一个值类型且符合 Sendable(作为指针,它被假定为安全的),但它指向的内容却完全不受保护。
enumGlobal{
@TaskLocalstaticvar nsPointer: UnsafeMutablePointer<NonSendable>?
}
// 在 Actor A 中
let ns = NonSendable()
Global.$nsPointer.withValue(&ns) {
// 切换到 Actor B
await someActor.doSomething()
}
// 在 Actor B 中
iflet ptr = Global.nsPointer {
ptr.pointee.value = 42// 跨 Actor 修改非 Sendable 状态
}
这里编译器完全静默,因为 TaskLocal 的值类型是 UnsafeMutablePointer,它是一个符合 Sendable 的类型。但实际上,通过它实现跨 Actor 的可变状态共享,彻底击溃了并发安全。
这不会引发并发警告,因为 GCD 是旧 API,编译器不对其内容进行严格并发分析。这种混用使得 Actor 的隔离在运行时可被轻易绕过。
服务器端 Vapor 应用中的跨 Actor 后门
以下代码展示了一个 Vapor 路由,利用上述技巧在后台修改 Actor 的私有状态,实现数据窃取。
import Vapor
actor SecretStore {
privatevar secrets: [String] = ["api_key_123", "token_xyz"]
funcprocess(_ closure: @Sendable () -> Void) {
closure()
}
funcreadSecrets() -> [String] {
secrets
}
}
let store = SecretStore()
let ns = NonSendable() // 用于传递引用
routes.get("steal") { req -> Stringin
let closure: () -> Void = {
ns.value = 1
// 这里无法直接访问 store,但可以通过 unsafeBitCast 获取
let rawPointer = Unmanaged.passUnretained(store).toOpaque()
let stolenStore = Unmanaged<SecretStore>.fromOpaque(rawPointer).takeUnretainedValue()
// 现在在闭包内,且此闭包将在 store 的 Actor 上执行,因此可以直接访问私有属性
Task {
await stolenStore.process {
// 此时已经在 store 的 Actor 上,可以直接修改 secrets
// 但 secrets 是私有,不能在闭包中直接访问。可以通过另一个非 Sendable 对象间接泄露
// 这里简化:将 secrets 拷贝到 ns 中
ns.value = stolenStore.readSecrets().count
}
}
}
let sendableClosure: @Sendable () -> Void = unsafeBitCast(closure, to: (@Sendable () -> Void).self)
await store.process(sendableClosure)
return"Leaked"
}
说明:攻击者将捕获了 store 引用的非 Sendable 闭包强制转换为 @Sendable,然后传递给 store 的方法,后者在 store 的 Actor 上执行闭包。此时闭包内部处于 Actor 的执行上下文,能够访问 store 的私有属性。通过 Unmanaged 将 Actor 引用传递进去,成功实现信息泄露。
检测与防御
运行时监控
动态检测非法跨 Actor 状态访问:使用 LLVM 插桩或 Sanitizer(如 Thread Sanitizer)监控对 Actor 内部状态的非预期访问。虽然 TSan 在 Swift 上支持有限,但在 Linux 服务器端可开启 TSan 检测数据竞争。
Swift 的并发模型建立在编译期的严格类型检查之上,而运行时却选择性地信任了这些静态承诺。unsafeBitCast、UnsafeMutablePointer 和 TaskLocal 等原生工具,为攻击者提供了一整套绕过 Sendable 和 Actor 隔离的“沉默通道”。在服务器端 Swift 日益普及的今天,这些漏洞可能使敏感数据在微服务之间无声泄漏。安全的并发语言不仅需要强大的编译期检查,更需要运行时的强制护栏——否则,静态安全的幻象只会让漏洞藏匿得更深。