Skip to content

macOS: SIGSEGV in NSPasteboardItem stringForType: during pasteboardItems() iteration when external app mutates pasteboard mid-read #218

@louis030195

Description

@louis030195

Summary

Clipboard::get_text() (and friends that go through string_from_type) on macOS can SIGSEGV during the per-item stringForType: iteration when another process mutates the pasteboard at the same time. The crash lands in objc_msgSend after _updateTypeCacheIfNeeded deref's a freed Objective-C pointer.

Environment

  • arboard 3.6.1 (latest)
  • macOS 26.5 (build 25F5068a — beta seed)
  • Apple Silicon (Mac16,6)
  • Crash thread name: clipboard-capture (a worker thread the embedding app routes Cmd+C/X/V events to via a serial dispatch queue)

What I see

Thread 106 Crashed:: clipboard-capture
Exception: EXC_BAD_ACCESS (SIGSEGV) — KERN_INVALID_ADDRESS at 0x0071fae168e6b598

0  libobjc.A.dylib    objc_msgSend + 32
1  AppKit             -[NSPasteboard _updateTypeCacheIfNeeded] + 1064
2  AppKit             -[NSPasteboard _typesAtIndex:combinesItems:] + 36
3  AppKit             -[NSPasteboard _canRequestDataForType:index:usesPboardTypes:combinesItems:] + 156
4  AppKit             -[NSPasteboard _dataForType:index:usesPboardTypes:combinesItems:securityScoped:] + 236
5  AppKit             -[NSPasteboardItem __dataForType:async:completionHandler:] + 316
6  AppKit             -[NSPasteboardItem stringForType:] + 28
7  app                objc2_app_kit::generated::__NSPasteboardItem::NSPasteboardItem::stringForType
8  app                arboard::platform::osx::Clipboard::string_from_type + 292
9  app                cidre::blocks::Layout1Mut<Closure>::invoke0 + 88
10 libdispatch.dylib  _dispatch_client_callout + 16
11 libdispatch.dylib  _dispatch_lane_barrier_sync_invoke_and_complete + 56

The bad address 0x0071fae168e6b598 looks like a freed Objective-C reference whose memory was reused — _updateTypeCacheIfNeeded is iterating through cached type entries and one of them no longer points to a live object.

Why I think it happens

src/platform/osx.rs deliberately uses pasteboardItems() + per-item stringForType: instead of NSPasteboard.string(forType:) directly, with this comment explaining the choice:

// XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat
// multiple strings, if present, into one and return it instead of reading just the first which is
// `arboard`'s historical behavior.
let contents = unsafe { self.pasteboard.pasteboardItems() }
    .ok_or_else(|| Error::unknown("NSPasteboard#pasteboardItems errored"))?;

for item in contents {
    if let Some(string) = unsafe { item.stringForType(type_) } {
        return Ok(string.to_string());
    }
}

The NSPasteboardItem instances returned by pasteboardItems() reference into AppKit's type-cache. If another app calls setData:forType: on the general pasteboard between pasteboardItems() returning and the stringForType: call hitting the bad item, the cache is invalidated mid-iteration and the now-stale item pointer crashes.

The embedding app already serializes all clipboard reads through a single GCD serial queue with an autorelease pool wrapper — that prevents intra-process races, but cannot stop external apps (system clipboard manager, paste apps, etc.) from mutating the pasteboard at the same instant.

This crash signature has been observed at least twice in the wild on macOS 26.x:

  • earlier crash key 57E6EDAB-D2D1-44D3-9BD0-82DCA482DBFF
  • recent crash key 4D7ABCBF-D341-CA69-AD26-30FFB67F0BBC / incident 56416840-0903-4FAB-8869-5D471B78335C

Reproduction

I don't have a reliable repro yet — both observed crashes occurred after multi-hour sessions of normal Cmd+C/X/V activity. The pattern that seems to make it more likely: macOS 26.x (especially the 26.5 beta seed) + a third-party clipboard manager running concurrently. Happy to gather more data if there's a specific instrumented build you'd want to ship.

Proposed mitigation

The NSPasteboard.string(forType:) direct API would avoid the per-item iteration entirely — AppKit handles the cache internally and atomically. The trade-off, per the existing comment, is the "first string only" semantic vs. concatenated content.

For my use case (general clipboard tracking) the concatenated content is actually preferable. Would you be open to either:

  1. An opt-in API like Clipboard::get_text_concatenated() (or a GetExtApple extension) that uses string(forType:) directly, for callers who explicitly don't need the historical "first string" semantic, or
  2. An internal try-catch around the per-item iteration with a fallback to string(forType:) when the items become invalid?

Happy to send a PR for either if you have a preferred direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    O-AppleWork related to the macOS or iOS clipboardbugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions