Summary
On macOS the desktop client segfaults inside NSPasteboard, killing the whole process. Sync
stops silently: the app is gone, there is no notification, and the client's own log is truncated
on the next start, so nothing is left to explain it.
The clipboard monitor polls NSPasteboard from a background Python thread. AppKit's pasteboard
APIs are not safe to call off the main thread, and the crash lands in AppKit's own bookkeeping for
pasteboard ownership changes.
Version: ClipCascade Desktop v3.2.0 (macOS build), against a self-hosted server
OS: macOS 26.5.2 (25F84), Apple Silicon
What happened
The client had been connected for hours. At 19:54:54 its websocket session ended and the process
died. It never came back on its own, and nothing else on the machine was disturbed — DNS traffic
shows the Mac was awake the whole time. I only found the cause because macOS wrote a crash report.
Crash
~/Library/Logs/DiagnosticReports/ClipCascade-2026-09-06-195526.ips
exception: EXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS
termination: Segmentation fault: 11
captureTime: 2026-09-06 19:54:54.7743
Faulting thread:
libobjc.A.dylib objc_retain
CoreFoundation -[__NSFastEnumerationEnumerator initWithObject:]
Foundation -[NSConcreteMapTable keyEnumerator]
AppKit -[_NSPasteboardOwnersCollection handleOwnershipChange]
AppKit -[NSPasteboard changeCount]
_native.cpython-311-darwin.so pasteboard_get_file_urls
Python _PyEval_EvalFrameDefault
Python thread_run
libsystem_pthread.dylib _pthread_start
Where it comes from
ClipCascade_Desktop/src/clipboard/clipboard_monitor_mac.py runs _runner on a daemon thread
started in _start, and that loop calls pb_files.get_file_urls(diff=True) every 0.3s. That is
the pasteboard_get_file_urls frame above. -[NSPasteboard changeCount] walks
_NSPasteboardOwnersCollection, which AppKit mutates from the main thread whenever pasteboard
ownership changes — another app taking the clipboard, for instance. Enumerating that collection
concurrently is what releases an object out from under objc_retain.
Two things make it hard to live with:
_pasteboard_lock serializes ClipCascade's own calls, but the race is against AppKit's main
thread, which does not take that lock. The lock cannot help here.
- The
except Exception around the loop cannot catch a SIGSEGV, so a fault in the polling thread
takes the process down rather than just the monitor.
It is timing-dependent, so it is rare — but at a 0.3s poll it eventually lands.
Reproducing
I have not found a deterministic trigger; it needs the poll to coincide with an ownership change.
Copying files repeatedly from Finder while another app also writes to the clipboard is the
likeliest way to provoke it. On my machine it has produced three crash reports in three days, all
with the same faulting frames.
Setting enable_file_sharing: false avoids the get_file_urls path, at the cost of the feature.
I have not run long enough that way to say whether get_contents faults the same way; the same
changeCount call sits underneath it, so I would expect it can.
Suggested fix
Call the pasteboard APIs on the main thread. Keeping the 0.3s cadence but hopping each read onto
the main run loop — via PyObjC's performSelectorOnMainThread: or an equivalent — would keep the
polling design and remove the race. Failing that, running the reads inside an autorelease pool on
a thread AppKit knows about would be closer to what AppKit expects than a bare Python thread.
Happy to test a build, or to collect the full .ips files if they would help.
Summary
On macOS the desktop client segfaults inside
NSPasteboard, killing the whole process. Syncstops silently: the app is gone, there is no notification, and the client's own log is truncated
on the next start, so nothing is left to explain it.
The clipboard monitor polls
NSPasteboardfrom a background Python thread. AppKit's pasteboardAPIs are not safe to call off the main thread, and the crash lands in AppKit's own bookkeeping for
pasteboard ownership changes.
Version: ClipCascade Desktop v3.2.0 (macOS build), against a self-hosted server
OS: macOS 26.5.2 (25F84), Apple Silicon
What happened
The client had been connected for hours. At 19:54:54 its websocket session ended and the process
died. It never came back on its own, and nothing else on the machine was disturbed — DNS traffic
shows the Mac was awake the whole time. I only found the cause because macOS wrote a crash report.
Crash
~/Library/Logs/DiagnosticReports/ClipCascade-2026-09-06-195526.ipsWhere it comes from
ClipCascade_Desktop/src/clipboard/clipboard_monitor_mac.pyruns_runneron a daemon threadstarted in
_start, and that loop callspb_files.get_file_urls(diff=True)every 0.3s. That isthe
pasteboard_get_file_urlsframe above.-[NSPasteboard changeCount]walks_NSPasteboardOwnersCollection, which AppKit mutates from the main thread whenever pasteboardownership changes — another app taking the clipboard, for instance. Enumerating that collection
concurrently is what releases an object out from under
objc_retain.Two things make it hard to live with:
_pasteboard_lockserializes ClipCascade's own calls, but the race is against AppKit's mainthread, which does not take that lock. The lock cannot help here.
except Exceptionaround the loop cannot catch a SIGSEGV, so a fault in the polling threadtakes the process down rather than just the monitor.
It is timing-dependent, so it is rare — but at a 0.3s poll it eventually lands.
Reproducing
I have not found a deterministic trigger; it needs the poll to coincide with an ownership change.
Copying files repeatedly from Finder while another app also writes to the clipboard is the
likeliest way to provoke it. On my machine it has produced three crash reports in three days, all
with the same faulting frames.
Setting
enable_file_sharing: falseavoids theget_file_urlspath, at the cost of the feature.I have not run long enough that way to say whether
get_contentsfaults the same way; the samechangeCountcall sits underneath it, so I would expect it can.Suggested fix
Call the pasteboard APIs on the main thread. Keeping the 0.3s cadence but hopping each read onto
the main run loop — via PyObjC's
performSelectorOnMainThread:or an equivalent — would keep thepolling design and remove the race. Failing that, running the reads inside an autorelease pool on
a thread AppKit knows about would be closer to what AppKit expects than a bare Python thread.
Happy to test a build, or to collect the full
.ipsfiles if they would help.