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)前線程,第一步默認 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)時天真的認為 ___lldb_unnamed_symbol7940 是通過 CFArrayReleaseCallBack 添加的回調(diào)方法, 這個調(diào)用邏輯看起來合情合理。CFRelease 雖然不能被 hook,那是不是可以通過修改 CallBack 來打破遞歸調(diào)用呢?按照這種方式去嘗試了仍然不可行。斷點 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 的過程不詳細介紹了,這個過程太無聊了,直接貼個結(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)存保護
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 掉了,但是這也是我最常用的排障方法,所以寫在這里跟大家分享下。整個排查過程中也存在很多最終都沒有搞清楚的點,但是這些細節(jié)問題都沒有影響到最終的結(jié)論,所以最終選擇了佛系看待。
以上就是iOS 16 CocoaAsyncSocket 崩潰修復(fù)詳解的詳細內(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)上的方法也很多。但是并不是很全面吧,這里全面進行了總結(jié),有需要的朋友們可以參考借鑒,下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2016-11-11
iOS開發(fā)中Swift3 監(jiān)聽UITextView文字改變的方法(三種方法)
在項目中使用文本輸入框出UITextField之外還會經(jīng)常使用 UITextView ,難免會有需求監(jiān)聽UITextView文本框內(nèi)文本數(shù)量.下面介紹在swift3中兩種常用方式,需要的朋友參考下吧2016-11-11

