From 0ac618fc5537787273838d1d84fd6c63ce32595e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Wed, 8 Jul 2026 13:24:20 +0200 Subject: [PATCH 1/2] Report chdir/exec failures in spawned children to the parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the child of cpproc_forkAndExec fails to chdir or exec, the failure is never reported to the parent: the child simply calls abort(). Running a non-existing binary appears to succeed, and the failure is only observable as a SIGABRT exit code (-6) from Process.waitFor(), instead of an IOException thrown from Runtime.exec() or ProcessBuilder.start(). Fix this with an extra pipe (FD_CLOEXEC) between child and parent: if chdir or exec fails, the child writes its errno to the pipe and exits; on success the exec itself closes the pipe. The parent blocks reading the pipe: EOF means success, otherwise it reaps the failed child and returns the child's errno, which surfaces as an IOException. Also initialize the output fds so that the caller never sees stale values in them if the spawn fails, or in the unused stderr entry when redirection is requested. Fixes #41 (BZ#111585) Signed-off-by: Guillermo Rodríguez --- native/jni/native-lib/cpproc.c | 93 ++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/native/jni/native-lib/cpproc.c b/native/jni/native-lib/cpproc.c index 0c80703d63..61197f92ef 100644 --- a/native/jni/native-lib/cpproc.c +++ b/native/jni/native-lib/cpproc.c @@ -43,6 +43,7 @@ exception statement from your version. */ #include #include #include +#include #include static void close_all_fds(int *fds, int numFds) @@ -57,9 +58,18 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, int *fds, int pipe_count, pid_t *out_pid, const char *wd) { int local_fds[6]; + int fail_fds[2]; + int errnum; + ssize_t n; int i; pid_t pid; + /* 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 + when redirection is requested */ + for (i = 0; i < CPIO_EXEC_NUM_PIPES; i++) + fds[i] = -1; + for (i = 0; i < (pipe_count * 2); i += 2) { if (pipe(&local_fds[i]) < 0) @@ -67,16 +77,32 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, int err = errno; close_all_fds(local_fds, i); - + return err; } } - + + /* Extra pipe used by the child to report a failed chdir or exec to + the parent. On success the exec closes the write end (FD_CLOEXEC) + and the parent reads EOF. */ + if (pipe(fail_fds) < 0) + { + int err = errno; + + close_all_fds(local_fds, pipe_count * 2); + + return err; + } + pid = fork(); - + switch (pid) { case 0: + close(fail_fds[0]); + if (fcntl(fail_fds[1], F_SETFD, FD_CLOEXEC) < 0) + goto child_error; + dup2(local_fds[0], 0); dup2(local_fds[3], 1); if (pipe_count == 3) @@ -86,24 +112,58 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, close_all_fds(local_fds, pipe_count * 2); - i = chdir(wd); - /* FIXME: Handle the return value */ - if (newEnviron == NULL) - execvp(commandLine[0], commandLine); - else - execve(commandLine[0], commandLine, newEnviron); - - abort(); - - break; + if (wd == NULL || chdir(wd) == 0) + { + if (newEnviron == NULL) + execvp(commandLine[0], commandLine); + else + execve(commandLine[0], commandLine, newEnviron); + } + + child_error: + /* The fcntl, chdir or exec failed; send our errno to the parent */ + errnum = errno; + while (write(fail_fds[1], &errnum, sizeof(errnum)) < 0 + && errno == EINTR) + ; + _exit(127); + case -1: { int err = errno; - + close_all_fds(local_fds, pipe_count * 2); + close(fail_fds[0]); + close(fail_fds[1]); return err; } - default: + default: + close(fail_fds[1]); + + /* 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 != 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_all_fds(local_fds, pipe_count * 2); + return errnum; + } + close(local_fds[0]); close(local_fds[3]); if (pipe_count == 3) @@ -111,7 +171,8 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, fds[0] = local_fds[1]; fds[1] = local_fds[2]; - fds[2] = local_fds[4]; + if (pipe_count == 3) + fds[2] = local_fds[4]; *out_pid = pid; return 0; } From 5c564b5bb36f76e0e551686395f0339eaa147a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Wed, 8 Jul 2026 13:56:17 +0200 Subject: [PATCH 2/2] Fix PATH search when spawning processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cpproc_forkAndExec receives a non-NULL environment array (which is always the case for ProcessBuilder), the PATH is not searched. This happens because cpproc_forkAndExec internally uses execve when a non-NULL environment is passed, and execvp otherwise. What we'd want here is execvpe, but this is not POSIX; it is a GNU extension introduced in glibc 2.11. (Additionally, POSIX does not list execvp as async-signal-safe, so it shouldn't be used after fork() in a multi-threaded process. For a precedent, see [1].) Fix this by implementing an execvpe replacement which emulates its behaviour (pass environment if one is supplied, search parent's PATH, fallback to running via /bin/sh if execv/execve fails with ENOEXEC). Error handling follows execvp semantics: an empty command name fails with ENOENT, broken PATH entries are skipped, and EACCES is sticky. Both the null and non-null environment cases now go through the same implementation, so both spawn paths behave identically. [1] https://git.kernel.org/pub/scm/git/git.git/commit/?id=e3a434468f ("run-command: use the async-signal-safe execv instead of execvp") Fixes #42 (BZ#111586) Signed-off-by: Guillermo Rodríguez --- native/jni/native-lib/cpproc.c | 158 +++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 6 deletions(-) diff --git a/native/jni/native-lib/cpproc.c b/native/jni/native-lib/cpproc.c index 61197f92ef..6c114ee646 100644 --- a/native/jni/native-lib/cpproc.c +++ b/native/jni/native-lib/cpproc.c @@ -44,7 +44,14 @@ exception statement from your version. */ #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 static void close_all_fds(int *fds, int numFds) { @@ -54,13 +61,141 @@ static void close_all_fds(int *fds, int numFds) close(fds[i]); } +/* 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) +{ + /* - This runs in the child of a fork of a multi-threaded process, + so it may only execute async-signal-safe operations. + - 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; +} + int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, int *fds, int pipe_count, pid_t *out_pid, const char *wd) { int local_fds[6]; int fail_fds[2]; + const char *path; + char **sh_argv; int errnum; ssize_t n; + int argc; int i; pid_t pid; @@ -70,6 +205,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; + for (i = 0; i < (pipe_count * 2); i += 2) { if (pipe(&local_fds[i]) < 0) @@ -77,6 +224,7 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, int err = errno; close_all_fds(local_fds, i); + free(sh_argv); return err; } @@ -90,6 +238,7 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, int err = errno; close_all_fds(local_fds, pipe_count * 2); + free(sh_argv); return err; } @@ -113,12 +262,7 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, close_all_fds(local_fds, pipe_count * 2); if (wd == NULL || chdir(wd) == 0) - { - if (newEnviron == NULL) - execvp(commandLine[0], commandLine); - else - execve(commandLine[0], commandLine, newEnviron); - } + cp_execvpe(commandLine[0], commandLine, newEnviron, path, sh_argv); child_error: /* The fcntl, chdir or exec failed; send our errno to the parent */ @@ -135,9 +279,11 @@ int cpproc_forkAndExec (char * const *commandLine, char * const * newEnviron, close_all_fds(local_fds, pipe_count * 2); close(fail_fds[0]); close(fail_fds[1]); + free(sh_argv); return err; } default: + free(sh_argv); close(fail_fds[1]); /* Wait for the outcome of the exec: EOF if it succeeded, the