SIGN IN SIGN UP

Support AIX by forking where posix_spawn cannot change directory (#112)

* Initial port for AIX

* Detect both the presence and the spelling of the chdir file action

Adds SUBPROCESS_ADDCHDIR_IS_POSIX, which says whether the platform provides
posix_spawn_file_actions_addchdir under its POSIX 2024 name rather than the
older ..._np one, and extends SUBPROCESS_SPAWN_VIA_FORK to the platforms that
provide neither and therefore need fork()+exec(). The call site now asks those
two macros instead of naming platforms itself.

  SUBPROCESS_ADDCHDIR_IS_POSIX   macOS 26+, NetBSD 10+
  SUBPROCESS_SPAWN_VIA_FORK      AIX, OpenBSD, NetBSD < 10

The reason for two macros rather than one is that platforms differ in two
independent ways: whether the chdir file action exists at all, and what it is
called. Taken from each vendor's own spawn.h and libc:

  FreeBSD 13.1, 13.2, 14.3, 15.0   ..._addchdir_np
  NetBSD 10.0, 10.1                ..._addchdir, no _np
  NetBSD 9.3, 9.4                  neither
  OpenBSD 7.7, 7.8, 7.9            neither
  AIX 7.2 TL04                     neither, in header and in libc

This also corrects the call site's previous test, defined(__NetBSD__) with no
version check, which was right for NetBSD 10 and wrong for 9 and older, where
neither spelling exists.

__NetBSD_Version__ comes from <sys/param.h>, not from the compiler's
predefines, so that header is now included on NetBSD before the probes run. If
it is ever absent the version folds to 0 and every NetBSD takes the fork path:
heavier than needed, never wrong.

Verified by compiling against sysroots built from each vendor's release sets,
so nothing here rests on documentation. Every C source of the test suite
compiles for NetBSD 9.4 and 10.1, OpenBSD 7.9 and FreeBSD 14.3 under the flags
CMakeLists assigns. macOS 10.13 through 14.0 compile through osxcross against a
real 15.5 SDK, with SUBPROCESS_HAVE_CWD 0 below 10.15 and 1 from 10.15 up; the
macOS 26 branch cannot be checked because no mirrored SDK goes that far.

Unchanged where it was already working: Linux glibc and musl 441/441, AIX 7.2
on POWER8 441/441, Windows MSVC 387/387 in 32- and 64-bit and against both CRTs.
Forcing -DSUBPROCESS_SPAWN_VIA_FORK=1 on glibc also passes 441/441, which is how
the fork path is exercised without leaving Linux. clang's static analyser
reports nothing on either path, and a purpose-built test spawning from eight
threads, 640 spawns, passes on both paths under AddressSanitizer.

* Skip the divide-by-zero assertion on PowerPC and RISC-V

The test already excludes AArch64 because SDIV by zero yields 0 instead of
trapping. Two more architectures behave the same way, and are specified to:
PowerPC's divw/divd leave the result undefined and raise no exception unless
OE is set, and RISC-V's DIV returns -1, all bits set. On both the child exits
0, so subprocess_fail_divzero cannot hold.

Measured with the same source and compiler on each architecture rather than
taken from the manuals. x86_64, i386, armv7l, armv6 and s390x die on signal 8;
aarch64 exits 0 printing 0, ppc64le exits 0, and riscv64 exits 0 printing -1 —
each matching what its ISA specifies. Reproduced on AIX 7.2 TL04 on POWER8,
where the suite now passes 441 of 441.

32-bit ARM was checked deliberately and does trap, including ARMv6, which has
no divide instruction at all and goes through __aeabi_idiv. The plausible
assumption that a software divide routine returns 0 quietly is wrong, so
__arm__ is not excluded.

_ARCH_PPC is listed beside __powerpc__ because IBM XL C and OpenXL define only
the former, and that is the compiler most AIX users reach for.

* Shorten the two probe comments the review flagged

Review feedback on #112.

The paragraph above the <sys/param.h> include is gone. It explained that
__NetBSD_Version__ is not a compiler predefine and that omitting the header
would fold the version to 0 — true, but the include sits directly under
#if defined(__NetBSD__) and needs no defence.

The SUBPROCESS_SPAWN_VIA_FORK comment keeps what the macro does, why forking
buys anything, and that it can be overridden. Dropped: the list of which
platforms lack addchdir, which the #if immediately below states in code, and
the sentence about the capabilities this enables further down.

Comments only. Preprocessing both revisions with -fpreprocessed -dD -E -P
leaves 1563 identical lines of code. Rebuilt anyway: 441/441 on x86_64 glibc,
and 441/441 again with -DSUBPROCESS_SPAWN_VIA_FORK=1.

* Drop the exec-failure status macro and exit 127 directly

Review feedback on #112: "Why do we even need this define?"

We do not. The value was used exactly once, no test referenced it, the README
never mentioned it, and it only ever reaches a caller down a path that
practically cannot happen — the parent reads the real errno off the pipe and
reaps the child with a discarded status, so 127 surfaces only if that 4-byte
write into an empty pipe is lost.

Making it configurable was worse than pointless. POSIX requires posix_spawn's
child to exit with status 127 when exec fails, so 127 is what keeps the fork
path indistinguishable from the posix_spawn path for a caller. The knob's only
possible use was to break that equivalence, and it offered nothing on the
posix_spawn side, where the value comes from libc and cannot be overridden.

The macro was introduced by this PR and never released, so nothing depends on
it. 441/441 on x86_64 glibc, and again with -DSUBPROCESS_SPAWN_VIA_FORK=1.

* Declare execvpe above the function instead of inside it

Review feedback on #112: "Could we only extern to this in a define block for
AIX above the function instead then?"

Moved out of the function body, but guarded on SUBPROCESS_SPAWN_VIA_FORK
rather than _AIX. Restricting it to AIX breaks glibc: execvpe is a GNU
extension there, so <unistd.h> only declares it when _GNU_SOURCE is set, and
a consumer forcing the fork path without that macro would lose the
declaration. Measured both ways with -Werror=implicit-function-declaration —
the AIX-only variant fails without _GNU_SOURCE and builds with it. The suite
cannot see this either way, since test/CMakeLists.txt defines _GNU_SOURCE on
Linux. musl hides execvpe the same way.

The declaration is also no longer AIX-specific in what it claims: OpenBSD and
NetBSD reach this code too, and both do have execvpe.

441/441 on x86_64 glibc, and again with -DSUBPROCESS_SPAWN_VIA_FORK=1. The
-D_AIX arm still type-checks, and the fork path compiles on glibc and musl in
C and C++, with and without _GNU_SOURCE — the C++ runs matter because glibc
declares execvpe __THROW, and a file-scope redeclaration without noexcept
would clash where the in-body one did not.

* Clear FD_CLOEXEC on the child's standard descriptors after dup2

The merge brings in #115, which creates the stdio pipes close-on-exec. The
forked child relies on dup2 to strip that flag when it installs them on 0, 1
and 2 — and dup2 does clear it, except when source and target are the same
descriptor, where it is a no-op.

That case is reachable: with fd 0 free in the parent, the stdin pipe's read
end lands on 0 and the child's dup2(stdinfd[0], STDIN_FILENO) becomes
dup2(0, 0). exec then closes the child's stdin. Measured with a parent that
closes fd 0 before subprocess_create: this branch before the merge works,
upstream main works, the merge of the two does not, and clearing the flag
fixes it. fcntl is async-signal-safe, so the child stays within what it may
call before exec.

Nothing in the suite could see this. It stayed at 442/442 before and after
the fix, in both implementations, because no test ran with a standard
descriptor closed. The test added here does: it closes fd 0 around a
subprocess_create and asserts the child still has a stdin, using the
process_is_fd_open helper #115 brought along. It goes red on the fork path
without the fix above and green with it; on the posix_spawn path, which never
had the defect, it is green either way. The suite is now 443/443.

The test saves and restores fd 0 and defers its assertions until after the
restore — an assertion returns early in utest, which would otherwise leave
every later test in the binary without a stdin.

* Drop the FD_CLOEXEC clearing now that pipe ends avoid 0, 1 and 2

subprocess_fds_above_std, added in #118, moves every pipe end above STDERR
on both pipe-creation paths, so dup2 in the child can no longer be a no-op
on a standard descriptor. Reverts the addition from this branch as promised
in #118.

---------

Co-authored-by: mehendarkarprajwal <prajwal.b.mehendarkar@ibm.com>
B
Bernard Ladenthin committed
0dccaa9aa176dd6d7ef8afeca3c18d6e80a32795
Parent: 76cd934
Committed by GitHub <noreply@github.com> on 8/19/2026, 11:52:47 AM