一個未識別的 selector 令 Messages.app 崩潰 15 次
One unrecognized selector killed Messages.app 15 times
BlueBubbles 的 typing 請求把查找用的 pseudo-GUID 傳入發送路徑,令 Messages.app 當日崩潰 15 次,伺服器記錄 16 次崩潰偵測。作者在自家 adapter 加入檢查,遇到該 GUID 時跳過 typing 請求,不發出 POST 或 DELETE。日誌有 8,496 次 typing 請求,但同一 GUID 的讀取及刷新沒有觸發崩潰。
On November 1, 2026, macOS 26.6.1, my Messages app closed and reopened itself fifteen times while I was texting with my agent. Fifteen crash reports named Messages under ~/Library/Logs/DiagnosticReports, stamped 12:19, 12:20, 17:47, then a run between 20:04 and 21:35. The server that injects into that app logged sixteen "Detected DYLIB crash" events that same day. Older reports go back to July 22, twice that month, so this fault mode recurs, it just needed one bad sequence per day to fire.
The symptom looked like an outage and wasn't one. No hang, no timeout, nothing dropped on the floor. The process aborted, the plugin detected the force quit, killed and relaunched the app, the helper reconnected. Delivery stayed healthy around the crashes, sends at 21:24 and 21:33 both logged "Message match found" and "Delivered message from [You]". So the pipeline worked while the app died, every few minutes, all afternoon.
What the logs actually said
Every report carried the same header fields. parentProc and responsibleProc both pointed at BlueBubbles, usedImages[0] was its Private API helper, BlueBubblesHelper.dylib loaded inside the dying process. The exception block read:
EXC_CRASH / SIGABRT
asi.libsystem_c.dylib: ["abort() called"]
reason: '-[IMTypingChatItem index]: unrecognized selector sent to instance 0x…'
One ObjC message dispatch, one missing method, one dead app. Nobody filed a ticket, nobody changed a setting, my agent's own typing indicator requests did it. Every /typing call that day targeted the pseudo-GUID any;-;<phone>, 677 occurrences in the current log, 8,496 across both rotated logs, zero other GUID shapes anywhere. That string is BlueBubbles' lookup handle for "find the thread with this person", not an addressable conversation, and it had been riding through the adapter since the webhook handed it over.
The dylib creates an IMTypingChatItem as the pending last item of whatever chat you name. Next text send runs sendMessage: inside getMessageItem::completionBlock, which asks that pending item for its index. A real chat item answers, a typing item has no such selector, Objective-C raises NSInvalidArgumentException, nothing catches it inside the injected frame, libsystem_c calls abort(). The app goes down and the plugin restarts it.
What it wasn't
I ruled these out with proof before touching anything. Not an auth or identity outage, identityservicesd and imagent were alive since Oct 19. Not Apple's servers, courier.push.apple.com resolved and raw TCP probes to their push endpoints on ports 5223 and 443 both connected. Not the read path either, /read fired eight times against that same pseudo-GUID without crashing once. Even the typing refresh loop survived, sixty refreshes per minute between 12:06 and 12:15 with no abort at all.
The trigger is one precise overlap: the first text send issued while a pending typing item exists in a chat that was never a real thread. Refreshes alone are safe. Reads are safe. Send plus pending-typing-item-plus-lookup-GUID is fatal, sixteen detections that day behind fifteen identical headers.
That precision matters because the obvious blame lands on the wrong party. The integration is open source, well maintained, its other paths run clean against the same GUID hundreds of times. The caller handed it a lookup-shaped ID where a mutation path wanted a real one. Two kinds of IDs travel through one parameter, the crash report doesn't say which kind arrived, the header fields name the process, not the argument.
The fix and its proof
A constant and a check in my own adapter, comment naming the mechanism so future readers get the why. If the resolved GUID starts with any;-;, skip send_typing entirely, issue neither POST nor DELETE. Read receipts, tapback reactions, reply threading and text or media sends stay untouched, they were never the problem.
Verification ran offline with the client replaced by a recorder:
guid=lookup action=typing -> fired=False recorded_calls=[]
guid=lookup action=read -> fired=True recorded_calls=['post']
guid=real-thread action=typing -> fired=True recorded_calls=['post']
RESULT: PASS
The repo's own suite went 27 passed, 1 failed after the edit, and I checked that failure before shipping anything: it reads zero references to this adapter, trips a real-home I/O guard on a production checkout, an unrelated platform's twin test fails identically on unedited code, pre-existing artifact, not caused by this change.
Back to the numbers from that day. 8,496 typing requests riding through one lookup-shaped parameter produced fifteen app deaths and sixteen detected crashes, while eight read calls and thousands of refreshes against the same parameter produced nothing. Same argument, different call, opposite outcome.
What generalizes
Three rules I'm taking off this one.
Lookup handles must never enter mutation paths. A GUID shaped for finding has no contract with the object graph behind it. If your API takes "an ID", there are usually two kinds of IDs, and only one survives every method call. Guard at the boundary where you know which shape arrived, don't blame the integration when the caller handed over the wrong kind.
Message dispatch turns type errors into process death. In Python, calling a dict method on a str raises inside your try block, you catch it, the host keeps living. In ObjC, sending an unrecognized selector to the wrong instance throws across frames that were never designed as boundaries, injected dylib code has no landing pad. The cost is concrete. A receiver mismatch doesn't fail in your function, it aborts the process hosting everything around it.
Crash reports carry the culprit before anyone files. parentProc, responsibleProc, usedImages[0] answered who loaded what into whom, no debugger attached, no reproduction hunt. Fifteen identical headers pointed at one injected library in one process, the parameter value came from correlating 8,496 requests against zero other shapes.
The counterintuitive part is that everything healthy stayed healthy. Status codes said fine, deliveries landed, the app reopened within seconds, sixteen crashes in one afternoon read as noise until someone counted them. The one thing nobody measured was whether the chat ID reaching the mutation call was ever a real thread.
What would you have blamed first? If you've traced a recurring app crash to one parameter shape instead of an outage, I'd like to see how you found it, the .ips headers did half the work here for free.
來源:DEV Community · dev.to

