碧纱笼 發表於 2023-1-29 11:50:47

iOS 16 CocoaAsyncSocket 崩溃修复详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>背景</li><li>方案1:fishhook 替换掉 os_unfair_lock_lock</li><li>方案2: _schedulables 删除 _socket</li><ul class="second_class_ul"><li>#8 未解析符号: ___lldb_unnamed_symbol8050</li><li>#3 未解析符号: ___lldb_unnamed_symbol8533</li><li>逻辑分析</li></ul><li>方案3:_CFRelease</li><ul class="second_class_ul"></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>背景</h2>
<p>iOS 16 版本发布后, 我们监控到 <code>CocoaAsyncSocket</code> 有大量的新增崩溃,堆栈和这里提的 issue 一致:</p>
<div class="jb51code"><pre class="brush:cpp;">libsystem_platform.dylib                   0x210a5e08c _os_unfair_lock_recursive_abort + 36
libsystem_platform.dylib                   0x210a58898 _os_unfair_lock_lock_slow + 280
CoreFoundation                             0x1c42953ec CFSocketInvalidate + 132
CFNetwork                                  0x1c54a4e24 0x1c533f000 + 1465892
CoreFoundation                             0x1c41db030 CFArrayApplyFunction + 72
CFNetwork                                  0x1c54829a0 0x1c533f000 + 1325472
CoreFoundation                             0x1c4242d20 _CFRelease + 316
CoreFoundation                             0x1c4295724 CFSocketInvalidate + 956
CFNetwork                                  0x1c548f478 0x1c533f000 + 1377400
CoreFoundation                             0x1c420799c _CFStreamClose + 108
Test                                          0x102ca5228 - + 452
Test                                          0x102ca582c __28-_block_invoke + 80
libdispatch.dylib                          0x1cb649fdc _dispatch_client_callout + 20
libdispatch.dylib                          0x1cb6599a8 _dispatch_sync_invoke_and_complete_recurse + 64
libdispatch.dylib                          0x1cb659428 _dispatch_sync_f_slow + 172
Test                                          0x102ca57b0 - + 164
Test                                          0x102db951c - + 312
Test                                          0x102cdfa5c - + 396
Test                                          0x102d6b748 __27-_block_invoke + 2004
libdispatch.dylib                          0x1cb6484b4 _dispatch_call_block_and_release + 32
libdispatch.dylib                          0x1cb649fdc _dispatch_client_callout + 20
libdispatch.dylib                          0x1cb651694 _dispatch_lane_serial_drain + 672
libdispatch.dylib                          0x1cb6521e0 _dispatch_lane_invoke + 384
libdispatch.dylib                          0x1cb65ce10 _dispatch_workloop_worker_thread + 652
libsystem_pthread.dylib                    0x210aecdf8 _pthread_wqthread + 288
libsystem_pthread.dylib                    0x210aecb98 start_wqthread + 8
</pre></div>
<p>崩溃原因 <code>BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock</code> 原因非常简单,锁递归调用了,<code>os_unfair_lock_lock</code> 的递归调用是通过 lock 的当前 owner 等于当前线程来判断的,理论上只要打破这个递归调用就能解决这个问题。分析堆栈崩溃栈顶 <code>CoreFoundation</code> 中的 <code>CFSocketInvalidate</code> 函数调用了 <code>libsystem_platform.dylib</code> 中的 <code>os_unfair_lock</code>,两个动态库之间走 bind 的间接调用,那直接使用 fishhook hook 掉 <code>CoreFoundation</code> 中调用的 lock 方法,替换的 lock 方法里面判断 owner 是否是当前线程,是的话直接 return,那这个崩溃问题不就解了吗?于是就有了下面的第一版方案。 (注:方案 1&amp;2 最终都被 pass 了,方案 3 验证可行)</p>
<p class="maodian"></p><h2>方案1:fishhook 替换掉 os_unfair_lock_lock</h2>
<p>这个方案有两个关键的步骤 hook lock 方法,lock 方法判断 owner 是否是当前线程,第一步默认 fishhook 可行,第二步看起来更有挑战性,所以先从 lock 判断逻辑开始了调研,这里流下了悔恨的泪水。</p>
<p><code>&lt;os/lock.h&gt;</code> 里面提供了系统的 api <code>os_unfair_lock_assert_owner</code> 来判断 lock 当前的 owner</p>
<div class="jb51code"><pre class="brush:cpp;">/*!
&amp;nbsp;* **@function** os_*unfair_lock_assert_not_owner*
&amp;nbsp;*
&amp;nbsp;* **@abstract**
&amp;nbsp;* Asserts that the calling thread is not the current owner of the specified
&amp;nbsp;* unfair lock.
&amp;nbsp;*
&amp;nbsp;* **@discussion**
&amp;nbsp;* If the lock is unlocked or owned by a different thread, this function
&amp;nbsp;* returns.
&amp;nbsp;*
&amp;nbsp;* If the lock is currently owned by the current thread, this function asserts
&amp;nbsp;* and terminates the process.
&amp;nbsp;*
&amp;nbsp;* **@param** lock
&amp;nbsp;* Pointer to an os_unfair_lock.
&amp;nbsp;*/
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
**void** os_unfair_lock_assert_not_owner(**const** os_unfair_lock *lock);
</pre></div>
<p>如果 lock 被其它线程持有,这个方法直接 return,如果 lock 被当前线程持有,则直接触发 assert 并中断程序。因为 dev 会触发崩溃,这个 api 在我们这个场景下不能直接调用,好在苹果提供了这部分代码,参考下可以实现 lock owner 的判断逻辑,中间涉及到一些 tsd 的代码需要额外处理,这里不展开说明了。之后 fishhook 全局替换 <code>os_unfair_lock_lock</code> 开始测试。</p>
<div class="jb51code"><pre class="brush:cpp;">os_unfair_lock_lock(&amp;amp;test_lock);
os_unfair_lock_lock(&amp;amp;test_lock);
</pre></div>
<p>上述可以稳定复现递归锁的崩溃,添加 hook 代码后崩溃消失,到这里第一次以为问题解决了。</p>
<p>然而,测试代码在主可执行文件里面,而崩溃发生在 <code>CoreFoundation</code> 里面,<code>CoreFoundation</code> 的 lock 方法可以被 hook 吗?答案是不可以的。 后续业务部门的同学比较给力稳定复现了这个崩溃,崩溃栈顶 <code>CFSocketInvalidate</code> 对 lock 方法的调用如下 <code>0x1ba8b13e8 bl 0x1c0155a60</code>,这里并不是之前熟悉的 symbol stub 的调用,fishhook 不能生效。这种动态库之间的调用一直是我的知识盲区,不知从何下手,hook 这种方案被 pass 掉了。</p>
<div class="jb51code"><pre class="brush:cpp;">    0x1ba8b13d0 &lt;+104&gt;:tbz    w8, #0x0, 0x1ba8b13d8   ; &lt;+112&gt;
    0x1ba8b13d4 &lt;+108&gt;:bl   0x1ba920e7c               ; __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__
    0x1ba8b13d8 &lt;+112&gt;:mov    x0, x19
    0x1ba8b13dc &lt;+116&gt;:bl   0x1ba860e34               ; CFRetain
    0x1ba8b13e0 &lt;+120&gt;:adrp   x0, 354829
    0x1ba8b13e4 &lt;+124&gt;:add    x0, x0, #0x900            ; __CFAllSocketsLock
    0x1ba8b13e8 &lt;+128&gt;:bl   0x1c0155a60
-&gt;0x1ba8b13ec &lt;+132&gt;:add    x20, x19, #0x18
    0x1ba8b13f0 &lt;+136&gt;:mov    x0, x20
    0x1ba8b13f4 &lt;+140&gt;:bl   0x1ba99c984               ; symbol stub for: pthread_mutex_lock
</pre></div>
<div class="jb51code"><pre class="brush:cpp;">-&gt;0x1c0155a60: adrp   x16, 290593
    0x1c0155a64: add    x16, x16, #0x3b0          ; os_unfair_lock_lock
    0x1c0155a68: br   x16
    0x1c0155a6c: brk    #0x1
    0x1c0155a70: adrp   x16, 290593
    0x1c0155a74: add    x16, x16, #0x4e0          ; os_unfair_lock_lock_with_options
    0x1c0155a78: br   x16
    0x1c0155a7c: brk    #0x1
</pre></div>
<p>之后调试了 iOS 15 的设备,发现 iOS 15 调用的锁类型是 pthread_mutex_lock,iOS 16 替换为了 os_unfair_lock 大概是这里的更新导致了这个 crash。 既然直接从锁下手,无法修复这个问题,那么接下来就要分析下,这里为什么会出现递归调用。</p>
<p class="maodian"></p><h2>方案2: _schedulables 删除 _socket</h2>
<p>崩溃堆栈在 CFNetwork 库里的符号都没有正常解析,线下调试的时候 xcode 也无法解析,xcode 捕获到的堆栈如下:</p>
<div class="jb51code"><pre class="brush:cpp;">#0        0x000000020707a08c in _os_unfair_lock_recursive_abort ()
#1        0x0000000207074898 in _os_unfair_lock_lock_slow ()
#2        0x00000001ba8b13ec in CFSocketInvalidate ()
#3        0x00000001bbac0e24 in ___lldb_unnamed_symbol8533 ()
#4        0x00000001ba7f7030 in CFArrayApplyFunction ()
#5        0x00000001bba9e9a0 in ___lldb_unnamed_symbol7940 ()
#6        0x00000001ba85ed20 in _CFRelease ()
#7        0x00000001ba8b1724 in CFSocketInvalidate ()
#8        0x00000001bbaab478 in ___lldb_unnamed_symbol8050 ()
#9        0x00000001ba82399c in _CFStreamClose ()
#10        0x000000010844e934 in - at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:3213
#11        0x0000000108456b8c in - at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:5976
#12        0x0000000108457584 in __29-_block_invoke at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:6317
#13        0x00000001c1c644b4 in _dispatch_call_block_and_release ()
#14        0x00000001c1c65fdc in _dispatch_client_callout ()
#15        0x00000001c1c6d694 in _dispatch_lane_serial_drain ()
#16        0x00000001c1c6e1e0 in _dispatch_lane_invoke ()
#17        0x00000001c1c78e10 in _dispatch_workloop_worker_thread ()
#18        0x0000000207108df8 in _pthread_wqthread ()
</pre></div>
<p>看这个堆栈大致可以得到崩溃的原因 <code>CFSocketInvalidate</code> 执行了两次, <code>CFSocketInvalidate</code> 调用了 <code>os_unfair_lock_lock</code>, <code>os_unfair_lock_lock</code> 执行了两次导致了锁递归。分析出更加具体的原因还需要解析出对应的符号。</p>
<p class="maodian"></p><h3>#8 未解析符号: ___lldb_unnamed_symbol8050</h3>
<p><code>_CFStreamClose</code> 调用了 <code>___lldb_unnamed_symbol8050</code>,<code>___lldb_unnamed_symbol8050</code> 第一次调用了 <code>CFSocketInvalidate</code>。</p>
<p><code>CFNetwork</code> 中 <code>_CFStreamClose</code> 的源码如下:</p>
<div class="jb51code"><pre class="brush:cpp;">CF_PRIVATE void _CFStreamClose(struct _CFStream *stream) {
    CFStreamStatus status = _CFStreamGetStatus(stream);
    const struct _CFStreamCallBacks *cb = _CFStreamGetCallBackPtr(stream);
    if (status == kCFStreamStatusNotOpen || status == kCFStreamStatusClosed || (status == kCFStreamStatusError &amp;&amp; __CFBitIsSet(stream-&gt;flags, HAVE_CLOSED))) {
      // Stream is not open from the client's perspective; do not callout and do not update our status to "closed"
      return;
    }
    if (! __CFBitIsSet(stream-&gt;flags, HAVE_CLOSED)) {
      __CFBitSet(stream-&gt;flags, HAVE_CLOSED);
      __CFBitSet(stream-&gt;flags, CALLING_CLIENT);
      if (cb-&gt;close) {
            cb-&gt;close(stream, _CFStreamGetInfoPointer(stream));
      }
      if (stream-&gt;client) {
            _CFStreamDetachSource(stream);
      }
      _CFStreamSetStatusCode(stream, kCFStreamStatusClosed);
      __CFBitClear(stream-&gt;flags, CALLING_CLIENT);
    }
}
</pre></div>
<p>结合 xcode 的调试信息 <code>___lldb_unnamed_symbol8050</code> 大概率是 <code>cb-&gt;close</code> 方法。这里尝试映射了 <code>_CFStream</code> 的数据结构修改 <code>cb-&gt;close</code>:</p>
<div class="jb51code"><pre class="brush:cpp;">struct _CFStream {
    CFRuntimeBase _cfBase;
    CFOptionFlags flags;
    CFErrorRef error; // if callBacks-&amp;gt;version &amp;lt; 2, this is actually a pointer to a CFStreamError
    struct _CFStreamClient *client;
    /* NOTE: CFNetwork is still using _CFStreamGetInfoPointer, and so this slot needs to stay in this position (as the fifth field in the structure) */
    /* NOTE: This can be taken out once CFNetwork rebuilds */
    /* NOTE: &amp;lt;rdar://problem/13678879&amp;gt; Remove comment once CFNetwork has been rebuilt */
    void *info;
    const struct _CFStreamCallBacks *callBacks;// This will not exist (will not be allocated) if the callbacks are from our known, "blessed" set.
    CFLock_t streamLock;
    CFArrayRef previousRunloopsAndModes;
    dispatch_queue_t queue;
};
</pre></div>
<p>修改 callBacks 的 close 指针为 <code>_new_SocketStreamClose</code> 方法可以石锤 <code>___lldb_unnamed_symbol8050</code> 就是对 <code>cb-&gt;close</code> 的调用</p>
<div class="jb51code"><pre class="brush:cpp;">void (*_origin_SocketStreamClose)(CFTypeRef stream, void* ctxt);
void _new_SocketStreamClose(CFTypeRef stream, void* ctxt) {
_origin_SocketStreamClose(stream, ctxt);
}
</pre></div>
<p>继续翻看 CFNetwork 的代码最终可以找到 cb-&gt;close 指向函数 <code>SocketStreamClose</code> 这个函数比较长,我们只关注里面对 <code>CFSocketInvalidate</code> 的第一次调用部分:</p>
<div class="jb51code"><pre class="brush:cpp;">if (ctxt-&gt;_socket) {
    /* Make sure to invalidate the socket */
    CFSocketInvalidate(ctxt-&gt;_socket);
    /* Dump and forget it. */
    CFRelease(ctxt-&gt;_socket);
    ctxt-&gt;_socket = NULL;
}
</pre></div>
<p>ctxt 通过方法 <code>_CFStreamGetInfoPointer</code> 获取,取的值是 stream 的 info,<code>CoreFoundation</code> 中提供的 info 的数据结构</p>
<div class="jb51code"><pre class="brush:cpp;">typedef struct {
        CFSpinLock_t                                _lock;                                /* Protection for read-half versus write-half */
        UInt32                                                _flags;
        CFStreamError                                _error;
        CFReadStreamRef                                _clientReadStream;
        CFWriteStreamRef                        _clientWriteStream;
        CFSocketRef                                        _socket;                        /* Actual underlying CFSocket */
      CFMutableArrayRef                        _readloops;
      CFMutableArrayRef                        _writeloops;
      CFMutableArrayRef                        _sharedloops;
        CFMutableArrayRef                        _schedulables;                /* Items to be scheduled (i.e. socket, reachability, host, etc.) */
        CFMutableDictionaryRef                _properties;                /* Host and port and reachability should be here too. */
} _CFSocketStreamContext;
</pre></div>
<p>这个数据结构在 iOS 16 中有修改,但是调试的时候 lldb 可以通过 memory read 找到 <code>_socket</code> 的偏移以及 <code>_schedulables</code> 的偏移。<code>_schedulables</code> 也是一个比较关键的值,在分析第二次调用 <code>CFSocketInvalidate</code> 的时候会用到。</p>
<p>小结:第一次 <code>CFSocketInvalidate</code> 是在 <code>SocketStreamClose</code> 里面调用,入参是 <code>stream-&gt;info-&gt;_socket</code>。</p>
<p class="maodian"></p><h3>#3 未解析符号: ___lldb_unnamed_symbol8533</h3>
<p>第二次 <code>CFSocketInvalidate</code> 的调用在 <code>___lldb_unnamed_symbol8533</code> 里面,汇编代码如下:</p>
<div class="jb51code"><pre class="brush:cpp;">CFNetwork`___lldb_unnamed_symbol8533:
    0x1bbac0e00 &lt;+0&gt;:   pacibsp
    0x1bbac0e04 &lt;+4&gt;:   stp    x20, x19, !
    0x1bbac0e08 &lt;+8&gt;:   stp    x29, x30,
    0x1bbac0e0c &lt;+12&gt;:add    x29, sp, #0x10
    0x1bbac0e10 &lt;+16&gt;:mov    x19, x0
    0x1bbac0e14 &lt;+20&gt;:bl   0x1c015b020
    0x1bbac0e18 &lt;+24&gt;:mov    x20, x0
    0x1bbac0e1c &lt;+28&gt;:mov    x0, x19
    0x1bbac0e20 &lt;+32&gt;:bl   0x1bba0f498               ; ___lldb_unnamed_symbol5324
-&gt;0x1bbac0e24 &lt;+36&gt;:adrp   x8, 348073
    0x1bbac0e28 &lt;+40&gt;:ldr    x8,
    0x1bbac0e2c &lt;+44&gt;:cmn    x8, #0x1
    0x1bbac0e30 &lt;+48&gt;:b.ne   0x1bbac0ea4               ; &lt;+164&gt;
    0x1bbac0e34 &lt;+52&gt;:adrp   x8, 348073
    0x1bbac0e38 &lt;+56&gt;:ldr    x8,
    0x1bbac0e3c &lt;+60&gt;:ldr    x8,
    0x1bbac0e40 &lt;+64&gt;:cmp    x8, x20
    0x1bbac0e44 &lt;+68&gt;:b.ne   0x1bbac0e6c               ; &lt;+108&gt;
    0x1bbac0e48 &lt;+72&gt;:mov    x0, x19
    0x1bbac0e4c &lt;+76&gt;:mov    w1, #0x0
    0x1bbac0e50 &lt;+80&gt;:ldp    x29, x30,
    0x1bbac0e54 &lt;+84&gt;:ldp    x20, x19, , #0x20
    0x1bbac0e58 &lt;+88&gt;:autibsp
    0x1bbac0e5c &lt;+92&gt;:eor    x16, x30, x30, lsl #1
    0x1bbac0e60 &lt;+96&gt;:tbz    x16, #0x3e, 0x1bbac0e68   ; &lt;+104&gt;
    0x1bbac0e64 &lt;+100&gt;: brk    #0xc471
    0x1bbac0e68 &lt;+104&gt;: b      0x1bba16948               ; CFHostCancelInfoResolution
    0x1bbac0e6c &lt;+108&gt;: bl   0x1bba108f0               ; CFNetServiceGetTypeID
    0x1bbac0e70 &lt;+112&gt;: cmp    x0, x20
    0x1bbac0e74 &lt;+116&gt;: b.ne   0x1bbac0e98               ; &lt;+152&gt;
    0x1bbac0e78 &lt;+120&gt;: mov    x0, x19
    0x1bbac0e7c &lt;+124&gt;: ldp    x29, x30,
    0x1bbac0e80 &lt;+128&gt;: ldp    x20, x19, , #0x20
    0x1bbac0e84 &lt;+132&gt;: autibsp
    0x1bbac0e88 &lt;+136&gt;: eor    x16, x30, x30, lsl #1
    0x1bbac0e8c &lt;+140&gt;: tbz    x16, #0x3e, 0x1bbac0e94   ; &lt;+148&gt;
    0x1bbac0e90 &lt;+144&gt;: brk    #0xc471
    0x1bbac0e94 &lt;+148&gt;: b      0x1bba12ef8               ; CFNetServiceCancel
    0x1bbac0e98 &lt;+152&gt;: ldp    x29, x30,
    0x1bbac0e9c &lt;+156&gt;: ldp    x20, x19, , #0x20
    0x1bbac0ea0 &lt;+160&gt;: retab
    0x1bbac0ea4 &lt;+164&gt;: adrp   x0, 348073
    0x1bbac0ea8 &lt;+168&gt;: add    x0, x0, #0x4a0
    0x1bbac0eac &lt;+172&gt;: adrp   x1, 356609
    0x1bbac0eb0 &lt;+176&gt;: add    x1, x1, #0xaa8
    0x1bbac0eb4 &lt;+180&gt;: bl   0x1bbbd3b80               ; symbol stub for: dispatch_once
    0x1bbac0eb8 &lt;+184&gt;: b      0x1bbac0e34               ; &lt;+52&gt;
</pre></div>
<p>结合一些关键特征: 函数开始会调用 <code>CFSocketInvalidate</code>,之后会调用 <code>CFHostCancelInfoResolution</code>、<code>CFNetServiceGetTypeID</code> 等,在 <code>CFNetwork</code> 里面找到了一个匹配度非常高的方法 <code>_SchedulablesInvalidateApplierFunction</code>。</p>
<div class="jb51code"><pre class="brush:cpp;">/* static */ void
_SchedulablesInvalidateApplierFunction(CFTypeRef obj, void* context) {
        (void)context;/* unused */
        CFTypeID type = CFGetTypeID(obj);
        /* Invalidate the process. */
        _CFTypeInvalidate(obj);
        /* For CFHost and CFNetService, make sure to cancel too. */
        if (CFHostGetTypeID() == type)
                CFHostCancelInfoResolution((CFHostRef)obj, kCFHostAddresses);
        else if (CFNetServiceGetTypeID() == type)
                CFNetServiceCancel((CFNetServiceRef)obj);
}
</pre></div>
<p><code>_CFTypeInvalidate</code> 方法里面会判断 CF 类型如果是 <code>CFSocketGetTypeID</code> 会执行 <code>CFSocketInvalidate</code> 方法。 <code>_SchedulablesInvalidateApplierFunction</code> 在 <code>CFNetwork</code> 里面搜索有两处调用,调用方式和入参相同,传入的参数都是 <code>ctxt-&gt;_schedulables</code> 这个数组包含的 item,ctxt 是 stream 的 info 字段。</p>
<div class="jb51code"><pre class="brush:cpp;">CFArrayApplyFunction(ctxt-&gt;_schedulables, r, (CFArrayApplierFunction)_SchedulablesInvalidateApplierFunction, NULL);
</pre></div>
<p>小结:第二次 <code>CFSocketInvalidate</code> 是在 <code>_SchedulablesInvalidateApplierFunction</code> 里面执行,入参是 <code>stream-&gt;info-&gt;_schedulables</code> 包含的 item。</p>
<p class="maodian"></p><h3>逻辑分析</h3>
<p>造成递归的两次调用</p>
<blockquote><p>CFSocketInvalidate(stream-&gt;info-&gt;_socket)</p>
<p>CFSocketInvalidate(stream-&gt;info-&gt;_schedulables item)</p></blockquote>
<p><code>info-&gt;_socket</code> 是个 <code>CFSocketRef</code> 对象,崩溃发生时在操作 <code>_schedulables</code> 数组里面的 <code>CFSocketRef</code> 对象,说明 <code>_schedulables</code> 里面也包含 <code>CFSocketRef</code> 对象,两者都是 info 持有的属性值,那 <code>_schedulables</code> 包含的 <code>CFSocketRef</code> 对象和 <code>_socket</code> 对象有什么关联呢?如果相等重复执行 <code>CFSocketInvalidate</code> 就没有意义了,从 <code>_schedulables</code> 直接删除掉 <code>_socket</code> 对象,递归被打破,那这个问题也可以解决了。</p>
<p>尝试映射 <code>stream-&gt;info</code> 的数据结构,需要注意的是 <code>_CFSocketStreamContext</code> 中 <code>_schedulables</code> 这个值在 iOS 16 中是个二级指针,和 <code>CFNetwork</code> 中提供的数据结构不一致,在内存中查找起来比较麻烦。最终会发现 <code>info-&gt;_schedulables</code> 中包含的 <code>CFSocketRef</code> 对象就是 <code>info-&gt;_socket</code>。</p>
<p>尝试我们的修复方案映射 info 拿到 <code>_schedulables</code>,崩溃发生时 <code>_schedulables</code> 只包含 <code>_socket</code> 一个元素,所以直接简单粗暴的调用了 RemoveAll 方法,到这里我第二次以为这个问题解决了:</p>
<div class="jb51code"><pre class="brush:cpp;">CFArrayRemoveAllValues(stream-&gt;info-&gt;_schedulables)
</pre></div>
<p>然后噩梦开始了,很多对 <code>_schedulables</code> 的调用并没有判空操作,结果就是直接崩,比如下面这个代码</p>
<div class="jb51code"><pre class="brush:cpp;">CFArrayApplyFunction(ctxt-&gt;_schedulables,
                     CFRangeMake(0, CFArrayGetCount(ctxt-&gt;_schedulables)),
                     (CFArrayApplierFunction)_SchedulablesScheduleApplierFunction,
                     loopAndMode);
</pre></div>
<p>用非常脏的方式绕过了这些没有判空的崩溃,结果还是复现了最初锁递归的崩溃。栈顶操作的包含 _socket 数组根据代码分析是 <code>_schedulables</code>,但实际上最终崩溃时栈顶操作的数组地址并不是 <code>stream-&gt;info-&gt;_schedulables</code>。从 <code>_schedulables</code> 删除 <code>_socket</code> 的方案行不通了,其实此时还可以继续分析栈顶的数组是从哪儿生成的,但属实是更加困难,另外加上对数组操作没有判空的逻辑会触发新的崩溃,清空栈顶数组这种方案也存在风险,这条路虽然不甘心但还是暂时搁置了,毕竟尽快解决问题才是关键。</p>
<p class="maodian"></p><h2>方案3:_CFRelease</h2>
<p>虽然方案 2 没有能解决问题,但通过方案 2 我们得到了一个大概的调用栈:</p>
<div class="jb51code"><pre class="brush:cpp;">#0        0x000000020707a08c in _os_unfair_lock_recursive_abort ()
#1        0x0000000207074898 in _os_unfair_lock_lock_slow ()
#2        0x00000001ba8b13ec in CFSocketInvalidate ()
#3        0x00000001bbac0e24 in _SchedulablesInvalidateApplierFunction ()
#4        0x00000001ba7f7030 in CFArrayApplyFunction ()
#5        0x00000001bba9e9a0 in ___lldb_unnamed_symbol7940 ()
#6        0x00000001ba85ed20 in _CFRelease ()
#7        0x00000001ba8b1724 in CFSocketInvalidate ()
#8        0x00000001bbaab478 in _SocketStreamClose ()
#9        0x00000001ba82399c in _CFStreamClose ()
#10        0x000000010844e934 in - at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:3213
#11        0x0000000108456b8c in - at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:5976
#12        0x0000000108457584 in __29-_block_invoke at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:6317
#13        0x00000001c1c644b4 in _dispatch_call_block_and_release ()
#14        0x00000001c1c65fdc in _dispatch_client_callout ()
#15        0x00000001c1c6d694 in _dispatch_lane_serial_drain ()
#16        0x00000001c1c6e1e0 in _dispatch_lane_invoke ()
#17        0x00000001c1c78e10 in _dispatch_workloop_worker_thread ()
#18        0x0000000207108df8 in _pthread_wqthread ()
</pre></div>
<p>继续研究这个堆栈,有个非常奇怪的地方 <code>CoreFoundation: _CFRelease</code> 调用了 <code>CFNetwork: ___lldb_unnamed_symbol7940</code>, <code>CoreFoundation</code> 应该是更底层的库才合理,<code>CoreFoundation </code>不应该调用到 <code>CFNetwork</code>。 查看 <code>CFSocketInvalidate</code> 里面对 <code>_CFRelease</code> 的调用,代码比较长截取部分关键信息:</p>
<div class="jb51code"><pre class="brush:cpp;">void CFSocketInvalidate(CFSocketRef s) {
    CFRetain(s);
    __CFLock(&amp;__CFAllSocketsLock);
    __CFSocketLock(s);
    if (__CFSocketIsValid(s)) {      
      contextInfo = s-&gt;_context.info;
      contextRelease = s-&gt;_context.release;
      // Do this after the socket unlock to avoid deadlock (10462525)
      for (idx = CFArrayGetCount(runLoops); idx--;) {
            CFRunLoopWakeUp((CFRunLoopRef)CFArrayGetValueAtIndex(runLoops, idx));
      }
      CFRelease(runLoops);
      if (NULL != contextRelease) {
            contextRelease(contextInfo);
      }
      if (NULL != source0) {
            CFRunLoopSourceInvalidate(source0);
            CFRelease(source0);
      }
    } else {
      __CFSocketUnlock(s);
    }
    __CFUnlock(&amp;__CFAllSocketsLock);
    CFRelease(s);
}
</pre></div>
<p>结合 Xcode 的调试信息:</p>
<div class="jb51code"><pre class="brush:cpp;">    0x1ba8b16fc &lt;+916&gt;:bl   0x1ba862870               ; CFArrayGetValueAtIndex
    0x1ba8b1700 &lt;+920&gt;:bl   0x1ba8945a0               ; CFRunLoopWakeUp
    0x1ba8b1704 &lt;+924&gt;:sub    x24, x24, #0x1
    0x1ba8b1708 &lt;+928&gt;:subs   w20, w20, #0x1
    0x1ba8b170c &lt;+932&gt;:b.ne   0x1ba8b16f4               ; &lt;+908&gt;
    0x1ba8b1710 &lt;+936&gt;:mov    x0, x22
    0x1ba8b1714 &lt;+940&gt;:bl   0x1ba860cec               ; CFRelease
    0x1ba8b1718 &lt;+944&gt;:cbz    x25, 0x1ba8b1724          ; &lt;+956&gt;
    0x1ba8b171c &lt;+948&gt;:mov    x0, x23
    0x1ba8b1720 &lt;+952&gt;:blraaz x25
-&gt;0x1ba8b1724 &lt;+956&gt;:cbz    x21, 0x1ba8b1738          ; &lt;+976&gt;
    0x1ba8b1728 &lt;+960&gt;:mov    x0, x21
    0x1ba8b172c &lt;+964&gt;:bl   0x1ba8b1a54               ; CFRunLoopSourceInvalidate
    0x1ba8b1730 &lt;+968&gt;:mov    x0, x21
    0x1ba8b1734 &lt;+972&gt;:bl   0x1ba860cec               ; CFRelease
    0x1ba8b1738 &lt;+976&gt;:adrp   x0, 354829
    0x1ba8b173c &lt;+980&gt;:add    x0, x0, #0x900            ; __CFAllSocketsLock
</pre></div>
<p>执行完 <code>CFRelease</code> 之后会执行 <code>CFRunLoopSourceInvalidate</code>, 那这里的 <code>CFRelease</code> 只有 <code>CFRelease(source0)</code>; source0 是个数组,当时天真的认为 <code>___lldb_unnamed_symbol7940</code> 是通过 <code>CFArrayReleaseCallBack</code> 添加的回调方法, 这个调用逻辑看起来合情合理。<code>CFRelease</code> 虽然不能被 hook,那是不是可以通过修改 CallBack 来打破递归调用呢?按照这种方式去尝试了仍然不可行。断点 <code>CFRelease</code> 发现此时 release 的对象类型是 <code>SocketStream</code> 并不是之前的 source0 数组。<code>CFSocketInvalidate</code> 这个函数里面查找类型是 <code>SocketStream</code> 的对象,最终找到了 <code>s-&gt;_context.info</code>,顺藤摸瓜找到了我们解决这个问题最关键的三行代码:</p>
<div class="jb51code"><pre class="brush:cpp;">if (NULL != contextRelease) {
    contextRelease(contextInfo);
}
</pre></div>
<p>按照 xcode 的调试信息 <code>contextRelease</code> == <code>CFRelease</code> 而 <code>contextRelease</code> 在代码中取值 <code>s-&gt;_context.release</code>。只要拿到了 <code>s-&gt;_context</code> 的数据结构,修改 <code>release</code> 这个指针,就可以实现对崩溃栈里面 <code>CFRelease</code> 的 hook,造成锁递归的两次 <code>CFSocketInvalidate</code> 调用分别在 <code>CFRelease</code> 之前和之后,如果把 <code>CFRelease</code> 修改为异步调用,<code>CFSocketInvalidate</code> 两次调用的 <code>os_unfair_lock_lock</code> 在两个不同的线程,锁递归判断的条件是 lock 当前的 owner 是当前线程,lock 方法在不同的线程执行,那这个问题也就迎刃而解了。映射 stream 和 socket 的过程不详细介绍了,这个过程太无聊了,直接贴个结果吧:</p>
<div class="jb51code"><pre class="brush:cpp;">struct __CFSocket {
    int64_t offset;
    CFSocketContext _context;    /* immutable */
};
typedef struct {
    int64_t offset;
    struct __CFSocket *          _socket;
} __CFSocketStreamContext;
struct __CFStream {
    int64_t offset;
    __CFSocketStreamContext *info;
};
</pre></div>
<p>最终的解决方案概括如下述代码, 因为这里映射了很多系统的数据结构,这并不是一个安全的操作,需要添加一些内存可读写的判断,内存包换这部分代码参考 kscrash,另外业务层也需要 <strong>加好开关加好开关加好开关对特定系统生效</strong>,如果新系统 stream 或者是 socket 的数据结构发生变化可能会造成一些内存访问的崩溃。</p>
<div class="jb51code"><pre class="brush:cpp;">// 内存保护
static inline int copySafely(const void* restrict const src, void* restrict const dst, const int byteCount)
{
    vm_size_t bytesCopied = 0;
    kern_return_t result = vm_read_overwrite(mach_task_self(),
                                             (vm_address_t)src,
                                             (vm_size_t)byteCount,
                                             (vm_address_t)dst,
                                             &amp;bytesCopied);
    if(result != KERN_SUCCESS)
    {
      return 0;
    }
    return (int)bytesCopied;
}
static char g_memoryTestBuffer;
static inline bool isMemoryReadable(const void* const memory, const int byteCount)
{
    const int testBufferSize = sizeof(g_memoryTestBuffer);
    int bytesRemaining = byteCount;
    while(bytesRemaining &gt; 0)
    {
      int bytesToCopy = bytesRemaining &gt; testBufferSize ? testBufferSize : bytesRemaining;
      if(copySafely(memory, g_memoryTestBuffer, bytesToCopy) != bytesToCopy)
      {
            break;
      }
      bytesRemaining -= bytesToCopy;
    }
    return bytesRemaining == 0;
}
// 异步 CFRelease
static dispatch_queue_t socket_context_release_queue = nil;
void (*origin_context_release)(const void *info);
void new_context_release(const void *info) {
    if (socket_context_release_queue == nil) {
      socket_context_release_queue = dispatch_queue_create("socketContextReleaseQueue", 0x0);
    }
    dispatch_async(socket_context_release_queue, ^{
      origin_context_release(info);
    });
}
// CocoaAsyncSocket 修改 writeStream
if (@available(iOS 16.0, *)) {
    struct __CFStream *cfstream= (struct __CFStream *)writeStream;
    if (isMemoryReadable(cfstream, sizeof(*cfstream))
       &amp;&amp; isMemoryReadable(cfstream-&gt;info, sizeof(*(cfstream-&gt;info)))
       &amp;&amp; isMemoryReadable(cfstream-&gt;info-&gt;_socket, sizeof(*(cfstream-&gt;info-&gt;_socket)))
       &amp;&amp; isMemoryReadable(&amp;(cfstream-&gt;info-&gt;_socket-&gt;_context), sizeof(cfstream-&gt;info-&gt;_socket-&gt;_context))
       &amp;&amp; isMemoryReadable(cfstream-&gt;info-&gt;_socket-&gt;_context.release, sizeof(*(cfstream-&gt;info-&gt;_socket-&gt;_context.release)))) {
      if (cfstream-&gt;info != NULL &amp;&amp; cfstream-&gt;info-&gt;_socket != NULL) {
            if ((uintptr_t)cfstream-&gt;info-&gt;_socket-&gt;_context.release == (uintptr_t)CFRelease) {
                origin_context_release = cfstream-&gt;info-&gt;_socket-&gt;_context.release;
                cfstream-&gt;info-&gt;_socket-&gt;_context.release = new_context_release;
            }
      }
}
</pre></div>
<p class="maodian"></p><h2>总结</h2>
<p>这个问题并不是只出现在 <code>CocoaAsyncSocket</code> 这个库里面,后续在一些系统的线程里面也发现了这个崩溃堆栈,但是量级不大,评估了下没有解决的必要。</p>
<p>另外虽然方案1和方案2最终都被 pass 掉了,但是这也是我最常用的排障方法,所以写在这里跟大家分享下。整个排查过程中也存在很多最终都没有搞清楚的点,但是这些细节问题都没有影响到最终的结论,所以最终选择了佛系看待。</p>
<p>以上就是iOS 16 CocoaAsyncSocket 崩溃修复详解的详细内容,更多关于iOS CocoaAsyncSocket崩溃修复的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>react native reanimated实现动画示例详解</li><li>react-native&nbsp;封装视频播放器react-native-video的使用</li><li>React&nbsp;Native全面屏状态栏和底部导航栏适配教程详细讲解</li><li>java Nio使用NioSocket客户端与服务端交互实现方式</li><li>ios下OC与JS交互之WKWebView</li><li>iOS底层实例解析Swift闭包及OC闭包</li><li>React Native与iOS OC之间的交互示例详解</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: iOS 16 CocoaAsyncSocket 崩溃修复详解