If you don’t know what a runloop is, you can learn more about runloops here.

CFRunloop is thread safe, as stated in the apple documentation:

Thread safety varies depending on which API you are using to manipulate your run loop. The functions in Core Foundation are generally thread-safe and can be called from any thread. If you are performing operations that alter the configuration of the run loop, however, it is still good practice to do so from the thread that owns the run loop whenever possible.

It should be noted, however, that cunning Apple uses the vague word generally.

In practice, some operations of CFRunloop during the stopping phase of runloop are potentially multithreaded.

Unsafe CFRunloopSource

CFRunloop is thread-safe, but not with CFRunloopSource. Such as CFSocket.

The sample code

Look at this code for a custom thread:

@interface MyThread() @property (nonatomic, strong) NSThread *currentThread; @property (nonatomic, assign) CFRunLoopSourceRef socketSource; @property (nonatomic, assign) CFSocketRef socket; @property (nonatomic, assign) CFRunLoopRef currentRunloop; @end@implementation MyThread - (instanceType)init {if (self = [super init]) {
        _currentThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
    }
    returnself; } // start the thread; - (void)startThread {[self.currentThread start]; } // thread entry - (void)runThread {@autoreleasepool {// Return runloop, which can be stopped by other threads self.currentRunloop = CFRunLoopGetCurrent(); [self addSocketSource]; CFRunLoopRun(); } NSLog(@"Thread exit"); } - (void)stopThread {[self removeSocketSource]; @synchronized (_currentRunloop) {if(_currentRunloop) { CFRunLoopStop(_currentRunloop); self.currentRunloop = NULL; - (void)addSocketSource {int sock; sock = socket(AF_INET6, SOCK_STREAM, 0); CFSocketContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; self.socket = CFSocketCreateWithNative(NULL, sock, kCFSocketReadCallBack, socketCallBack, &context); self.socketSource = CFSocketCreateRunLoopSource(NULL, self.socket, 0); CFRunLoopAddSource(_currentRunloop, _socketSource, kCFRunLoopDefaultMode); } - (void)removeSocketSource { @synchronized (_socket) {if(_socket) {//CFSocketInvalidate may be thrown to another thread to execute, So CFSocketInvalidate and CFRunLoopStop might have multiple threads calling CFSocketInvalidate(_socket); CFRelease(_socket); self.socket = NULL; }}}Copy the code

In practice, CFSocket is managed by another socket class, so addSocketSource and removeSocketSource are in another class. CFSocketInvalidate and CFRunLoopStop may be called simultaneously by multiple threads.

Crash Example Analysis

There doesn’t seem to be a problem, the locks are all locked, and the methods at the beginning of CF are thread-safe. However, if CFSocketInvalidate and CFRunLoopStop are called at the same time, crash may occur. For example, we received a crash in our project:

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   CoreFoundation                  0x000000018e6a9144 CFRunLoopWakeUp + 92
1   CoreFoundation                  0x000000018e6a9140 CFRunLoopWakeUp + 88
2   CoreFoundation                  0x000000018e6d71e8 CFSocketInvalidate + 712
3   MyApp                           0x00000001000fe424 (-[MySocket stop] + 136)
4   MyApp                           0x00000001000fcd50 (-[MySocket dealloc] + 56)
5   libsystem_blocks.dylib          0x000000018d6afa28 _Block_release + 144
6   libdispatch.dylib               0x000000018d65a1bc _dispatch_client_callout + 16
7   libdispatch.dylib               0x000000018d65ed68 _dispatch_main_queue_callback_4CF + 1000
8   CoreFoundation                  0x000000018e77e810 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
9   CoreFoundation                  0x000000018e77c3fc __CFRunLoopRun + 1660
10  CoreFoundation                  0x000000018e6aa2b8 CFRunLoopRunSpecific + 444
11  GraphicsServices                0x000000019015e198 GSEventRunModal + 180
12  UIKit                           0x00000001946f17fc -[UIApplication _run] + 684
13  UIKit                           0x00000001946ec534 UIApplicationMain + 208
14  DuoYiIM                         0x000000010003ca58 0x100024000 + 100952 (main + 132)
15  libdyld.dylib                   0x000000018d68d5b8 start + 4

Thread 0 crashed with ARM-64 Thread State:
  cpsr: 0x0000000020000000     fp: 0x000000016fddab30     lr: 0x000000018e6a9140     pc: 0x000000018e6a9144 
    sp: 0x000000016fddaa00     x0: 0x0000000000000000     x1: 0x0000000000000000    x10: 0x0000000000000000 
   x11: 0x0000000000000000    x12: 0x0000000000000000    x13: 0x0000000000000000    x14: 0x0000000000000000 
   x15: 0x0000000000001203    x16: 0x000000000000012d    x17: 0x000000018f1eef74    x18: 0x0000000000000000 
   x19: 0x000000017056cb50     x2: 0x0000000000001000    x20: 0x000000017056cb40    x21: 0x96e73914144e0055 
   x22: 0x0000000174452990    x23: 0x000000017048bae0    x24: 0x0000000000000000    x25: 0x00000000ffffffff 
   x26: 0xffffffffffffffff    x27: 0x000000017426f1c0    x28: 0x0000000002ffffff    x29: 0x000000016fddab30 
    x3: 0x000000000017e4a6     x4: 0x0000000000012068     x5: 0x0000000000000000     x6: 0x0000000000000036 
    x7: 0xffffffffffffffec     x8: 0x8c8c8c8c8c8c8c8c     x9: 0x000000000000000c
Copy the code

CFSocketInvalidate is called on the main thread. Look at the stack. The crash occurred when CFRunLoopWakeUp was called inside CFSocketInvalidate.

I can’t see the specific cause of crash, so I need to check where the CFRunLoopWakeUp is hung. See the assembly code for the corresponding version of CoreFoundation:

_CFRunLoopWakeUp:
0x0000000181521b9c FF0305D1               sub        sp, sp, #0x140 ; CODE XREF=_CFRunLoopAddTimer+696, _CFRunLoopTimerSetNextFireDate+592, _CFSocketInvalidate+708, __wakeUpRunLoop+276, __CFXRegistrationPost+344, -[CFPrefsSearchListSource asynchronouslyNotifyOfChangesFromDictionary:toDictionary:]+172, ___CFSocketPerformV0+1408, ___CFSocketManager+2004, ___CFSocketManager+4248, _boundPairRead+604, _boundPairReadClose + 124,...
0x0000000181521ba0 FC6F11A9               stp        x28, x27, [sp, #0x110]
0x0000000181521ba4 F44F12A9               stp        x20, x19, [sp, #0x120]
0x0000000181521ba8 FD7B13A9               stp        x29, x30, [sp, #0x130]
0x0000000181521bac FDC30491               add        x29, sp, #0x130
0x0000000181521bb0 F40300AA               mov        x20, x0
0x0000000181521bb4 C80C10F0               adrp       x8, #0x1a16bc000
0x0000000181521bb8 084140F9               ldr        x8, [x8, #0x80] ; -[_CFXPreferences init]_1a16bc080
0x0000000181521bbc 080140F9               ldr        x8, [x8]
0x0000000181521bc0 292013F0               adrp       x9, #0x1a7928000
0x0000000181521bc4 29E90791               add        x9, x9, #0x1fa ; ___CF120290
0x0000000181521bc8 A8831DF8               stur       x8, [x29, #-0x28]
0x0000000181521bcc E8030032               orr        w8, wzr, #0x1
0x0000000181521bd0 28010039               strb       w8, [x9]                   ; ___CF120290
0x0000000181521bd4 E8731290               adrp       x8, #0x1a639d000
0x0000000181521bd8 08F13F91               add        x8, x8, #0xffc ; ___CF120293
0x0000000181521bdc 08014039               ldrb       w8, [x8]                   ; ___CF120293
0x0000000181521be0 48000034               cbz        w8, loc_181521be8

0x0000000181521be4 E3560394               bl         ___THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__

                                      loc_181521be8:
0x0000000181521be8 93420091               add        x19, x20, #0x10 ; CODE XREF=_CFRunLoopWakeUp+680x0000000181521bec E00313AA mov x0, x19 0x0000000181521bf0 70300694 bl imp___stubs_-[NSOrderedSet SortedArrayFromRange: options: usingComparator:] / / machine system libraries made confusion, __CFRunLoopLock 0x0000000181521BF4 882E40F9 LDR x8, [x20,#0x58]
0x0000000181521bf8 080D40B9               ldr        w8, [x8, #0xc]
0x0000000181521bfc A8010034               cbz        w8, loc_181521c30
Copy the code

Crash at CFRunLoopWakeUp + 92, assembly address 0x0000000181521b9c + 92= 0x0000000181521BF8, LDR W8, [x8, #0xc] X8:0x8c8c8c8c8c8c8c8c8c8c8c8c8c8c8C8C8C8C8C8C8C8C8C8C X8 is derived from LDR x8, [x20, #0x58] and x20 is derived from mov x20, x0. X0 is the first parameter to CFRunloopWakeUp. So x8 is the value of CFRunLoopRef offset 0x58.

The code for CoreFoundation is open source and can be downloaded here: CF-1153.18.

CFRunloopWakeUp

void CFRunLoopWakeUp(CFRunLoopRef rl) {
    CHECK_FOR_FORK();
    __CFRunLoopLock(rl);
    if (__CFRunLoopIsIgnoringWakeUps(rl)) {
        __CFRunLoopUnlock(rl);
        return;
    }
    kern_return_t ret;
    ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
    if(ret ! = MACH_MSG_SUCCESS && ret ! = MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
    __CFRunLoopUnlock(rl);
}

CF_INLINE Boolean __CFRunLoopIsIgnoringWakeUps(CFRunLoopRef rl) {
    return (rl->_perRunData->ignoreWakeUps) ? true : false;    
}
Copy the code

CFRunloop:

struct __CFRunLoop { CFRuntimeBase _base; //16 byte pthread_mutex_t _lock; //64 byte __CFPort _wakeUpPort; //mach_port_t (unsign int), 4 byte Boolean _unused; Volatile _per_run_data *_perRunData; volatile _perrun_data *_perRunData; volatile _perrun_data *_perRunData; pthread_t _pthread; uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; }; typedef struct __CFRuntimeBase { uintptr_t _cfisa; //unsigned long 8 byte uint8_t _cfinfo[4]; //unsigned char 4 byte#if __LP64__
    uint32_t _rc;	//unsigned int 4 byte
#endif
} CFRuntimeBase;

struct pthread_mutex_t {
	long __sig;	//8 byte
	char __opaque[56]; //56 byte
};
Copy the code

LDR x8 [x20, #0x58] is runloop-> _perRunData. Namely when invoking __CFRunLoopIsIgnoringWakeUps CFRunLoopRef has been released.

Analysis of theCFSocketThe source code

CFSocketInvalidate

void CFSocketInvalidate(CFSocketRef s) {
    CHECK_FOR_FORK();
    CFRetain(s);
    __CFLock(&__CFAllSocketsLock);
    __CFSocketLock(s);
    if(__CFSocketIsValid(s)) {// omit some code... CFArrayRef Runloop = (CFArrayRef)CFRetain(s->_runLoops); // CFRelease(s->_runLoops); s->_runLoops = NULL; // omit some code... __CFSocketUnlock(s); // Do this after the socket unlock to avoid deadlock (10462525)for(idx = CFArrayGetCount(runLoops); idx--;) { CFRunLoopWakeUp((CFRunLoopRef)CFArrayGetValueAtIndex(runLoops, idx)); } // CFRelease(runLoops); // omit some code... }else {
        __CFSocketUnlock(s);
    }
    __CFUnlock(&__CFAllSocketsLock);
    CFRelease(s);
}
Copy the code

The only place CFSocketInvalidate uses CFRunLoopWakeUp is for the last runloops operation. But the CFRunLoopRef is still in the array, it’s being strongly referenced by the array, how can it be released in the CFRunLoopWakeUp?

Note that CFSocketInvalidate iterates over Runloops outside the lock, indicating that CFSocket probably did not manage its Runloops array properly, resulting in the array being freed during iteration. Do this after the socket unlock to avoid deadlock (10462525) Apple’s Bug Report is not public, and only a potentially relevant discussion can be found here: Bug #10462525.

Most likely, it appears in __CFSocketCancel. When the runloop stops, the remove source operation is also performed. In CFRunLoopRemoveSource, the cancel function of source0 is executed, which is __CFSocketCancel:

void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) \
    CHECK_FOR_FORK();
    Boolean doVer0Callout = false.doRLSRelease = false;
    __CFRunLoopLock(rl);
    if(modeName == kCFRunLoopCommonModes) {// Omit code... }else {
	CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
	if(NULL ! = rlm && ((NULL ! = rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL ! = rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) { CFRetain(rls); // omit code...if (0 == rls->_context.version0.version) {
	        if(NULL ! = rls->_context.version0.cancel) {doVer0Callout = true; }}doRLSRelease = true; } // omit code... } } __CFRunLoopUnlock(rl);if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName);	/* CALLOUT */
    }
    if (doRLSRelease) CFRelease(rls);
}
Copy the code

__CFSocketCancel source code:

static void __CFSocketCancel(void *info, CFRunLoopRef rl, CFStringRef mode) {
    CFSocketRef s = (CFSocketRef)info;
    __CFSocketLock(s);
    if(0 == s->_socketSetCount) {// omit code...if(NULL ! = s->_runLoops) {// Remove this runloop from the runloop array; CFMutableArrayRef runLoopsOrig = s->_runLoops; CFMutableArrayRef runLoopsOrig = s->_runLoops CFMutableArrayRef runLoopsCopy = CFArrayCreateMutableCopy(kCFAllocatorSystemDefault, 0, s->_runLoops); idx = CFArrayGetFirstIndexOfValue(runLoopsCopy, CFRangeMake(0, CFArrayGetCount(runLoopsCopy)), rl);if(0 <= idx) CFArrayRemoveValueAtIndex(runLoopsCopy, idx); s->_runLoops = runLoopsCopy; //CFRunloop release operation 2 CFRelease(runLoopsOrig); } __CFSocketUnlock(s); }Copy the code

__CFSocketCancel also has a release on the CFRunloopRef, plus the two CFSocketInvalidate entries for a total of three releases.

Therefore, if __CFSocketCancel and CFSocketInvalidate are executed at the same time in multiple threads, it is possible to overfree the RUNloops array in the CFSocket, thus causing the CFRunLoopRef to be freed when traversing runloops. Although the probability of crash is relatively low, it will occur steadily at intervals in the project.

So, instead of adding a lock, CFSocketInvalidate should add a retain before iterating through the groups to ensure security.

The solution

  • Since it is a bug in CFSocket, it can only be avoidedCFSocketInvalidateandCFRunloopStopMultithreaded execution of code.
  • If your socket is only running in this thread, call itCFRunloopStopRunloop will automatically clean up all sources.
  • If the thread needs to be reused, it does not need to stop. Instead, it stops the socket and creates a new socket in the same thread.

Automatic stop Runloop

So, if you change the stop code to this, you should be fine, right?

- (void)runThread {
    @autoreleasepool {
        self.currentRunloop = CFRunLoopGetCurrent();
        [self addRunloopSource];
        [self addSocketSource];
        
        CFRunLoopRun();
    }
    NSLog(@"Thread exit");
}

- (void)stopThread {
    if(_currentRunloop) {// Ensure that the removeSocketSource operation is executed only here, without multithreading [self removeSocketSource]; CFRunLoopStop(_currentRunloop); self.currentRunloop = NULL; }}Copy the code

Unfortunately, it’s not safe to write that.

The reason for this is that after removeSocketSource, the Source in the Runloop will be empty. If the Runloop detects an empty source, it will automatically stop the Runloop and destroy the thread.

So if you call stopThread in another thread, the thread will stop at any time after removeSocketSource, and the runloop may have been released when CFRunLoopStop was called.

The crash of a car crash

- (void)stopThread {
    if(_currentRunloop) { [self removeSocketSource]; // Insert a time-consuming operation sleep(2); // Must crash CFRunLoopStop(_currentRunloop); self.currentRunloop = NULL; }}Copy the code

Add a retain operation to the runloop and you’ll be fine:

- (void)runThread {@autoReleasepool {// Perform a retain operation self.currentRunloop = CFRetain(CFRunLoopGetCurrent()); [self addRunloopSource]; [self addSocketSource]; CFRunLoopRun(); } NSLog(@"Thread exit");
}

- (void)stopThread {
    if(_currentRunloop) { [self removeSocketSource]; CFRunLoopStop(_currentRunloop); CFRelease(_currentRunloop); self.currentRunloop = NULL; }}Copy the code

conclusion

Be careful when using runloop sources, especially during the stop phase. Other sources may have similar problems.

When a variable has multiple operations, the operation outside the lock, even if it is read-only, is not safe. It is recommended to perform a retain operation before reading the variable to prevent it from being released during reading.