From e5dd6aba3801c1ed1b94f0f4a45411f7620d4da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Thu, 6 Aug 2026 11:56:07 +0200 Subject: [PATCH 1/4] Split the child side of process spawning into cpproc-child.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the child-side process spawning implementation to a separate source file, so that it can later be reused by the spawn helper in the forthcoming posix_spawn implementation. Make sure all child-side functions called after fork() are static and live in the same translation unit, so that calls between them cannot be lazily bound (resolving a lazy binding runs the dynamic linker, which can deadlock the child). The entry point itself (cpproc_child_fork_exec) is called before fork(), so its binding is resolved safely in the parent. This commit introduces no functional changes. Signed-off-by: Guillermo Rodríguez --- native/jni/native-lib/Makefile.am | 4 +- native/jni/native-lib/cpproc-child.c | 363 ++++++++++++++++++++++++ native/jni/native-lib/cpproc-child.h | 60 ++++ native/jni/native-lib/cpproc.c | 405 ++++----------------------- 4 files changed, 477 insertions(+), 355 deletions(-) create mode 100644 native/jni/native-lib/cpproc-child.c create mode 100644 native/jni/native-lib/cpproc-child.h diff --git a/native/jni/native-lib/Makefile.am b/native/jni/native-lib/Makefile.am index 0de723eb1f..878840cdaf 100644 --- a/native/jni/native-lib/Makefile.am +++ b/native/jni/native-lib/Makefile.am @@ -5,7 +5,9 @@ libclasspathnative_la_SOURCES = cpnet.c \ cpio.h \ cpnative.h \ cpproc.h \ - cpproc.c + cpproc.c \ + cpproc-child.h \ + cpproc-child.c AM_LDFLAGS = @CLASSPATH_CONVENIENCE@ AM_CPPFLAGS = @CLASSPATH_INCLUDES@ diff --git a/native/jni/native-lib/cpproc-child.c b/native/jni/native-lib/cpproc-child.c new file mode 100644 index 0000000000..c65c1394a7 --- /dev/null +++ b/native/jni/native-lib/cpproc-child.c @@ -0,0 +1,363 @@ +/* cpproc-child.c - + Copyright (C) 2003, 2004, 2005, 2006 Free Software Foundation, Inc. + Copyright (C) 2026 INGELABS S.L. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +/* For close_range() */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + +#include "config.h" +#include "cpproc-child.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* PATH_MAX is not guaranteed to be defined (e.g. on GNU Hurd) */ +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +/* Child side of the process spawning implementation. + + Everything in this file may run in the child of a fork() of a + multi-threaded process, so it should only use async-signal-safe + operations. Avoid malloc and functions that may take locks. Any + buffers that require allocation must be preallocated by the parent + and passed in. + + All child-side functions defined here are static, so calls between + them cannot be lazily bound after fork() (resolving a lazy binding + runs the dynamic linker, which can deadlock the child). The entry + point itself (cpproc_child_fork_exec) is called before fork(), so + its binding is resolved safely in the parent. */ + +static int mark_fd_cloexec(int fd) +{ + int flags = fcntl(fd, F_GETFD); + + if (flags < 0) + return -1; + + if (!(flags & FD_CLOEXEC)) + return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); + + return 0; +} + +/* Walk the process' open fds directory to avoid scanning all possible + fds up to OPEN_MAX. This uses opendir/readdir/closedir, which are + not specified async-signal-safe, but should be safe in practice after + fork (not vfork!) on Linux (glibc, musl >= 1.2.2) and macOS. OpenJDK + uses the same approach, as does CPython in its non-Linux fallback. + + We deliberately do not use this on *BSD: without fdescfs mounted + (not the default), /dev/fd is a static directory (0, 1, 2, or 0..63) + and enumerating it would silently miss open descriptors. */ +#if defined(__linux__) +#define FD_DIR "/proc/self/fd" +#elif defined(__APPLE__) +#define FD_DIR "/dev/fd" +#endif + +#ifdef FD_DIR +static int mark_dir_fds_cloexec(void) +{ + DIR *dir; + int result; + + dir = opendir(FD_DIR); + if (dir == NULL) + return -1; + + /* The directory stream's own fd may appear in FD_DIR. We don't + want to close it while we walk the dir, but setting FD_CLOEXEC + on it is harmless: it remains open until closedir(). */ + + for (;;) + { + struct dirent *entry; + char *name; + int fd; + + errno = 0; + entry = readdir(dir); + if (entry == NULL) + { + result = errno == 0 ? 0 : -1; + break; + } + + name = entry->d_name; + if (name[0] >= '0' && name[0] <= '9' + && (fd = strtol(name, NULL, 10)) >= 3 + && mark_fd_cloexec(fd) < 0) + { + result = -1; + break; + } + } + + closedir(dir); + return result; +} +#endif + +/* Mark every non-standard descriptor close-on-exec. */ +static int mark_nonstd_fds_cloexec(int maxfd) +{ + int fd; + +#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC) + if (close_range(3, UINT_MAX, CLOSE_RANGE_CLOEXEC) == 0) + return 0; +#endif + +#ifdef FD_DIR + if (mark_dir_fds_cloexec() == 0) + return 0; +#endif + + for (fd = 3; fd < maxfd; fd++) + if (mark_fd_cloexec(fd) < 0 && errno != EBADF) + return -1; + + return 0; +} + +/* Like execve, but also implementing execvp's "shell fallback" + behaviour: if execve fails with ENOEXEC, try to execute as a + script via /bin/sh. The shell receives the script path (file) + followed by the original arguments minus argv[0], which is + dropped. If envp is NULL the environment is inherited (execv is + used instead of execve). */ +static void cp_execve_sh(const char *file, char * const *argv, + char * const *envp, char **sh_argv) +{ + if (envp != NULL) + execve(file, argv, envp); + else + execv(file, argv); + + if (errno == ENOEXEC) + { + int i; + + sh_argv[0] = (char *) "/bin/sh"; + sh_argv[1] = (char *) file; + for (i = 1; argv[i] != NULL; i++) + sh_argv[i + 1] = argv[i]; + sh_argv[i + 1] = NULL; + + if (envp != NULL) + execve("/bin/sh", sh_argv, envp); + else + execv("/bin/sh", sh_argv); + } +} + +/* Replacement for execvpe, which is a GNU extension and not available + everywhere. If envp is NULL the environment is inherited. The + supplied preallocated sh_argv array must have room for one entry + more than argv, including its terminating NULL. */ +static void cp_execvpe(const char *file, char * const *argv, + char * const *envp, const char *path, + char **sh_argv) +{ + /* - If execve fails with ENOEXEC, we assume it is a script with +x + permission (otherwise we would have seen EACCES) but without a + shebang line, and execute it via /bin/sh, as execvp would do. + The fallback is implemented explicitly because execve does not + provide it, and execvp (which does) is not async-signal-safe. + - OpenJDK implements a similar execvpe replacement, except that + they do use execvp in fork mode (see childproc.c). */ + char buffer[PATH_MAX]; + const char *p, *next; + size_t filelen = strlen(file); + int got_eacces = 0; + + /* An empty command name fails with ENOENT */ + if (*file == '\0') + { + errno = ENOENT; + return; + } + + /* Command names containing a slash are not looked up in the PATH */ + if (strchr(file, '/') != NULL) + { + cp_execve_sh(file, argv, envp, sh_argv); + return; + } + + for (p = path; p != NULL; p = next) + { + const char *candidate; + const char *sep; + size_t len; + + sep = strchr(p, ':'); + next = (sep != NULL) ? sep + 1 : NULL; + len = (sep != NULL) ? (size_t) (sep - p) : strlen(p); + if (len == 0) + { + /* An empty PATH element means the current directory */ + candidate = file; + } + else if (len + filelen + 2 <= sizeof(buffer)) + { + memcpy(buffer, p, len); + buffer[len] = '/'; + strcpy(buffer + len + 1, file); + candidate = buffer; + } + else + { + errno = ENAMETOOLONG; + continue; + } + + cp_execve_sh(candidate, argv, envp, sh_argv); + switch (errno) + { + case EACCES: + /* Keep searching, but report EACCES if nothing is found */ + got_eacces = 1; + break; + case ENOENT: + case ENOTDIR: +#ifdef ELOOP + case ELOOP: +#endif +#ifdef ESTALE + case ESTALE: +#endif +#ifdef ENODEV + case ENODEV: +#endif +#ifdef ETIMEDOUT + case ETIMEDOUT: +#endif + break; + default: + return; + } + } + + if (got_eacces) + errno = EACCES; +} + +static void child_process(char * const *commandLine, + char * const *newEnviron, + int *local_fds, int pipe_count, int *fail_fds, + const char *path, char **sh_argv, const char *wd, + int maxfd) +{ + sigset_t sigmask; + int errnum; + int i; + + close(fail_fds[0]); + + if (dup2(local_fds[0], 0) < 0) + goto child_error; + if (dup2(local_fds[3], 1) < 0) + goto child_error; + if (pipe_count == 3) + { + if (dup2(local_fds[5], 2) < 0) + goto child_error; + } + else if (dup2(1, 2) < 0) + goto child_error; + + for (i = 0; i < pipe_count * 2; i++) + close(local_fds[i]); + + /* Mark non-standard fds (>= 3) close-on-exec. This includes fail_fds[1], + which must stay open until exec(), and should be closed automatically + if exec() succeeds. */ + if (mark_nonstd_fds_cloexec(maxfd) < 0) + goto child_error; + + if (wd != NULL && chdir(wd) != 0) + goto child_error; + + /* Reset the signal mask so that the executed program starts with all + signals unblocked. */ + sigemptyset(&sigmask); + if (sigprocmask(SIG_SETMASK, &sigmask, NULL) < 0) + goto child_error; + + cp_execvpe(commandLine[0], commandLine, newEnviron, path, sh_argv); + + child_error: + /* Child setup or exec itself failed; send our errno to the parent */ + errnum = errno; + while (write(fail_fds[1], &errnum, sizeof(errnum)) < 0 + && errno == EINTR) + ; + _exit(127); +} + +/* Entry point */ + +pid_t cpproc_child_fork_exec(char * const *commandLine, + char * const *newEnviron, + int *local_fds, int pipe_count, + int *fail_fds, const char *path, + char **sh_argv, const char *wd, int maxfd) +{ + pid_t pid = fork(); + + if (pid == 0) + { + child_process(commandLine, newEnviron, local_fds, pipe_count, + fail_fds, path, sh_argv, wd, maxfd); + /* child_process() does not return. */ + _exit(127); + } + + return pid; +} diff --git a/native/jni/native-lib/cpproc-child.h b/native/jni/native-lib/cpproc-child.h new file mode 100644 index 0000000000..6e77400feb --- /dev/null +++ b/native/jni/native-lib/cpproc-child.h @@ -0,0 +1,60 @@ +/* cpproc-child.h - + Copyright (C) 2026 INGELABS S.L. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +#ifndef _CPPROC_CHILD_H +#define _CPPROC_CHILD_H + +#include + +/* These functions are internal to the native library, so keep them + out of its dynamic symbol table. This is hygiene only: child-safety + does not depend on visibility, so the fallback to default visibility + is harmless. */ +#if defined(__GNUC__) +#define CP_HIDDEN __attribute__((visibility("hidden"))) +#else +#define CP_HIDDEN +#endif + +/* Fork and exec the target program; returns the child pid, or -1 + with errno set if fork() fails. */ +CP_HIDDEN pid_t cpproc_child_fork_exec(char * const *commandLine, char * const *newEnviron, + int *local_fds, int pipe_count, int *fail_fds, + const char *path, char **sh_argv, const char *wd, + int maxfd); + +#endif diff --git a/native/jni/native-lib/cpproc.c b/native/jni/native-lib/cpproc.c index b73b756c0f..f346b4ff70 100644 --- a/native/jni/native-lib/cpproc.c +++ b/native/jni/native-lib/cpproc.c @@ -1,5 +1,6 @@ /* cpproc.c - Copyright (C) 2003, 2004, 2005, 2006 Free Software Foundation, Inc. + Copyright (C) 2026 INGELABS S.L. This file is part of GNU Classpath. @@ -35,15 +36,10 @@ this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ -/* For close_range() */ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE 1 -#endif - #include "config.h" #include #include "cpproc.h" -#include +#include "cpproc-child.h" #include #include #include @@ -54,20 +50,16 @@ exception statement from your version. */ #include #include -/* PATH_MAX is not guaranteed to be defined (e.g. on GNU Hurd) */ -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif - /* Bound the last-resort fcntl scan when OPEN_MAX is pathologically large. */ #define MAX_FD_SCAN 65536 -static void close_fds(int *fds, int numFds); -static void child_process(char * const *commandLine, - char * const *newEnviron, - int *local_fds, int pipe_count, int *fail_fds, - const char *path, char **sh_argv, const char *wd, - int maxfd); +static void close_fds(int *fds, int numFds) +{ + int i; + + for (i = 0; i < numFds; i++) + close(fds[i]); +} /* Compute the fallback loop's upper bound in the parent. sysconf() is not async-signal-safe, so avoid calling it after fork. */ @@ -182,70 +174,60 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, sigfillset(&allsigs); pthread_sigmask(SIG_SETMASK, &allsigs, &savedmask); - pid = fork(); + pid = cpproc_child_fork_exec(commandLine, newEnviron, local_fds, + pipe_count, fail_fds, path, sh_argv, wd, + maxfd); - switch (pid) + if (pid == -1) { - case 0: - child_process(commandLine, newEnviron, local_fds, pipe_count, - fail_fds, path, sh_argv, wd, maxfd); - /* child_process() does not return. */ - _exit(127); - - case -1: - { - int err = errno; - - pthread_sigmask(SIG_SETMASK, &savedmask, NULL); - close_fds(local_fds, pipe_count * 2); - close(fail_fds[0]); - close(fail_fds[1]); - free(sh_argv); - return err; - } - default: + int err = errno; + pthread_sigmask(SIG_SETMASK, &savedmask, NULL); - free(sh_argv); + close_fds(local_fds, pipe_count * 2); + close(fail_fds[0]); close(fail_fds[1]); + free(sh_argv); + return err; + } - /* Wait for the outcome of the exec: EOF if it succeeded, the - child's errno if not */ - do - { - n = read(fail_fds[0], &errnum, sizeof(errnum)); - } - while (n < 0 && errno == EINTR); - close(fail_fds[0]); + pthread_sigmask(SIG_SETMASK, &savedmask, NULL); + free(sh_argv); + close(fail_fds[1]); - if (n != 0) - { - int status; + /* Wait for the outcome of the exec: EOF if it succeeded, the + child's errno if not */ + do + { + n = read(fail_fds[0], &errnum, sizeof(errnum)); + } + while (n < 0 && errno == EINTR); + close(fail_fds[0]); - if (n != (ssize_t) sizeof(errnum)) - errnum = EIO; + if (n != 0) + { + int status; - /* The child exited without exec'ing; reap it */ - while (waitpid(pid, &status, 0) < 0 && errno == EINTR) - ; + if (n != (ssize_t) sizeof(errnum)) + errnum = EIO; - close_fds(local_fds, pipe_count * 2); - return errnum; - } + /* The child exited without exec'ing; reap it */ + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; - close(local_fds[0]); - close(local_fds[3]); - if (pipe_count == 3) - close(local_fds[5]); - - fds[0] = local_fds[1]; - fds[1] = local_fds[2]; - if (pipe_count == 3) - fds[2] = local_fds[4]; - *out_pid = pid; - return 0; + close_fds(local_fds, pipe_count * 2); + return errnum; } - /* keep compiler happy */ + close(local_fds[0]); + close(local_fds[3]); + if (pipe_count == 3) + close(local_fds[5]); + + fds[0] = local_fds[1]; + fds[1] = local_fds[2]; + if (pipe_count == 3) + fds[2] = local_fds[4]; + *out_pid = pid; return 0; } @@ -268,288 +250,3 @@ int cpproc_kill (pid_t pid, int signal) return 0; } - - -/* Child-side implementation. - - Everything below this point may run in the child of a fork() of a - multi-threaded process, so it should only use async-signal-safe - operations. Avoid malloc and functions that may take locks. Any - buffers that require allocation must be preallocated by the parent - and passed in. */ - -/* Also used by the parent. */ -static void close_fds(int *fds, int numFds) -{ - int i; - - for (i = 0; i < numFds; i++) - close(fds[i]); -} - -static int mark_fd_cloexec(int fd) -{ - int flags = fcntl(fd, F_GETFD); - - if (flags < 0) - return -1; - - if (!(flags & FD_CLOEXEC)) - return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); - - return 0; -} - -/* Walk the process' open fds directory to avoid scanning all possible - fds up to OPEN_MAX. This uses opendir/readdir/closedir, which are - not specified async-signal-safe, but should be safe in practice after - fork (not vfork!) on Linux (glibc, musl >= 1.2.2) and macOS. OpenJDK - uses the same approach, as does CPython in its non-Linux fallback. - - We deliberately do not use this on *BSD: without fdescfs mounted - (not the default), /dev/fd is a static directory (0, 1, 2, or 0..63) - and enumerating it would silently miss open descriptors. */ -#if defined(__linux__) -#define FD_DIR "/proc/self/fd" -#elif defined(__APPLE__) -#define FD_DIR "/dev/fd" -#endif - -#ifdef FD_DIR -static int mark_dir_fds_cloexec(void) -{ - DIR *dir; - int result; - - dir = opendir(FD_DIR); - if (dir == NULL) - return -1; - - /* The directory stream's own fd may appear in FD_DIR. We don't - want to close it while we walk the dir, but setting FD_CLOEXEC - on it is harmless: it remains open until closedir(). */ - - for (;;) - { - struct dirent *entry; - char *name; - int fd; - - errno = 0; - entry = readdir(dir); - if (entry == NULL) - { - result = errno == 0 ? 0 : -1; - break; - } - - name = entry->d_name; - if (name[0] >= '0' && name[0] <= '9' - && (fd = strtol(name, NULL, 10)) >= 3 - && mark_fd_cloexec(fd) < 0) - { - result = -1; - break; - } - } - - closedir(dir); - return result; -} -#endif - -/* Mark every non-standard descriptor close-on-exec in the child after fork. */ -static int mark_nonstd_fds_cloexec(int maxfd) -{ - int fd; - -#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC) - if (close_range(3, UINT_MAX, CLOSE_RANGE_CLOEXEC) == 0) - return 0; -#endif - -#ifdef FD_DIR - if (mark_dir_fds_cloexec() == 0) - return 0; -#endif - - for (fd = 3; fd < maxfd; fd++) - if (mark_fd_cloexec(fd) < 0 && errno != EBADF) - return -1; - - return 0; -} - -/* Like execve, but also implementing execvp's "shell fallback" - behaviour: if execve fails with ENOEXEC, try to execute as a - script via /bin/sh. The shell receives the script path (file) - followed by the original arguments minus argv[0], which is - dropped. If envp is NULL the environment is inherited (execv is - used instead of execve). */ -static void cp_execve_sh(const char *file, char * const *argv, - char * const *envp, char **sh_argv) -{ - if (envp != NULL) - execve(file, argv, envp); - else - execv(file, argv); - - if (errno == ENOEXEC) - { - int i; - - sh_argv[0] = (char *) "/bin/sh"; - sh_argv[1] = (char *) file; - for (i = 1; argv[i] != NULL; i++) - sh_argv[i + 1] = argv[i]; - sh_argv[i + 1] = NULL; - - if (envp != NULL) - execve("/bin/sh", sh_argv, envp); - else - execv("/bin/sh", sh_argv); - } -} - -/* Replacement for execvpe, which is a GNU extension and not available - everywhere. If envp is NULL the environment is inherited. The - supplied preallocated sh_argv array must have room for one entry - more than argv, including its terminating NULL. */ -static void cp_execvpe(const char *file, char * const *argv, - char * const *envp, const char *path, - char **sh_argv) -{ - /* - If execve fails with ENOEXEC, we assume it is a script with +x - permission (otherwise we would have seen EACCES) but without a - shebang line, and execute it via /bin/sh, as execvp would do. - The fallback is implemented explicitly because execve does not - provide it, and execvp (which does) is not async-signal-safe. - - OpenJDK implements a similar execvpe replacement, except that - they do use execvp in fork mode (see childproc.c). */ - char buffer[PATH_MAX]; - const char *p, *next; - size_t filelen = strlen(file); - int got_eacces = 0; - - /* An empty command name fails with ENOENT */ - if (*file == '\0') - { - errno = ENOENT; - return; - } - - /* Command names containing a slash are not looked up in the PATH */ - if (strchr(file, '/') != NULL) - { - cp_execve_sh(file, argv, envp, sh_argv); - return; - } - - for (p = path; p != NULL; p = next) - { - const char *candidate; - const char *sep; - size_t len; - - sep = strchr(p, ':'); - next = (sep != NULL) ? sep + 1 : NULL; - len = (sep != NULL) ? (size_t) (sep - p) : strlen(p); - if (len == 0) - { - /* An empty PATH element means the current directory */ - candidate = file; - } - else if (len + filelen + 2 <= sizeof(buffer)) - { - memcpy(buffer, p, len); - buffer[len] = '/'; - strcpy(buffer + len + 1, file); - candidate = buffer; - } - else - { - errno = ENAMETOOLONG; - continue; - } - - cp_execve_sh(candidate, argv, envp, sh_argv); - switch (errno) - { - case EACCES: - /* Keep searching, but report EACCES if nothing is found */ - got_eacces = 1; - break; - case ENOENT: - case ENOTDIR: -#ifdef ELOOP - case ELOOP: -#endif -#ifdef ESTALE - case ESTALE: -#endif -#ifdef ENODEV - case ENODEV: -#endif -#ifdef ETIMEDOUT - case ETIMEDOUT: -#endif - break; - default: - return; - } - } - - if (got_eacces) - errno = EACCES; -} - -static void child_process(char * const *commandLine, - char * const *newEnviron, - int *local_fds, int pipe_count, int *fail_fds, - const char *path, char **sh_argv, const char *wd, - int maxfd) -{ - sigset_t sigmask; - int errnum; - - close(fail_fds[0]); - - if (dup2(local_fds[0], 0) < 0) - goto child_error; - if (dup2(local_fds[3], 1) < 0) - goto child_error; - if (pipe_count == 3) - { - if (dup2(local_fds[5], 2) < 0) - goto child_error; - } - else if (dup2(1, 2) < 0) - goto child_error; - - close_fds(local_fds, pipe_count * 2); - - /* Mark non-standard fds (>= 3) close-on-exec. This includes fail_fds[1], - which must stay open until exec(), and should be closed automatically - if exec() succeeds. */ - if (mark_nonstd_fds_cloexec(maxfd) < 0) - goto child_error; - - if (wd != NULL && chdir(wd) != 0) - goto child_error; - - /* Reset the signal mask so that the executed program starts with all - signals unblocked. */ - sigemptyset(&sigmask); - if (sigprocmask(SIG_SETMASK, &sigmask, NULL) < 0) - goto child_error; - - cp_execvpe(commandLine[0], commandLine, newEnviron, path, sh_argv); - - child_error: - /* Child setup or exec itself failed; send our errno to the parent */ - errnum = errno; - while (write(fail_fds[1], &errnum, sizeof(errnum)) < 0 - && errno == EINTR) - ; - _exit(127); -} From 37c2dd99c3d46a7038a948bda1d71ab551a7e5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Tue, 11 Aug 2026 10:31:07 +0200 Subject: [PATCH 2/4] Add support for spawning processes using posix_spawn() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spawning through fork() can be expensive when the parent process is large. Add support for spawning using posix_spawn(), if available. The portable posix_spawn() interface cannot perform the whole child setup, though: there is no way to ask it to mark every remaining descriptor close-on-exec or to run our existing PATH search. Execute the target through a small helper program, cpspawnhelper, which reuses the same child-side code as the fork path. Invoke posix_spawn() without file actions or attributes, so that older versions of glibc use vfork() instead of fork() internally. Pass the descriptor numbers, the working directory, the parent's PATH and the target arguments in the helper's argv. Launch the helper with an empty environment to prevent the dynamic linker from acting on the target's loader settings before main(). The target environment is sent through a dedicated pipe to keep it out of the helper's command line. The new posix_spawn-based mechanism is selected in VMProcess, but it is disabled for now. Signed-off-by: Guillermo Rodríguez --- configure.ac | 1 + native/jni/java-lang/java_lang_VMProcess.c | 7 +- native/jni/native-lib/.gitignore | 1 + native/jni/native-lib/Makefile.am | 14 +- native/jni/native-lib/cpproc-child.c | 95 ++++---- native/jni/native-lib/cpproc-child.h | 44 +++- native/jni/native-lib/cpproc.c | 240 ++++++++++++++++++--- native/jni/native-lib/cpproc.h | 3 +- native/jni/native-lib/cpspawnhelper.c | 210 ++++++++++++++++++ vm/reference/java/lang/VMProcess.java | 7 +- 10 files changed, 544 insertions(+), 78 deletions(-) create mode 100644 native/jni/native-lib/cpspawnhelper.c diff --git a/configure.ac b/configure.ac index 03495aecb3..5c79f3cfd3 100644 --- a/configure.ac +++ b/configure.ac @@ -445,6 +445,7 @@ if test "x${COMPILE_JNI}" = xyes; then AC_CHECK_FUNCS([ftruncate fsync select \ gethostname socket strerror fork pipe execve open close close_range \ + posix_spawn \ lseek fstat read readv write writev htonl memset htons connect \ getsockname getpeername bind listen accept \ recvfrom send sendto setsockopt getsockopt time mktime clock_gettime \ diff --git a/native/jni/java-lang/java_lang_VMProcess.c b/native/jni/java-lang/java_lang_VMProcess.c index 6cd2292ac9..1ae0c63aff 100644 --- a/native/jni/java-lang/java_lang_VMProcess.c +++ b/native/jni/java-lang/java_lang_VMProcess.c @@ -117,14 +117,15 @@ copy_elem (JNIEnv * env, jobject stringArray, jint i) } /* - * private final native void nativeSpawn(String[], String[], File) + * private final native void nativeSpawn(String[], String[], File, boolean, boolean) * throws java/io/IOException */ JNIEXPORT void JNICALL Java_java_lang_VMProcess_nativeSpawn (JNIEnv * env, jobject this, jobjectArray cmdArray, jobjectArray envArray, jobject dirFile, - jboolean redirect) + jboolean redirect, + jboolean usePosixSpawn) { int fds[CPIO_EXEC_NUM_PIPES] = { -1, -1, -1 }; jobject streams[CPIO_EXEC_NUM_PIPES] = { NULL, NULL, NULL }; @@ -206,7 +207,7 @@ Java_java_lang_VMProcess_nativeSpawn (JNIEnv * env, jobject this, } /* Create inter-process pipes */ - err = cpproc_forkAndExec(strings, newEnviron, fds, pipe_count, &pid, dir); + err = cpproc_forkAndExec(strings, newEnviron, fds, pipe_count, &pid, dir, usePosixSpawn); if (err != 0) { strncpy(errbuf, cpnative_getErrorString (err), sizeof(errbuf)); diff --git a/native/jni/native-lib/.gitignore b/native/jni/native-lib/.gitignore index e9f2658a69..37f175252f 100644 --- a/native/jni/native-lib/.gitignore +++ b/native/jni/native-lib/.gitignore @@ -6,3 +6,4 @@ .deps Makefile Makefile.in +cpspawnhelper diff --git a/native/jni/native-lib/Makefile.am b/native/jni/native-lib/Makefile.am index 878840cdaf..3d4cf32cab 100644 --- a/native/jni/native-lib/Makefile.am +++ b/native/jni/native-lib/Makefile.am @@ -9,7 +9,19 @@ libclasspathnative_la_SOURCES = cpnet.c \ cpproc-child.h \ cpproc-child.c +spawnhelperdir = $(pkglibdir) +spawnhelper_PROGRAMS = cpspawnhelper +cpspawnhelper_SOURCES = cpspawnhelper.c \ + cpproc-child.h \ + cpproc-child.c + +# Keep the helper's objects separate from the libtool objects built from +# the same sources. Per-target CFLAGS trigger Automake's target-specific +# object names; assigning AM_CFLAGS leaves the effective flags unchanged. +cpspawnhelper_CFLAGS = $(AM_CFLAGS) + AM_LDFLAGS = @CLASSPATH_CONVENIENCE@ -AM_CPPFLAGS = @CLASSPATH_INCLUDES@ +AM_CPPFLAGS = @CLASSPATH_INCLUDES@ \ + -DCPPROC_SPAWN_HELPER='"$(pkglibdir)/cpspawnhelper"' AM_CFLAGS = @WARNING_CFLAGS@ @STRICT_WARNING_CFLAGS@ @ERROR_CFLAGS@ \ @EXTRA_CFLAGS@ diff --git a/native/jni/native-lib/cpproc-child.c b/native/jni/native-lib/cpproc-child.c index c65c1394a7..84a300ed70 100644 --- a/native/jni/native-lib/cpproc-child.c +++ b/native/jni/native-lib/cpproc-child.c @@ -58,19 +58,20 @@ exception statement from your version. */ #define PATH_MAX 4096 #endif -/* Child side of the process spawning implementation. +/* Child side of the process spawning implementation, shared by + the fork and posix_spawn mechanisms. - Everything in this file may run in the child of a fork() of a - multi-threaded process, so it should only use async-signal-safe - operations. Avoid malloc and functions that may take locks. Any - buffers that require allocation must be preallocated by the parent - and passed in. + Any function that may be called after fork() must only use + async-signal-safe operations: the child may inherit locks held + by other threads in the parent. Avoid malloc and functions that + may take locks. Any buffers that require allocation must be + preallocated by the caller and passed in. - All child-side functions defined here are static, so calls between - them cannot be lazily bound after fork() (resolving a lazy binding - runs the dynamic linker, which can deadlock the child). The entry - point itself (cpproc_child_fork_exec) is called before fork(), so - its binding is resolved safely in the parent. */ + Any function defined here and called after fork() is static, + so calls between them cannot be lazily bound (resolving a lazy + binding runs the dynamic linker, which can deadlock the child). + The entry point itself (cpproc_child_fork_exec) is called before + fork(), so its binding is resolved safely in the parent. */ static int mark_fd_cloexec(int fd) { @@ -288,34 +289,30 @@ static void cp_execvpe(const char *file, char * const *argv, errno = EACCES; } -static void child_process(char * const *commandLine, - char * const *newEnviron, - int *local_fds, int pipe_count, int *fail_fds, - const char *path, char **sh_argv, const char *wd, - int maxfd) +/* Prepare the child state and exec the target, reporting failures + through fail_fd. fds_to_close are closed after wiring stdio. */ +static void exec_target(char * const *commandLine, + char * const *newEnviron, + int in_fd, int out_fd, int err_fd, int fail_fd, + int *fds_to_close, int num_fds_to_close, + const char *path, char **sh_argv, const char *wd, + int maxfd) { sigset_t sigmask; int errnum; int i; - close(fail_fds[0]); - - if (dup2(local_fds[0], 0) < 0) + if (dup2(in_fd, 0) < 0) goto child_error; - if (dup2(local_fds[3], 1) < 0) + if (dup2(out_fd, 1) < 0) goto child_error; - if (pipe_count == 3) - { - if (dup2(local_fds[5], 2) < 0) - goto child_error; - } - else if (dup2(1, 2) < 0) + if (dup2(err_fd, 2) < 0) goto child_error; - for (i = 0; i < pipe_count * 2; i++) - close(local_fds[i]); + for (i = 0; i < num_fds_to_close; i++) + close(fds_to_close[i]); - /* Mark non-standard fds (>= 3) close-on-exec. This includes fail_fds[1], + /* Mark non-standard fds (>= 3) close-on-exec. This includes fail_fd, which must stay open until exec(), and should be closed automatically if exec() succeeds. */ if (mark_nonstd_fds_cloexec(maxfd) < 0) @@ -335,29 +332,49 @@ static void child_process(char * const *commandLine, child_error: /* Child setup or exec itself failed; send our errno to the parent */ errnum = errno; - while (write(fail_fds[1], &errnum, sizeof(errnum)) < 0 + while (write(fail_fd, &errnum, sizeof(errnum)) < 0 && errno == EINTR) ; - _exit(127); + _exit(CPPROC_EXIT_ERROR); } -/* Entry point */ +/* Entry points */ pid_t cpproc_child_fork_exec(char * const *commandLine, char * const *newEnviron, - int *local_fds, int pipe_count, - int *fail_fds, const char *path, - char **sh_argv, const char *wd, int maxfd) + int *local_fds, int pipe_count, int *fail_fds, + const char *path, char **sh_argv, const char *wd, + int maxfd) { pid_t pid = fork(); if (pid == 0) { - child_process(commandLine, newEnviron, local_fds, pipe_count, - fail_fds, path, sh_argv, wd, maxfd); - /* child_process() does not return. */ - _exit(127); + close(fail_fds[0]); + exec_target(commandLine, newEnviron, + local_fds[0], local_fds[3], + pipe_count == 3 ? local_fds[5] : local_fds[3], + fail_fds[1], + local_fds, pipe_count * 2, + path, sh_argv, wd, maxfd); + /* exec_target() does not return. */ + _exit(CPPROC_EXIT_ERROR); } return pid; } + +void cpproc_child_exec(char * const *commandLine, + char * const *newEnviron, + int in_fd, int out_fd, int err_fd, int fail_fd, + const char *path, char **sh_argv, const char *wd, + int maxfd) +{ + /* Unlike the fork child, this process holds no parent-side pipe ends + (the parent marked its own close-on-exec before spawning), so the + close-on-exec sweep covers everything that remains. */ + exec_target(commandLine, newEnviron, + in_fd, out_fd, err_fd, fail_fd, + NULL, 0, + path, sh_argv, wd, maxfd); +} diff --git a/native/jni/native-lib/cpproc-child.h b/native/jni/native-lib/cpproc-child.h index 6e77400feb..ebe8285661 100644 --- a/native/jni/native-lib/cpproc-child.h +++ b/native/jni/native-lib/cpproc-child.h @@ -50,11 +50,49 @@ exception statement from your version. */ #define CP_HIDDEN #endif -/* Fork and exec the target program; returns the child pid, or -1 - with errno set if fork() fails. */ -CP_HIDDEN pid_t cpproc_child_fork_exec(char * const *commandLine, char * const *newEnviron, +/* Entry point for fork-based spawning. Fork and execute the target; + returns the child pid, or -1 with errno set if fork() fails. */ +CP_HIDDEN pid_t cpproc_child_fork_exec(char * const *commandLine, + char * const *newEnviron, int *local_fds, int pipe_count, int *fail_fds, const char *path, char **sh_argv, const char *wd, int maxfd); +/* Entry point for the spawn helper. Execute the target in the current + process; never returns. */ +CP_HIDDEN void cpproc_child_exec(char * const *commandLine, + char * const *newEnviron, + int in_fd, int out_fd, int err_fd, int fail_fd, + const char *path, char **sh_argv, const char *wd, + int maxfd); + +/* Once either entry point is reached, failures before exec are + reported as an int errno through fail_fd; a successful exec closes + fail_fd. */ + +/* Exit status of a child that fails before executing the target. + posix_spawn() uses the same value for failures between spawn and + exec. */ +#define CPPROC_EXIT_ERROR 127 + +/* Spawn environment transfer from the parent to the spawn helper: + + The helper is launched with an empty environment, so the target + environment cannot affect its dynamic loader or appear in its + command line. The env pipe carries the effective target + environment instead. + + The pipe format is: + + int magic CPPROC_SPAWN_MAGIC + int length byte length of the string block + char block[length] concatenated NUL-terminated strings + + The block is either empty or ends in a NUL, so the helper can safely + walk it and derive the number of strings. */ + +/* Magic number in the pipe format header. Bump whenever the format + changes so that a stale helper rejects the block. */ +#define CPPROC_SPAWN_MAGIC 0x43505331 /* "CPS1" */ + #endif diff --git a/native/jni/native-lib/cpproc.c b/native/jni/native-lib/cpproc.c index f346b4ff70..78e4445ab7 100644 --- a/native/jni/native-lib/cpproc.c +++ b/native/jni/native-lib/cpproc.c @@ -38,9 +38,11 @@ exception statement from your version. */ #include "config.h" #include +#include /* for environ */ #include "cpproc.h" #include "cpproc-child.h" #include +#include #include #include #include @@ -50,6 +52,13 @@ exception statement from your version. */ #include #include +#ifdef HAVE_POSIX_SPAWN +#include +#ifndef CPPROC_SPAWN_HELPER +#error Path to the spawn helper not defined +#endif +#endif + /* Bound the last-resort fcntl scan when OPEN_MAX is pathologically large. */ #define MAX_FD_SCAN 65536 @@ -107,21 +116,185 @@ static int pipe_above_stdio(int *fds) return 0; } +#ifdef HAVE_POSIX_SPAWN +/* Send the effective target environment to the helper. + Returns 0, or an errno. */ +static int write_spawn_env(int fd, char * const *newEnviron) +{ + char * const *env = (newEnviron != NULL) ? newEnviron : environ; + int header[2]; + size_t total; + size_t len = 0; + size_t off; + char *buf; + int count; + int err = 0; + int i; + + for (count = 0; env[count] != NULL; count++) + len += strlen(env[count]) + 1; + + if (len > INT_MAX) + return E2BIG; + + header[0] = CPPROC_SPAWN_MAGIC; + header[1] = (int) len; + + total = sizeof(header) + len; + buf = malloc(total); + if (buf == NULL) + return ENOMEM; + + memcpy(buf, header, sizeof(header)); + off = sizeof(header); + for (i = 0; i < count; i++) + { + size_t n = strlen(env[i]) + 1; + + memcpy(buf + off, env[i], n); + off += n; + } + + for (off = 0; off < total; ) + { + ssize_t n = write(fd, buf + off, total - off); + + if (n < 0) + { + if (errno == EINTR) + continue; + err = errno; + break; + } + off += (size_t) n; + } + + free(buf); + + return err; +} + + +/* Decimal needs ~2.4 chars per byte, plus NUL terminator */ +#define FD_ARG_SIZE ((3 * sizeof(int)) + 1) + +/* Spawn the helper without file actions or attributes, so that older + glibc versions will use vfork() internally, and not fork(). Returns + the helper pid, or -1 with errno set. */ +static pid_t spawn_via_helper(char * const *commandLine, + char * const *newEnviron, int *local_fds, + int pipe_count, int *fail_fds, + const char *path, const char *wd, int argc) +{ + char * const empty_envp[] = { NULL }; + char fd_args[5][FD_ARG_SIZE]; + int parent_fds[5]; + int env_fds[2]; + char **helper_argv; + pid_t pid = -1; + int err; + int i; + + helper_argv = malloc((8 + argc + 1) * sizeof(char *)); + if (helper_argv == NULL) + { + errno = ENOMEM; + return -1; + } + + if (pipe_above_stdio(env_fds) < 0) + { + err = errno; + free(helper_argv); + errno = err; + return -1; + } + + /* Mark the parent's ends of these pipes close-on-exec, so the helper + inherits only their child ends. In particular, if the helper also + inherited the env pipe's write end, it would never see EOF if the + parent dies mid-spawn, leaving the orphaned helper blocked forever + (JDK-8307990). */ + parent_fds[0] = env_fds[1]; + parent_fds[1] = fail_fds[0]; + parent_fds[2] = local_fds[1]; + parent_fds[3] = local_fds[2]; + parent_fds[4] = (pipe_count == 3) ? local_fds[4] : -1; + for (i = 0; i < 5; i++) + { + if (parent_fds[i] != -1 + && fcntl(parent_fds[i], F_SETFD, FD_CLOEXEC) < 0) + { + err = errno; + free(helper_argv); + close(env_fds[0]); + close(env_fds[1]); + errno = err; + return -1; + } + } + + snprintf(fd_args[0], FD_ARG_SIZE, "%d", local_fds[0]); + snprintf(fd_args[1], FD_ARG_SIZE, "%d", local_fds[3]); + snprintf(fd_args[2], FD_ARG_SIZE, "%d", + pipe_count == 3 ? local_fds[5] : local_fds[3]); + snprintf(fd_args[3], FD_ARG_SIZE, "%d", fail_fds[1]); + snprintf(fd_args[4], FD_ARG_SIZE, "%d", env_fds[0]); + + helper_argv[0] = (char *) CPPROC_SPAWN_HELPER; + for (i = 0; i < 5; i++) + helper_argv[1 + i] = fd_args[i]; + helper_argv[6] = (char *) (wd != NULL ? wd : "."); + helper_argv[7] = (char *) path; + for (i = 0; i < argc; i++) + helper_argv[8 + i] = commandLine[i]; + helper_argv[8 + argc] = NULL; + + /* The helper is launched with an empty environment, so it must be + loadable without environment-dependent search paths. */ + err = posix_spawn(&pid, CPPROC_SPAWN_HELPER, NULL, NULL, helper_argv, empty_envp); + free(helper_argv); + close(env_fds[0]); + + if (err == 0) + err = write_spawn_env(env_fds[1], newEnviron); + close(env_fds[1]); + + if (err != 0) + { + if (pid > 0) + { + /* Helper was spawned, but the environment transfer failed. + We already closed the pipe's write end, so the helper's + read returns EOF and it exits. Reap it. */ + int status; + + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + } + errno = err; + return -1; + } + + return pid; +} +#endif /* HAVE_POSIX_SPAWN */ + int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, - int *fds, int pipe_count, pid_t *out_pid, const char *wd) + int *fds, int pipe_count, pid_t *out_pid, const char *wd, + int use_posix_spawn) { int local_fds[6]; int fail_fds[2]; const char *path; - char **sh_argv; sigset_t allsigs; sigset_t savedmask; int errnum; ssize_t n; int argc; + int err; int i; - int maxfd; - pid_t pid; + pid_t pid = -1; /* Initialize the output fds so that the caller sees no garbage in them if we return with an error, or in the unused stderr entry @@ -129,28 +302,18 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, for (i = 0; i < CPIO_EXEC_NUM_PIPES; i++) fds[i] = -1; - /* Preallocate the buffer used by cp_execvpe in the child: after the - fork of a multi-threaded process only async-signal-safe operations - may be executed, so no malloc there */ path = getenv("PATH"); if (path == NULL) path = "/bin:/usr/bin"; for (argc = 0; commandLine[argc] != NULL; argc++) ; - sh_argv = malloc((argc + 2) * sizeof(char *)); - if (sh_argv == NULL) - return ENOMEM; - - maxfd = get_max_fd(); for (i = 0; i < (pipe_count * 2); i += 2) { if (pipe_above_stdio(&local_fds[i]) < 0) { - int err = errno; - + err = errno; close_fds(local_fds, i); - free(sh_argv); return err; } @@ -161,37 +324,56 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, parent reads EOF. */ if (pipe_above_stdio(fail_fds) < 0) { - int err = errno; - + err = errno; close_fds(local_fds, pipe_count * 2); - free(sh_argv); return err; } /* Block all signals before we fork() to ensure that the child's - setup is not interrupted, so no call can fail with EINTR. */ + setup is not interrupted, so no call can fail with EINTR. The + mask also crosses posix_spawn's exec into the helper, whose + setup runs equally shielded. */ sigfillset(&allsigs); pthread_sigmask(SIG_SETMASK, &allsigs, &savedmask); - pid = cpproc_child_fork_exec(commandLine, newEnviron, local_fds, - pipe_count, fail_fds, path, sh_argv, wd, - maxfd); + if (use_posix_spawn) + { +#ifdef HAVE_POSIX_SPAWN + pid = spawn_via_helper(commandLine, newEnviron, local_fds, + pipe_count, fail_fds, path, wd, argc); + err = errno; +#else + err = ENOSYS; +#endif + } + else + { + /* Preallocate cp_execvpe's buffer because malloc is unsafe after fork. + The child gets its own copy, so the parent can free it immediately. */ + char **sh_argv = malloc((argc + 2) * sizeof(char *)); + + if (sh_argv == NULL) + err = ENOMEM; + else + { + pid = cpproc_child_fork_exec(commandLine, newEnviron, local_fds, + pipe_count, fail_fds, path, sh_argv, + wd, get_max_fd()); + err = errno; + free(sh_argv); + } + } + + pthread_sigmask(SIG_SETMASK, &savedmask, NULL); if (pid == -1) { - int err = errno; - - pthread_sigmask(SIG_SETMASK, &savedmask, NULL); close_fds(local_fds, pipe_count * 2); close(fail_fds[0]); close(fail_fds[1]); - free(sh_argv); return err; } - - pthread_sigmask(SIG_SETMASK, &savedmask, NULL); - free(sh_argv); close(fail_fds[1]); /* Wait for the outcome of the exec: EOF if it succeeded, the diff --git a/native/jni/native-lib/cpproc.h b/native/jni/native-lib/cpproc.h index 5e8db5800a..f1a12941fa 100644 --- a/native/jni/native-lib/cpproc.h +++ b/native/jni/native-lib/cpproc.h @@ -45,7 +45,8 @@ exception statement from your version. */ #define CPIO_EXEC_NUM_PIPES 3 JNIEXPORT int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, - int *fds, int pipe_count, pid_t *pid, const char *wd); + int *fds, int pipe_count, pid_t *pid, const char *wd, + int use_posix_spawn); JNIEXPORT int cpproc_waitpid (pid_t pid, int *status, pid_t *outpid, int options); JNIEXPORT int cpproc_kill (pid_t pid, int signal); diff --git a/native/jni/native-lib/cpspawnhelper.c b/native/jni/native-lib/cpspawnhelper.c new file mode 100644 index 0000000000..29b6035d5b --- /dev/null +++ b/native/jni/native-lib/cpspawnhelper.c @@ -0,0 +1,210 @@ +/* cpspawnhelper.c - + Copyright (C) 2026 INGELABS S.L. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +#include +#include +#include +#include +#include + +#include "cpproc-child.h" + +/* Bound the last-resort fcntl scan when OPEN_MAX is pathologically large. */ +#define MAX_FD_SCAN 65536 + +static int get_max_fd(void) +{ + long value = sysconf(_SC_OPEN_MAX); + + if (value <= 0 || value > MAX_FD_SCAN) + return MAX_FD_SCAN; + + return (int) value; +} + +static void report_errnum(int fail_fd, int errnum) +{ + while (write(fail_fd, &errnum, sizeof(errnum)) < 0 + && errno == EINTR) + ; +} + +static int read_full(int fd, void *buf, size_t len) +{ + char *p = buf; + size_t off = 0; + + while (off < len) + { + ssize_t n = read(fd, p + off, len - off); + + if (n < 0) + { + if (errno == EINTR) + continue; + return errno; + } + if (n == 0) + return EPIPE; /* the data was never sent in full */ + off += (size_t) n; + } + + return 0; +} + +/* Read the target environment from the env pipe. On success, + stores the environment and returns 0. On error, returns an errno. + See cpproc-child.h for the layout. */ +static int read_spawn_env(int fd, char ***out_env) +{ + int header[2]; + size_t len; + size_t off; + size_t count; + char *blob; + char **env; + int err; + int i; + + err = read_full(fd, header, sizeof(header)); + if (err != 0) + return err; + + if (header[0] != CPPROC_SPAWN_MAGIC || header[1] < 0) + return EINVAL; + + len = (size_t) header[1]; + blob = malloc(len + 1); + if (blob == NULL) + return ENOMEM; + + err = read_full(fd, blob, len); + if (err != 0) + { + free(blob); + return err; + } + + /* The strings are walked with strlen(), so require the block to end + in NUL to prevent the final scan from reading past it. */ + if (len > 0 && blob[len - 1] != '\0') + { + free(blob); + return EINVAL; + } + blob[len] = '\0'; + + count = 0; + for (off = 0; off < len; off += strlen(blob + off) + 1) + count++; + + env = malloc((count + 1) * sizeof(char *)); + if (env == NULL) + { + free(blob); + return ENOMEM; + } + + off = 0; + for (i = 0; i < (int) count; i++) + { + env[i] = blob + off; + off += strlen(blob + off) + 1; + } + env[count] = NULL; + + *out_env = env; + return 0; +} + +int main(int argc, char **argv) +{ + char * const *target_argv; + char **target_envp; + char **sh_argv; + const char *path; + const char *wd; + int target_argc; + int stdin_fd; + int stdout_fd; + int stderr_fd; + int fail_fd; + int env_fd; + int err; + + if (argc < 9) + { + fprintf(stderr, + "Usage: %s stdin-fd stdout-fd stderr-fd fail-fd env-fd wd path program [args...]\n", + argv[0]); + /* No fail fd to report through; just exit. */ + return CPPROC_EXIT_ERROR; + } + + stdin_fd = atoi(argv[1]); + stdout_fd = atoi(argv[2]); + stderr_fd = atoi(argv[3]); + fail_fd = atoi(argv[4]); + env_fd = atoi(argv[5]); + /* wd == "." means no directory change. */ + wd = (strcmp(argv[6], ".") == 0) ? NULL : argv[6]; + path = argv[7]; + target_argv = &argv[8]; + + err = read_spawn_env(env_fd, &target_envp); + if (err != 0) + { + report_errnum(fail_fd, err); + return CPPROC_EXIT_ERROR; + } + close(env_fd); + + for (target_argc = 0; target_argv[target_argc] != NULL; target_argc++) + ; + sh_argv = malloc((target_argc + 2) * sizeof(char *)); + if (sh_argv == NULL) + { + report_errnum(fail_fd, ENOMEM); + return CPPROC_EXIT_ERROR; + } + + cpproc_child_exec(target_argv, target_envp, stdin_fd, stdout_fd, + stderr_fd, fail_fd, path, sh_argv, wd, get_max_fd()); + + /* cpproc_child_exec() does not return. */ + return CPPROC_EXIT_ERROR; +} diff --git a/vm/reference/java/lang/VMProcess.java b/vm/reference/java/lang/VMProcess.java index e89aaecca7..7cf97033d2 100644 --- a/vm/reference/java/lang/VMProcess.java +++ b/vm/reference/java/lang/VMProcess.java @@ -73,6 +73,9 @@ final class VMProcess extends Process private static final int RUNNING = 1; private static final int TERMINATED = 2; + // Whether to spawn processes via posix_spawn() instead of fork() + private static final boolean USE_POSIX_SPAWN = false; + // Dedicated thread that does all the fork()'ing and wait()'ing. static Thread processThread; @@ -217,7 +220,7 @@ private void spawn(VMProcess process) try { process.nativeSpawn(process.cmd, process.env, process.dir, - process.redirect); + process.redirect, USE_POSIX_SPAWN); process.state = RUNNING; activeMap.put(new Long(process.pid), process); } @@ -425,7 +428,7 @@ private void waitForStateUninterruptibly(int state, boolean eq) * @throws IOException if the O/S process could not be created. */ native void nativeSpawn(String[] cmd, String[] env, File dir, - boolean redirect) + boolean redirect, boolean usePosixSpawn) throws IOException; /** From 3bccc041307d9f58b97146bca4727aaffbc703ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Tue, 11 Aug 2026 11:43:18 +0200 Subject: [PATCH 3/4] Detect a spawn helper that fails before main() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POSIX allows posix_spawn() to report an exec failure in the child only through an exit status of 127, with the call itself returning success (glibc did this before 2.24; see glibc #18433). That status cannot be distinguished from a successfully executed target that later exits with 127. The helper can also die after its own exec succeeds but before main() runs, for example if its dynamic loader fails. In either failure case, the parent would read EOF from the fail pipe and report a successful spawn even though the target never ran. Make the helper write a readiness marker to the fail pipe after parsing its arguments, and require the parent to receive it before treating a subsequent EOF as success. EOF without the marker now reports an error instead of a successful spawn. Signed-off-by: Guillermo Rodríguez --- native/jni/native-lib/cpproc-child.h | 6 +++++ native/jni/native-lib/cpproc.c | 32 +++++++++++++++++++++++++++ native/jni/native-lib/cpspawnhelper.c | 4 ++++ 3 files changed, 42 insertions(+) diff --git a/native/jni/native-lib/cpproc-child.h b/native/jni/native-lib/cpproc-child.h index ebe8285661..28d8e9ca6e 100644 --- a/native/jni/native-lib/cpproc-child.h +++ b/native/jni/native-lib/cpproc-child.h @@ -75,6 +75,12 @@ CP_HIDDEN void cpproc_child_exec(char * const *commandLine, exec. */ #define CPPROC_EXIT_ERROR 127 +/* Written to fail_fd once the spawn helper has reached main() and + parsed its arguments. The parent requires this marker before + treating a subsequent EOF as a successful exec. Negative, so it + cannot be a valid errno. */ +#define CPPROC_HELPER_ALIVE (-0x4C495645) /* -("LIVE") */ + /* Spawn environment transfer from the parent to the spawn helper: The helper is launched with an empty environment, so the target diff --git a/native/jni/native-lib/cpproc.c b/native/jni/native-lib/cpproc.c index 78e4445ab7..74fb1c2e07 100644 --- a/native/jni/native-lib/cpproc.c +++ b/native/jni/native-lib/cpproc.c @@ -376,6 +376,38 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, } close(fail_fds[1]); +#ifdef HAVE_POSIX_SPAWN + if (use_posix_spawn) + { + /* Require the helper's readiness marker before trusting the + fail-pipe EOF below. POSIX allows posix_spawn() to report a + failed exec of the helper only through an exit status of 127 + (glibc < 2.24; see glibc #18433), and the helper can also die + after a successful exec but before main() runs; either way, + EOF would then be misread as success. */ + do + { + n = read(fail_fds[0], &errnum, sizeof(errnum)); + } + while (n < 0 && errno == EINTR); + + if (n != (ssize_t) sizeof(errnum) || errnum != CPPROC_HELPER_ALIVE) + { + int status; + + /* The helper failed before main(), or a stale helper + reported an errno after rejecting the protocol magic. */ + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + + close_fds(local_fds, pipe_count * 2); + close(fail_fds[0]); + return (n == (ssize_t) sizeof(errnum) && errnum > 0) + ? errnum : ENOEXEC; + } + } +#endif + /* Wait for the outcome of the exec: EOF if it succeeded, the child's errno if not */ do diff --git a/native/jni/native-lib/cpspawnhelper.c b/native/jni/native-lib/cpspawnhelper.c index 29b6035d5b..e3c256ad12 100644 --- a/native/jni/native-lib/cpspawnhelper.c +++ b/native/jni/native-lib/cpspawnhelper.c @@ -185,6 +185,10 @@ int main(int argc, char **argv) path = argv[7]; target_argv = &argv[8]; + /* Tell the parent we are alive, before doing anything that may + block or fail. */ + report_errnum(fail_fd, CPPROC_HELPER_ALIVE); + err = read_spawn_env(env_fd, &target_envp); if (err != 0) { From d29d2464309ae4aa73cdebcec103dbebcd2b924d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Tue, 11 Aug 2026 12:31:11 +0200 Subject: [PATCH 4/4] Allow selecting the process spawning mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow selecting the process spawning mechanism through the gnu.lang.process.posixSpawn system property: "false" forces fork(), any other value (including empty) forces posix_spawn(), and an unset property selects the platform default. Default to posix_spawn() on Linux and macOS, and to fork() on other platforms, where the behavior of posix_spawn() has not been verified. Signed-off-by: Guillermo Rodríguez --- vm/reference/java/lang/VMProcess.java | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/vm/reference/java/lang/VMProcess.java b/vm/reference/java/lang/VMProcess.java index 7cf97033d2..d0ffbf5328 100644 --- a/vm/reference/java/lang/VMProcess.java +++ b/vm/reference/java/lang/VMProcess.java @@ -73,8 +73,28 @@ final class VMProcess extends Process private static final int RUNNING = 1; private static final int TERMINATED = 2; - // Whether to spawn processes via posix_spawn() instead of fork() - private static final boolean USE_POSIX_SPAWN = false; + // Whether to spawn processes via posix_spawn() and the spawn helper + // instead of fork(). Controlled by the gnu.lang.process.posixSpawn + // property: + // - unset: platform default (enabled on Linux and macOS) + // - "false": disabled + // - any other value: enabled + private static final boolean usePosixSpawn; + static + { + String prop = System.getProperty("gnu.lang.process.posixSpawn"); + if (prop != null) + { + usePosixSpawn = !prop.equalsIgnoreCase("false"); + } + else + { + String os = System.getProperty("os.name", "").toLowerCase(); + usePosixSpawn = os.contains("linux") + || os.contains("mac") + || os.contains("darwin"); + } + } // Dedicated thread that does all the fork()'ing and wait()'ing. static Thread processThread; @@ -220,7 +240,7 @@ private void spawn(VMProcess process) try { process.nativeSpawn(process.cmd, process.env, process.dir, - process.redirect, USE_POSIX_SPAWN); + process.redirect, usePosixSpawn); process.state = RUNNING; activeMap.put(new Long(process.pid), process); }