tauri-apps / tauri-apps/plugins-workspace
Clipboard operations crash on macOS due to thread safety violation
- Dominant language
- Rust
- Stars
- 1.8k
- Forks
- 602
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 9
Description
### Describe the bug
# [Bug] Clipboard operations crash on macOS due to thread safety violation
## Describe the bug
The application crashes with `EXC_BAD_ACCESS (SIGSEGV)` on macOS when using `writeText()` from `@tauri-apps/plugin-clipboard-manager`. The crash occurs due to a race condition between the clipboard plugin (running on a tokio worker thread) and WebKit's pasteboard monitoring (running on the main thread).
## Reproduction
1. Create a Tauri 2.x application with `tauri-plugin-clipboard-manager`
2. Call `writeText()` from the frontend while the WebView is active
3. The app may crash intermittently, especially under frequent clipboard operations
## Expected behavior
Clipboard operations should complete without crashing the application.
## Platform and versions
| Component | Version |
| ---------------------------------------- | -------------------------------------------- |
| **OS** | macOS 15.6.1 (Sequoia) |
| **Architecture** | Apple Silicon (running x86_64 via Rosetta 2) |
| **Tauri** | 2.9.3 |
| **tauri-plugin-clipboard-manager** | 2.3.2 |
| **arboard** | 3.x |
| **Rust** | 1.77.2+ |
## Crash Report
### Exception Info
```
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x00006e227e7baba0
Termination Reason: SIGNAL 11 Segmentation fault: 11
```
### Thread 0 (Main Thread - WebKit)
```
0 libobjc.A.dylib objc_msgSend + 16
1 libobjc.A.dylib +[NSObject self] + 0
2 AppKit -[NSPasteboard(NSInternal) _pasteboardWithName:uniqueId:] + 264
3 AppKit +[NSPasteboard pasteboardWithName:] + 44
4 WebKit WebKit::WebPasteboardProxy::getPasteboardChangeCount(...) + 52
5 WebKit WebKit::WebPasteboardProxy::didReceiveSyncMessage(...) + 1204
...
14 JavaScriptCore WTF::RunLoop::performWork() + 204
...
27 libdyld.dylib start + 1
```
### Thread 11 (tokio-runtime-worker - Concurrent)
```
0 libsystem_kernel.dylib mach_msg2_trap + 8
...
12 arboard arboard::platform::osx::Set::text + 492
13 arboard arboard::Clipboard::set_text + 32
14 tauri_plugin_clipboard_manager tauri_plugin_clipboard_manager::desktop::Clipboard::write_text + 136
...
```
## Root Cause Analysis
### 1. NSPasteboard is NOT thread-safe
From Apple's documentation:
> All AppKit objects, including NSPasteboard, should only be accessed from the main thread.
### 2. Tauri async commands run on tokio worker threads
```rust
// commands.rs
#[command]
pub(crate) async fn write_text(
_app: AppHandle,
clipboard: State<'_, Clipboard>,
text: &str,
...
) -> Result<()> {
clipboard.write_text(text) // ← Executed on tokio worker thread
}
```
### 3. arboard directly calls NSPasteboard
```rust
// desktop.rs
pub fn write_text<'a, T: Into>>(&self, text: T) -> crate::Result<()> {
match &self.clipboard {
Ok(clipboard) => clipboard
.lock()
.unwrap()
.as_mut()
.unwrap()
.set_text(text) // → arboard::Clipboard::set_text()
// → NSPasteboard::writeObjects() on worker thread!
.map_err(Into::into),
...
}
}
```
### 4. WebKit monitors clipboard on main thread
WebKit's `WebPasteboardProxy::getPasteboardChangeCount()` periodically checks the clipboard from the main thread to implement:
- `paste` events
- `navigator.clipboard` API
- Clipboard permission checks
### Race Condition Timeline
```
T0: Frontend calls writeText()
T1: Tauri IPC receives request, schedules to tokio runtime
T2: tokio worker (Thread 11) executes:
→ clipboard.write_text()
→ arboard::Clipboard::set_text()
→ NSPasteboard::writeObjects() ← On worker thread!
T3: Simultaneously, WebKit (Thread 0) executes:
→ WebPasteboardProxy::getPasteboardChangeCount()
→ Accesses NSPasteboard internal state
T4: Race condition! Both threads access NSPasteboard concurrently
T5: Thread 0 accesses memory being modified by Thread 11
T6: SIGSEGV at invalid address
```
## AI Suggested Fix
Dispatch clipboard operations to the main thread on macOS:
```rust
#[cfg(target_os = "macos")]
use dispatch::Queue;
impl Clipboard {
pub fn write_text<'a, T: Into>>(&self, text: T) -> crate::Result<()> {
#[cfg(target_os = "macos")]
{
let text_owned = text.into().into_owned();
// Execute clipboard operation on main thread
Queue::main().exec_sync(move || {
### Reproduction
_No response_
### Expected behavior
_No response_
### Full `tauri info` output
```text
[✔] Environment
- OS: Mac OS 26.2.0 arm64 (X64)
✔ Xcode Command Line Tools: installed
✔ Xcode: 26.1.1
✔ rustc: 1.92.0 (ded5c06cf 2025-12-08)
✔ cargo: 1.92.0 (344c4567c 2025-10-21)
✔ rustup: 1.28.2 (e4f3ad6f8 2025-04-28)
✔ Rust toolchain: stable-aarch64-apple-darwin (default)
- node: 20.19.1
- pnpm: 10.14.0
- npm: 10.8.2
[-] Packages
- tauri 🦀: 2.9.5
- tauri-build 🦀: 2.5.3
- wry 🦀: 0.53.5
- tao 🦀: 0.34.5
- @tauri-apps/api ⱼₛ: 2.9.0 (outdated, latest: 2.9.1)
- @tauri-apps/cli ⱼₛ: 2.9.3 (outdated, latest: 2.9.6)
[-] Plugins
- tauri-plugin-single-instance 🦀: 2.3.6, (outdated, latest: 2.3.7)
- @tauri-apps/plugin-single-instance ⱼₛ: not installed!
- tauri-plugin-os 🦀: 2.3.2
- @tauri-apps/plugin-os ⱼₛ: 2.3.2
- tauri-plugin-updater 🦀: 2.9.0
- @tauri-apps/plugin-updater ⱼₛ: 2.9.0
- tauri-plugin-window-state 🦀: 2.4.1
- @tauri-apps/plugin-window-state ⱼₛ: 2.4.1
- tauri-plugin-deep-link 🦀: 2.4.5, (outdated, latest: 2.4.6)
- @tauri-apps/plugin-deep-link ⱼₛ: 2.4.5 (outdated, latest: 2.4.6)
- tauri-plugin-http 🦀: 2.5.4, (outdated, latest: 2.5.5)
- @tauri-apps/plugin-http ⱼₛ: 2.5.4 (outdated, latest: 2.5.5)
- tauri-plugin-store 🦀: 2.4.1, (outdated, latest: 2.4.2)
- @tauri-apps/plugin-store ⱼₛ: 2.4.1 (outdated, latest: 2.4.2)
- tauri-plugin-dialog 🦀: 2.4.2, (outdated, latest: 2.5.0)
- @tauri-apps/plugin-dialog ⱼₛ: 2.4.2 (outdated, latest: 2.5.0)
- tauri-plugin-websocket 🦀: 2.4.1, (outdated, latest: 2.4.2)
- @tauri-apps/plugin-websocket ⱼₛ: 2.4.1 (outdated, latest: 2.4.2)
- tauri-plugin-fs 🦀: 2.4.4, (outdated, latest: 2.4.5)
- @tauri-apps/plugin-fs ⱼₛ: 2.4.4 (outdated, latest: 2.4.5)
- tauri-plugin-opener 🦀: 2.5.2, (outdated, latest: 2.5.3)
- @tauri-apps/plugin-opener ⱼₛ: 2.5.2 (outdated, latest: 2.5.3)
- tauri-plugin-clipboard-manager 🦀: 2.3.2
- @tauri-apps/plugin-clipboard-manager ⱼₛ: 2.3.2
- tauri-plugin-global-shortcut 🦀: 2.3.1
- @tauri-apps/plugin-global-shortcut ⱼₛ: 2.3.1
- tauri-plugin-upload 🦀: 2.3.2, (outdated, latest: 2.4.0)
- @tauri-apps/plugin-upload ⱼₛ: 2.3.2 (outdated, latest: 2.4.0)
- tauri-plugin-process 🦀: 2.3.1
- @tauri-apps/plugin-process ⱼₛ: 2.3.1
```
### Stack trace
```text
```
### Additional context
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.