背景
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
/*! * **@function** os_*unfair_lock_assert_not_owner* * * **@abstract** * asserts that the calling thread is not the current owner of the specified * unfair lock. * * **@discussion** * if the lock is unlocked or owned by a different thread, this function * returns. * * if the lock is currently owned by the current thread, this function asserts * and terminates the process. * * **@param** lock * pointer to an os_unfair_lock. */ 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(&test_lock); os_unfair_lock_lock(&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_lock
, os_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->version < 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: <rdar://problem/13678879> 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; };
修改 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) { /* make sure to invalidate the socket */ cfsocketinvalidate(ctxt->_socket); /* dump and forget it. */ cfrelease(ctxt->_socket); ctxt->_socket = null; }
ctxt 通过方法 _cfstreamgetinfopointer
获取,取的值是 stream 的 info,corefoundation
中提供的 info 的数据结构
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;
这个数据结构在 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
,之后会调用 cfhostcancelinforesolution
、cfnetservicegettypeid
等,在 cfnetwork
里面找到了一个匹配度非常高的方法 _schedulablesinvalidateapplierfunction
。
/* 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); }
_cftypeinvalidate
方法里面会判断 cf 类型如果是 cfsocketgettypeid
会执行 cfsocketinvalidate
方法。 _schedulablesinvalidateapplierfunction
在 cfnetwork
里面搜索有两处调用,调用方式和入参相同,传入的参数都是 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
== cfrelease
而 contextrelease
在代码中取值 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; /* immutable */ }; 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崩溃修复的资料请关注代码网其它相关文章!
发表评论