From ef181796ea92b11c47ec49593dc9567ee662d249 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Mon, 27 Jul 2026 17:57:59 +0000 Subject: [PATCH] util: treat zero O_CLOEXEC as always requested in pipe2()/mkostemp() shims On pre-10.7 SDKs there is no O_CLOEXEC; MacPorts legacy-support defines it to 0 so that code compiles. That silently turns 'flags & O_CLOEXEC' into dead code in the Darwin pipe2() and mkostemp() shims: slave_spawn()'s fork-synchronization pipe then survives the child's exec(), the parent blocks forever in read() waiting for EOF, and foot hangs before mapping its window -- with the shell alive underneath. Observed on Mac OS X 10.6/PPC; 10.15 is unaffected because its SDK has the real flag (0x01000000). Since every pipe2()/mkostemp() call site in foot passes O_CLOEXEC, treat a zero O_CLOEXEC as 'always requested' rather than 'never requested'. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Kvag8nXEuGz9XJL3WdSxKC --- util.h | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/util.h b/util.h index 0dfc27f1..8ba978d7 100644 --- a/util.h +++ b/util.h @@ -47,6 +47,25 @@ fd_set_cloexec_nonblock(int fd) return true; } +/* + * Pre-10.7 SDKs have no O_CLOEXEC (the open(2) flag arrived in + * Lion); MacPorts legacy-support defines it to 0 there so that code + * compiles. That silently turns "flags & O_CLOEXEC" into dead code, + * which for the pipe2() shim below is fatal: slave_spawn()'s + * fork-synchronization pipe never gets FD_CLOEXEC, survives the + * child's exec(), and the parent then blocks forever in read() + * waiting for the EOF that signals a successful exec -- foot hangs + * before mapping its window, with the shell alive underneath. Since + * every pipe2()/mkostemp() call site in foot passes O_CLOEXEC, + * treat a zero O_CLOEXEC as "always requested" rather than "never + * requested". + */ +#if O_CLOEXEC == 0 + #define CLOEXEC_REQUESTED(flags) true +#else + #define CLOEXEC_REQUESTED(flags) (((flags) & O_CLOEXEC) != 0) +#endif + /* All current call sites only ever pass O_CLOEXEC and/or O_NONBLOCK. */ static inline int pipe2(int fildes[2], int flags) @@ -67,7 +86,7 @@ pipe2(int fildes[2], int flags) } } - if (flags & O_CLOEXEC) { + if (CLOEXEC_REQUESTED(flags)) { for (int i = 0; i < 2; i++) { int f = fcntl(fildes[i], F_GETFD, 0); if (f < 0 || fcntl(fildes[i], F_SETFD, f | FD_CLOEXEC) < 0) @@ -87,10 +106,17 @@ fail: { } /* - * mkostemp() is a Linux/glibc extension (later adopted by *BSD), - * never implemented by Darwin on any macOS version. All current call - * sites only ever pass O_CLOEXEC. + * mkostemp() was only added to Darwin in macOS 10.12 (Sierra); on + * earlier SDKs/deployment targets (e.g. 10.6) it's undeclared, same + * as on Linux/glibc before it was adopted there. Only polyfill it + * when the target SDK doesn't already provide it, to avoid a + * static-declaration-follows-non-static-declaration conflict against + * the real prototype on 10.12+. All current call sites + * only ever pass O_CLOEXEC. */ +#include +#if !(defined(MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) static inline int mkostemp(char *template, int flags) { @@ -103,7 +129,7 @@ mkostemp(char *template, int flags) if (fd < 0) return -1; - if (flags & O_CLOEXEC) { + if (CLOEXEC_REQUESTED(flags)) { int f = fcntl(fd, F_GETFD); if (f < 0 || fcntl(fd, F_SETFD, f | FD_CLOEXEC) < 0) { int saved_errno = errno; @@ -117,6 +143,7 @@ mkostemp(char *template, int flags) return fd; } #endif +#endif static inline const char * thrd_err_as_string(int thrd_err) -- 2.43.0