Mac app sandboxing interferes with drag & drop

Failed to get a sandbox extension

Right from there, you know you’re going to have a bad day. šŸ˜”

Then you try to actually use the file dropped on your app, and you get:

Upload preparation for claim 1C0F9013-4DEB-4E5D-8896-F522AA979BA6 completed with error: Error Domain=NSCocoaErrorDomain Code=513 "ā€œExample.jpgā€ couldnā€™t be copied because you donā€™t have permission to access ā€œCoordinatedZipFilejxc2lCā€." UserInfo={NSSourceFilePathErrorKey=/Users/SadPanda/Pictures/Example.jpg, NSUserStringVariant=(
    Copy
), NSDestinationFilePath=/Users/SadPanda/Library/Containers/com.me.MyApp/Data/tmp/CoordinatedZipFilejxc2lC/Example.jpg, NSFilePath=/Users/SadPanda/Pictures/Example.jpg, NSUnderlyingError=0x600000ad0cf0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}

There’s a variety of ways to run afoul of this. Here’s a particular one, for illustration.

Noticing when drag & drop operations start

Say you want to have a drop zone in your app that draws attention to itself whenever the user starts dragging a relevant file. Merely intelligent, courteous UI.

There’s no built-in facility for this – both AppKit and SwiftUI drag & drop APIs only start functioning once the dragged item enters the drop zone itself (the view, or at best window, that you’ve made a drop receiver).

There is a convention on how to work around this – you can deduce that a drag is occurring by watching mouse events and the drag pasteboard.

Drag & drop implementation detail

macOS uses pasteboards to share data between applications (represented in AppKit by NSPasteboard), not just for copy & paste as you’re probably already familiar, but for drag & drop as well, among several other things. You can also create your own custom pasteboards, namespaced to be completely independent of the ‘built-in’ ones, for your own purposes.

Pasteboard implementation detail

The standard system pasteboards are always accessible to your application, even when they’re nominally not relevant (such as when no drag is actually occurring). In fact the contents of the last drag are left indefinitely on the pasteboard (by default, unless an app explicitly clears it). Which is an annoyance as it complicates the followingā€¦

Deducing what’s going on by spying on global mouse events

When you see a drag event you don’t immediately know anything more than that the mouse moved with the left mouse button held down. That can mean anything – the user might be pulling out a selection rectangle, or drawing in a graphics application, or just randomly dragging the mouse around.

Since the contents of the drag pasteboard are left there indefinitely after a drag concludes, you can’t just use the existence of something on the drag pasteboard as an indication that a drag is in progress.

However, pasteboards have a “change count”. What this is actually counting is ill-defined and seemingly not about the actual contents of the pasteboard, but in a nutshell it can be used to mean essentially that – the count might change even if the contents don’t change, but it seems it will always change if the contents do change. So, some false positives, but no false negatives, which is the important thing.

So, when you see a mouse drag event, you can look at the drag pasteboard’s change count and see if it changed since the last mouse drag event. If it did, that’s a pretty strong suggestion – although admittedly not a guarantee – that the user is performing a drag & drop operation.

There is a race condition here. Mouse events are handled by the WindowServer which runs at very high priority and completely asynchronous to app activity like loading up the pasteboard with dragged items – and it does actually take some time for an app like the Finder to populate the pasteboard when the drag is initiated. Only tens of milliseconds, typically, but that’s an eon in computer terms.

So you might observe the key events – the pasteboard changing and the first mouse down of a drag – in any order.

Fortunately, it’s rare to lose the race in a way that matters, because you’ll get a mouse dragged event virtually every time the mouse moves, even if just by a single pixel. So even if you get the first mouse dragged event before the source app has actually populated the pasteboard – which is in fact common, in my experience – you’ll invariably get a bunch more practically immediately as the user continues the drag. Sooner or later the pasteboard will be updated, so you’ll eventually notice. Technically you’re late in recognising that a drag & drop operation has started, but in general the delay is imperceptible.

The essential code is:

let dragPasteboard = NSPasteboard(name: .drag)
var lastDragPasteboardChangeCount: Int = dragPasteboard.changeCount

let mouseDragWatcher = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDragged],
                                                         handler: { event in
    let currentChangeCount = dragPasteboard.changeCount

    if lastDragPasteboardChangeCount != currentChangeCount {
        // The user very likely just started dragging something.
    }

    lastDragPasteboardChangeCount = currentChangeCount
}

let mouseUpWatcher = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseUp],
                                                       handler: { event in
    // The drag ended.
}

Note that I’ve omitted various ancillary details, such as avoiding strong retains of self (where applicable), error handling (addGlobalMonitorForEvents can return nil), removing the monitors when you’re done with them, etc.

Also, NSEvent‘s monitoring implicitly relies on RunLoop, so actually using the above – and actually getting your handler called – is predicated on having a suitable runloop going (in my experience it has to be the main runloop, but maybe it’s possible to use a different runloop in the right mode – presumably eventTracking). In a typical GUI application that’s all set up for you automagically, but in other cases you have to do it yourself – refer to this for more details and suggestions.

And the above code works fine as presented. The problem arises if & when you start actually looking at the pasteboard during the dragā€¦

You get only one look at the dragged files

Files, when dragged, are represented mainly as their paths (technically, NSURLs). That’s the “public.file-url” UTI (and the Apple URL pasteboard type for backwards compatibility). Though you’ll also see a bunch of other types reported if you ask the pasteboard what it’s carrying, e.g.:

Apple URL pasteboard type
CorePasteboardFlavorType 0x6675726C
NSFilenamesPboardType
com.apple.finder.node
dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
public.file-url

Merely inspecting the UTIs in the pasteboard is fine – that doesn’t interfere with anything. So if all you care about is if any kind of file (or folder) is being dragged, you’re set. But if you want to only react to some types of files or folders, you need to know more.

If you ask for the URL – even without actually using it – you trigger some behind the scenes activity involving app sandboxing. This prevents the file being made accessible to your app if & when it actually is dropped into your app.

When things are working correctly, when a file is dragged and dropped onto a receptive view in your app a link to that file is created inside your own app’s container. It’s that link that you actually have access to – the original file cannot be accessed directly. That link seems to persist for a while – perhaps until your app is quit – so once you have it you’re set.

I don’t know why merely peeking at the file path (URL) prevents this link being created, but it does.

Sigh.

FB13520048.

Failed workarounds

Sadly the only meaningful existing documentation of this problem that Bing or Google can find is this one StackOverflow question. Which is unresolved – the claimed hacks & workarounds don’t actually work, at least not anymore.

Supposedly on some older versions of macOS you could create a bookmark of the file (presumably a security-scoped one) and that would implicitly secure your access to it. But that seems pretty clearly to no longer work – assuming it ever did – because you can’t create a bookmark for files you can’t read, and until the drop occurs you don’t have read access to the file.

Similarly, you can supposedly use startAccessingSecurityScopedResource to indefinitely lock in your access to the file, but this doesn’t apply if you don’t have access to it to begin with. So, again, not useful here.

Partial workaround

While I knew that using canReadObject(forClasses: [NSImage.self]) ran afoul of this, I happened to notice there’s a logically equivalent method on NSImage itself – NSImage.canInit(with:) – which I tried, and lo and behold it works! No sandboxing issues.

I disassembled it (thanks Hopper!) and saw that it’s basically just doing some UTI pre-checks – already known to be safe – to see if there’s a file URL in the pasteboard, and then it fetches that file URL using readObjects(forClasses: [NSURL.self]), the same method that if you or I call results in this bug surfacing. The difference is it uses a secret reading option key – “NSPasteboardURLReadingSecurityScopedFileURLsKey”! Apparently that’s the magic sauce which allows you to read the URL without triggering the bug. You still can’t actually access the file, but at least you can e.g. examine its file extension (which is essentially what NSImage.canInit(with:) does).

if let URLs = dragPasteboard.readObjects(forClasses: [NSURL.self],
                                         options: [NSPasteboard.ReadingOptionKey(rawValue: "NSPasteboardURLReadingSecurityScopedFileURLsKey") : kCFBooleanTrue]) {
    for u in URLs {
        // Go nuts!
    }
}

Incidentally, I noticed it also calls startAccessingSecurityScopedResource but in my testing it always gets a failure response from it too. So that apparently is irrelevant.


Addendum: Michael Tsai’s Blog

I was reading through articles in NetNewsWire, including a few of the latest from Michael Tsai, and for one in particular I thought, “this sounds really familiar for some reason”. Hah, because he was quoting part of this post.

I’m very flattered to be linked to by Michael – I have no idea how he found my obscure website to begin with – as his blog is one of my favourite Apple developer news / discussion feeds. He does such a great job of collecting valuable links and collating them together on timely and/or valuable topics. His own commentary – if he adds any – is concise and valuable. If you’re not following his blog, you should rectify that. šŸ™‚

I also now wish I’d written this post better. šŸ˜

1 thought on “Mac app sandboxing interferes with drag & drop”

Leave a Comment