Skip to content

Clipboard operations crash on macOS due to thread safety violation #3205

Open

Description

@NEOcjz

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<R>::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

// commands.rs
#[command]
pub(crate) async fn write_text<R: Runtime>(
    _app: AppHandle<R>,
    clipboard: State<'_, Clipboard<R>>,
    text: &str,
    ...
) -> Result<()> {
    clipboard.write_text(text)  // ← Executed on tokio worker thread
}

3. arboard directly calls NSPasteboard

// desktop.rs
pub fn write_text<'a, T: Into<Cow<'a, str>>>(&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:

#[cfg(target_os = "macos")]
use dispatch::Queue;

impl<R: Runtime> Clipboard<R> {
    pub fn write_text<'a, T: Into<Cow<'a, str>>>(&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


Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions