diff --git a/configure.ac b/configure.ac index 03495aecb..5c79f3cfd 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 6cd2292ac..1ae0c63af 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 e9f2658a6..37f175252 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 0de723eb1..3d4cf32ca 100644 --- a/native/jni/native-lib/Makefile.am +++ b/native/jni/native-lib/Makefile.am @@ -5,9 +5,23 @@ libclasspathnative_la_SOURCES = cpnet.c \ cpio.h \ cpnative.h \ cpproc.h \ - cpproc.c + cpproc.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 new file mode 100644 index 000000000..84a300ed7 --- /dev/null +++ b/native/jni/native-lib/cpproc-child.c @@ -0,0 +1,380 @@ +/* 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, shared by + the fork and posix_spawn mechanisms. + + 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. + + 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) +{ + 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; +} + +/* 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; + + if (dup2(in_fd, 0) < 0) + goto child_error; + if (dup2(out_fd, 1) < 0) + goto child_error; + if (dup2(err_fd, 2) < 0) + goto child_error; + + 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_fd, + 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_fd, &errnum, sizeof(errnum)) < 0 + && errno == EINTR) + ; + _exit(CPPROC_EXIT_ERROR); +} + +/* 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) +{ + pid_t pid = fork(); + + if (pid == 0) + { + 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 new file mode 100644 index 000000000..28d8e9ca6 --- /dev/null +++ b/native/jni/native-lib/cpproc-child.h @@ -0,0 +1,104 @@ +/* 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 + +/* 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 + +/* 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 + 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 b73b756c0..74fb1c2e0 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,16 +36,13 @@ 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 /* for environ */ #include "cpproc.h" -#include +#include "cpproc-child.h" #include +#include #include #include #include @@ -54,20 +52,23 @@ 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 +#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 -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. */ @@ -115,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 @@ -137,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; } @@ -169,83 +324,124 @@ 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 = fork(); + 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); + } + } - switch (pid) + pthread_sigmask(SIG_SETMASK, &savedmask, NULL); + + 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: - pthread_sigmask(SIG_SETMASK, &savedmask, NULL); - free(sh_argv); + close_fds(local_fds, pipe_count * 2); + close(fail_fds[0]); close(fail_fds[1]); + return err; + } + close(fail_fds[1]); - /* Wait for the outcome of the exec: EOF if it succeeded, the - child's errno if not */ +#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); - close(fail_fds[0]); - if (n != 0) + if (n != (ssize_t) sizeof(errnum) || errnum != CPPROC_HELPER_ALIVE) { int status; - if (n != (ssize_t) sizeof(errnum)) - errnum = EIO; - - /* The child exited without exec'ing; reap it */ + /* 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); - return errnum; + close(fail_fds[0]); + return (n == (ssize_t) sizeof(errnum) && errnum > 0) + ? errnum : ENOEXEC; } + } +#endif - 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; + /* 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]); - /* keep compiler happy */ + if (n != 0) + { + int status; + + if (n != (ssize_t) sizeof(errnum)) + errnum = EIO; + + /* The child exited without exec'ing; reap it */ + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + + close_fds(local_fds, pipe_count * 2); + return errnum; + } + + 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 +464,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); -} diff --git a/native/jni/native-lib/cpproc.h b/native/jni/native-lib/cpproc.h index 5e8db5800..f1a12941f 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 000000000..e3c256ad1 --- /dev/null +++ b/native/jni/native-lib/cpspawnhelper.c @@ -0,0 +1,214 @@ +/* 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]; + + /* 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) + { + 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 e89aaecca..d0ffbf532 100644 --- a/vm/reference/java/lang/VMProcess.java +++ b/vm/reference/java/lang/VMProcess.java @@ -73,6 +73,29 @@ final class VMProcess extends Process private static final int RUNNING = 1; private static final int TERMINATED = 2; + // 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; @@ -217,7 +240,7 @@ private void spawn(VMProcess process) try { process.nativeSpawn(process.cmd, process.env, process.dir, - process.redirect); + process.redirect, usePosixSpawn); process.state = RUNNING; activeMap.put(new Long(process.pid), process); } @@ -425,7 +448,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; /**