diff --git a/make/test/JtregNativeJdk.gmk b/make/test/JtregNativeJdk.gmk index 00ef4bece58..884fd62df9e 100644 --- a/make/test/JtregNativeJdk.gmk +++ b/make/test/JtregNativeJdk.gmk @@ -63,7 +63,8 @@ ifeq ($(call isTargetOs, windows), true) BUILD_JDK_JTREG_EXCLUDE += libDirectIO.c libInheritedChannel.c \ libExplicitAttach.c libImplicitAttach.c \ exelauncher.c \ - libChangeSignalDisposition.c exePrintSignalDisposition.c + libChangeSignalDisposition.c exePrintSignalDisposition.c \ + libConcNativeFork.c libPipesCloseOnExec.c BUILD_JDK_JTREG_EXECUTABLES_LIBS_exeNullCallerTest := $(LIBCXX) BUILD_JDK_JTREG_EXECUTABLES_LIBS_exerevokeall := advapi32.lib @@ -77,6 +78,8 @@ else BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libLinkerInvokerUnnamed := -pthread BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libLinkerInvokerModule := -pthread BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libLoaderLookupInvoker := -pthread + BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libConcNativeFork := -pthread + BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libPipesCloseOnExec := -pthread BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libExplicitAttach := -pthread BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libImplicitAttach := -pthread diff --git a/src/java.base/unix/native/libjava/ProcessImpl_md.c b/src/java.base/unix/native/libjava/ProcessImpl_md.c index f7531ad5abe..6199d0e7ce4 100644 --- a/src/java.base/unix/native/libjava/ProcessImpl_md.c +++ b/src/java.base/unix/native/libjava/ProcessImpl_md.c @@ -44,6 +44,8 @@ #include #include +#include +#include #include #include "childproc.h" @@ -666,6 +668,28 @@ startChild(JNIEnv *env, jobject process, ChildStuff *c, const char *helperpath) } } +static int pipeSafely(int fd[2]) { + /* Pipe filedescriptors must be CLOEXEC as early as possible - ideally from the point of + * creation on - since at any moment a concurrent (third-party) fork() could inherit copies + * of these descriptors and accidentally keep the pipes open. That could cause the parent + * process to hang (see e.g. JDK-8377907). + * We use pipe2(2), if we have it. If we don't, we use pipe(2) + fcntl(2) immediately. + * The latter is still racy and can therefore still cause hangs as described in JDK-8377907, + * but at least the dangerous time window is as short as we can make it. + */ + int rc = -1; +#ifdef HAVE_PIPE2 + rc = pipe2(fd, O_CLOEXEC); +#else + rc = pipe(fd); + if (rc == 0) { + fcntl(fd[0], F_SETFD, FD_CLOEXEC); + fcntl(fd[1], F_SETFD, FD_CLOEXEC); + } +#endif /* HAVE_PIPE2 */ + return rc; +} + JNIEXPORT jint JNICALL Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, jobject process, @@ -727,14 +751,33 @@ Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, fds = (*env)->GetIntArrayElements(env, std_fds, NULL); if (fds == NULL) goto Catch; - if ((fds[0] == -1 && pipe(in) < 0) || - (fds[1] == -1 && pipe(out) < 0) || - (fds[2] == -1 && pipe(err) < 0) || - (pipe(childenv) < 0) || - (pipe(fail) < 0)) { - throwInternalIOException(env, errno, "Bad file descriptor", mode); - goto Catch; + if (mode == MODE_FORK || mode == MODE_VFORK) { + /* We cannot downport the full breadth of JDK-8377907 to JDK 25 and earlier, since such + * a change (including prerequisites) would be far too invasive for LTS releases. We + * therefore only downport a very small part of it, namely the fix for the FORK/VFORK + * modes. Fixing FORK/VFORK just requires us to open pipes with CLOEXEC. Fixing the + * POSIX_SPAWN mode, otoh, would require rewriting large parts of the spawn layer + * (see JDK-8377907). + */ + if ((fds[0] == -1 && pipeSafely(in) < 0) || + (fds[1] == -1 && pipeSafely(out) < 0) || + (fds[2] == -1 && pipeSafely(err) < 0) || + (pipeSafely(childenv) < 0) || + (pipeSafely(fail) < 0)) { + throwInternalIOException(env, errno, "Bad file descriptor", mode); + goto Catch; + } + } else { + if ((fds[0] == -1 && pipe(in) < 0) || + (fds[1] == -1 && pipe(out) < 0) || + (fds[2] == -1 && pipe(err) < 0) || + (pipe(childenv) < 0) || + (pipe(fail) < 0)) { + throwInternalIOException(env, errno, "Bad file descriptor", mode); + goto Catch; + } } + c->fds[0] = fds[0]; c->fds[1] = fds[1]; c->fds[2] = fds[2]; diff --git a/src/java.base/unix/native/libjava/childproc.h b/src/java.base/unix/native/libjava/childproc.h index 974fac3bddd..53d0dad5be9 100644 --- a/src/java.base/unix/native/libjava/childproc.h +++ b/src/java.base/unix/native/libjava/childproc.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 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 @@ -138,4 +138,11 @@ int childProcess(void *arg); void jtregSimulateCrash(pid_t child, int stage); #endif +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#define HAVE_PIPE2 +#else +// Neither MacOS nor AIX support pipe2, unfortunately +#undef HAVE_PIPE2 #endif + +#endif /* CHILDPROC_MD_H */ diff --git a/test/jdk/java/lang/ProcessBuilder/Basic.java b/test/jdk/java/lang/ProcessBuilder/Basic.java index 687f21323b8..04fad2a40fe 100644 --- a/test/jdk/java/lang/ProcessBuilder/Basic.java +++ b/test/jdk/java/lang/ProcessBuilder/Basic.java @@ -46,11 +46,23 @@ * @modules java.base/java.lang:open * java.base/java.io:open * java.base/jdk.internal.misc - * @requires (os.family == "linux" & !vm.musl) + * @requires !vm.musl + * @requires vm.flagless * @library /test/lib * @run main/othervm/timeout=300 -Djdk.lang.Process.launchMechanism=posix_spawn Basic */ +/* + * @test + * @modules java.base/java.lang:open + * java.base/java.io:open + * java.base/jdk.internal.misc + * @requires (os.family == "linux" & !vm.musl) + * @requires vm.flagless + * @library /test/lib + * @run main/othervm/timeout=300 -Djdk.lang.Process.launchMechanism=vfork Basic + */ + import java.lang.ProcessBuilder.Redirect; import java.lang.ProcessHandle; import static java.lang.ProcessBuilder.Redirect.*; @@ -1222,6 +1234,20 @@ static void testIORedirection() throws Throwable { equal(r.out(), "standard output"); equal(r.err(), "standard error"); } + + //---------------------------------------------------------------- + // Default: should go to pipes (use a fresh ProcessBuilder) + //---------------------------------------------------------------- + { + ProcessBuilder pb2 = new ProcessBuilder(childArgs); + Process p = pb2.start(); + new PrintStream(p.getOutputStream()).print("standard input"); + p.getOutputStream().close(); + ProcessResults r = run(p); + equal(r.exitValue(), 0); + equal(r.out, "standard output"); + equal(r.err, "standard error"); + } } static void checkProcessPid() { @@ -1260,6 +1286,8 @@ private static void realMain(String[] args) throws Throwable { if (UnicodeOS.is()) System.out.println("This appears to be a Unicode-based OS."); + System.out.println("Using:" + System.getProperty("jdk.lang.Process.launchMechanism")); + try { testIORedirection(); } catch (Throwable t) { unexpected(t); } diff --git a/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/ConcNativeForkTest.java b/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/ConcNativeForkTest.java new file mode 100644 index 00000000000..9b1c78a0b8d --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/ConcNativeForkTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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 8377907 + * @summary Test that demonstrates the hanging-parent-on-native-concurrent-forks problem + * @requires os.family != "windows" + * @requires vm.flagless + * @library /test/lib + * @run main/othervm/manual -Djdk.lang.Process.launchMechanism=FORK ConcNativeForkTest + */ + +/* + * @test id=VFORK + * @bug 8377907 + * @summary Test that demonstrates the hanging-parent-on-native-concurrent-forks problem + * @requires os.family == "linux" + * @requires vm.flagless + * @library /test/lib + * @run main/othervm/manual -Djdk.lang.Process.launchMechanism=VFORK ConcNativeForkTest + */ + +public class ConcNativeForkTest { + + // How this works: + // - We start a child process via ProcessBuilder. Does not matter what, we just call "/bin/true". + // - Concurrently, we continuously (up to a limit) fork natively; these forks will all exec "sleep 30". + // - If the natively forked child process forks off at the right (wrong) moment, it will catch the open pipe from + // the "/bin/true" child process, and forcing the parent process (this test) to wait in ProcessBuilder.start() + // (inside forkAndExec()) until the natively forked child releases the pipe file descriptors it inherited. + + // Notes: + // + // Obviously, this is racy and depends on scheduler timings of the underlying OS. The test succeeding is + // no proof the bug does not exist (see PipesCloseOnExecTest as a complimentary test that is more reliable, but + // only works on Linux). + // That said, in tests it reliably reproduces the bug on Linux x64 and MacOS Arm. + // + // This test is not well suited for automatic test execution, since the test essentially + // fork-bombs itself, and that may run into issues in containerized CI/CD environments. + + native static boolean prepareNativeForkerThread(int numForks); + native static void releaseNativeForkerThread(); + native static void stopNativeForkerThread(); + + private static final int numIterations = 20; + + public static void main(String[] args) throws Exception { + + System.out.println("jdk.lang.Process.launchMechanism=" + + System.getProperty("jdk.lang.Process.launchMechanism")); + + System.loadLibrary("ConcNativeFork"); + + // A very simple program returning immediately (/bin/true) + ProcessBuilder pb = new ProcessBuilder("true").inheritIO(); + final int numJavaProcesses = 10; + final int numNativeProcesses = 250; + Process[] processes = new Process[numJavaProcesses]; + + for (int iteration = 0; iteration < numIterations; iteration ++) { + + if (!prepareNativeForkerThread(numNativeProcesses)) { + throw new RuntimeException("Failed to start native forker thread (see stdout)"); + } + + long[] durations = new long[numJavaProcesses]; + + releaseNativeForkerThread(); + + for (int np = 0; np < numJavaProcesses; np ++) { + long t1 = System.currentTimeMillis(); + Process p = pb.start(); + durations[np] = System.currentTimeMillis() - t1; + processes[np] = p; + } + + stopNativeForkerThread(); + + long longestDuration = 0; + for (int np = 0; np < numJavaProcesses; np ++) { + processes[np].waitFor(); + System.out.printf("Duration: %dms%n", durations[np]); + longestDuration = Math.max(durations[np], longestDuration); + } + + System.out.printf("Longest startup time: %dms%n", longestDuration); + + if (longestDuration >= 30000) { + throw new RuntimeException("Looks like we blocked on native fork"); + } + } + + } + +} diff --git a/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/libConcNativeFork.c b/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/libConcNativeFork.c new file mode 100644 index 00000000000..59729308558 --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/ConcNativeForkTest/libConcNativeFork.c @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "testlib_thread_barriers.h" + +static void trc(const char* fmt, ...) { + char buf [1024]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + printf("(pid: %d): %s\n", (int)getpid(), buf); + fflush(stdout); +} + +static pthread_t tid_forker; + +static pthread_barrier_t start_barrier; +static atomic_bool stop_now = false; + +static void* forkerLoop(void* info) { + + const int numForks = (int)(intptr_t)info; + pid_t* pids = calloc(numForks, sizeof(pid_t)); + + trc("Forker: Waiting for Go."); + + pthread_barrier_wait(&start_barrier); + + for (int i = 0; i < numForks; i++) { + const pid_t pid = fork(); + if (pid == 0) { + /* Exec sleep. Properly opened file descriptors in parents (tagged CLOEXEC) should be released now. + * Note that we use bash to not have to deal with path resolution. For our case, it does not matter if + * sleep is a builtin or not. */ + char* env[] = { "PATH=/usr/bin:/bin", NULL }; + char* argv[] = { "sh", "-c", "sleep 30", NULL }; + execve("/bin/sh", argv, env); + trc("Native child: sleep exec failed? %d", errno); + /* The simplest way to handle this is to just wait here; this *will* cause the test to fail. */ + sleep(120); + trc("Native child: exiting"); + exit(0); + } else { + pids[i] = pid; + sched_yield(); + } + } + + trc("Forker: All native child processes started."); + + /* Wait for test to signal end */ + while (!atomic_load(&stop_now)) { + sleep(1); + } + + trc("Forker: Cleaning up."); + + /* Reap children */ + for (int i = 0; i < numForks; i ++) { + if (pids[i] != 0) { + kill(pids[i], SIGKILL); /* if still running */ + waitpid(pids[i], NULL, 0); + } + } + + trc("Forker: Done."); + + return NULL; +} + +JNIEXPORT jboolean JNICALL +Java_ConcNativeForkTest_prepareNativeForkerThread(JNIEnv* env, jclass cls, jint numForks) +{ + pthread_attr_t attr; + int rc = 0; + + const int cap = 1000; + const int numForksCapped = numForks > cap ? cap : numForks; + if (numForks > numForksCapped) { + trc("Main: Capping max. number of forks at %d", numForksCapped); /* don't forkbomb me */ + } + + if (pthread_barrier_init(&start_barrier, NULL, 2) != 0) { + trc("Main: pthread_barrier_init failed (%d)", errno); + return false; + } + + pthread_attr_init(&attr); + if (pthread_create(&tid_forker, &attr, forkerLoop, (void*)(intptr_t)numForksCapped) != 0) { + trc("Main: pthread_create failed (%d)", errno); + return JNI_FALSE; + } + + trc("Main: Prepared native forker thread"); + + return JNI_TRUE; +} + +JNIEXPORT void JNICALL +Java_ConcNativeForkTest_releaseNativeForkerThread(JNIEnv* env, jclass cls) +{ + pthread_barrier_wait(&start_barrier); + trc("Main: signaled GO"); +} + +JNIEXPORT void JNICALL +Java_ConcNativeForkTest_stopNativeForkerThread(JNIEnv* env, jclass cls) +{ + atomic_store(&stop_now, true); + pthread_join(tid_forker, NULL); + pthread_barrier_destroy(&start_barrier); +} diff --git a/test/jdk/java/lang/ProcessBuilder/NonPipelineLeaksFD.java b/test/jdk/java/lang/ProcessBuilder/NonPipelineLeaksFD.java new file mode 100644 index 00000000000..e930c7522d3 --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/NonPipelineLeaksFD.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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. + */ + +import java.io.*; + +/* + * @test id=FORK + * @summary Check that we don't accumulate leaked FDs in the parent process + * @requires os.family == "linux" + * @library /test/lib + * @run main/othervm -Djdk.lang.Process.launchMechanism=fork NonPipelineLeaksFD + */ + +/* + * @test id=VFORK + * @summary Check that we don't accumulate leaked FDs in the parent process + * @requires os.family == "linux" + * @library /test/lib + * @run main/othervm -Djdk.lang.Process.launchMechanism=vfork NonPipelineLeaksFD + */ + +public class NonPipelineLeaksFD { + + final static int repeatCount = 50; + + // Similar to PilelineLeaksFD, but where PilelineLeaksFD checks that the parent process + // does not leak file descriptors when invoking a pipeline, here we check that we don't + // leak FDs when executing simple (non-pipelined) programs but we test a wider span of + // redirection modes in both successful and failing variants. + + // How this works: + // + // We execute a mix of failing and succeeding child process starts with various + // flavors of IO redirections many times; we observe the open file descriptors + // before and afterwards. Test fails if we have significantly more file descriptors + // open afterwards than before. + + static int countNumberOfOpenFileDescriptors() { + return new File("/proc/self/fd").list().length; + } + + static void printOpenFileDescriptors() { + long mypid = ProcessHandle.current().pid(); + try{ + Process p = new ProcessBuilder("lsof", "-p", Long.toString(mypid)) + .inheritIO().start(); + p.waitFor(); + } catch (InterruptedException | IOException ignored) { + // Quietly swallow; it was just an attempt. + } + } + + static String readFirstLineOf(File f) { + String result; + try (BufferedReader b = new BufferedReader(new FileReader(f))){ + result = b.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return result; + } + + static void runThisExpectSuccess(ProcessBuilder bld) { + try { + Process p = bld.start(); + p.waitFor(); + if (p.exitValue() != 0) { + throw new RuntimeException("Unexpected exitcode"); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + static void runThisExpectError(ProcessBuilder bld) { + boolean failed = false; + try{ + Process p = bld.start(); + p.waitFor(); + } catch (IOException | InterruptedException e) { + failed = true; + } + if (!failed) { + throw new RuntimeException("Expected Error"); + } + } + + static void runPosWithPipes() { + ProcessBuilder bld = new ProcessBuilder("sh", "-c", "echo hallo"); + runThisExpectSuccess(bld); + } + + static void runPosWithInheritIO() { + ProcessBuilder bld = new ProcessBuilder("sh", "-c", "echo hallo").inheritIO(); + runThisExpectSuccess(bld); + } + + static void runPosWithRedirectToFile() { + File fo = new File("test.out"); + ProcessBuilder bld = new ProcessBuilder("sh", "-c", "echo hallo"); + bld.redirectOutput(ProcessBuilder.Redirect.to(fo)); + runThisExpectSuccess(bld); + if (!readFirstLineOf(fo).equals("hallo")) { + throw new RuntimeException("mismatch"); + } + } + + static void runNegWithPipes() { + ProcessBuilder bld = new ProcessBuilder("doesnotexist"); + runThisExpectError(bld); + } + + static void runNegWithInheritIO() { + ProcessBuilder bld = new ProcessBuilder("doesnotexist").inheritIO(); + runThisExpectError(bld); + } + + static void runNegWithRedirectToFile() { + File fo = new File("test.out"); + ProcessBuilder bld = new ProcessBuilder("doesnotexist"); + bld.redirectOutput(ProcessBuilder.Redirect.to(fo)); + runThisExpectError(bld); + } + + static void doTestNTimesAndCountFDs(Runnable runnable, String name) { + System.out.println(name); + int c1 = countNumberOfOpenFileDescriptors(); + for (int i = 0; i < repeatCount; i++) { + runnable.run(); + } + int c2 = countNumberOfOpenFileDescriptors(); + System.out.printf("%d->%d", c1, c2); + } + + public static void main(String[] args) throws Exception { + + System.out.println("jdk.lang.Process.launchMechanism=" + + System.getProperty("jdk.lang.Process.launchMechanism")); + + int c1 = countNumberOfOpenFileDescriptors(); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runPosWithPipes, "runPosWithPipes"); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runPosWithInheritIO, "runPosWithInheritIO"); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runPosWithRedirectToFile, "runPosWithRedirectToFile"); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runNegWithPipes, "runNegWithPipes"); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runNegWithInheritIO, "runNegWithInheritIO"); + doTestNTimesAndCountFDs(NonPipelineLeaksFD::runNegWithRedirectToFile, "runNegWithRedirectToFile"); + int c2 = countNumberOfOpenFileDescriptors(); + + System.out.printf("All tests: %d->%d", c1, c2); + printOpenFileDescriptors(); + + final int fudge = 10; + if (c2 > (c1 + fudge)) { + throw new RuntimeException( + String.format("Leak suspected (%d->%d) - see lsof output", c1, c2)); + } + } +} diff --git a/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/PipesCloseOnExecTest.java b/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/PipesCloseOnExecTest.java new file mode 100644 index 00000000000..56a49ad9dd6 --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/PipesCloseOnExecTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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 8377907 + * @summary Check that we don't open pipes without CLOEXCEC + * @requires os.family == "linux" + * @requires vm.flagless + * @library /test/lib + * @run main/othervm/native -Djdk.lang.Process.launchMechanism=FORK PipesCloseOnExecTest + */ + +/* + * @test id=VFORK + * @bug 8377907 + * @summary Check that we don't open pipes without CLOEXCEC + * @requires os.family == "linux" + * @requires vm.flagless + * @library /test/lib + * @run main/othervm/native -Djdk.lang.Process.launchMechanism=VFORK PipesCloseOnExecTest + */ + +import jdk.test.lib.process.OutputAnalyzer; + +import java.io.IOException; +import java.time.LocalTime; + +public class PipesCloseOnExecTest { + + // How this works: + // - We start a child process A. Does not matter what, we just call "/bin/date". + // - Concurrently, we (natively, continuously) iterate all pipe file descriptors in + // the process and count all those that are not tagged with CLOEXEC. Finding one + // counts as error. + + // Note that this test can only reliably succeed with Linux and the xxxBSDs, where + // we have pipe2(2). + // + // On MacOs and AIX, we emulate pipe2(2) with pipe(2) and fcntl(2); therefore we + // have a tiny time window in which a concurrent thread can can observe pipe + // filedescriptors without CLOEXEC. Furthermore, on MacOS, we also have to employ + // the double-dup-trick to workaround a the buggy MacOS implementation of posix_spawn. + // Therefore, on these platforms, the test would (correctly) spot "bad" file descriptors. + + native static boolean startTester(); + native static boolean stopTester(); + + static final int num_tries = 100; + + static void printOpenFileDescriptors() { + long mypid = ProcessHandle.current().pid(); + try { + Process p = new ProcessBuilder("lsof", "-p", Long.toString(mypid)) + .inheritIO().start(); + p.waitFor(); + } catch (InterruptedException | IOException ignored) { + // Quietly swallow; it was just an attempt. + } + } + + public static void main(String[] args) throws Exception { + + System.out.println("jdk.lang.Process.launchMechanism=" + + System.getProperty("jdk.lang.Process.launchMechanism")); + + System.loadLibrary("PipesCloseOnExec"); + + if (!startTester()) { + throw new RuntimeException("Failed to start testers (see stdout)"); + } + + System.out.println(LocalTime.now() + ": Call ProcessBuilder.start..."); + + for (int i = 0; i < num_tries; i ++) { + ProcessBuilder pb = new ProcessBuilder("true").inheritIO(); + new OutputAnalyzer(pb.start()).shouldHaveExitValue(0); + } + + if (!stopTester()) { + printOpenFileDescriptors(); + throw new RuntimeException("Catched FDs without CLOEXEC? Check output."); + } + } +} diff --git a/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/libPipesCloseOnExec.c b/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/libPipesCloseOnExec.c new file mode 100644 index 00000000000..9a090f3bbd5 --- /dev/null +++ b/test/jdk/java/lang/ProcessBuilder/PipesCloseOnExecTest/libPipesCloseOnExec.c @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "testlib_thread_barriers.h" + +static void trc(const char* fmt, ...) { + char buf [1024]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + printf("%s\n", buf); + fflush(stdout); +} + +/* Set 1 to restrict this test to pipes, 0 to test all file descriptors. + * (for now, we ignore regular files opened with CLOEXEC since loaded jars seem not tagged as CLOEXEc. + * We should probably fix that eventually. */ +#define TEST_PIPES_ONLY 1 + +/* stdin/out/err file descriptors are usually not CLOEXEC */ +#define IGNORE_BELOW 4 + +/* Only query file descriptors up to this point */ +#define MAX_FD 1024 + +static pthread_t tid_tester; + +static pthread_barrier_t start_barrier; +static atomic_bool stop_now = false; + +/* Mainly to prevent tracing the same fd over and over again: + * 1 - present, 2 - present, cloexec */ +static unsigned fd_state[MAX_FD]; + +static bool is_pipe(int fd) { + struct stat mystat; + if (fstat(fd, &mystat) == -1) { + return false; + } + return mystat.st_mode & S_IFIFO; +} + +static void print_fd_details(int fd, char* out, size_t outlen) { + const char* type = "unknown"; + char link[1024] = { 0 }; + char procfd[129]; + struct stat mystat; + + out[0] = '\0'; + + if (fstat(fd, &mystat) == -1) { + snprintf(out, outlen, "%s", errno == EBADF ? "EBADF" : "???"); + return; + } + + switch (mystat.st_mode & S_IFMT) { + case S_IFBLK: type = "blk"; break; + case S_IFCHR: type = "char"; break; + case S_IFDIR: type = "dir"; break; + case S_IFIFO: type = "fifo"; break; + case S_IFLNK: type = "lnk"; break; + case S_IFREG: type = "reg"; break; + case S_IFSOCK: type = "sock"; break; + } + + snprintf(procfd, sizeof(procfd) - 1, "/proc/self/fd/%d", fd); + int linklen = readlink(procfd, link, sizeof(link) - 1); + if (linklen > 0) { + link[linklen] = '\0'; + snprintf(out, outlen, "%s (%s)", type, link); + } else { + snprintf(out, outlen, "%s", type); + } +} + +/* Returns true for error */ +static bool testFD(int fd) { + + int rc = fcntl(fd, F_GETFD); + if (rc == -1) { + return false; + } + + const bool has_cloexec = (rc & FD_CLOEXEC); + const bool is_a_pipe = is_pipe(fd); + const unsigned state = has_cloexec ? 2 : 1; + bool had_error = false; + + if (fd_state[fd] != state) { + fd_state[fd] = state; + char buf[1024]; + print_fd_details(fd, buf, sizeof(buf)); + if (has_cloexec) { + trc("%d: %s", fd, buf); + } else { + if (fd < IGNORE_BELOW) { + trc("%d: %s ** CLOEXEC MISSING ** (ignored - below scanned range)", fd, buf); + } else if (TEST_PIPES_ONLY && !is_a_pipe) { + trc("%d: %s ** CLOEXEC MISSING ** (ignored - not a pipe)", fd, buf); + } else { + trc("%d: %s ** CLOEXEC MISSING ** (ERROR)", fd, buf); + had_error = true; + } + } + } + + return had_error; +} + +static void* testerLoop(void* dummy) { + + pthread_barrier_wait(&start_barrier); + + trc("Tester is alive"); + + bool had_error = false; + + while (!atomic_load(&stop_now)) { + for (int fd = 0; fd < MAX_FD; fd++) { + bool rc = testFD(fd); + had_error = had_error || rc; + } + } + + trc("Tester dies"); + + return had_error ? (void*)1 : NULL; +} + +JNIEXPORT jboolean JNICALL +Java_PipesCloseOnExecTest_startTester(JNIEnv* env, jclass cls) +{ + pthread_attr_t attr; + int rc = 0; + + if (pthread_barrier_init(&start_barrier, NULL, 2) != 0) { + trc("pthread_barrier_init failed (%d)", errno); + return false; + } + + pthread_attr_init(&attr); + if (pthread_create(&tid_tester, &attr, testerLoop, NULL) != 0) { + trc("pthread_create failed (%d)", errno); + return JNI_FALSE; + } + + pthread_barrier_wait(&start_barrier); + + trc("Started tester"); + + return JNI_TRUE; +} + +JNIEXPORT jboolean JNICALL +Java_PipesCloseOnExecTest_stopTester(JNIEnv* env, jclass cls) +{ + atomic_store(&stop_now, true); + + void* retval = NULL; + pthread_join(tid_tester, &retval); + pthread_barrier_destroy(&start_barrier); + + return retval == NULL ? JNI_TRUE : JNI_FALSE; +} diff --git a/test/lib/native/testlib_thread_barriers.h b/test/lib/native/testlib_thread_barriers.h new file mode 100644 index 00000000000..8e0b717b57a --- /dev/null +++ b/test/lib/native/testlib_thread_barriers.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, IBM Corp. + * 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. + */ + +#ifndef TESTLIB_THREAD_BARRIERS_H +#define TESTLIB_THREAD_BARRIERS_H + +/* MacOS does not have pthread barriers; implement a fallback using condvars. */ + +#ifndef _WIN32 +#if !defined _POSIX_BARRIERS || _POSIX_BARRIERS < 0 + +#include + +#define PTHREAD_BARRIER_SERIAL_THREAD 1 + +#define pthread_barrier_t barr_t +#define pthread_barrier_init(barr, attr, need) barr_init(barr, attr, need) +#define pthread_barrier_destroy(barr) barr_destroy(barr) +#define pthread_barrier_wait(barr) barr_wait(barr) + +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + int have, need, trigger_count; +} barr_t; + +int barr_init(barr_t* b, void* ignored, int need) { + b->have = b->trigger_count = 0; + b->need = need; + pthread_mutex_init(&b->mutex, NULL); + pthread_cond_init(&b->cond, NULL); + return 0; +} + +int barr_destroy(barr_t* b) { + pthread_mutex_destroy(&b->mutex); + pthread_cond_destroy(&b->cond); + return 0; +} + +int barr_wait(barr_t* b) { + pthread_mutex_lock(&b->mutex); + int my_trigger_count = b->trigger_count; + b->have++; + if (b->have == b->need) { + b->have = 0; + b->trigger_count++; + pthread_cond_broadcast(&b->cond); + pthread_mutex_unlock(&b->mutex); + return PTHREAD_BARRIER_SERIAL_THREAD; + } + while (my_trigger_count == b->trigger_count) { // no spurious wakeups + pthread_cond_wait(&b->cond, &b->mutex); + } + pthread_mutex_unlock(&b->mutex); + return 0; +} + +#endif // !_POSIX_BARRIERS +#endif // !_WIN32 + +#endif // TESTLIB_THREAD_BARRIERS_H