iis服务器助手广告广告
返回顶部
首页 > 资讯 > 移动开发 >iOS16CocoaAsyncSocket崩溃修复详解
  • 507
分享到

iOS16CocoaAsyncSocket崩溃修复详解

iOSCocoaAsyncSocket崩溃修复iOSCocoaAsyncSocket 2023-01-29 12:01:39 507人浏览 独家记忆
摘要

目录背景方案1:fishhook 替换掉 os_unfair_lock_lock方案2: _schedulables 删除 _Socket#8 未解析符号: ___lldb_unna

背景

iOS 16 版本发布后, 我们监控CocoaAsyncSocket 有大量的新增崩溃,堆栈和这里提的 issue 一致:

  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 -[GCDAsyncSocket closeWithError:] + 452
  Test                   	               0x102ca582c __28-[GCDAsyncSocket disconnect]_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 -[GCDAsyncSocket disconnect] + 164
  Test                   	               0x102db951c -[TestSocket forceDisconnect] + 312
  Test                   	               0x102cdfa5c -[TestSocket forceDisconnect] + 396
  Test                   	               0x102d6b748 __27-[TestSocketManager didConnectWith:]_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

崩溃原因 BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock 原因非常简单,递归调用了,os_unfair_lock_lock 的递归调用是通过 lock 的当前 owner 等于当前线程来判断的,理论上只要打破这个递归调用就能解决这个问题。分析堆栈崩溃栈顶 CoreFoundation 中的 CFSocketInvalidate 函数调用了 libsystem_platform.dylib 中的 os_unfair_lock,两个动态库之间走 bind 的间接调用,那直接使用 fishhook hook 掉 CoreFoundation 中调用的 lock 方法,替换的 lock 方法里面判断 owner 是否是当前线程,是的话直接 return,那这个崩溃问题不就解了吗?于是就有了下面的第一版方案。 (注:方案 1&2 最终都被 pass 了,方案 3 验证可行)

方案1:fishhook 替换掉 os_unfair_lock_lock

这个方案有两个关键的步骤 hook lock 方法,lock 方法判断 owner 是否是当前线程,第一步默认 fishhook 可行,第二步看起来更有挑战性,所以先从 lock 判断逻辑开始了调研,这里流下了悔恨的泪水。

<os/lock.h> 里面提供了系统的 api os_unfair_lock_assert_owner 来判断 lock 当前的 owner


OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
**void** os_unfair_lock_assert_not_owner(**const** os_unfair_lock *lock);

如果 lock 被其它线程持有,这个方法直接 return,如果 lock 被当前线程持有,则直接触发 assert 并中断程序。因为 dev 会触发崩溃,这个 api 在我们这个场景下不能直接调用,好在苹果提供了这部分代码,参考下可以实现 lock owner 的判断逻辑,中间涉及到一些 tsd 的代码需要额外处理,这里不展开说明了。之后 fishhook 全局替换 os_unfair_lock_lock 开始测试

os_unfair_lock_lock(&amp;test_lock);
os_unfair_lock_lock(&amp;test_lock);

上述可以稳定复现递归锁的崩溃,添加 hook 代码后崩溃消失,到这里第一次以为问题解决了。

然而,测试代码在主可执行文件里面,而崩溃发生在 CoreFoundation 里面,CoreFoundation 的 lock 方法可以被 hook 吗?答案是不可以的。 后续业务部门的同学比较给力稳定复现了这个崩溃,崩溃栈顶 CFSocketInvalidate 对 lock 方法的调用如下 0x1ba8b13e8 bl 0x1c0155a60,这里并不是之前熟悉的 symbol stub 的调用,fishhook 不能生效。这种动态库之间的调用一直是我的知识盲区,不知从何下手,hook 这种方案被 pass 掉了。

    0x1ba8b13D0 <+104>:  tbz    w8, #0x0, 0x1ba8b13d8     ; <+112>
    0x1ba8b13d4 <+108>:  bl     0x1ba920e7c               ; __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__
    0x1ba8b13d8 <+112>:  mov    x0, x19
    0x1ba8b13dc <+116>:  bl     0x1ba860e34               ; CFRetain
    0x1ba8b13e0 <+120>:  adrp   x0, 354829
    0x1ba8b13e4 <+124>:  add    x0, x0, #0x900            ; __CFAllSocketsLock
    0x1ba8b13e8 <+128>:  bl     0x1c0155a60
->  0x1ba8b13ec <+132>:  add    x20, x19, #0x18
    0x1ba8b13f0 <+136>:  mov    x0, x20
    0x1ba8b13f4 <+140>:  bl     0x1ba99c984               ; symbol stub for: pthread_mutex_lock
->  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

之后调试了 iOS 15 的设备,发现 iOS 15 调用的锁类型是 pthread_mutex_lock,iOS 16 替换为了 os_unfair_lock 大概是这里的更新导致了这个 crash。 既然直接从锁下手,无法修复这个问题,那么接下来就要分析下,这里为什么会出现递归调用。

方案2: _schedulables 删除 _socket

崩溃堆栈在 CFNetwork 库里的符号都没有正常解析,线下调试的时候 xcode 也无法解析,xcode 捕获到的堆栈如下:

#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 -[GCDAsyncSocket closeWithError:] at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:3213
#11	0x0000000108456b8c in -[GCDAsyncSocket maybeDequeueWrite] at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:5976
#12	0x0000000108457584 in __29-[GCDAsyncSocket doWriteData]_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 ()

看这个堆栈大致可以得到崩溃的原因 CFSocketInvalidate 执行了两次, CFSocketInvalidate 调用了 os_unfair_lock_lockos_unfair_lock_lock 执行了两次导致了锁递归。分析出更加具体的原因还需要解析出对应的符号。

#8 未解析符号: ___lldb_unnamed_symbol8050

_CFStreamClose 调用了 ___lldb_unnamed_symbol8050___lldb_unnamed_symbol8050 第一次调用了 CFSocketInvalidate

CFNetwork_CFStreamClose源码如下:

CF_PRIVATE void _CFStreamClose(struct _CFStream *stream) {
    CFStreamStatus status = _CFStreamGetStatus(stream);
    const struct _CFStreamCallBacks *cb = _CFStreamGetCallBackPtr(stream);
    if (status == kCFStreamStatusNotOpen || status == kCFStreamStatusClosed || (status == kCFStreamStatusError && __CFBitIsSet(stream->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->flags, HAVE_CLOSED)) {
        __CFBitSet(stream->flags, HAVE_CLOSED);
        __CFBitSet(stream->flags, CALLING_CLIENT);
        if (cb->close) {
            cb->close(stream, _CFStreamGetInfoPointer(stream));
        }
        if (stream->client) {
            _CFStreamDetachSource(stream);
        }
        _CFStreamSetStatusCode(stream, kCFStreamStatusClosed);
        __CFBitClear(stream->flags, CALLING_CLIENT);
    }
}

结合 xcode 的调试信息 ___lldb_unnamed_symbol8050 大概率是 cb->close 方法。这里尝试映射了 _CFStream数据结构修改 cb->close

struct _CFStream {
    CFRuntimeBase _cfBase;
    CFOptionFlags flags;
    CFErrorRef error; // if callBacks-&gt;version &lt; 2, this is actually a pointer to a CFStreamError
    struct _CFStreamClient *client;
    
    
    
    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;
};

修改 callBacks 的 close 指针为 _new_SocketStreamClose 方法可以石锤 ___lldb_unnamed_symbol8050 就是对 cb->close 的调用

void (*_origin_SocketStreamClose)(CFTypeRef stream, void* ctxt);
void _new_SocketStreamClose(CFTypeRef stream, void* ctxt) {
  _origin_SocketStreamClose(stream, ctxt);
}

继续翻看 CFNetwork 的代码最终可以找到 cb->close 指向函数 SocketStreamClose 这个函数比较长,我们只关注里面对 CFSocketInvalidate 的第一次调用部分:

if (ctxt->_socket) {
    
    CFSocketInvalidate(ctxt->_socket);
    
    CFRelease(ctxt->_socket);
    ctxt->_socket = NULL;
}

ctxt 通过方法 _CFStreamGetInfoPointer 获取,取的值是 stream 的 info,CoreFoundation 中提供的 info 的数据结构

typedef struct {
	CFSpinLock_t				_lock;				
	UInt32						_flags;
	CFStreamError				_error;
	CFReadStreamRef				_clientReadStream;
	CFWriteStreamRef			_clientWriteStream;
	CFSocketRef					_socket;			
        CFMutableArrayRef			_readloops;
        CFMutableArrayRef			_writeloops;
        CFMutableArrayRef			_sharedloops;
	CFMutableArrayRef			_schedulables;		
	CFMutableDictionaryRef		_properties;		
} _CFSocketStreamContext;

这个数据结构在 iOS 16 中有修改,但是调试的时候 lldb 可以通过 memory read 找到 _socket 的偏移以及 _schedulables 的偏移。_schedulables 也是一个比较关键的值,在分析第二次调用 CFSocketInvalidate 的时候会用到。

小结:第一次 CFSocketInvalidate 是在 SocketStreamClose 里面调用,入参是 stream->info->_socket

#3 未解析符号: ___lldb_unnamed_symbol8533

第二次 CFSocketInvalidate 的调用在 ___lldb_unnamed_symbol8533 里面,汇编代码如下:

CFNetwork`___lldb_unnamed_symbol8533:
    0x1bbac0e00 <+0>:   pacibsp 
    0x1bbac0e04 <+4>:   stp    x20, x19, [sp, #-0x20]!
    0x1bbac0e08 <+8>:   stp    x29, x30, [sp, #0x10]
    0x1bbac0e0c <+12>:  add    x29, sp, #0x10
    0x1bbac0e10 <+16>:  mov    x19, x0
    0x1bbac0e14 <+20>:  bl     0x1c015b020
    0x1bbac0e18 <+24>:  mov    x20, x0
    0x1bbac0e1c <+28>:  mov    x0, x19
    0x1bbac0e20 <+32>:  bl     0x1bba0f498               ; ___lldb_unnamed_symbol5324
->  0x1bbac0e24 <+36>:  adrp   x8, 348073
    0x1bbac0e28 <+40>:  ldr    x8, [x8, #0x4a0]
    0x1bbac0e2c <+44>:  cmn    x8, #0x1
    0x1bbac0e30 <+48>:  b.ne   0x1bbac0ea4               ; <+164>
    0x1bbac0e34 <+52>:  adrp   x8, 348073
    0x1bbac0e38 <+56>:  ldr    x8, [x8, #0x4c0]
    0x1bbac0e3c <+60>:  ldr    x8, [x8, #0x60]
    0x1bbac0e40 <+64>:  cmp    x8, x20
    0x1bbac0e44 <+68>:  b.ne   0x1bbac0e6c               ; <+108>
    0x1bbac0e48 <+72>:  mov    x0, x19
    0x1bbac0e4c <+76>:  mov    w1, #0x0
    0x1bbac0e50 <+80>:  ldp    x29, x30, [sp, #0x10]
    0x1bbac0e54 <+84>:  ldp    x20, x19, [sp], #0x20
    0x1bbac0e58 <+88>:  autibsp 
    0x1bbac0e5c <+92>:  eor    x16, x30, x30, lsl #1
    0x1bbac0e60 <+96>:  tbz    x16, #0x3e, 0x1bbac0e68   ; <+104>
    0x1bbac0e64 <+100>: brk    #0xc471
    0x1bbac0e68 <+104>: b      0x1bba16948               ; CFHostCancelInfoResolution
    0x1bbac0e6c <+108>: bl     0x1bba108f0               ; CFNetServiceGetTypeID
    0x1bbac0e70 <+112>: cmp    x0, x20
    0x1bbac0e74 <+116>: b.ne   0x1bbac0e98               ; <+152>
    0x1bbac0e78 <+120>: mov    x0, x19
    0x1bbac0e7c <+124>: ldp    x29, x30, [sp, #0x10]
    0x1bbac0e80 <+128>: ldp    x20, x19, [sp], #0x20
    0x1bbac0e84 <+132>: autibsp 
    0x1bbac0e88 <+136>: eor    x16, x30, x30, lsl #1
    0x1bbac0e8c <+140>: tbz    x16, #0x3e, 0x1bbac0e94   ; <+148>
    0x1bbac0e90 <+144>: brk    #0xc471
    0x1bbac0e94 <+148>: b      0x1bba12ef8               ; CFNetServiceCancel
    0x1bbac0e98 <+152>: ldp    x29, x30, [sp, #0x10]
    0x1bbac0e9c <+156>: ldp    x20, x19, [sp], #0x20
    0x1bbac0ea0 <+160>: retab  
    0x1bbac0ea4 <+164>: adrp   x0, 348073
    0x1bbac0ea8 <+168>: add    x0, x0, #0x4a0
    0x1bbac0eac <+172>: adrp   x1, 356609
    0x1bbac0eb0 <+176>: add    x1, x1, #0xaa8
    0x1bbac0eb4 <+180>: bl     0x1bbbd3b80               ; symbol stub for: dispatch_once
    0x1bbac0eb8 <+184>: b      0x1bbac0e34               ; <+52>

结合一些关键特征: 函数开始会调用 CFSocketInvalidate,之后会调用 CFHostCancelInfoResolutionCFNetServiceGetTypeID 等,在 CFNetwork 里面找到了一个匹配度非常高的方法 _SchedulablesInvalidateApplierFunction

 void
_SchedulablesInvalidateApplierFunction(CFTypeRef obj, void* context) {
	(void)context;  
	CFTypeID type = CFGetTypeID(obj);
	
	_CFTypeInvalidate(obj);
	
	if (CFHostGetTypeID() == type)
		CFHostCancelInfoResolution((CFHostRef)obj, kCFHostAddresses);
	else if (CFNetServiceGetTypeID() == type)
		CFNetServiceCancel((CFNetServiceRef)obj);
}

_CFTypeInvalidate 方法里面会判断 CF 类型如果是 CFSocketGetTypeID 会执行 CFSocketInvalidate 方法。 _SchedulablesInvalidateApplierFunctionCFNetwork 里面搜索有两处调用,调用方式和入参相同,传入的参数都是 ctxt->_schedulables 这个数组包含的 item,ctxt 是 stream 的 info 字段。

CFArrayApplyFunction(ctxt->_schedulables, r, (CFArrayApplierFunction)_SchedulablesInvalidateApplierFunction, NULL);

小结:第二次 CFSocketInvalidate 是在 _SchedulablesInvalidateApplierFunction 里面执行,入参是 stream->info->_schedulables 包含的 item。

逻辑分析

造成递归的两次调用

CFSocketInvalidate(stream->info->_socket)

CFSocketInvalidate(stream->info->_schedulables item)

info->_socket 是个 CFSocketRef 对象,崩溃发生时在操作 _schedulables 数组里面的 CFSocketRef 对象,说明 _schedulables 里面也包含 CFSocketRef 对象,两者都是 info 持有的属性值,那 _schedulables 包含的 CFSocketRef 对象和 _socket 对象有什么关联呢?如果相等重复执行 CFSocketInvalidate 就没有意义了,从 _schedulables 直接删除掉 _socket 对象,递归被打破,那这个问题也可以解决了。

尝试映射 stream->info 的数据结构,需要注意的是 _CFSocketStreamContext_schedulables 这个值在 iOS 16 中是个二级指针,和 CFNetwork 中提供的数据结构不一致,在内存中查找起来比较麻烦。最终会发现 info->_schedulables 中包含的 CFSocketRef 对象就是 info->_socket

尝试我们的修复方案映射 info 拿到 _schedulables,崩溃发生时 _schedulables 只包含 _socket 一个元素,所以直接简单粗暴的调用了 RemoveAll 方法,到这里我第二次以为这个问题解决了:

CFArrayRemoveAllValues(stream->info->_schedulables)

然后噩梦开始了,很多对 _schedulables 的调用并没有判空操作,结果就是直接崩,比如下面这个代码

CFArrayApplyFunction(ctxt->_schedulables,
                     CFRangeMake(0, CFArrayGetCount(ctxt->_schedulables)),
                     (CFArrayApplierFunction)_SchedulablesScheduleApplierFunction,
                     loopAndMode);

用非常脏的方式绕过了这些没有判空的崩溃,结果还是复现了最初锁递归的崩溃。栈顶操作的包含 _socket 数组根据代码分析是 _schedulables,但实际上最终崩溃时栈顶操作的数组地址并不是 stream->info->_schedulables。从 _schedulables 删除 _socket 的方案行不通了,其实此时还可以继续分析栈顶的数组是从哪儿生成的,但属实是更加困难,另外加上对数组操作没有判空的逻辑会触发新的崩溃,清空栈顶数组这种方案也存在风险,这条路虽然不甘心但还是暂时搁置了,毕竟尽快解决问题才是关键。

方案3:_CFRelease

虽然方案 2 没有能解决问题,但通过方案 2 我们得到了一个大概的调用栈:

#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 -[GCDAsyncSocket closeWithError:] at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:3213
#11	0x0000000108456b8c in -[GCDAsyncSocket maybeDequeueWrite] at /Users/yuencong/workplace/gif2/.gundam/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m:5976
#12	0x0000000108457584 in __29-[GCDAsyncSocket doWriteData]_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 ()

继续研究这个堆栈,有个非常奇怪的地方 CoreFoundation: _CFRelease 调用了 CFNetwork: ___lldb_unnamed_symbol7940, CoreFoundation 应该是更底层的库才合理,CoreFoundation 不应该调用到 CFNetwork。 查看 CFSocketInvalidate 里面对 _CFRelease 的调用,代码比较长截取部分关键信息:

void CFSocketInvalidate(CFSocketRef s) {
    CFRetain(s);
    __CFLock(&__CFAllSocketsLock);
    __CFSocketLock(s);
    if (__CFSocketIsValid(s)) {        
        contextInfo = s->_context.info;
        contextRelease = s->_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(&__CFAllSocketsLock);
    CFRelease(s);
}

结合 Xcode 的调试信息:

    0x1ba8b16fc <+916>:  bl     0x1ba862870               ; CFArrayGetValueAtIndex
    0x1ba8b1700 <+920>:  bl     0x1ba8945a0               ; CFRunLoopWakeUp
    0x1ba8b1704 <+924>:  sub    x24, x24, #0x1
    0x1ba8b1708 <+928>:  subs   w20, w20, #0x1
    0x1ba8b170c <+932>:  b.ne   0x1ba8b16f4               ; <+908>
    0x1ba8b1710 <+936>:  mov    x0, x22
    0x1ba8b1714 <+940>:  bl     0x1ba860cec               ; CFRelease
    0x1ba8b1718 <+944>:  cbz    x25, 0x1ba8b1724          ; <+956>
    0x1ba8b171c <+948>:  mov    x0, x23
    0x1ba8b1720 <+952>:  blraaz x25
->  0x1ba8b1724 <+956>:  cbz    x21, 0x1ba8b1738          ; <+976>
    0x1ba8b1728 <+960>:  mov    x0, x21
    0x1ba8b172c <+964>:  bl     0x1ba8b1a54               ; CFRunLoopSourceInvalidate
    0x1ba8b1730 <+968>:  mov    x0, x21
    0x1ba8b1734 <+972>:  bl     0x1ba860cec               ; CFRelease
    0x1ba8b1738 <+976>:  adrp   x0, 354829
    0x1ba8b173c <+980>:  add    x0, x0, #0x900            ; __CFAllSocketsLock

执行完 CFRelease 之后会执行 CFRunLoopSourceInvalidate, 那这里的 CFRelease 只有 CFRelease(source0); source0 是个数组,当时天真的认为 ___lldb_unnamed_symbol7940 是通过 CFArrayReleaseCallBack 添加的回调方法, 这个调用逻辑看起来合情合理。CFRelease 虽然不能被 hook,那是不是可以通过修改 CallBack 来打破递归调用呢?按照这种方式去尝试了仍然不可行。断点 CFRelease 发现此时 release 的对象类型是 SocketStream 并不是之前的 source0 数组。CFSocketInvalidate 这个函数里面查找类型是 SocketStream 的对象,最终找到了 s->_context.info,顺藤摸瓜找到了我们解决这个问题最关键的三行代码:

if (NULL != contextRelease) {
    contextRelease(contextInfo);
}

按照 xcode 的调试信息 contextRelease == CFReleasecontextRelease 在代码中取值 s->_context.release。只要拿到了 s->_context 的数据结构,修改 release 这个指针,就可以实现对崩溃栈里面 CFRelease 的 hook,造成锁递归的两次 CFSocketInvalidate 调用分别在 CFRelease 之前和之后,如果把 CFRelease 修改为异步调用,CFSocketInvalidate 两次调用的 os_unfair_lock_lock 在两个不同的线程,锁递归判断的条件是 lock 当前的 owner 是当前线程,lock 方法在不同的线程执行,那这个问题也就迎刃而解了。映射 stream 和 socket 的过程不详细介绍了,这个过程太无聊了,直接贴个结果吧:

struct __CFSocket {
    int64_t offset[27];
    CFSocketContext _context;    
};
typedef struct {
    int64_t offset[33];
    struct __CFSocket *          _socket;
} __CFSocketStreamContext;
struct __CFStream {
    int64_t offset[5];
    __CFSocketStreamContext *info;
};

最终的解决方案概括如下述代码, 因为这里映射了很多系统的数据结构,这并不是一个安全的操作,需要添加一些内存可读写的判断,内存包换这部分代码参考 kscrash,另外业务层也需要 加好开关加好开关加好开关对特定系统生效,如果新系统 stream 或者是 socket 的数据结构发生变化可能会造成一些内存访问的崩溃。

// 内存保护
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,
                                             &bytesCopied);
    if(result != KERN_SUCCESS)
    {
        return 0;
    }
    return (int)bytesCopied;
}
static char g_memoryTestBuffer[10240];
static inline bool isMemoryReadable(const void* const memory, const int byteCount)
{
    const int testBufferSize = sizeof(g_memoryTestBuffer);
    int bytesRemaining = byteCount;
    while(bytesRemaining > 0)
    {
        int bytesToCopy = bytesRemaining > 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))
       && isMemoryReadable(cfstream->info, sizeof(*(cfstream->info)))
       && isMemoryReadable(cfstream->info->_socket, sizeof(*(cfstream->info->_socket)))
       && isMemoryReadable(&(cfstream->info->_socket->_context), sizeof(cfstream->info->_socket->_context))
       && isMemoryReadable(cfstream->info->_socket->_context.release, sizeof(*(cfstream->info->_socket->_context.release)))) {
        if (cfstream->info != NULL && cfstream->info->_socket != NULL) {
            if ((uintptr_t)cfstream->info->_socket->_context.release == (uintptr_t)CFRelease) {
                origin_context_release = cfstream->info->_socket->_context.release;
                cfstream->info->_socket->_context.release = new_context_release;
            }
        }
}

总结

这个问题并不是只出现在 CocoaAsyncSocket 这个库里面,后续在一些系统的线程里面也发现了这个崩溃堆栈,但是量级不大,评估了下没有解决的必要。

另外虽然方案1和方案2最终都被 pass 掉了,但是这也是我最常用的排障方法,所以写在这里跟大家分享下。整个排查过程中也存在很多最终都没有搞清楚的点,但是这些细节问题都没有影响到最终的结论,所以最终选择了佛系看待。

以上就是iOS 16 CocoaAsyncSocket 崩溃修复详解的详细内容,更多关于iOS CocoaAsyncSocket崩溃修复的资料请关注编程网其它相关文章!

--结束END--

本文标题: iOS16CocoaAsyncSocket崩溃修复详解

本文链接: https://www.lsjlt.com/news/178607.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • iOS16CocoaAsyncSocket崩溃修复详解
    目录背景方案1:fishhook 替换掉 os_unfair_lock_lock方案2: _schedulables 删除 _socket#8 未解析符号: ___lldb_unna...
    99+
    2023-01-29
    iOS CocoaAsyncSocket崩溃修复 iOS CocoaAsyncSocket
  • win10崩溃怎么修复
    这篇文章主要介绍“win10崩溃怎么修复”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“win10崩溃怎么修复”文章能帮助大家解决问题。win10系统出错后,不是一定需要重新安装系统的,可以直接使用通...
    99+
    2023-07-01
  • windows7系统崩溃后如何恢复windows7系统崩溃修复方式详细介绍
    windows7客户在应用计算机的历程中遇上了系统崩溃的状况,那麼应当如何恢复呢?有效的方法便是升级目前系统,去遮盖原先的,那样就能立即解决困难了。可以根据系统的还原作用,恢复系统到预先创建的还原点。你还可以通过在cmd对话框中输入sfc/...
    99+
    2023-07-16
  • win11电脑崩溃如何修复
    这篇文章主要讲解了“win11电脑崩溃如何修复”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“win11电脑崩溃如何修复”吧!方法一:直接按住shif并进行电脑重启,就可以直接跳转到安全模式点...
    99+
    2023-07-01
  • win11桌面崩溃如何修复
    当Windows 11桌面崩溃时,可以尝试以下几种方法修复:1. 重启电脑:首先尝试重新启动电脑,有时候这可以解决桌面崩溃的问题。2...
    99+
    2023-08-22
    win11
  • Win10系统崩溃如何修复
    本篇内容介绍了“Win10系统崩溃如何修复”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!win10系统出错后,不是一定需要重新安装系统的,可...
    99+
    2023-07-01
  • win10系统崩溃如何修复cmd
    本篇内容主要讲解“win10系统崩溃如何修复cmd”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“win10系统崩溃如何修复cmd”吧!首先,鼠标右击屏幕左下角的 Win 徽标图标,然后在弹出的右...
    99+
    2023-07-01
  • win11安全中心崩溃如何修复
    如果Windows 11的安全中心崩溃,您可以尝试以下方法来修复它:1. 重启计算机:有时候安全中心崩溃可能是暂时的问题,通过重新启...
    99+
    2023-08-22
    win11
  • win10系统崩溃无法开机修复怎么解决
    当Windows 10系统崩溃并且无法正常开机时,有几种可能的解决方法:1. 启动修复:重启电脑,按下F8或Shift键,进入启动修...
    99+
    2023-09-05
    win10
  • windows谷歌浏览器崩溃如何修复
    这篇文章主要介绍“windows谷歌浏览器崩溃如何修复”,在日常操作中,相信很多人在windows谷歌浏览器崩溃如何修复问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”windows谷歌浏览器崩溃如何修复”的疑...
    99+
    2023-06-30
  • win10系统崩溃修复的方法有哪些
    这篇文章主要讲解了“win10系统崩溃修复的方法有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“win10系统崩溃修复的方法有哪些”吧! ...
    99+
    2023-04-06
    win10
  • win10系统崩溃无法开机如何修复
    本篇内容主要讲解“win10系统崩溃无法开机如何修复”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“win10系统崩溃无法开机如何修复”吧!win10系统崩溃无法开机解决方法首先需要重启电脑,接着...
    99+
    2023-07-01
  • 线程崩溃不会导致 JVM 崩溃的原因解析
    目录线程崩溃,进程一定会崩溃吗进程是如何崩溃的-信号机制简介为什么线程崩溃不会导致 JVM 进程崩溃openJDK 源码解析总结参考文章网上看到一个很有意思的据说是美团的面试题:为什...
    99+
    2024-04-02
  • MySQL 崩溃恢复过程分析
    天有不测风云,数据库有旦夕祸福。 前面写 Redo 日志的文章介绍过,数据库正常运行时,Redo 日志就是个累赘。 现在,终于到了 Redo 日志扬眉吐气,大显身手的时候了。 本文我们一起来看看,My...
    99+
    2023-09-16
    mysql 数据库 php java 程序员
  • excel崩溃怎么恢复数据
    当Excel崩溃时,可以尝试以下方法恢复数据:1. 重新打开Excel:关闭当前的Excel程序,然后重新打开Excel,看看是否能...
    99+
    2023-09-29
    excel
  • Win7崩溃如何恢复数据
    本篇内容主要讲解“Win7崩溃如何恢复数据”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Win7崩溃如何恢复数据”吧!一、使用最后一次正确配置电脑重新开机后按“F8”键进入高级启动选项,选择“最...
    99+
    2023-07-01
  • 如何解决MySQL报错:表被标记为崩溃,需要修复
    要解决MySQL报错“表被标记为崩溃,需要修复”,可以尝试以下几种方法:1. 使用MySQL自带的修复工具:可以使用MySQL提供的...
    99+
    2023-10-12
    MySQL
  • windows7操作系统崩溃后的修复技巧(整理)
    XP系统已将是日落西山,虽然还有很多用户依然在使用着XP系统,但不可否认的是,XP系统用户正在慢慢地被win7和win8系统蚕食,并不是XP系统不够好,相反XP系统非常好用,速度也非常快,主要是因为现在电脑配置提高了,X...
    99+
    2023-05-30
    win7 崩溃 修复 技巧 windows7
  • win10崩溃了如何解决
    这篇文章主要介绍“win10崩溃了如何解决”,在日常操作中,相信很多人在win10崩溃了如何解决问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”win10崩溃了如何解决”的疑惑有所帮助!接下来,请跟着小编一起来...
    99+
    2023-07-01
  • win10预览版steam崩溃怎么办?windows10 9879steam崩溃解决方法
      win10预览版steam崩溃怎么办 小编胖胖带来了win10预览版9879steam崩溃解决方法,如果你现在使用的版本是Windows10预览版9879并且经常出现steam崩溃的情况可以试一试下文的解决方法。 ...
    99+
    2023-06-09
    win10 steam 崩溃 windows10 9879 预览版 方法
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作