Skip to content

Commit 496f8e2

Browse files
JanWielemakerclaude
andcommitted
FIXED: use GCD dispatch semaphores on macOS instead of named POSIX semaphores
macOS does not implement unnamed POSIX semaphores (sem_init() is a deprecated ENOSYS stub), so this code used a named semaphore opened with a fixed name "pl" via sem_open()/sem_unlink(). That name lives in a single machine-global namespace, which has two failure modes when many short-lived Prolog processes run concurrently: - Two processes in the sem_open()..sem_unlink() window collide on O_CREAT|O_EXCL and one gets EEXIST. - A process killed between sem_open() and sem_unlink() leaves the name registered, after which every later sem_open(O_EXCL) fails until reboot. Both surface as hard-to-reproduce thread teardown ("threads wouldn't die") and thread CPU-time failures on macOS, and also break under sandboxes that deny ipc-posix-sem. The old failure path made it worse: my_sem_open() tested the result against NULL, but sem_open() reports failure as SEM_FAILED, so a failed open stored a broken handle and could exit(1) at startup. Replace the named semaphores with GCD dispatch semaphores: in-process, unnamed, with a native timed wait. dispatch_semaphore_signal() is a lock-free atomic increment with a Mach semaphore_signal() trap on its slow path -- no libc locks or malloc -- so it stays safe to call from the SIG_SYNCTIME signal handler. The dispatch timed wait also replaces the setitimer()-based grace timeout (USE_TIMER_WAIT), now unused on macOS, while still honouring halt_grace_time and reporting threads that outlive it. If dispatch_semaphore_create() ever fails (OOM), the halt path degrades to polling thread status rather than aborting. The selecting macro is renamed USE_SEM_OPEN -> USE_DISPATCH_SEM to match. Only the macOS paths change; other platforms are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0526294 commit 496f8e2

3 files changed

Lines changed: 88 additions & 36 deletions

File tree

cmake/port/Darwin.cmake

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,9 @@ endif()
159159

160160
set(CMAKE_MACOSX_RPATH ON)
161161

162-
# Prefer sem_open() over deprecated sem_init()
163-
set(USE_SEM_OPEN 1)
162+
# macOS has no unnamed POSIX semaphores (sem_init() is a deprecated ENOSYS
163+
# stub); use GCD dispatch semaphores instead of named sem_open() semaphores.
164+
set(USE_DISPATCH_SEM 1)
164165
set(SO_PATH DYLD_LIBRARY_PATH)
165166

166167
endif(APPLE)

src/config.h.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@
261261
#cmakedefine TIME_WITH_SYS_TIME @TIME_WITH_SYS_TIME@
262262
#cmakedefine USE_COPY_STACK_SIZE @USE_COPY_STACK_SIZE@
263263
#cmakedefine USE_GIT_VERSION_H @USE_GIT_VERSION_H@
264-
#cmakedefine USE_SEM_OPEN @USE_SEM_OPEN@
264+
#cmakedefine USE_DISPATCH_SEM @USE_DISPATCH_SEM@
265265
#cmakedefine VOID_UNSETENV @VOID_UNSETENV@
266266
#cmakedefine _FILE_OFFSET_BITS @_FILE_OFFSET_BITS@
267267
#cmakedefine _LARGE_FILES @_LARGE_FILES@

src/pl-thread.c

Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,9 @@ typedef sema_t sem_t;
170170

171171
#endif /*HAVE_SEMA_INIT*/
172172

173-
#ifdef USE_SEM_OPEN /* see below */
174-
static sem_t *sem_canceled_ptr;
173+
#ifdef USE_DISPATCH_SEM /* GCD dispatch semaphore; see below */
174+
#include <dispatch/dispatch.h>
175+
static dispatch_semaphore_t sem_canceled_ptr;
175176
#else
176177
static sem_t sem_canceled; /* used on halt */
177178
#define sem_canceled_ptr (&sem_canceled)
@@ -180,44 +181,47 @@ static sem_t sem_canceled; /* used on halt */
180181
#ifndef __WINDOWS__
181182
#include <signal.h>
182183

183-
#ifdef USE_SEM_OPEN
184+
#ifdef USE_DISPATCH_SEM
184185

185186
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
186-
Apple Darwin (6.6) only contains the sem_init() function as a stub. It
187-
only provides named semaphores through sem_open(). These defines and
188-
my_sem_open() try to hide the details of this as much as possible from
189-
the rest of the code. Note that we unlink the semaphore right after
190-
creating it, using the common Unix trick to keep access to it as long as
191-
we do not close it. We assume the OS will close the semaphore as the
192-
application terminates. All this is highly undesirable, but it will do
193-
for now. The USE_SEM_OPEN define is set by configure based on the
194-
substring "darwin" in the architecture identifier.
187+
macOS does not implement unnamed POSIX semaphores: sem_init() returns ENOSYS
188+
and is deprecated. It only provides *named* semaphores through sem_open(),
189+
which share a single machine-global namespace. Using a fixed name ("pl")
190+
races between concurrent processes and -- worse -- a process killed between
191+
sem_open() and sem_unlink() leaves the name registered, after which every
192+
later sem_open(O_CREAT|O_EXCL) fails with EEXIST until the machine reboots.
193+
194+
We therefore use GCD dispatch semaphores: in-process, unnamed, with a native
195+
timed wait. dispatch_semaphore_signal() is a lock-free atomic increment with
196+
a Mach semaphore_signal() trap on its slow path -- no libc locks or malloc --
197+
so it is safe to call from the SIG_SYNCTIME signal handler (SyncUserCPU()).
198+
199+
These macros keep the rest of this file using the POSIX sem_*() spelling.
200+
The bounded wait in exitPrologThreads() uses dispatch_semaphore_wait() with a
201+
deadline directly.
195202
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
196203

197-
#define sem_init(ptr, flags, val) my_sem_open(&ptr, val)
198-
#define sem_destroy(ptr) ((void)0)
199-
200204
static int
201-
my_sem_open(sem_t **ptr, unsigned int val)
202-
{ if ( !*ptr )
203-
{ sem_t *sem = sem_open("pl", O_CREAT|O_EXCL, 0600, val);
204-
205-
DEBUG(MSG_THREAD, Sdprintf("sem = %p\n", sem));
206-
207-
if ( sem == NULL )
208-
{ perror("sem_open");
209-
exit(1);
210-
}
211-
212-
*ptr = sem;
205+
pl_dispatch_sem_init(dispatch_semaphore_t *ptr, unsigned int val)
206+
{ dispatch_semaphore_t sem = dispatch_semaphore_create(val);
213207

214-
sem_unlink("pl");
208+
if ( !sem ) /* only fails under severe memory pressure */
209+
{ Sdprintf("WARNING: dispatch_semaphore_create() failed; "
210+
"thread synchronization degraded.\n");
211+
return -1; /* leave *ptr NULL; callers degrade */
215212
}
216213

214+
*ptr = sem;
217215
return 0;
218216
}
219217

220-
#endif /*USE_SEM_OPEN*/
218+
#define sem_init(ptr, flags, val) pl_dispatch_sem_init(&(ptr), (val))
219+
#define sem_post(s) ((void)dispatch_semaphore_signal(s))
220+
#define sem_wait(s) ((int)dispatch_semaphore_wait((s), \
221+
DISPATCH_TIME_FOREVER))
222+
#define sem_destroy(s) dispatch_release(s)
223+
224+
#endif /*USE_DISPATCH_SEM*/
221225

222226
#ifndef SA_RESTART
223227
#define SA_RESTART 0
@@ -1114,7 +1118,7 @@ Unfortunately, this may not work, so we need a rescue. We have three
11141118
Unfortunately that typically causes a 0.1 sec delay in terminating.
11151119
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
11161120

1117-
#if !defined(HAVE_SEM_TIMEDWAIT) && defined(HAVE_SETITIMER)
1121+
#if !defined(HAVE_SEM_TIMEDWAIT) && defined(HAVE_SETITIMER) && !defined(USE_DISPATCH_SEM)
11181122
#define USE_TIMER_WAIT 1
11191123

11201124
#include <sys/time.h>
@@ -1214,6 +1218,48 @@ exitPrologThreads(void)
12141218

12151219
if ( canceled > 0 ) /* see (*) above */
12161220
{
1221+
#ifdef USE_DISPATCH_SEM
1222+
if ( !sem_canceled_ptr ) /* semaphore create failed (OOM): poll status */
1223+
{ double grace_time = halt_grace_time();
1224+
double waited = 0.0;
1225+
1226+
DEBUG(MSG_CLEANUP_THREAD,
1227+
Sdprintf("No cancel semaphore; polling %d threads\n", canceled));
1228+
1229+
for(;;)
1230+
{ int i, running = 0;
1231+
1232+
for(i=1; i<=GD->thread.highest_id; i++)
1233+
{ PL_thread_info_t *info = GD->thread.threads[i];
1234+
1235+
if ( info && info->thread_data && i != me &&
1236+
!info->is_engine && info->status == PL_THREAD_RUNNING )
1237+
running++;
1238+
}
1239+
1240+
if ( running == 0 || waited >= grace_time )
1241+
{ canceled = running;
1242+
break;
1243+
}
1244+
Pause(0.05);
1245+
waited += 0.05;
1246+
}
1247+
} else /* dispatch semaphore: native timed wait */
1248+
{ double grace_time = halt_grace_time();
1249+
dispatch_time_t deadline =
1250+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(grace_time*1000000000.0));
1251+
1252+
DEBUG(MSG_CLEANUP_THREAD,
1253+
Sdprintf("Waiting for %d threads (dispatch semaphore)\n", canceled));
1254+
1255+
while ( canceled > 0 )
1256+
{ if ( dispatch_semaphore_wait(sem_canceled_ptr, deadline) != 0 )
1257+
break; /* grace period elapsed */
1258+
canceled--;
1259+
}
1260+
}
1261+
#else /*USE_DISPATCH_SEM*/
1262+
{
12171263
#ifdef USE_TIMER_WAIT
12181264
double grace_time = halt_grace_time();
12191265
DEBUG(MSG_CLEANUP_THREAD,
@@ -1277,6 +1323,8 @@ exitPrologThreads(void)
12771323
}
12781324

12791325
#endif
1326+
}
1327+
#endif /*USE_DISPATCH_SEM*/
12801328
DEBUG(MSG_CLEANUP_THREAD, Sdprintf("Left: %d threads\n", canceled));
12811329
}
12821330

@@ -7663,8 +7711,8 @@ up-to-date, but the C library is very much out of data (glibc 2.3)
76637711
#define SIG_SYNCTIME SIGUSR1
76647712
#endif
76657713

7666-
#ifdef USE_SEM_OPEN
7667-
static sem_t *sem_synctime_ptr;
7714+
#ifdef USE_DISPATCH_SEM
7715+
static dispatch_semaphore_t sem_synctime_ptr; /* GCD dispatch semaphore */
76687716
#else
76697717
static sem_t sem_synctime; /* used for atom-gc */
76707718
#define sem_synctime_ptr (&sem_synctime)
@@ -7747,7 +7795,10 @@ ThreadCPUTime(DECL_LD int which)
77477795
int ok;
77487796

77497797
blockSignals(&set);
7750-
sem_init(sem_synctime_ptr, USYNC_THREAD, 0);
7798+
if ( sem_init(sem_synctime_ptr, USYNC_THREAD, 0) != 0 )
7799+
{ unblockSignals(&set);
7800+
return 0.0; /* no semaphore: CPU time unavailable */
7801+
}
77517802
allSignalMask(&sigmask);
77527803
memset(&new, 0, sizeof(new));
77537804
new.sa_handler = (which == CPU_USER ? SyncUserCPU : SyncSystemCPU);

0 commit comments

Comments
 (0)