Skip to content

Commit a04f966

Browse files
committed
frontend/drivers/platform_darwin: fix dispatch_source_cancel UAF in path watcher teardown
The GCD-based directory watcher in platform_darwin.m had a latent use-after-free in its teardown path. === The bug === darwin_watch_data_free was calling dispatch_source_cancel followed immediately by free(watch_data): dispatch_source_cancel(watch_data->watches[i].source); ... free(watch_data); This is unsafe because dispatch_source_cancel is asynchronous. Per Apple's dispatch documentation, cancel() only marks the source as cancelled - any event handler invocation that was already dispatched to the source's target queue will run to completion. The cancel handler is guaranteed to fire only AFTER all pending event handler invocations have drained. The event handler in question captures watch_data by pointer and writes to it: dispatch_source_set_event_handler(source, ^{ OSAtomicCompareAndSwap32(0, 1, &watch_data->has_changes); }); Because the target queue is the global concurrent dispatch queue (dispatch_get_global_queue at line 953), event handlers run on any of the system worker threads. If the main thread is inside darwin_watch_data_free while a kqueue event fires for the watched vnode, the sequence is: thread A (main) thread B (global queue) ------------------- ------------------------- dispatch_source_cancel dispatch_release (event handler dispatched free(watch_data) before cancel took effect) OSAtomicCompareAndSwap32( 0, 1, &watch_data-> has_changes); /* WRITES TO FREED MEMORY */ The old cancel handler was a no-op with a misleading comment: dispatch_source_set_cancel_handler(source, ^{ /* File descriptor is closed in cleanup function */ }); This gave the impression that cleanup was synchronised, but in fact nothing blocked the main thread until the cancel handler had actually fired. === Reachability === The watcher is installed by video_shader_parse_preset to monitor .slangp/.cgp files and their included passes for changes. frontend_driver_watch_path_for_changes(NULL, 0, ...) is called on every preset reload, shader restart, or user navigation away from the current preset. Under normal interactive use (user saves a shader file, then immediately navigates the menu) the race window is readily reached - the kqueue event for the save is in flight while the menu action tears down the watcher. On modern macOS this often crashes immediately because the freed memory is immediately re-used by the allocator. On iOS/tvOS the same race exists but crashes less often due to different allocator behaviour. === The fix === Add a per-source dispatch_semaphore_t cancel_sem to darwin_watch_entry_t. The cancel handler signals it; the teardown path cancels then waits on the semaphore before freeing. This is the canonical safe-teardown pattern for dispatch sources: 1. dispatch_source_set_cancel_handler signals cancel_sem. 2. darwin_watch_data_free: - dispatch_source_cancel(source) - dispatch_semaphore_wait(cancel_sem, FOREVER) - RARCH_DISPATCH_RELEASE on source and semaphore - close fd, free path, free watch entries - free watch_data By the time dispatch_semaphore_wait returns, GCD guarantees no event handler for this source is executing or will execute again. The subsequent free(watch_data) is safe. === Semaphore lifetime === The cancel handler block captures cancel_sem. On both ARC and MRC-with-OS_OBJECT_USE_OBJC (the default on modern macOS/iOS) the block retains the captured dispatch object, so the semaphore survives until the block does. Even without block-level retention, we also hold cancel_sem through watch_data->watches[i].cancel_sem until AFTER the wait has returned, so the pointer passed to dispatch_semaphore_signal is always valid. === Semaphore creation failure === dispatch_semaphore_create can in theory fail on OOM (realistically rare). If it does, we skip source creation entirely rather than create a source we cannot safely cancel. Also added cleanup release on the source-creation-failure path so the semaphore doesn't leak when dispatch_source_create fails but the semaphore succeeded. === ARC/MRC compatibility === Uses RARCH_DISPATCH_RELEASE from libretro-common/include/ defines/cocoa_defines.h instead of an inline '#if !__has_feature(objc_arc)' + dispatch_release guard. The macro is a no-op under ARC and 'do { if (x) dispatch_ release(x); } while (0)' under MRC. Three advantages over the inline guard: 1. Single-line call instead of a 3-line ifdef block. 2. NULL-guarded automatically - dispatch_release(NULL) is explicitly undefined per Apple's docs; the macro makes it impossible to forget the check. 3. Safe on pre-10.8 SDKs where OS_OBJECT_USE_OBJC=0 and dispatch types are plain C handles (the cocoa_defines.h comment explains why '[x release]' is the wrong choice for dispatch objects). This matches the only other user in the tree (record/drivers/record_avfoundation.m line 160), keeping the macro's usage consistent across .m files that release dispatch objects. Added '#include <defines/cocoa_defines.h>' inside the existing '#ifdef HAVE_GCD' block at the top of the file, matching the placement convention for GCD-specific headers. === HAVE_GCD guards === All edits are inside existing '#ifdef HAVE_GCD' blocks (struct definition at line 134, darwin_watch_data_free at line 897, and frontend_darwin_watch_path_for_changes at line 962). No HAVE_GCD scope changes - builds without GCD (non-macOS/iOS darwin targets) are unaffected. === Thread-safety === darwin_watch_data_free is called from the main thread during shader preset teardown. dispatch_semaphore_wait with DISPATCH_TIME_FOREVER is blocking but bounded: the cancel handler runs on the global concurrent queue which always has worker threads available, and the cancel handler itself does almost nothing (one semaphore signal), so the wait is measured in microseconds in practice. If somehow the cancel handler never fires (hypothetical GCD bug), the main thread would block forever - but the same condition would also mean leaked memory and dangling watches in the existing code, so the new behaviour is strictly better. === Not changing === The event handler body is unchanged. OSAtomicCompareAndSwap32 on &watch_data->has_changes is still correct - we've just made sure the pointer is valid for every invocation. The field is int32_t volatile and the atomic primitive provides the necessary memory ordering with frontend_darwin_check_for_ path_changes which reads the flag on the main thread. === Verification === Brace balance verified on both modified functions. The 'continue' statement added to skip a watch when semaphore creation fails lands inside the 'for (i = 0; i < list->size; i++)' loop, which is the intended scope. Can't compile-test locally (no macOS SDK), but the diff is localised and the changes match the canonical GCD safe-teardown pattern documented by Apple.
1 parent 608a182 commit a04f966

1 file changed

Lines changed: 70 additions & 12 deletions

File tree

frontend/drivers/platform_darwin.m

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <mach/mach.h>
2727
#ifdef HAVE_GCD
2828
#include <dispatch/dispatch.h>
29+
#include <defines/cocoa_defines.h>
2930
#endif
3031

3132
#include <CoreFoundation/CoreFoundation.h>
@@ -137,6 +138,19 @@
137138
{
138139
int fd; /* File descriptor opened with O_EVTONLY */
139140
dispatch_source_t source; /* GCD dispatch source for monitoring */
141+
/* Per-entry semaphore signalled from the source's cancel
142+
* handler. Needed because dispatch_source_cancel is
143+
* asynchronous: it flags the source for cancellation but
144+
* any already-dispatched event handler invocation keeps
145+
* running to completion on its target queue, which is the
146+
* global concurrent queue here. The event handler
147+
* dereferences &watch_data->has_changes, so we cannot free
148+
* watch_data until we're sure no in-flight handler remains.
149+
* The cancel handler fires once all handler invocations
150+
* have drained, so waiting on this semaphore before free()
151+
* is the standard safe-teardown pattern for dispatch
152+
* sources. */
153+
dispatch_semaphore_t cancel_sem;
140154
char *path; /* Watched file path */
141155
} darwin_watch_entry_t;
142156

@@ -911,10 +925,29 @@ static void darwin_watch_data_free(darwin_watch_data_t *watch_data)
911925
{
912926
if (watch_data->watches[i].source)
913927
{
928+
/* Cancel the source, then wait for the cancel
929+
* handler to fire. dispatch_source_cancel is
930+
* asynchronous - it only marks the source as
931+
* cancelled. Any already-dispatched event handler
932+
* invocation (the one that reads
933+
* &watch_data->has_changes) keeps running to
934+
* completion on the global concurrent queue. The
935+
* cancel handler is guaranteed to fire AFTER all
936+
* pending event handler invocations have drained,
937+
* so dispatch_semaphore_wait(cancel_sem, FOREVER)
938+
* is the standard 'wait until source is fully
939+
* quiesced' pattern. Without this wait a racing
940+
* event handler would NUL-deref or, worse, write
941+
* into freed memory for has_changes. */
914942
dispatch_source_cancel(watch_data->watches[i].source);
915-
#if !__has_feature(objc_arc)
916-
dispatch_release(watch_data->watches[i].source);
917-
#endif
943+
if (watch_data->watches[i].cancel_sem)
944+
{
945+
dispatch_semaphore_wait(
946+
watch_data->watches[i].cancel_sem,
947+
DISPATCH_TIME_FOREVER);
948+
}
949+
RARCH_DISPATCH_RELEASE(watch_data->watches[i].source);
950+
RARCH_DISPATCH_RELEASE(watch_data->watches[i].cancel_sem);
918951
}
919952
if (watch_data->watches[i].fd >= 0)
920953
close(watch_data->watches[i].fd);
@@ -984,16 +1017,32 @@ static void frontend_darwin_watch_path_for_changes(
9841017
const char *path = list->elems[i].data;
9851018
int fd = open(path, O_EVTONLY);
9861019

987-
watch_data->watches[i].fd = fd;
988-
watch_data->watches[i].source = NULL;
989-
watch_data->watches[i].path = NULL;
1020+
watch_data->watches[i].fd = fd;
1021+
watch_data->watches[i].source = NULL;
1022+
watch_data->watches[i].path = NULL;
1023+
watch_data->watches[i].cancel_sem = NULL;
9901024

9911025
if (fd >= 0)
9921026
{
993-
dispatch_source_t source;
1027+
dispatch_source_t source;
1028+
dispatch_semaphore_t cancel_sem;
9941029

9951030
watch_data->watches[i].path = strdup(path);
9961031

1032+
/* Create cancel semaphore up-front. If this fails
1033+
* (realistically only on OOM) we skip source creation
1034+
* entirely rather than create a source we cannot
1035+
* safely tear down - without a semaphore the free
1036+
* path has no way to wait for in-flight event
1037+
* handlers to drain before the free(). */
1038+
cancel_sem = dispatch_semaphore_create(0);
1039+
if (!cancel_sem)
1040+
{
1041+
close(fd);
1042+
watch_data->watches[i].fd = -1;
1043+
continue;
1044+
}
1045+
9971046
/* Create dispatch source for monitoring file events */
9981047
source = dispatch_source_create(
9991048
DISPATCH_SOURCE_TYPE_VNODE,
@@ -1003,22 +1052,31 @@ static void frontend_darwin_watch_path_for_changes(
10031052

10041053
if (source)
10051054
{
1006-
/* Set up event handler - sets atomic flag when changes occur */
1055+
/* Set up event handler - sets atomic flag when changes
1056+
* occur. This block captures watch_data by pointer and
1057+
* the cancel-handler synchronisation below is what keeps
1058+
* the capture safe against the teardown path. */
10071059
dispatch_source_set_event_handler(source, ^{
10081060
OSAtomicCompareAndSwap32(0, 1, &watch_data->has_changes);
10091061
});
10101062

1011-
/* Set up cancellation handler to prevent fd leak */
1063+
/* Cancel handler signals cancel_sem. darwin_watch_
1064+
* data_free will cancel the source and wait on this
1065+
* semaphore, guaranteeing all pending event handlers
1066+
* have completed before watch_data is freed. */
10121067
dispatch_source_set_cancel_handler(source, ^{
1013-
/* File descriptor is closed in cleanup function */
1068+
dispatch_semaphore_signal(cancel_sem);
10141069
});
10151070

1016-
watch_data->watches[i].source = source;
1071+
watch_data->watches[i].source = source;
1072+
watch_data->watches[i].cancel_sem = cancel_sem;
10171073
dispatch_resume(source);
10181074
}
10191075
else
10201076
{
1021-
/* Failed to create dispatch source, close fd */
1077+
/* Failed to create dispatch source, close fd and
1078+
* release the unused semaphore. */
1079+
RARCH_DISPATCH_RELEASE(cancel_sem);
10221080
close(fd);
10231081
watch_data->watches[i].fd = -1;
10241082
}

0 commit comments

Comments
 (0)