欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

iOS 16 CocoaAsyncSocket 崩潰修復(fù)詳解

 更新時間:2023年01月29日 11:50:47   作者:yuec  
這篇文章主要為大家介紹了iOS 16 CocoaAsyncSocket 崩潰修復(fù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景

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

/*!
&nbsp;* **@function** os_*unfair_lock_assert_not_owner*
&nbsp;*
&nbsp;* **@abstract**
&nbsp;* Asserts that the calling thread is not the current owner of the specified
&nbsp;* unfair lock.
&nbsp;*
&nbsp;* **@discussion**
&nbsp;* If the lock is unlocked or owned by a different thread, this function
&nbsp;* returns.
&nbsp;*
&nbsp;* If the lock is currently owned by the current thread, this function asserts
&nbsp;* and terminates the process.
&nbsp;*
&nbsp;* **@param** lock
&nbsp;* Pointer to an os_unfair_lock.
&nbsp;*/
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(&amp;test_lock);
os_unfair_lock_lock(&amp;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-&gt;version &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: &lt;rdar://problem/13678879&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;
};

修改 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 方法。 _SchedulablesInvalidateApplierFunctionCFNetwork 里面搜索有兩處調(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 == CFReleasecontextRelease 在代碼中取值 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通過按鈕點(diǎn)擊異步加載圖片

    ios通過按鈕點(diǎn)擊異步加載圖片

    本文給大家匯總了幾種IOS中實現(xiàn)異步加載圖片的方法,十分的簡單實用,有需要的小伙伴可以參考下。
    2015-05-05
  • iOS UITextField最大字符數(shù)和字節(jié)數(shù)的限制詳解

    iOS UITextField最大字符數(shù)和字節(jié)數(shù)的限制詳解

    在開發(fā)中我們經(jīng)常遇到這樣的需求:在UITextField或者UITextView中限制用戶可以輸入的最大字符數(shù)。但在UITextView , UITextfield 中有很多坑,網(wǎng)上的方法也很多。但是并不是很全面吧,這里全面進(jìn)行了總結(jié),有需要的朋友們可以參考借鑒,下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。
    2016-11-11
  • iOS實現(xiàn)攝像頭實時采集圖像

    iOS實現(xiàn)攝像頭實時采集圖像

    這篇文章主要為大家詳細(xì)介紹了iOS實現(xiàn)攝像頭實時采集圖像,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-04-04
  • IOS開發(fā)第三方語音-微信語音

    IOS開發(fā)第三方語音-微信語音

    微信語音開放平臺致力于為開發(fā)者提供免費(fèi)的語音技術(shù),目前已經(jīng)開放的語音技術(shù)包括在線語音識別、在線語音合成等,下面通過本篇文章給大家介紹IOS開發(fā)第三方語言-微信語言,需要的朋友可以一起來學(xué)習(xí)下
    2015-08-08
  • iOS開發(fā)中Swift3 監(jiān)聽UITextView文字改變的方法(三種方法)

    iOS開發(fā)中Swift3 監(jiān)聽UITextView文字改變的方法(三種方法)

    在項目中使用文本輸入框出UITextField之外還會經(jīng)常使用 UITextView ,難免會有需求監(jiān)聽UITextView文本框內(nèi)文本數(shù)量.下面介紹在swift3中兩種常用方式,需要的朋友參考下吧
    2016-11-11
  • iOS App初次啟動時的用戶引導(dǎo)頁制作實例分享

    iOS App初次啟動時的用戶引導(dǎo)頁制作實例分享

    這篇文章主要介紹了iOS App初次啟動時的用戶引導(dǎo)頁制作實例分享,其中判斷程序是否是第一次或版本更新以后第一次啟動是一個關(guān)鍵點(diǎn),需要的朋友可以參考下
    2016-03-03
  • IOS10 配置權(quán)限詳細(xì)介紹

    IOS10 配置權(quán)限詳細(xì)介紹

    這篇文章主要介紹了IOS10 配置權(quán)限詳細(xì)介紹的相關(guān)資料,訪問相機(jī)、相冊、麥克風(fēng)、通訊錄等等權(quán)限,都需要進(jìn)行相關(guān)的配置,不然會直接crash,需要的朋友可以參考下
    2016-12-12
  • IOS關(guān)閉鍵盤的方法

    IOS關(guān)閉鍵盤的方法

    在iOS應(yīng)用開發(fā)中,有三類視圖對象會打開虛擬鍵盤,進(jìn)行輸入操作,但如何關(guān)閉虛擬鍵盤,卻沒有提供自動化的方法。這個需要我們自己去實現(xiàn)。
    2015-05-05
  • IOS中對Url進(jìn)行編碼和解碼示例

    IOS中對Url進(jìn)行編碼和解碼示例

    本篇文章主要介紹了IOS中對Url進(jìn)行編碼和解碼示例,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-11-11
  • iOS中幾種定時器的實現(xiàn)小結(jié)

    iOS中幾種定時器的實現(xiàn)小結(jié)

    這篇文章主要介紹了iOS中幾種定時器的實現(xiàn)小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01

最新評論