From 2240ff41795d1de76aab1ce69758fcbd827208de Mon Sep 17 00:00:00 2001 From: Thomas Stuefe Date: Mon, 16 Mar 2026 13:31:38 +0000 Subject: [PATCH] 8379967: (process) Improve ProcessBuilder error reporting Reviewed-by: mbaesken, rriggs --- make/modules/java.base/Launcher.gmk | 3 +- .../unix/native/jspawnhelper/jspawnhelper.c | 77 ++++++---- .../unix/native/libjava/ProcessImpl_md.c | 46 ++++-- src/java.base/unix/native/libjava/childproc.c | 140 ++++++++++++------ src/java.base/unix/native/libjava/childproc.h | 7 - .../native/libjava/childproc_errorcodes.c | 63 ++++++++ .../native/libjava/childproc_errorcodes.h | 117 +++++++++++++++ .../lang/ProcessBuilder/InvalidWorkDir.java | 62 ++++++++ .../ProcessBuilder/JspawnhelperWarnings.java | 46 ++++-- 9 files changed, 455 insertions(+), 106 deletions(-) create mode 100644 src/java.base/unix/native/libjava/childproc_errorcodes.c create mode 100644 src/java.base/unix/native/libjava/childproc_errorcodes.h create mode 100644 test/jdk/java/lang/ProcessBuilder/InvalidWorkDir.java diff --git a/make/modules/java.base/Launcher.gmk b/make/modules/java.base/Launcher.gmk index 3a3920acb12..bfae0925c07 100644 --- a/make/modules/java.base/Launcher.gmk +++ b/make/modules/java.base/Launcher.gmk @@ -95,7 +95,8 @@ ifeq ($(call isTargetOsType, unix), true) CFLAGS := $(VERSION_CFLAGS), \ EXTRA_HEADER_DIRS := libjava, \ EXTRA_OBJECT_FILES := \ - $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libjava/childproc$(OBJ_SUFFIX), \ + $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libjava/childproc$(OBJ_SUFFIX) \ + $(SUPPORT_OUTPUTDIR)/native/$(MODULE)/libjava/childproc_errorcodes$(OBJ_SUFFIX), \ LD_SET_ORIGIN := false, \ OUTPUT_DIR := $(SUPPORT_OUTPUTDIR)/modules_libs/$(MODULE), \ )) diff --git a/src/java.base/unix/native/jspawnhelper/jspawnhelper.c b/src/java.base/unix/native/jspawnhelper/jspawnhelper.c index d2302d0c2e7..f26d0d618e3 100644 --- a/src/java.base/unix/native/jspawnhelper/jspawnhelper.c +++ b/src/java.base/unix/native/jspawnhelper/jspawnhelper.c @@ -34,6 +34,7 @@ #include #include "childproc.h" +#include "childproc_errorcodes.h" extern int errno; @@ -41,36 +42,34 @@ extern int errno; void *mptr; \ mptr = malloc (Y); \ if (mptr == 0) { \ - error (fdout, ERR_MALLOC); \ + sendErrorCodeAndExit (fdout, ESTEP_JSPAWN_ALLOC_FAILED, (int)Y, errno); \ } \ X = mptr; \ } -#define ERR_MALLOC 1 -#define ERR_PIPE 2 -#define ERR_ARGS 3 - #ifndef VERSION_STRING #error VERSION_STRING must be defined #endif -void error (int fd, int err) { - if (write (fd, &err, sizeof(err)) != sizeof(err)) { - /* Not sure what to do here. I have no one to speak to. */ - exit(0x80 + err); +/* Attempts to send an error code to the parent (which may or may not + * work depending on whether the fail pipe exists); then exits with an + * error code corresponding to the fail step. */ +void sendErrorCodeAndExit(int failpipe_fd, int step, int hint, int errno_) { + errcode_t errcode; + buildErrorCode(&errcode, step, hint, errno_); + if (failpipe_fd == -1 || !sendErrorCode(failpipe_fd, errcode)) { + /* Write error code to stdout, in the hope someone reads this. */ + printf("jspawnhelper fail: " ERRCODE_FORMAT "\n", ERRCODE_FORMAT_ARGS(errcode)); } - exit (1); + exit(exitCodeFromErrorCode(errcode)); } -void shutItDown() { - fprintf(stdout, "jspawnhelper version %s\n", VERSION_STRING); - fprintf(stdout, "This command is not for general use and should "); - fprintf(stdout, "only be run as the result of a call to\n"); - fprintf(stdout, "ProcessBuilder.start() or Runtime.exec() in a java "); - fprintf(stdout, "application\n"); - fflush(stdout); - _exit(1); -} +static const char* usageErrorText = + "jspawnhelper version " VERSION_STRING "\n" + "This command is not for general use and should " + "only be run as the result of a call to\n" + "ProcessBuilder.start() or Runtime.exec() in a java " + "application\n"; /* * read the following off the pipefd @@ -84,22 +83,31 @@ void initChildStuff (int fdin, int fdout, ChildStuff *c) { int bufsize, offset=0; int magic; int res; + const int step = ESTEP_JSPAWN_RCV_CHILDSTUFF_COMM_FAIL; + int substep = 0; res = readFully (fdin, &magic, sizeof(magic)); - if (res != 4 || magic != magicNumber()) { - error (fdout, ERR_PIPE); + if (res != 4) { + sendErrorCodeAndExit(fdout, step, substep, errno); + } + + substep ++; + if (magic != magicNumber()) { + sendErrorCodeAndExit(fdout, step, substep, errno); } #ifdef DEBUG jtregSimulateCrash(0, 5); #endif + substep ++; if (readFully (fdin, c, sizeof(*c)) != sizeof(*c)) { - error (fdout, ERR_PIPE); + sendErrorCodeAndExit(fdout, step, substep, errno); } + substep ++; if (readFully (fdin, &sp, sizeof(sp)) != sizeof(sp)) { - error (fdout, ERR_PIPE); + sendErrorCodeAndExit(fdout, step, substep, errno); } bufsize = sp.argvBytes + sp.envvBytes + @@ -107,8 +115,9 @@ void initChildStuff (int fdin, int fdout, ChildStuff *c) { ALLOC(buf, bufsize); + substep++; if (readFully (fdin, buf, bufsize) != bufsize) { - error (fdout, ERR_PIPE); + sendErrorCodeAndExit(fdout, step, substep, errno); } /* Initialize argv[] */ @@ -150,25 +159,29 @@ int main(int argc, char *argv[]) { #endif if (argc != 3) { - fprintf(stdout, "Incorrect number of arguments: %d\n", argc); - shutItDown(); + printf("Incorrect number of arguments: %d\n", argc); + puts(usageErrorText); + sendErrorCodeAndExit(-1, ESTEP_JSPAWN_ARG_ERROR, 0, 0); } if (strcmp(argv[1], VERSION_STRING) != 0) { - fprintf(stdout, "Incorrect Java version: %s\n", argv[1]); - shutItDown(); + printf("Incorrect Java version: %s\n", argv[1]); + puts(usageErrorText); + sendErrorCodeAndExit(-1, ESTEP_JSPAWN_VERSION_ERROR, 0, 0); } r = sscanf (argv[2], "%d:%d:%d", &fdinr, &fdinw, &fdout); if (r == 3 && fcntl(fdinr, F_GETFD) != -1 && fcntl(fdinw, F_GETFD) != -1) { fstat(fdinr, &buf); if (!S_ISFIFO(buf.st_mode)) { - fprintf(stdout, "Incorrect input pipe\n"); - shutItDown(); + printf("Incorrect input pipe\n"); + puts(usageErrorText); + sendErrorCodeAndExit(-1, ESTEP_JSPAWN_NOT_A_PIPE, fdinr, errno); } } else { - fprintf(stdout, "Incorrect FD array data: %s\n", argv[2]); - shutItDown(); + printf("Incorrect FD array data: %s\n", argv[2]); + puts(usageErrorText); + sendErrorCodeAndExit(-1, ESTEP_JSPAWN_NOT_A_PIPE, fdinr, errno); } // Close the writing end of the pipe we use for reading from the parent. diff --git a/src/java.base/unix/native/libjava/ProcessImpl_md.c b/src/java.base/unix/native/libjava/ProcessImpl_md.c index 12597fbb650..29522f3c822 100644 --- a/src/java.base/unix/native/libjava/ProcessImpl_md.c +++ b/src/java.base/unix/native/libjava/ProcessImpl_md.c @@ -47,6 +47,7 @@ #include #include "childproc.h" +#include "childproc_errorcodes.h" /* * @@ -678,7 +679,6 @@ Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, jintArray std_fds, jboolean redirectErrorStream) { - int errnum; int resultPid = -1; int in[2], out[2], err[2], fail[2], childenv[2]; jint *fds = NULL; @@ -782,9 +782,11 @@ Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, } close(fail[1]); fail[1] = -1; /* See: WhyCantJohnnyExec (childproc.c) */ + errcode_t errcode; + /* If we expect the child to ping aliveness, wait for it. */ if (c->sendAlivePing) { - switch(readFully(fail[0], &errnum, sizeof(errnum))) { + switch(readFully(fail[0], &errcode, sizeof(errcode))) { case 0: /* First exec failed; */ { int tmpStatus = 0; @@ -792,13 +794,15 @@ Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, throwExitCause(env, p, tmpStatus, c->mode); goto Catch; } - case sizeof(errnum): - if (errnum != CHILD_IS_ALIVE) { - /* This can happen if the spawn helper encounters an error - * before or during the handshake with the parent. */ - throwInternalIOException(env, 0, - "Bad code from spawn helper (Failed to exec spawn helper)", - c->mode); + case sizeof(errcode): + if (errcode.step != ESTEP_CHILD_ALIVE) { + /* This can happen if the child process encounters an error + * before or during initial handshake with the parent. */ + char msg[256]; + snprintf(msg, sizeof(msg), + "Bad early code from spawn helper " ERRCODE_FORMAT " (Failed to exec spawn helper)", + ERRCODE_FORMAT_ARGS(errcode)); + throwInternalIOException(env, 0, msg, c->mode); goto Catch; } break; @@ -808,11 +812,29 @@ Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, } } - switch (readFully(fail[0], &errnum, sizeof(errnum))) { + switch (readFully(fail[0], &errcode, sizeof(errcode))) { case 0: break; /* Exec succeeded */ - case sizeof(errnum): + case sizeof(errcode): + /* Always reap first! */ waitpid(resultPid, NULL, 0); - throwIOException(env, errnum, "Exec failed"); + /* Most of these errors are implementation errors and should result in an internal IOE, but + * a few can be caused by bad user input and need to be communicated to the end user. */ + switch(errcode.step) { + case ESTEP_CHDIR_FAIL: + throwIOException(env, errcode.errno_, "Failed to access working directory"); + break; + case ESTEP_EXEC_FAIL: + throwIOException(env, errcode.errno_, "Exec failed"); + break; + default: { + /* Probably implementation error */ + char msg[256]; + snprintf(msg, sizeof(msg), + "Bad code from spawn helper " ERRCODE_FORMAT " (Failed to exec spawn helper)", + ERRCODE_FORMAT_ARGS(errcode)); + throwInternalIOException(env, 0, msg, c->mode); + } + }; goto Catch; default: throwInternalIOException(env, errno, "Read failed", c->mode); diff --git a/src/java.base/unix/native/libjava/childproc.c b/src/java.base/unix/native/libjava/childproc.c index 9c6334e52d2..a45674e3f82 100644 --- a/src/java.base/unix/native/libjava/childproc.c +++ b/src/java.base/unix/native/libjava/childproc.c @@ -34,16 +34,27 @@ #include #include "childproc.h" +#include "childproc_errorcodes.h" #include "jni_util.h" const char * const *parentPathv; -static int -restartableDup2(int fd_from, int fd_to) +/* All functions taking an errcode_t* as output behave the same: upon error, they populate + * errcode_t::hint and errcode_t::errno, but leave errcode_t::step as ESTEP_UNKNOWN since + * this information will be provided by the outer caller */ + +static bool +restartableDup2(int fd_from, int fd_to, errcode_t* errcode) { int err; RESTARTABLE(dup2(fd_from, fd_to), err); - return err; + if (err == -1) { + /* use fd_to (the destination descriptor) as hint: it is a bit more telling + * than fd_from in our case */ + buildErrorCode(errcode, ESTEP_UNKNOWN, fd_to, errno); + return false; + } + return true; } int @@ -52,6 +63,16 @@ closeSafely(int fd) return (fd == -1) ? 0 : close(fd); } +/* Like closeSafely, but sets errcode (hint = fd, errno) on error and returns false */ +static bool +closeSafely2(int fd, errcode_t* errcode) { + if (closeSafely(fd) == -1) { + buildErrorCode(errcode, ESTEP_UNKNOWN, fd, errno); + return false; + } + return true; +} + int markCloseOnExec(int fd) { @@ -128,15 +149,19 @@ markDescriptorsCloseOnExec(void) return 0; } -static int -moveDescriptor(int fd_from, int fd_to) +static bool +moveDescriptor(int fd_from, int fd_to, errcode_t* errcode) { if (fd_from != fd_to) { - if ((restartableDup2(fd_from, fd_to) == -1) || - (close(fd_from) == -1)) - return -1; + if (!restartableDup2(fd_from, fd_to, errcode)) { + return false; + } + if (close(fd_from) == -1) { + buildErrorCode(errcode, ESTEP_UNKNOWN, fd_from, errno); + return false; + } } - return 0; + return true; } int @@ -367,15 +392,16 @@ int childProcess(void *arg) { const ChildStuff* p = (const ChildStuff*) arg; - int fail_pipe_fd = p->fail[1]; - if (p->sendAlivePing) { - /* Child shall signal aliveness to parent at the very first - * moment. */ - int code = CHILD_IS_ALIVE; - if (writeFully(fail_pipe_fd, &code, sizeof(code)) != sizeof(code)) { - goto WhyCantJohnnyExec; - } + int fail_pipe_fd = p->fail[1]; + /* error information for WhyCantJohnnyExec */ + errcode_t errcode; + + /* Child shall signal aliveness to parent at the very first + * moment. */ + if (p->sendAlivePing && !sendAlivePing(fail_pipe_fd)) { + buildErrorCode(&errcode, ESTEP_SENDALIVE_FAIL, fail_pipe_fd, errno); + goto WhyCantJohnnyExec; } #ifdef DEBUG @@ -384,34 +410,49 @@ childProcess(void *arg) /* Close the parent sides of the pipes. Closing pipe fds here is redundant, since markDescriptorsCloseOnExec() would do it anyways, but a little paranoia is a good thing. */ - if ((closeSafely(p->in[1]) == -1) || - (closeSafely(p->out[0]) == -1) || - (closeSafely(p->err[0]) == -1) || - (closeSafely(p->childenv[0]) == -1) || - (closeSafely(p->childenv[1]) == -1) || - (closeSafely(p->fail[0]) == -1)) + if (!closeSafely2(p->in[1], &errcode) || + !closeSafely2(p->out[0], &errcode) || + !closeSafely2(p->err[0], &errcode) || + !closeSafely2(p->childenv[0], &errcode) || + !closeSafely2(p->childenv[1], &errcode) || + !closeSafely2(p->fail[0], &errcode)) + { + errcode.step = ESTEP_PIPECLOSE_FAIL; goto WhyCantJohnnyExec; + } /* Give the child sides of the pipes the right fileno's. */ /* Note: it is possible for in[0] == 0 */ - if ((moveDescriptor(p->in[0] != -1 ? p->in[0] : p->fds[0], - STDIN_FILENO) == -1) || - (moveDescriptor(p->out[1]!= -1 ? p->out[1] : p->fds[1], - STDOUT_FILENO) == -1)) + if (!moveDescriptor(p->in[0] != -1 ? p->in[0] : p->fds[0], + STDIN_FILENO, &errcode)) { + errcode.step = ESTEP_DUP2_STDIN_FAIL; goto WhyCantJohnnyExec; - - if (p->redirectErrorStream) { - if ((closeSafely(p->err[1]) == -1) || - (restartableDup2(STDOUT_FILENO, STDERR_FILENO) == -1)) - goto WhyCantJohnnyExec; - } else { - if (moveDescriptor(p->err[1] != -1 ? p->err[1] : p->fds[2], - STDERR_FILENO) == -1) - goto WhyCantJohnnyExec; } - if (moveDescriptor(fail_pipe_fd, FAIL_FILENO) == -1) + if (!moveDescriptor(p->out[1] != -1 ? p->out[1] : p->fds[1], + STDOUT_FILENO, &errcode)) { + errcode.step = ESTEP_DUP2_STDOUT_FAIL; goto WhyCantJohnnyExec; + } + + if (p->redirectErrorStream) { + if (!closeSafely2(p->err[1], &errcode) || + !restartableDup2(STDOUT_FILENO, STDERR_FILENO, &errcode)) { + errcode.step = ESTEP_DUP2_STDERR_REDIRECT_FAIL; + goto WhyCantJohnnyExec; + } + } else { + if (!moveDescriptor(p->err[1] != -1 ? p->err[1] : p->fds[2], + STDERR_FILENO, &errcode)) { + errcode.step = ESTEP_DUP2_STDERR_REDIRECT_FAIL; + goto WhyCantJohnnyExec; + } + } + + if (!moveDescriptor(fail_pipe_fd, FAIL_FILENO, &errcode)) { + errcode.step = ESTEP_DUP2_FAILPIPE_FAIL; + goto WhyCantJohnnyExec; + } /* We moved the fail pipe fd */ fail_pipe_fd = FAIL_FILENO; @@ -424,14 +465,19 @@ childProcess(void *arg) if (markDescriptorsCloseOnExec() == -1) { /* failed, close the old way */ int max_fd = (int)sysconf(_SC_OPEN_MAX); int fd; - for (fd = STDERR_FILENO + 1; fd < max_fd; fd++) - if (markCloseOnExec(fd) == -1 && errno != EBADF) + for (fd = STDERR_FILENO + 1; fd < max_fd; fd++) { + if (markCloseOnExec(fd) == -1 && errno != EBADF) { + buildErrorCode(&errcode, ESTEP_CLOEXEC_FAIL, fd, errno); goto WhyCantJohnnyExec; + } + } } /* change to the new working directory */ - if (p->pdir != NULL && chdir(p->pdir) < 0) + if (p->pdir != NULL && chdir(p->pdir) < 0) { + buildErrorCode(&errcode, ESTEP_CHDIR_FAIL, 0, errno); goto WhyCantJohnnyExec; + } // Reset any mask signals from parent, but not in VFORK mode if (p->mode != MODE_VFORK) { @@ -442,28 +488,32 @@ childProcess(void *arg) // Children should be started with default signal disposition for SIGPIPE if (signal(SIGPIPE, SIG_DFL) == SIG_ERR) { + buildErrorCode(&errcode, ESTEP_SET_SIGPIPE, 0, errno); goto WhyCantJohnnyExec; } JDK_execvpe(p->mode, p->argv[0], p->argv, p->envv); + /* Still here. Hmm. */ + buildErrorCode(&errcode, ESTEP_EXEC_FAIL, 0, errno); + WhyCantJohnnyExec: /* We used to go to an awful lot of trouble to predict whether the * child would fail, but there is no reliable way to predict the * success of an operation without *trying* it, and there's no way * to try a chdir or exec in the parent. Instead, all we need is a * way to communicate any failure back to the parent. Easy; we just - * send the errno back to the parent over a pipe in case of failure. + * send the errorcode back to the parent over a pipe in case of failure. * The tricky thing is, how do we communicate the *success* of exec? * We use FD_CLOEXEC together with the fact that a read() on a pipe * yields EOF when the write ends (we have two of them!) are closed. */ - { - int errnum = errno; - writeFully(fail_pipe_fd, &errnum, sizeof(errnum)); + if (!sendErrorCode(fail_pipe_fd, errcode)) { + printf("childproc fail: " ERRCODE_FORMAT "\n", ERRCODE_FORMAT_ARGS(errcode)); } + int exitcode = exitCodeFromErrorCode(errcode); close(fail_pipe_fd); - _exit(-1); + _exit(exitcode); return 0; /* Suppress warning "no return value from function" */ } diff --git a/src/java.base/unix/native/libjava/childproc.h b/src/java.base/unix/native/libjava/childproc.h index 974fac3bddd..4271c786635 100644 --- a/src/java.base/unix/native/libjava/childproc.h +++ b/src/java.base/unix/native/libjava/childproc.h @@ -107,13 +107,6 @@ typedef struct _SpawnInfo { int parentPathvBytes; /* total number of bytes in parentPathv array */ } SpawnInfo; -/* If ChildStuff.sendAlivePing is true, child shall signal aliveness to - * the parent the moment it gains consciousness, before any subsequent - * pre-exec errors could happen. - * This code must fit into an int and not be a valid errno value on any of - * our platforms. */ -#define CHILD_IS_ALIVE 65535 - /** * The cached and split version of the JDK's effective PATH. * (We don't support putenv("PATH=...") in native code) diff --git a/src/java.base/unix/native/libjava/childproc_errorcodes.c b/src/java.base/unix/native/libjava/childproc_errorcodes.c new file mode 100644 index 00000000000..4dc8e927616 --- /dev/null +++ b/src/java.base/unix/native/libjava/childproc_errorcodes.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include + +#include +#include "childproc.h" +#include "childproc_errorcodes.h" + +void buildErrorCode(errcode_t* errcode, int step, int hint, int errno_) { + errcode_t e; + + assert(step < (1 << 8)); + e.step = step; + + assert(errno_ < (1 << 8)); + e.errno_ = errno_; + + const int maxhint = (1 << 16); + e.hint = hint < maxhint ? hint : maxhint; + + (*errcode) = e; +} + +int exitCodeFromErrorCode(errcode_t errcode) { + /* We use the fail step number as exit code, but avoid 0 and 1 + * and try to avoid the [128..256) range since that one is used by + * shells to codify abnormal kills by signal. */ + return 0x10 + errcode.step; +} + +bool sendErrorCode(int fd, errcode_t errcode) { + return writeFully(fd, &errcode, sizeof(errcode)) == sizeof(errcode); +} + +bool sendAlivePing(int fd) { + errcode_t errcode; + buildErrorCode(&errcode, ESTEP_CHILD_ALIVE, getpid(), 0); + return sendErrorCode(fd, errcode); +} diff --git a/src/java.base/unix/native/libjava/childproc_errorcodes.h b/src/java.base/unix/native/libjava/childproc_errorcodes.h new file mode 100644 index 00000000000..8379db4ad2b --- /dev/null +++ b/src/java.base/unix/native/libjava/childproc_errorcodes.h @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#ifndef CHILDPROC_ERRORCODES_H +#define CHILDPROC_ERRORCODES_H + +#include +#include + +typedef struct errcode_t_ { + unsigned step : 8; + unsigned hint : 16; + unsigned errno_ : 8; +} errcode_t; + +/* Helper macros for printing an errcode_t */ +#define ERRCODE_FORMAT "(%u-%u-%u)" +#define ERRCODE_FORMAT_ARGS(errcode) errcode.step, errcode.hint, errcode.errno_ + + +/* Builds up an error code. + * Note: + * - hint will be capped at 2^16 + * - both step and errno_ must fit into 8 bits. */ +void buildErrorCode(errcode_t* errcode, int step, int hint, int errno_); + +/* Sends an error code down a pipe. Returns true if sent successfully. */ +bool sendErrorCode(int fd, errcode_t errcode); + +/* Build an exit code for an errcode (used as child process exit code + * in addition to the errcode being sent to parent). */ +int exitCodeFromErrorCode(errcode_t errcode); + +/* Sends alive ping down a pipe. Returns true if sent successfully. */ +bool sendAlivePing(int fd); + +#define ESTEP_UNKNOWN 0 + +/* not an error code, but an "I am alive" ping from the child. + * hint is child pid, errno is 0. */ +#define ESTEP_CHILD_ALIVE 255 + +/* JspawnHelper */ +#define ESTEP_JSPAWN_ARG_ERROR 1 +#define ESTEP_JSPAWN_VERSION_ERROR 2 + +/* Checking file descriptor setup + * hint is the (16-bit-capped) fd number */ +#define ESTEP_JSPAWN_INVALID_FD 3 +#define ESTEP_JSPAWN_NOT_A_PIPE 4 + +/* Allocation fail in jspawnhelper. + * hint is the (16-bit-capped) fail size */ +#define ESTEP_JSPAWN_ALLOC_FAILED 5 + +/* Receiving Childstuff from parent, communication error. + * hint is the substep. */ +#define ESTEP_JSPAWN_RCV_CHILDSTUFF_COMM_FAIL 6 + +/* Expand if needed ... */ + +/* childproc() */ + +/* Failed to send aliveness ping + * hint is the (16-bit-capped) fd. */ +#define ESTEP_SENDALIVE_FAIL 10 + +/* Failed to close a pipe in fork mode + * hint is the (16-bit-capped) fd. */ +#define ESTEP_PIPECLOSE_FAIL 11 + +/* Failed to dup2 a file descriptor in fork mode. + * hint is the (16-bit-capped) fd_to (!) */ +#define ESTEP_DUP2_STDIN_FAIL 13 +#define ESTEP_DUP2_STDOUT_FAIL 14 +#define ESTEP_DUP2_STDERR_REDIRECT_FAIL 15 +#define ESTEP_DUP2_STDERR_FAIL 16 +#define ESTEP_DUP2_FAILPIPE_FAIL 17 + +/* Failed to mark a file descriptor as CLOEXEC + * hint is the (16-bit-capped) fd */ +#define ESTEP_CLOEXEC_FAIL 18 + +/* Failed to chdir into the target working directory */ +#define ESTEP_CHDIR_FAIL 19 + +/* Failed to change signal disposition for SIGPIPE to default */ +#define ESTEP_SET_SIGPIPE 20 + +/* Expand if needed ... */ + +/* All modes: exec() failed */ +#define ESTEP_EXEC_FAIL 30 + +#endif /* CHILDPROC_MD_H */ diff --git a/test/jdk/java/lang/ProcessBuilder/InvalidWorkDir.java b/test/jdk/java/lang/ProcessBuilder/InvalidWorkDir.java new file mode 100644 index 00000000000..310ecf03f97 --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/InvalidWorkDir.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test id=FORK + * @bug 8379967 + * @summary Check that passing an invalid work dir yields a corresponding IOE text. + * @requires (os.family != "windows") + * @requires vm.flagless + * @library /test/lib + * @run main/othervm -Xmx64m -Djdk.lang.Process.launchMechanism=FORK InvalidWorkDir + */ + +/** + * @test id=POSIX_SPAWN + * @bug 8379967 + * @summary Check that passing an invalid work dir yields a corresponding IOE text. + * @requires (os.family != "windows") + * @requires vm.flagless + * @library /test/lib + * @run main/othervm -Xmx64m -Djdk.lang.Process.launchMechanism=FORK InvalidWorkDir + */ + +import jdk.test.lib.process.OutputAnalyzer; + +import java.io.File; +import java.io.IOException; + +public class InvalidWorkDir { + + public static void main(String[] args) { + ProcessBuilder bld = new ProcessBuilder("ls").directory(new File("./doesnotexist")); + try(Process p = bld.start()) { + throw new RuntimeException("IOE expected"); + } catch (IOException e) { + if (!e.getMessage().matches(".*Failed to access working directory.*No such file or directory.*")) { + throw new RuntimeException(String.format("got IOE but with different text (%s)", e.getMessage())); + } + } + } + +} diff --git a/test/jdk/java/lang/ProcessBuilder/JspawnhelperWarnings.java b/test/jdk/java/lang/ProcessBuilder/JspawnhelperWarnings.java index d9896f16e00..cb2d504951d 100644 --- a/test/jdk/java/lang/ProcessBuilder/JspawnhelperWarnings.java +++ b/test/jdk/java/lang/ProcessBuilder/JspawnhelperWarnings.java @@ -22,11 +22,19 @@ */ /* - * @test - * @bug 8325567 8325621 + * @test id=badargs + * @bug 8325567 8325621 8379967 * @requires (os.family == "linux") | (os.family == "aix") | (os.family == "mac") * @library /test/lib - * @run driver JspawnhelperWarnings + * @run driver JspawnhelperWarnings badargs + */ + +/* + * @test id=badversion + * @bug 8325567 8325621 8379967 + * @requires (os.family == "linux") | (os.family == "aix") | (os.family == "mac") + * @library /test/lib + * @run driver JspawnhelperWarnings badversion */ import java.nio.file.Paths; @@ -36,6 +44,13 @@ import jdk.test.lib.process.ProcessTools; public class JspawnhelperWarnings { + // See childproc_errorcodes.h + static final int ESTEP_JSPAWN_ARG_ERROR = 1; + static final int ESTEP_JSPAWN_VERSION_ERROR = 2; + + // See exitCodeFromErrorCode() in childproc_errorcodes.c + static final int EXITCODE_OFFSET = 0x10; + private static void tryWithNArgs(int nArgs) throws Exception { System.out.println("Running jspawnhelper with " + nArgs + " args"); String[] args = new String[nArgs + 1]; @@ -43,7 +58,8 @@ public class JspawnhelperWarnings { args[0] = Paths.get(System.getProperty("java.home"), "lib", "jspawnhelper").toString(); Process p = ProcessTools.startProcess("jspawnhelper", new ProcessBuilder(args)); OutputAnalyzer oa = new OutputAnalyzer(p); - oa.shouldHaveExitValue(1); + oa.shouldHaveExitValue(EXITCODE_OFFSET + ESTEP_JSPAWN_ARG_ERROR); + oa.shouldContain("jspawnhelper fail: (1-0-0)"); oa.shouldContain("This command is not for general use"); if (nArgs != 2) { oa.shouldContain("Incorrect number of arguments"); @@ -59,16 +75,28 @@ public class JspawnhelperWarnings { args[2] = "1:1:1"; Process p = ProcessTools.startProcess("jspawnhelper", new ProcessBuilder(args)); OutputAnalyzer oa = new OutputAnalyzer(p); - oa.shouldHaveExitValue(1); + oa.shouldHaveExitValue(EXITCODE_OFFSET + ESTEP_JSPAWN_VERSION_ERROR); + oa.shouldContain("jspawnhelper fail: (2-0-0)"); oa.shouldContain("This command is not for general use"); oa.shouldContain("Incorrect Java version: wrongVersion"); } public static void main(String[] args) throws Exception { - for (int nArgs = 0; nArgs < 10; nArgs++) { - tryWithNArgs(nArgs); + if (args.length != 1) { + throw new RuntimeException("test argument error"); + } + switch (args[0]) { + case "badargs" -> { + for (int nArgs = 0; nArgs < 10; nArgs++) { + if (nArgs != 2) { + tryWithNArgs(nArgs); + } + } + } + case "badversion" -> { + testVersion(); + } + default -> throw new RuntimeException("test argument error"); } - - testVersion(); } }