<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/"
>

<channel>
	<title>NSPasteboard &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/nspasteboard/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Thu, 12 Sep 2024 23:08:48 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://wadetregaskis.com/wp-content/uploads/2016/03/Stitch-512x512-1-256x256.png</url>
	<title>NSPasteboard &#8211; Wade Tregaskis</title>
	<link>https://wadetregaskis.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">226351702</site>	<item>
		<title>NSPasteboard crashes due to unsafe, internal concurrent memory mutation when handling file promises</title>
		<link>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/</link>
					<comments>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Aug 2024 05:01:02 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[AppKit]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[memory corruption]]></category>
		<category><![CDATA[NSItemProvider]]></category>
		<category><![CDATA[NSPasteboard]]></category>
		<category><![CDATA[NSPasteboardItem]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8369</guid>

					<description><![CDATA[This is a public reposting of FB14885505, in case it&#8217;s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it. NSPasteboard mutates itself simultaneously from the main thread and the global concurrent Dispatch pool, w.r.t. to its internal type cache. This is surprisingly trivial&#8230; <a class="read-more-link" href="https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>This is a public reposting of FB14885505, in case it&#8217;s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it.</p>
</div></div>



<p><code><a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a></code> mutates itself simultaneously from the main thread and the global concurrent <a href="https://developer.apple.com/documentation/DISPATCH" data-wpel-link="external" target="_blank" rel="external noopener">Dispatch</a> pool, w.r.t. to its internal type cache. This is surprisingly trivial to reproduce (sample code below) by just dropping, e.g. a file promise (such as by opening a PNG in Preview, revealing the thumbnails sidebar, and then dragging the thumbnail onto the sample project’s window).</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">import</span><span style="color: #000000"> </span><span style="color: #267F99">SwiftUI</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">ContentView</span><span style="color: #000000">: View {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some View {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">Rectangle</span><span style="color: #000000">().</span><span style="color: #795E26">onDrop</span><span style="color: #000000">(</span><span style="color: #795E26">of</span><span style="color: #000000">: NSImage.</span><span style="color: #001080">imageTypes</span><span style="color: #000000">, </span><span style="color: #795E26">isTargeted</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">) { </span><span style="color: #001080">_</span><span style="color: #000000"> </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> pb = </span><span style="color: #795E26">NSPasteboard</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: .</span><span style="color: #001080">drag</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #001080">_</span><span style="color: #000000"> = pb.</span><span style="color: #001080">pasteboardItems</span><span style="color: #000000"> </span><span style="color: #008000">// Seems to be necessary for the crash.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #001080">_</span><span style="color: #000000"> = NSImage.</span><span style="color: #001080">imageTypes</span><span style="color: #000000"> </span><span style="color: #008000">// Not strictly necessary for the crash, but seems to make it more likely. 🤷‍♂️</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Judging from the callstack that runs in the concurrent pool, this is specific to file promises (and that seems to match my experience &#8211; it only crashes for some test cases, all of which involve file promises being present in the drag pasteboard at the time of the drop).</p>



<p>Since this bug causes semi-random memory corruption, it manifests in a large number of ways &#8211; not all of which are all that helpful.  But at least one case I’ve seen a few times is helpful, as it clearly shows the offending internal <code>NSPasteboard</code> code running concurrently with itself, e.g.:</p>



<pre class="wp-block-preformatted">Main queue / thread:
#0	0x00007ff80108dab5 in _platform_bzero$VARIANT$Haswell ()
#1	0x000000010df8774d in GuardMalloc_mallocInternal ()
#2	0x00007ff90792542f in stack_logging_lite_malloc ()
#3	0x00007ff800ea8733 in _malloc_zone_malloc_instrumented_or_legacy ()
#4	0x00007ff800f39a72 in _vasprintf ()
#5	0x00007ff800f16922 in asprintf ()
#6	0x00007ff80125655a in -[NSObject(NSObject) __dealloc_zombie] ()
#7	0x00007ff8020e039a in -[NSConcreteMapTable dealloc] ()
#8	0x00007ff804876983 in -[NSPasteboard _updateTypeCacheIfNeeded] ()
#9	0x00007ff8048763df in -[NSPasteboard _typesAtIndex:combinesItems:] ()
#10	0x00007ff804aad148 in NSCoreDragReceiveMessageProc ()
#11	0x00007ff807517b1a in CallReceiveMessageCollectionWithMessage ()
#12	0x00007ff8075124fa in DoMultipartDropMessage ()
#13	0x00007ff8075122ce in DoDropMessage ()
#14	0x00007ff8075159a9 in CoreDragMessageHandler ()
#15	0x00007ff8011d776b in __CFMessagePortPerform ()
#16	0x00007ff80113e5b7 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
#17	0x00007ff80113e4ee in __CFRunLoopDoSource1 ()
#18	0x00007ff80113d166 in __CFRunLoopRun ()
#19	0x00007ff80113c112 in CFRunLoopRunSpecific ()
#20	0x00007ff80bb55a09 in RunCurrentEventLoopInMode ()
#21	0x00007ff80bb55646 in ReceiveNextEventCommon ()
#22	0x00007ff80bb55561 in _BlockUntilNextEventMatchingListInModeWithFilter ()
#23	0x00007ff8047acc61 in _DPSNextEvent ()
#24	0x00007ff8050c0dc0 in -[NSApplication(NSEventRouting) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] ()
#25	0x00007ff80479e075 in -[NSApplication run] ()
#26	0x00007ff804771ff3 in NSApplicationMain ()
#27	0x00007ff90dc24557 in ___lldb_unnamed_symbol57096 ()
#28	0x00007ff90e31fe64 in ___lldb_unnamed_symbol104448 ()
#29	0x00007ff90e6e63ff in static SwiftUI.App.main() -&gt; () ()
#30	0x000000010dfa5cce in static NSPasteboardItem_CrashApp.$main() ()
#31	0x000000010dfa5d69 in main at /Users/SadPanda/Documents/NSPasteboardItem Crash/NSPasteboardItem Crash/NSPasteboardItem_CrashApp.swift:11
#32	0x00007ff800cd5366 in start ()

Dispatch concurrent queue (default QoS):
#0	0x00007ff80111b45c in -[__NSSetM addObject:] ()
#1	0x00007ff80487692e in -[NSPasteboard _updateTypeCacheIfNeeded] ()
#2	0x00007ff8048763df in -[NSPasteboard _typesAtIndex:combinesItems:] ()
#3	0x00007ff804aa9597 in -[NSPasteboard _canRequestDataForType:index:usesPboardTypes:combinesItems:] ()
#4	0x00007ff804fdd161 in -[NSPasteboard _dataForType:index:usesPboardTypes:combinesItems:securityScoped:] ()
#5	0x00007ff804aa7c4b in -[NSPasteboardItem dataForType:] ()
#6	0x00007ff8055804af in -[NSFilePromiseReceiver receivePromisedFilesAtDestination:options:operationQueue:reader:] ()
#7	0x00007ff90e787b51 in ___lldb_unnamed_symbol131674 ()
#8	0x00007ff90e9bc340 in ___lldb_unnamed_symbol148147 ()
#9	0x00007ff8020d00ba in __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ ()
#10	0x00007ff8020cffb8 in -[NSBlockOperation main] ()
#11	0x00007ff8020cff4b in __NSOPERATION_IS_INVOKING_MAIN__ ()
#12	0x00007ff8020cf1ec in -[NSOperation start] ()
#13	0x00007ff8020cef0d in __NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ ()
#14	0x00007ff8020cedde in __NSOQSchedule_f ()
#15	0x000000010e68ce7d in _dispatch_block_async_invoke2 ()
#16	0x000000010e67ca7b in _dispatch_client_callout ()
#17	0x000000010e67fa09 in _dispatch_continuation_pop ()
#18	0x000000010e67eae8 in _dispatch_async_redirect_invoke ()
#19	0x000000010e6906a9 in _dispatch_root_queue_drain ()
#20	0x000000010e6911ba in _dispatch_worker_thread2 ()
#21	0x000000010dfb832f in _pthread_wqthread ()
#22	0x000000010dfbebeb in start_wqthread ()</pre>



<p>There doesn’t appear to be any workaround (short of not supporting drops at all!).</p>



<p>The more complicated the drop handler the more likely it is to promptly crash upon drop &#8211; in my real code with a non-trivial handler, it’s virtually guaranteed to crash on the second drop containing a file promise, while in the vastly reduced sample code (above) it can take dozens of drops before it finally crashes outright.</p>



<p>I have not <em>directly</em> tested whether this <code>NSPasteboard</code> bug occurs in the absence of <a href="https://developer.apple.com/documentation/SwiftUI" data-wpel-link="external" target="_blank" rel="external noopener">SwiftUI</a>, so I don’t strictly know if the root cause is in <a href="https://developer.apple.com/documentation/appkit" data-wpel-link="external" target="_blank" rel="external noopener">AppKit</a> or SwiftUI. However, since most SwiftUI apps don’t support drag-and-drop, but plenty of AppKit ones do and manage to not crash when given the exact same test cases, I do strongly suspect SwiftUI is causing this somehow.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ You may wonder why I’m directly accessing the drag pasteboard rather than using the <code><a href="https://developer.apple.com/documentation/foundation/nsitemprovider" data-wpel-link="external" target="_blank" rel="external noopener">NSItemProvider</a></code>s provided by SwiftUI. It’s because that API is horribly broken &#8211; in many cases the provided <code>NSItemProvider</code>(s) are duds that contain no actual data. So I have to use the drag pasteboard directly in order to stand any chance of supporting drag &amp; drop.</p>



<p>Also, the <code>NSItemProvider</code>-based API is harder to use and doesn&#8217;t support important aspects of drag-and-drop, like file promises (although, with <code>NSPasteboard</code> apparently corrupting itself when file promises are received, I guess none of Apple&#8217;s APIs do anymore 😔).</p>
</div></div>



<hr class="wp-block-separator has-alpha-channel-opacity is-style-dots"/>



<h3 class="wp-block-heading">Follow-up (September 12th, 2024)</h3>



<p>I actually received a response from Apple, from a real human (or at least a convincing AI).  Ultimately their response didn&#8217;t help as it contained some mistakes, but I&#8217;m hopeful there&#8217;ll be more follow-up and a productive conclusion.  And in the interim, they did assert a few things which are important to know, and are not otherwise documented by Apple:</p>



<ul class="wp-block-list">
<li>SwiftUI&#8217;s <code><a href="https://developer.apple.com/documentation/swiftui/view/ondrop(of:istargeted:perform:)-f15m" data-wpel-link="external" target="_blank" rel="external noopener">onDrop(of:isTargeted:perform:)</a></code> method makes no claims or promises as to what thread / queue it executes the closure on, and in fact according to the anonymous Apple engineer it <em>never</em> executes the closure on the main thread.<br><br>Now, while that may be the intent, the reality of that is wrong &#8211; in my experience it <em>always</em> executes the closure on the main thread (which makes a lot of sense to me as drag-and-drop event handling in AppKit has always been on the main thread in practice).<br><br>Nonetheless, Apple says one cannot rely on the current behaviour and should in fact assume it <em>never</em> executes on the main thread (though in practice that means you have to <em>check</em>, not assume, since if you blindly do something like <code>DispatchQueue.main.sync</code><code style="font-size: 15px;"> { … }</code> in your drop handler your code <em>will</em> deadlock, today).</li>



<li><code><a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a></code> is not safe to use outside the main thread / queue.<br><br>This isn&#8217;t documented anywhere public &#8211; not in <code>NSPasteboard</code>&#8216;s documentation itself, nor the ancient <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123351-BBCFIIEB" data-wpel-link="external" target="_blank" rel="external noopener">Application Kit Framework Thread Safety</a> documentation.<br><br>I wouldn&#8217;t be surprised if it&#8217;s broadly true, as the AppKit APIs involving it always seemed main-thread centric anyway (all the handlers and delegate methods involving pasteboards are invoked on the main thread, in my experience).  And it&#8217;s generally best to assume everything in AppKit is main-thread-only unless it&#8217;s explicitly documented otherwise.<br><br>However, it&#8217;s important to note that Apple&#8217;s <em>own</em> code doesn&#8217;t follow this rule &#8211; e.g. <code><a href="https://developer.apple.com/documentation/appkit/nsfilepromisereceiver" data-wpel-link="external" target="_blank" rel="external noopener">NSFilePromiseReceiver</a></code>, internally, uses <code>NSPasteboard</code> from the global concurrent queue.</li>
</ul>



<p>Even though Apple&#8217;s initial response to this bug report hasn&#8217;t been all that fruitful, I do want to emphasise the fact that they <em>did</em> respond, which was a pleasant surprise and very much appreciated.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8369</post-id>	</item>
		<item>
		<title>Mac app sandboxing interferes with drag &#038; drop</title>
		<link>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/</link>
					<comments>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Jan 2024 02:06:10 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[App sandboxing]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[Insecure]]></category>
		<category><![CDATA[NSEvent]]></category>
		<category><![CDATA[NSPasteboard]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7375</guid>

					<description><![CDATA[Failed to get a sandbox extension Right from there, you know you&#8217;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&#8230; <a class="read-more-link" href="https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<pre class="wp-block-preformatted">Failed to get a sandbox extension</pre>



<p>Right from there, you know you&#8217;re going to have a bad day. 😔</p>



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



<pre class="wp-block-preformatted">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"}}</pre>



<p>There&#8217;s a variety of ways to run afoul of this.  Here&#8217;s a particular one, for illustration.</p>



<h2 class="wp-block-heading">Noticing when drag &amp; drop operations <em>start</em></h2>



<p>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.</p>



<p>There&#8217;s no built-in facility for this &#8211; both AppKit and SwiftUI drag &amp; drop APIs only start functioning once the dragged item enters the drop zone itself (the view, or at best window, that you&#8217;ve made a drop receiver).</p>



<p>There <em>is</em> a convention on how to work around this &#8211; you can <em>deduce</em> that a drag is occurring by watching mouse events and the drag pasteboard.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading">Drag &amp; drop implementation detail</h3>



<p>macOS uses <em>pasteboards</em> to share data between applications (represented in AppKit by <a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a>), not just for copy &amp; paste as you&#8217;re probably already familiar, but for drag &amp; drop as well, among several other things.  You can also create your own custom pasteboards, namespaced to be completely independent of the &#8216;built-in&#8217; ones, for your own purposes.</p>



<h3 class="wp-block-heading">Pasteboard implementation detail</h3>



<p>The standard system pasteboards are <em>always</em> accessible to your application, even when they&#8217;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…</p>
</div></div>



<h3 class="wp-block-heading">Deducing what&#8217;s going on by spying on global mouse events</h3>



<p>When you see a drag event you don&#8217;t immediately know anything more than that the mouse moved with the left mouse button held down.  That can mean anything &#8211; the user might be pulling out a selection rectangle, or drawing in a graphics application, or just randomly dragging the mouse around.</p>



<p>Since the contents of the drag pasteboard are left there indefinitely after a drag concludes, you can&#8217;t just use the existence of something on the drag pasteboard as an indication that a drag is in progress.</p>



<p>However, pasteboards have a &#8220;change count&#8221;.  What this is actually counting is ill-defined and seemingly not about the actual <em>contents</em> of the pasteboard, but in a nutshell it can be used to mean essentially that &#8211; the count might change even if the contents don&#8217;t change, but it seems it will <em>always</em> change if the contents do change.  So, some false positives, but no false negatives, which is the important thing.</p>



<p>So, when you see a mouse drag event, you can look at the drag pasteboard&#8217;s change count and see if it changed since the last mouse drag event.  If it did, that&#8217;s a pretty strong suggestion &#8211; although admittedly not a guarantee &#8211; that the user is performing a drag &amp; drop operation.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>There is a race condition here.  Mouse events are handled by the WindowServer which runs at <em>very</em> high priority and completely asynchronous to app activity like loading up the pasteboard with dragged items &#8211; 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&#8217;s an eon in computer terms.</p>



<p>So you might observe the key events &#8211; the pasteboard changing and the first mouse down of a drag &#8211; in any order.</p>



<p>Fortunately, it&#8217;s rare to lose the race in a way that matters, because you&#8217;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 &#8211; which is in fact common, in my experience &#8211; you&#8217;ll invariably get a bunch more practically immediately as the user continues the drag.  Sooner or later the pasteboard will be updated, so you&#8217;ll eventually notice.  Technically you&#8217;re late in recognising that a drag &amp; drop operation has started, but in general the delay is imperceptible.</p>
</div></div>



<p>The essential code is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> dragPasteboard = </span><span style="color: #795E26">NSPasteboard</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: .</span><span style="color: #001080">drag</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #0000FF">var</span><span style="color: #000000"> lastDragPasteboardChangeCount: </span><span style="color: #267F99">Int</span><span style="color: #000000"> = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseDragWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseDragged</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                         </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> currentChangeCount = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> lastDragPasteboardChangeCount != currentChangeCount {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// The user very likely just started dragging something.</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    lastDragPasteboardChangeCount = currentChangeCount</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseUpWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseUp</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                       </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// The drag ended.</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Note that I&#8217;ve omitted various ancillary details, such as avoiding strong retains of <code>self</code> (where applicable), error handling (<code><a href="https://developer.apple.com/documentation/appkit/nsevent/1535472-addglobalmonitorforevents" data-wpel-link="external" target="_blank" rel="external noopener">addGlobalMonitorForEvents</a></code> can return nil), removing the monitors when you&#8217;re done with them, etc.</p>



<p>Also, <code><a href="https://developer.apple.com/documentation/appkit/nsevent" data-wpel-link="external" target="_blank" rel="external noopener">NSEvent</a></code>&#8216;s monitoring implicitly relies on <code><a href="https://developer.apple.com/documentation/foundation/runloop" data-wpel-link="external" target="_blank" rel="external noopener">RunLoop</a></code>, so actually using the above &#8211; and <em>actually</em> getting your handler called &#8211; is predicated on having a suitable runloop going (in my experience it has to be the <em>main</em> runloop, but maybe it&#8217;s possible to use a different runloop in the right <a href="https://developer.apple.com/documentation/foundation/runloop/mode" data-wpel-link="external" target="_blank" rel="external noopener">mode</a> &#8211; presumably <code><a href="https://developer.apple.com/documentation/foundation/runloop/mode/1428765-eventtracking" data-wpel-link="external" target="_blank" rel="external noopener">eventTracking</a></code>).  In a typical GUI application that&#8217;s all set up for you automagically, but in other cases you have to do it yourself &#8211; refer to <a href="https://stackoverflow.com/questions/25496336/addglobalmonitorforeventsmatchingmask-not-working" data-wpel-link="external" target="_blank" rel="external noopener">this</a> for more details and suggestions.</p>
</div></div>



<p>And the above code works fine as presented.  The problem arises if &amp; when you start actually looking at the pasteboard during the drag…</p>



<h2 class="wp-block-heading">You get only one look at the dragged files</h2>



<p>Files, when dragged, are represented mainly as their paths (technically, <code><a href="https://developer.apple.com/documentation/foundation/nsurl" data-wpel-link="external" target="_blank" rel="external noopener">NSURL</a></code>s).  That&#8217;s the &#8220;public.file-url&#8221; UTI (and the <a href="https://developer.apple.com/documentation/appkit/nspasteboard/pasteboardtype/2919747-fileurl" data-wpel-link="external" target="_blank" rel="external noopener">Apple URL pasteboard type</a> for backwards compatibility).  Though you&#8217;ll also see a bunch of other types reported if you ask the pasteboard what it&#8217;s carrying, e.g.:</p>



<pre class="wp-block-preformatted">Apple URL pasteboard type
CorePasteboardFlavorType 0x6675726C
NSFilenamesPboardType
com.apple.finder.node
dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
public.file-url</pre>



<p>Merely inspecting the UTIs in the pasteboard is fine &#8211; that doesn&#8217;t interfere with anything.  So if all you care about is if <em>any</em> kind of file (or folder) is being dragged, you&#8217;re set.  But if you want to only react to <em>some</em> types of files or folders, you need to know more.</p>



<p>If you ask for the URL &#8211; even without actually <em>using</em> it &#8211; you trigger some behind the scenes activity involving app sandboxing.  This prevents the file being made accessible to your app if &amp; when it actually is dropped into your app.</p>



<p><em>When things are working correctly</em>, 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&#8217;s container.  It&#8217;s that <em>link</em> that you actually have access to &#8211; the original file cannot be accessed directly.  That link seems to persist for a while &#8211; perhaps until your app is quit &#8211; so once you have it you&#8217;re set.</p>



<p>I don&#8217;t know why merely peeking at the file path (URL) prevents this link being created, but it does.</p>



<p>Sigh.</p>



<p>FB13520048.</p>



<h3 class="wp-block-heading">Failed workarounds</h3>



<p>Sadly the <em>only</em> meaningful existing documentation of this problem that Bing or Google can find is <a href="https://stackoverflow.com/questions/67295419/got-an-error-when-dragging-files-using-nsevent-macos" data-wpel-link="external" target="_blank" rel="external noopener">this one StackOverflow question</a>.  Which is unresolved &#8211; the claimed hacks &amp; workarounds don&#8217;t actually work, at least not anymore.</p>



<p><a href="https://stackoverflow.com/questions/67295419/got-an-error-when-dragging-files-using-nsevent-macos/67295974#67295974" data-wpel-link="external" target="_blank" rel="external noopener">Supposedly</a> on some older versions of macOS you could <a href="https://developer.apple.com/documentation/foundation/nsurl/1417795-bookmarkdata" data-wpel-link="external" target="_blank" rel="external noopener">create a bookmark of the file</a> (presumably a <a href="https://developer.apple.com/documentation/foundation/nsurl/bookmarkcreationoptions/1413824-withsecurityscope" data-wpel-link="external" target="_blank" rel="external noopener">security-scoped</a> one) and that would implicitly secure your access to it.  But that seems pretty clearly to no longer work &#8211; assuming it ever did &#8211; because you can&#8217;t create a bookmark for files you can&#8217;t read, and until the drop occurs you don&#8217;t have read access to the file.</p>



<p>Similarly, you can <em>supposedly</em> use <code><a href="https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso" data-wpel-link="external" target="_blank" rel="external noopener">startAccessingSecurityScopedResource</a></code> to indefinitely lock in your access to the file, but this doesn&#8217;t apply if you don&#8217;t have access to it to begin with.  So, again, not useful here.</p>



<h3 class="wp-block-heading">Partial workaround</h3>



<p>While I knew that using <code>canReadObject(forClasses: [NSImage.self])</code> ran afoul of this, I happened to notice there&#8217;s a logically equivalent method on <code>NSImage</code> itself &#8211; <code><a href="https://developer.apple.com/documentation/appkit/nsimage/1520039-caninit" data-type="link" data-id="https://developer.apple.com/documentation/appkit/nsimage/1520039-caninit" data-wpel-link="external" target="_blank" rel="external noopener">NSImage.canInit(with:)</a></code> &#8211; which I tried, and lo and behold it works!  No sandboxing issues.</p>



<p>I disassembled it (thanks <a href="https://www.hopperapp.com" data-wpel-link="external" target="_blank" rel="external noopener">Hopper</a>!) and saw that it&#8217;s basically just doing some UTI pre-checks &#8211; already known to be safe &#8211; to see if there&#8217;s a file URL in the pasteboard, and then it fetches that file URL using <code>readObjects(forClasses: [NSURL.self])</code>, the same method that if you or I call results in this bug surfacing.  The difference is it uses a secret <a href="https://developer.apple.com/documentation/appkit/nspasteboard/readingoptionkey" data-wpel-link="external" target="_blank" rel="external noopener">reading option key</a> &#8211; &#8220;NSPasteboardURLReadingSecurityScopedFileURLsKey&#8221;!  Apparently that&#8217;s the magic sauce which allows you to read the URL <em>without</em> triggering the bug.  You still can&#8217;t actually <em>access</em> the file, but at least you can e.g. examine its file extension (which is essentially what <code>NSImage.canInit(with:)</code> does).</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> URLs = dragPasteboard.</span><span style="color: #795E26">readObjects</span><span style="color: #000000">(</span><span style="color: #795E26">forClasses</span><span style="color: #000000">: [NSURL.self],</span></span>
<span class="line"><span style="color: #000000">                                         </span><span style="color: #795E26">options</span><span style="color: #000000">: [NSPasteboard.</span><span style="color: #795E26">ReadingOptionKey</span><span style="color: #000000">(</span><span style="color: #795E26">rawValue</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;NSPasteboardURLReadingSecurityScopedFileURLsKey&quot;</span><span style="color: #000000">) : kCFBooleanTrue]) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> u </span><span style="color: #AF00DB">in</span><span style="color: #000000"> URLs {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Go nuts!</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



<hr class="wp-block-separator has-alpha-channel-opacity is-style-dots"/>



<h2 class="wp-block-heading">Addendum: Michael Tsai&#8217;s Blog</h2>



<p>I was reading through articles in <a href="https://netnewswire.com" data-wpel-link="external" target="_blank" rel="external noopener">NetNewsWire</a>, including a few of the latest from <a href="https://mjtsai.com" data-wpel-link="external" target="_blank" rel="external noopener">Michael Tsai</a>, and for one in particular I thought, &#8220;this sounds <em>really</em> familiar for some reason&#8221;.  Hah, because <a href="https://mjtsai.com/blog/2024/01/10/mac-app-sandboxing-interferes-with-drag-drop/" data-wpel-link="external" target="_blank" rel="external noopener">he was quoting part of this post</a>.</p>



<p>I&#8217;m very flattered to be linked to by Michael &#8211; I have no idea how he found my obscure website to begin with &#8211; 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 &#8211; if he adds any &#8211; is concise and valuable.  If you&#8217;re not following <a href="https://mjtsai.com/blog/" data-wpel-link="external" target="_blank" rel="external noopener">his blog</a>, you should rectify that. 🙂</p>



<p>I also now wish I&#8217;d written this post better. 😝</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7375</post-id>	</item>
	</channel>
</rss>
