背景
在使用内网的某二进制组件 BinaryPodA 时发现了部分情况下会导致触发 swift::TargetMetadataswift::InProcess::getTypeContextDescriptor 崩溃
其他公司的也有过类似问题反馈 github.com/facebook/fa…
崩溃现场的堆栈
1 | PlainText复制代码(lldb) bt |
堆栈分析
简单地看了下崩溃堆栈上能看到与 NSRunLoopMode 和 NSAttributedStringKey 的 Hashable 相关调用有关
发现在 Demo 中添加如下代码可以规避此 crash
1 | Swift复制代码func magic() { // No need to call magic() explicit |
我们按照堆栈逆着从上往下进行分析
先在 BinaryPodA.SymbolB 处添加符号断点,触发crash进行frame 0
Frame 0
Frame 0 TargetMetadata::getTypeContextDescriptor
- 相关汇编代码
1 | C复制代码libswiftCore.dylib`swift::TargetMetadata<swift::InProcess>::getTypeContextDescriptor: |
- 相关 Swift 源码
1 | C复制代码 ConstTargetMetadataPointer<Runtime, TargetTypeContextDescriptor> |
可以得知是上游调用传入的x0/this指针为NULL导致
Frame 1
Frame 1 SubstGenericParametersFromMetadata::SubstGenericParametersFromMetadata
- 相关汇编代码
1 | C复制代码libswiftCore.dylib`swift::SubstGenericParametersFromMetadata::SubstGenericParametersFromMetadata: |
- 相关 Swift 源码
1 | C复制代码 explicit SubstGenericParametersFromMetadata(const Metadata *base) |
Frame 0 的 x0 为此处的 x1,可以得知是上游传入的 x1/base 为NULL导致
Frame 2
Frame 2 swift_getAssociatedTypeWitnessSlowImpl
- 相关汇编代码
1 | C++复制代码 0x1877a4610 <+296>: b.eq 0x1877a46e8 ; <+512> |
x1 = x0 = findConformingSuperclass(xx) result = NULL 导致
和加入 magic 函数后不会 crash 的 case 对传入 findConformingSuperclass 的 x0 和 x1 进行对比,发现问题可能出现在这里的 x1 处
1 | PlainText复制代码// crash case |
- 相关 Swift 源码
1 | C++复制代码SWIFT_CC(swift) |
上面 frame 1 异常的 conformance 是通过本次入口的 x1/wtable 进行 getDescription() 调用获取的
- 相关 Swift 源码
1 | C++复制代码template <typename Runtime> |
可以得知这里的 description 就是对应的 [wtable+0x0]
在函数入口处我们已确认 x1/wtable = 0x0000000145d36800,使用 memory read 查看对应的内存地址,并用 disassemble -a [$x1+0x0] 确认对应的值
1 | PlainText复制代码(lldb) mem read 0x0000000145d36800 -fx -s8 |
发现这里上游传入的 x1/wtable 异常为 PWT NSRunLoopMode**: **RawRepresentable,因此会导致后续的 findConformingSuperclass 结果为空
同时对比加上magic function不会crash的相关结果为正常的 PWT NSAttributedStringKey: RawRepresentable
1 | PlainText复制代码(lldb) dis -a 0x0000000101879ed8 |
综上说明是上游 Frame 3 调用 Frame 2 时传入了错误的 x1 导致
[Skip] Frame 3
Frame 3 swift_getAssociatedTypeWitness
- 相关 Swift 源码
1 | C++复制代码MetadataResponse |
这里没有对 x1/wtable 进行任何改动,透传了上游的 x1/wtable 参数
[Skip] Frame 4
Frame 4 Swift._SwiftNewtypeWrapper._rawHashValue
1 | C复制代码libswiftCore.dylib`Swift._SwiftNewtypeWrapper< where τ_0_0: Swift.Hashable, τ_0_0.Swift.RawRepresentable.RawValue: Swift.Hashable>._rawHashValue(seed: Swift.Int) -> Swift.Int: |
x1 = x23 = [x3+0x8]
[Skip] Frame 5
Frame 5 protocol witness for Hashable._rawHashValue(seed:) in conformance NSRunLoopMode
理论上不应该在这里触发这个符号
1 | C++复制代码DemoKit`protocol witness for Hashable._rawHashValue(seed:) in conformance NSRunLoopMode: |
预期行为 / 加入magic()函数后不会崩的crash对应的Frame
- protocol witness for Hashable._rawHashValue(seed:) in conformance NSAttributeString:
Frame 6
Frame 6 _NativeDictionary.init(_unsafeUninitializedCapacity:allowingDuplicates:initializingWith:) ()
1 | C复制代码Foundation`Swift._NativeDictionary.init(_unsafeUninitializedCapacity: Swift.Int, allowingDuplicates: Swift.Bool, initializingWith: (Swift.UnsafeMutableBufferPointer<τ_0_0>, Swift.UnsafeMutableBufferPointer<τ_0_1>) -> Swift.Int) -> Swift._NativeDictionary<τ_0_0, τ_0_1>: |
先添加 BinaryPodA.SymbolB 的符合断点,然后给 0x18c862a10 添加断点并进入
1 | C++复制代码-> 0x18c862a14: add x16, x16, #0xa78 |
继续给 x16 对应的地址 0x000000018774aa78** **添加断点,然后进入
1 | C复制代码libswiftCore.dylib`dispatch thunk of Swift.Hashable._rawHashValue(seed: Swift.Int) -> Swift.Int: |
这里的 x4 目前为止看起来仍然正常,继续给 x4 对应的地址添加断点并进入
1 | C复制代码DemoKit`protocol witness for Swift.Hashable._rawHashValue(seed: Swift.Int) -> Swift.Int in conformance __C.NSAttributedStringKey : Swift.Hashable in __C_Synthesized: |
发现了异常的情况是 witness _rawHashValue of PWT NSAttributedStringKey: Hashable 走了 NSRunLoopMode 的对应实现
初步结论是compiler/linker在优化中将二者实现合并到一起了
1 | Assembly复制代码 _$sSo13NSRunLoopModeaSHSCSH13_rawHashValue4seedS2i_tFTW: // protocol witness for Swift.Hashable._rawHashValue(seed: Swift.Int) -> Swift.Int in conformance __C.NSRunLoopMode : Swift.Hashable in __C_Synthesized |
1 | Assembly复制代码 _$sSo13NSRunLoopModeaABs20_SwiftNewtypeWrapperSCWl: // lazy protocol witness table accessor for type __C.NSRunLoopMode and conformance __C.NSRunLoopMode : Swift._SwiftNewtypeWrapper in __C_Synthesized |
1 | Assembly复制代码 _$sSo13NSRunLoopModeaABs20_SwiftNewtypeWrapperSCWL: // lazy protocol witness table cache variable for type __C.NSRunLoopMode and conformance __C.NSRunLoopMode : Swift._SwiftNewtypeWrapper in __C_Synthesized |
1 | Assembly复制代码 _$sSo21NSAttributedStringKeyaABs20_SwiftNewtypeWrapperSCWL: // lazy protocol witness table cache variable for type __C.NSAttributedStringKey and conformance __C.NSAttributedStringKey : Swift._SwiftNewtypeWrapper in __C_Synthesized |
查看对应实现,发现里面的实现逻辑包含一个 lazy protocol witness table cache variable for type __C.NSRunLoopMode and conformance __C.NSRunLoopMode : Swift._SwiftNewtypeWrapper in __C_Synthesized
而 NSAttributedStringKey 也含有一个自己的 lazy protocol witness table cache variable,说明这里不应该进行合并
符号分析
发现出现问题的二者都是原本定义在 NSFoundation 中的 NSString * 的 typedef 并通过 swift_newtype(struct) 重新以 struct 导入到了 Swift
1 | Swift复制代码/// An implementation detail used to implement support importing |
修复方案
需要对上游的 compiler / linker 进行修复,通过咨询相关同学发现之前 Lark 等业务方也发现过类似问题
并最终在llvm上进行了修复,在内部工具链上得到了验证
长期方案为确认相关修复是否已合入上游并被 Xcode Toolchain 使用以及是否有其他 bug 需要进一步 patch
短期方案为使用类似 magic() 函数类似的方案,通过主动调用来规避同名跳板符号异常merge的bug进行绕过
本文转载自: 掘金