iOS 16 CocoaAsyncSocket 崩潰修復(fù)詳解
背景
iOS 16 版本發(fā)布后, 我們監(jiān)控到 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
原因非常簡單,鎖遞歸調(diào)用了,os_unfair_lock_lock
的遞歸調(diào)用是通過 lock 的當(dāng)前 owner 等于當(dāng)前線程來判斷的,理論上只要打破這個遞歸調(diào)用就能解決這個問題。分析堆棧崩潰棧頂 CoreFoundation
中的 CFSocketInvalidate
函數(shù)調(diào)用了 libsystem_platform.dylib
中的 os_unfair_lock
,兩個動態(tài)庫之間走 bind 的間接調(diào)用,那直接使用 fishhook hook 掉 CoreFoundation
中調(diào)用的 lock 方法,替換的 lock 方法里面判斷 owner 是否是當(dāng)前線程,是的話直接 return,那這個崩潰問題不就解了嗎?于是就有了下面的第一版方案。 (注:方案 1&2 最終都被 pass 了,方案 3 驗證可行)
方案1:fishhook 替換掉 os_unfair_lock_lock
這個方案有兩個關(guān)鍵的步驟 hook lock 方法,lock 方法判斷 owner 是否是當(dāng)前線程,第一步默認(rèn) fishhook 可行,第二步看起來更有挑戰(zhàn)性,所以先從 lock 判斷邏輯開始了調(diào)研,這里流下了悔恨的淚水。
<os/lock.h>
里面提供了系統(tǒng)的 api os_unfair_lock_assert_owner
來判斷 lock 當(dāng)前的 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 被當(dāng)前線程持有,則直接觸發(fā) assert 并中斷程序。因為 dev 會觸發(fā)崩潰,這個 api 在我們這個場景下不能直接調(diào)用,好在蘋果提供了這部分代碼,參考下可以實現(xiàn) lock owner 的判斷邏輯,中間涉及到一些 tsd 的代碼需要額外處理,這里不展開說明了。之后 fishhook 全局替換 os_unfair_lock_lock
開始測試。
os_unfair_lock_lock(&test_lock); os_unfair_lock_lock(&test_lock);
上述可以穩(wěn)定復(fù)現(xiàn)遞歸鎖的崩潰,添加 hook 代碼后崩潰消失,到這里第一次以為問題解決了。
然而,測試代碼在主可執(zhí)行文件里面,而崩潰發(fā)生在 CoreFoundation
里面,CoreFoundation
的 lock 方法可以被 hook 嗎?答案是不可以的。 后續(xù)業(yè)務(wù)部門的同學(xué)比較給力穩(wěn)定復(fù)現(xiàn)了這個崩潰,崩潰棧頂 CFSocketInvalidate
對 lock 方法的調(diào)用如下 0x1ba8b13e8 bl 0x1c0155a60
,這里并不是之前熟悉的 symbol stub 的調(diào)用,fishhook 不能生效。這種動態(tài)庫之間的調(diào)用一直是我的知識盲區(qū),不知從何下手,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
之后調(diào)試了 iOS 15 的設(shè)備,發(fā)現(xiàn) iOS 15 調(diào)用的鎖類型是 pthread_mutex_lock,iOS 16 替換為了 os_unfair_lock 大概是這里的更新導(dǎo)致了這個 crash。 既然直接從鎖下手,無法修復(fù)這個問題,那么接下來就要分析下,這里為什么會出現(xiàn)遞歸調(diào)用。
方案2: _schedulables 刪除 _socket
崩潰堆棧在 CFNetwork 庫里的符號都沒有正常解析,線下調(diào)試的時候 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
執(zhí)行了兩次, CFSocketInvalidate
調(diào)用了 os_unfair_lock_lock
, os_unfair_lock_lock
執(zhí)行了兩次導(dǎo)致了鎖遞歸。分析出更加具體的原因還需要解析出對應(yīng)的符號。
#8 未解析符號: ___lldb_unnamed_symbol8050
_CFStreamClose
調(diào)用了 ___lldb_unnamed_symbol8050
,___lldb_unnamed_symbol8050
第一次調(diào)用了 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); } }
結(jié)合 xcode 的調(diào)試信息 ___lldb_unnamed_symbol8050
大概率是 cb->close
方法。這里嘗試映射了 _CFStream
的數(shù)據(jù)結(jié)構(gòu)修改 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
的調(diào)用
void (*_origin_SocketStreamClose)(CFTypeRef stream, void* ctxt); void _new_SocketStreamClose(CFTypeRef stream, void* ctxt) { _origin_SocketStreamClose(stream, ctxt); }
繼續(xù)翻看 CFNetwork 的代碼最終可以找到 cb->close 指向函數(shù) SocketStreamClose
這個函數(shù)比較長,我們只關(guān)注里面對 CFSocketInvalidate
的第一次調(diào)用部分:
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 的數(shù)據(jù)結(jié)構(gòu)
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;
這個數(shù)據(jù)結(jié)構(gòu)在 iOS 16 中有修改,但是調(diào)試的時候 lldb 可以通過 memory read 找到 _socket
的偏移以及 _schedulables
的偏移。_schedulables
也是一個比較關(guān)鍵的值,在分析第二次調(diào)用 CFSocketInvalidate
的時候會用到。
小結(jié):第一次 CFSocketInvalidate
是在 SocketStreamClose
里面調(diào)用,入?yún)⑹?stream->info->_socket
。
#3 未解析符號: ___lldb_unnamed_symbol8533
第二次 CFSocketInvalidate
的調(diào)用在 ___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>
結(jié)合一些關(guān)鍵特征: 函數(shù)開始會調(diào)用 CFSocketInvalidate
,之后會調(diào)用 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
會執(zhí)行 CFSocketInvalidate
方法。 _SchedulablesInvalidateApplierFunction
在 CFNetwork
里面搜索有兩處調(diào)用,調(diào)用方式和入?yún)⑾嗤瑐魅氲膮?shù)都是 ctxt->_schedulables
這個數(shù)組包含的 item,ctxt 是 stream 的 info 字段。
CFArrayApplyFunction(ctxt->_schedulables, r, (CFArrayApplierFunction)_SchedulablesInvalidateApplierFunction, NULL);
小結(jié):第二次 CFSocketInvalidate
是在 _SchedulablesInvalidateApplierFunction
里面執(zhí)行,入?yún)⑹?stream->info->_schedulables
包含的 item。
邏輯分析
造成遞歸的兩次調(diào)用
CFSocketInvalidate(stream->info->_socket)
CFSocketInvalidate(stream->info->_schedulables item)
info->_socket
是個 CFSocketRef
對象,崩潰發(fā)生時在操作 _schedulables
數(shù)組里面的 CFSocketRef
對象,說明 _schedulables
里面也包含 CFSocketRef
對象,兩者都是 info 持有的屬性值,那 _schedulables
包含的 CFSocketRef
對象和 _socket
對象有什么關(guān)聯(lián)呢?如果相等重復(fù)執(zhí)行 CFSocketInvalidate
就沒有意義了,從 _schedulables
直接刪除掉 _socket
對象,遞歸被打破,那這個問題也可以解決了。
嘗試映射 stream->info
的數(shù)據(jù)結(jié)構(gòu),需要注意的是 _CFSocketStreamContext
中 _schedulables
這個值在 iOS 16 中是個二級指針,和 CFNetwork
中提供的數(shù)據(jù)結(jié)構(gòu)不一致,在內(nèi)存中查找起來比較麻煩。最終會發(fā)現(xiàn) info->_schedulables
中包含的 CFSocketRef
對象就是 info->_socket
。
嘗試我們的修復(fù)方案映射 info 拿到 _schedulables
,崩潰發(fā)生時 _schedulables
只包含 _socket
一個元素,所以直接簡單粗暴的調(diào)用了 RemoveAll 方法,到這里我第二次以為這個問題解決了:
CFArrayRemoveAllValues(stream->info->_schedulables)
然后噩夢開始了,很多對 _schedulables
的調(diào)用并沒有判空操作,結(jié)果就是直接崩,比如下面這個代碼
CFArrayApplyFunction(ctxt->_schedulables, CFRangeMake(0, CFArrayGetCount(ctxt->_schedulables)), (CFArrayApplierFunction)_SchedulablesScheduleApplierFunction, loopAndMode);
用非常臟的方式繞過了這些沒有判空的崩潰,結(jié)果還是復(fù)現(xiàn)了最初鎖遞歸的崩潰。棧頂操作的包含 _socket 數(shù)組根據(jù)代碼分析是 _schedulables
,但實際上最終崩潰時棧頂操作的數(shù)組地址并不是 stream->info->_schedulables
。從 _schedulables
刪除 _socket
的方案行不通了,其實此時還可以繼續(xù)分析棧頂?shù)臄?shù)組是從哪兒生成的,但屬實是更加困難,另外加上對數(shù)組操作沒有判空的邏輯會觸發(fā)新的崩潰,清空棧頂數(shù)組這種方案也存在風(fēng)險,這條路雖然不甘心但還是暫時擱置了,畢竟盡快解決問題才是關(guān)鍵。
方案3:_CFRelease
雖然方案 2 沒有能解決問題,但通過方案 2 我們得到了一個大概的調(diào)用棧:
#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 ()
繼續(xù)研究這個堆棧,有個非常奇怪的地方 CoreFoundation: _CFRelease
調(diào)用了 CFNetwork: ___lldb_unnamed_symbol7940
, CoreFoundation
應(yīng)該是更底層的庫才合理,CoreFoundation
不應(yīng)該調(diào)用到 CFNetwork
。 查看 CFSocketInvalidate
里面對 _CFRelease
的調(diào)用,代碼比較長截取部分關(guān)鍵信息:
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); }
結(jié)合 Xcode 的調(diào)試信息:
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
執(zhí)行完 CFRelease
之后會執(zhí)行 CFRunLoopSourceInvalidate
, 那這里的 CFRelease
只有 CFRelease(source0)
; source0 是個數(shù)組,當(dāng)時天真的認(rèn)為 ___lldb_unnamed_symbol7940
是通過 CFArrayReleaseCallBack
添加的回調(diào)方法, 這個調(diào)用邏輯看起來合情合理。CFRelease
雖然不能被 hook,那是不是可以通過修改 CallBack 來打破遞歸調(diào)用呢?按照這種方式去嘗試了仍然不可行。斷點(diǎn) CFRelease
發(fā)現(xiàn)此時 release 的對象類型是 SocketStream
并不是之前的 source0 數(shù)組。CFSocketInvalidate
這個函數(shù)里面查找類型是 SocketStream
的對象,最終找到了 s->_context.info
,順藤摸瓜找到了我們解決這個問題最關(guān)鍵的三行代碼:
if (NULL != contextRelease) { contextRelease(contextInfo); }
按照 xcode 的調(diào)試信息 contextRelease
== CFRelease
而 contextRelease
在代碼中取值 s->_context.release
。只要拿到了 s->_context
的數(shù)據(jù)結(jié)構(gòu),修改 release
這個指針,就可以實現(xiàn)對崩潰棧里面 CFRelease
的 hook,造成鎖遞歸的兩次 CFSocketInvalidate
調(diào)用分別在 CFRelease
之前和之后,如果把 CFRelease
修改為異步調(diào)用,CFSocketInvalidate
兩次調(diào)用的 os_unfair_lock_lock
在兩個不同的線程,鎖遞歸判斷的條件是 lock 當(dāng)前的 owner 是當(dāng)前線程,lock 方法在不同的線程執(zhí)行,那這個問題也就迎刃而解了。映射 stream 和 socket 的過程不詳細(xì)介紹了,這個過程太無聊了,直接貼個結(jié)果吧:
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; };
最終的解決方案概括如下述代碼, 因為這里映射了很多系統(tǒng)的數(shù)據(jù)結(jié)構(gòu),這并不是一個安全的操作,需要添加一些內(nèi)存可讀寫的判斷,內(nèi)存包換這部分代碼參考 kscrash,另外業(yè)務(wù)層也需要 加好開關(guān)加好開關(guān)加好開關(guān)對特定系統(tǒng)生效,如果新系統(tǒng) stream 或者是 socket 的數(shù)據(jù)結(jié)構(gòu)發(fā)生變化可能會造成一些內(nèi)存訪問的崩潰。
// 內(nèi)存保護(hù) 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; } } }
總結(jié)
這個問題并不是只出現(xiàn)在 CocoaAsyncSocket
這個庫里面,后續(xù)在一些系統(tǒng)的線程里面也發(fā)現(xiàn)了這個崩潰堆棧,但是量級不大,評估了下沒有解決的必要。
另外雖然方案1和方案2最終都被 pass 掉了,但是這也是我最常用的排障方法,所以寫在這里跟大家分享下。整個排查過程中也存在很多最終都沒有搞清楚的點(diǎn),但是這些細(xì)節(jié)問題都沒有影響到最終的結(jié)論,所以最終選擇了佛系看待。
以上就是iOS 16 CocoaAsyncSocket 崩潰修復(fù)詳解的詳細(xì)內(nèi)容,更多關(guān)于iOS CocoaAsyncSocket崩潰修復(fù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS UITextField最大字符數(shù)和字節(jié)數(shù)的限制詳解
在開發(fā)中我們經(jīng)常遇到這樣的需求:在UITextField或者UITextView中限制用戶可以輸入的最大字符數(shù)。但在UITextView , UITextfield 中有很多坑,網(wǎng)上的方法也很多。但是并不是很全面吧,這里全面進(jìn)行了總結(jié),有需要的朋友們可以參考借鑒,下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2016-11-11iOS開發(fā)中Swift3 監(jiān)聽UITextView文字改變的方法(三種方法)
在項目中使用文本輸入框出UITextField之外還會經(jīng)常使用 UITextView ,難免會有需求監(jiān)聽UITextView文本框內(nèi)文本數(shù)量.下面介紹在swift3中兩種常用方式,需要的朋友參考下吧2016-11-11