changeset 1696:120ec64ce0a9

6850720: (process) Use clone(CLONE_VM), not fork, on Linux to avoid swap exhaustion Summary: Use clone(CLONE_VM) on Linux; Reluctantly implement execvpe. Reviewed-by: michaelm
author martin
date Sun, 14 Jun 2009 14:23:22 -0700
parents 3d2f9f6b0f09
children d06c2218fab1
files src/solaris/native/java/lang/UNIXProcess_md.c test/java/lang/ProcessBuilder/Basic.java test/java/lang/ProcessBuilder/BigFork.java
diffstat 3 files changed, 471 insertions(+), 201 deletions(-) [+]
line wrap: on
line diff
--- a/src/solaris/native/java/lang/UNIXProcess_md.c	Wed Aug 30 15:42:01 2017 +0300
+++ b/src/solaris/native/java/lang/UNIXProcess_md.c	Sun Jun 14 14:23:22 2009 -0700
@@ -49,6 +49,18 @@
 #include <fcntl.h>
 #include <limits.h>
 
+#ifndef USE_CLONE
+#ifdef __linux__
+#define USE_CLONE 1
+#else
+#define USE_CLONE 0
+#endif
+#endif
+
+#if USE_CLONE
+#include <sched.h>
+#endif
+
 #ifndef STDIN_FILENO
 #define STDIN_FILENO 0
 #endif
@@ -362,81 +374,61 @@
 }
 #endif /* DEBUG_PROCESS */
 
-/* Version of execvpe when child's PATH differs from parent's */
-static int
-execvp_usingParentPath(const char *file, const char *const argv[])
+/**
+ * Exec FILE as a traditional Bourne shell script (i.e. one without #!).
+ * If we could do it over again, we would probably not support such an ancient
+ * misfeature, but compatibility wins over sanity.  The original support for
+ * this was imported accidentally from execvp().
+ */
+static void
+execve_as_traditional_shell_script(const char *file,
+                                   const char *argv[],
+                                   const char *const envp[])
 {
-    char expanded_file[PATH_MAX];
-    int filelen = strlen(file);
-    int sticky_errno = 0;
-    const char * const * dirs;
-    /* Search parent's PATH */
-    for (dirs = parentPathv; *dirs; dirs++) {
-        const char * dir = *dirs;
-        int dirlen = strlen(dir);
-        if (filelen + dirlen + 1 >= PATH_MAX) {
-            /* Resist the urge to remove this limit;
-             * calling malloc after fork is unsafe. */
-            errno = ENAMETOOLONG;
-            continue;
-        }
-        strcpy(expanded_file, dir);
-        strcpy(expanded_file + dirlen, file);
-        execvp(expanded_file, (char **) argv);
-        /* There are 3 responses to various classes of errno:
-         * return immediately, continue (especially for ENOENT),
-         * or continue with "sticky" errno.
-         *
-         * From exec(3):
-         *
-         * If permission is denied for a file (the attempted
-         * execve returned EACCES), these functions will continue
-         * searching the rest of the search path.  If no other
-         * file is found, however, they will return with the
-         * global variable errno set to EACCES.
-         */
-        switch (errno) {
-        case EACCES:
-            sticky_errno = errno;
-            /* FALLTHRU */
-        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; /* Try other directories in PATH */
-        default:
-            return -1;
-        }
-    }
-    if (sticky_errno != 0)
-        errno = sticky_errno;
-    return -1;
+    /* Use the extra word of space provided for us in argv by caller. */
+    const char *argv0 = argv[0];
+    const char *const *end = argv;
+    while (*end != NULL)
+        ++end;
+    memmove(argv+2, argv+1, (end-argv) * sizeof (*end));
+    argv[0] = "/bin/sh";
+    argv[1] = file;
+    execve(argv[0], (char **) argv, (char **) envp);
+    /* Can't even exec /bin/sh?  Big trouble, but let's soldier on... */
+    memmove(argv+1, argv+2, (end-argv) * sizeof (*end));
+    argv[0] = argv0;
 }
 
 /**
- * 'execvpe' should have been included in the Unix standards,
- * and is a GNU extension in glibc 2.10.
- *
- * JDK_execvpe is identical to execvp, except that the child environment is
+ * Like execve(2), except that in case of ENOEXEC, FILE is assumed to
+ * be a shell script and the system default shell is invoked to run it.
+ */
+static void
+execve_with_shell_fallback(const char *file,
+                           const char *argv[],
+                           const char *const envp[])
+{
+#if USE_CLONE
+    execve(file, (char **) argv, (char **) envp);
+    if (errno == ENOEXEC)
+        execve_as_traditional_shell_script(file, argv, envp);
+#else
+    /* Our address space is unshared, so can mutate environ. */
+    extern char **environ;
+    environ = (char **) envp;
+    execvp(file, (char **) argv);
+#endif
+}
+
+/**
+ * execvpe should have been included in the Unix standards.
+ * execvpe is identical to execvp, except that the child environment is
  * specified via the 3rd argument instead of being inherited from environ.
- *
- * This implementation of JDK_execvpe does not work if memory is shared
- * with the parent, when using vfork(2), or clone(2) with CLONE_VM.
  */
-static int
-JDK_execvpe(const char *file,
-            const char *const argv[],
-            const char *const envp[])
+static void
+execvpe(const char *file,
+        const char *argv[],
+        const char *const envp[])
 {
     /* This is one of the rare times it's more portable to declare an
      * external symbol explicitly, rather than via a system header.
@@ -451,28 +443,73 @@
      */
     extern char **environ;
 
-    if (envp != NULL)
-        environ = (char **) envp;
+    if (envp == NULL || (char **) envp == environ) {
+        execvp(file, (char **) argv);
+        return;
+    }
 
-    if (/* Parent and child environment the same?  Use child PATH. */
-        (envp == NULL)
+    if (*file == '\0') {
+        errno = ENOENT;
+        return;
+    }
 
-        /* http://www.opengroup.org/onlinepubs/009695399/functions/exec.html
-         * "If the file argument contains a slash character, it is used as
-         * the pathname for this file.  Otherwise, the path prefix for this
-         * file is obtained by a search of the directories passed in the
-         * PATH environment variable" */
-        || (strchr(file, '/') != NULL)
-
-        /* Parent and child PATH the same?  Use child PATH. */
-        || (strcmp(parentPath, effectivePath()) == 0)
-
-        /* We want ENOENT, not EACCES, for zero-length program names. */
-        || (*file == '\0'))
-
-        return execvp(file, (char **) argv);
-    else
-        return execvp_usingParentPath(file, argv);
+    if (strchr(file, '/') != NULL) {
+        execve_with_shell_fallback(file, argv, envp);
+    } else {
+        /* We must search PATH (parent's, not child's) */
+        char expanded_file[PATH_MAX];
+        int filelen = strlen(file);
+        int sticky_errno = 0;
+        const char * const * dirs;
+        for (dirs = parentPathv; *dirs; dirs++) {
+            const char * dir = *dirs;
+            int dirlen = strlen(dir);
+            if (filelen + dirlen + 1 >= PATH_MAX) {
+                errno = ENAMETOOLONG;
+                continue;
+            }
+            memcpy(expanded_file, dir, dirlen);
+            memcpy(expanded_file + dirlen, file, filelen);
+            expanded_file[dirlen + filelen] = '\0';
+            execve_with_shell_fallback(expanded_file, argv, envp);
+            /* There are 3 responses to various classes of errno:
+             * return immediately, continue (especially for ENOENT),
+             * or continue with "sticky" errno.
+             *
+             * From exec(3):
+             *
+             * If permission is denied for a file (the attempted
+             * execve returned EACCES), these functions will continue
+             * searching the rest of the search path.  If no other
+             * file is found, however, they will return with the
+             * global variable errno set to EACCES.
+             */
+            switch (errno) {
+            case EACCES:
+                sticky_errno = errno;
+                /* FALLTHRU */
+            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; /* Try other directories in PATH */
+            default:
+                return;
+            }
+        }
+        if (sticky_errno != 0)
+            errno = sticky_errno;
+    }
 }
 
 static void
@@ -513,10 +550,94 @@
     }
 }
 
-#ifndef __solaris__
-#undef fork1
-#define fork1() fork()
-#endif
+typedef struct _ChildStuff
+{
+    int in[2];
+    int out[2];
+    int err[2];
+    int fail[2];
+    const char **argv;
+    const char **envv;
+    const char *pdir;
+    jboolean redirectErrorStream;
+} ChildStuff;
+
+static void
+copyPipe(int from[2], int to[2])
+{
+    to[0] = from[0];
+    to[1] = from[1];
+}
+
+/**
+ * Child process after a successful fork() or clone().
+ * This function must not return, and must be prepared for either all
+ * of its address space to be shared with its parent, or to be a copy.
+ * It must not modify global variables such as "environ".
+ */
+static int
+childProcess(void *arg)
+{
+    const ChildStuff* p = (const ChildStuff*) arg;
+
+    /* Close the parent sides of the pipes.
+       Closing pipe fds here is redundant, since closeDescriptors()
+       would do it anyways, but a little paranoia is a good thing. */
+    closeSafely(p->in[1]);
+    closeSafely(p->out[0]);
+    closeSafely(p->err[0]);
+    closeSafely(p->fail[0]);
+
+    /* Give the child sides of the pipes the right fileno's. */
+    /* Note: it is possible for in[0] == 0 */
+    moveDescriptor(p->in[0], STDIN_FILENO);
+    moveDescriptor(p->out[1], STDOUT_FILENO);
+
+    if (p->redirectErrorStream) {
+      closeSafely(p->err[1]);
+      dup2(STDOUT_FILENO, STDERR_FILENO);
+    } else {
+      moveDescriptor(p->err[1], STDERR_FILENO);
+    }
+
+    moveDescriptor(p->fail[1], FAIL_FILENO);
+
+    /* close everything */
+    if (closeDescriptors() == 0) { /* failed,  close the old way */
+        int max_fd = (int)sysconf(_SC_OPEN_MAX);
+        int i;
+        for (i = FAIL_FILENO + 1; i < max_fd; i++)
+            close(i);
+    }
+
+    /* change to the new working directory */
+    if (p->pdir != NULL && chdir(p->pdir) < 0)
+        goto WhyCantJohnnyExec;
+
+    if (fcntl(FAIL_FILENO, F_SETFD, FD_CLOEXEC) == -1)
+        goto WhyCantJohnnyExec;
+
+    execvpe(p->argv[0], p->argv, p->envv);
+
+ 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.
+     * 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;
+        write(FAIL_FILENO, &errnum, sizeof(errnum));
+    }
+    close(FAIL_FILENO);
+    _exit(-1);
+    return 0;  /* Suppress warning "no return value from function" */
+}
 
 JNIEXPORT jint JNICALL
 Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env,
@@ -532,33 +653,42 @@
 {
     int errnum;
     int resultPid = -1;
+#if USE_CLONE
+    void *clone_stack = NULL;                                                                                                                            
+#endif
     int in[2], out[2], err[2], fail[2];
-    const char **argv = NULL;
-    const char **envv = NULL;
-    const char *pprog     = getBytes(env, prog);
-    const char *pargBlock = getBytes(env, argBlock);
-    const char *penvBlock = getBytes(env, envBlock);
-    const char *pdir      = getBytes(env, dir);
+    const char *pprog = NULL;
+    const char *pargBlock = NULL;
+    const char *penvBlock = NULL;
+    ChildStuff *c;
 
     in[0] = in[1] = out[0] = out[1] = err[0] = err[1] = fail[0] = fail[1] = -1;
 
-    assert(prog != NULL && argBlock != NULL);
-    if (pprog     == NULL) goto Catch;
-    if (pargBlock == NULL) goto Catch;
-    if (envBlock  != NULL && penvBlock == NULL) goto Catch;
-    if (dir       != NULL && pdir      == NULL) goto Catch;
+    if ((c = NEW(ChildStuff, 1)) == NULL) return -1;
+    c->argv = NULL;
+    c->envv = NULL;
+    c->pdir = NULL;
 
-    /* Convert pprog + pargBlock into a char ** argv */
-    if ((argv = NEW(const char *, argc + 2)) == NULL)
-        goto Catch;
-    argv[0] = pprog;
-    initVectorFromBlock(argv+1, pargBlock, argc);
+    /* Convert prog + argBlock into a char ** argv.
+     * Add one word room for expansion of argv for use by
+     * execve_as_traditional_shell_script.
+     */
+
+    if ((pprog     = getBytes(env, prog))       == NULL) goto Catch;
+    if ((pargBlock = getBytes(env, argBlock))   == NULL) goto Catch;
+    if ((c->argv = NEW(const char *, argc + 3)) == NULL) goto Catch;
+    c->argv[0] = pprog;
+    initVectorFromBlock(c->argv+1, pargBlock, argc);
 
     if (envBlock != NULL) {
-        /* Convert penvBlock into a char ** envv */
-        if ((envv = NEW(const char *, envc + 1)) == NULL)
-            goto Catch;
-        initVectorFromBlock(envv, penvBlock, envc);
+        /* Convert envBlock into a char ** envv */
+        if ((penvBlock = getBytes(env, envBlock))   == NULL) goto Catch;
+        if ((c->envv = NEW(const char *, envc + 1)) == NULL) goto Catch;
+        initVectorFromBlock(c->envv, penvBlock, envc);
+    }
+
+    if (dir != NULL) {
+        if ((c->pdir = getBytes(env, dir)) == NULL) goto Catch;
     }
 
     if ((pipe(in)   < 0) ||
@@ -569,68 +699,41 @@
         goto Catch;
     }
 
-    resultPid = fork1();
+    copyPipe(in,   c->in);
+    copyPipe(out,  c->out);
+    copyPipe(err,  c->err);
+    copyPipe(fail, c->fail);
+
+    c->redirectErrorStream = redirectErrorStream;
+
+    {
+#if USE_CLONE
+        /* See clone(2).
+         * Instead of worrying about which direction the stack grows, just
+         * allocate twice as much and start the stack in the middle. */
+        const int stack_size = 64 * 1024;
+        if ((clone_stack = NEW(char, 2 * stack_size)) == NULL) goto Catch;
+        resultPid = clone(childProcess, clone_stack + stack_size,
+                          /* CLONE_VFORK | // works, but unnecessary */
+                          CLONE_VM | SIGCHLD, c);
+#else
+        /* From fork(2): In Solaris 10, a call to fork() is identical
+         * to a call to fork1(); only the calling thread is replicated
+         * in the child process. This is the POSIX-specified behavior
+         * for fork(). */
+        resultPid = fork();
+        if (resultPid == 0) {
+          childProcess(c);
+          assert(0);  /* childProcess must not return */
+        }
+#endif
+    }
+
     if (resultPid < 0) {
         throwIOException(env, errno, "Fork failed");
         goto Catch;
     }
 
-    if (resultPid == 0) {
-        /* Child process */
-
-        /* Close the parent sides of the pipe.
-           Give the child sides of the pipes the right fileno's.
-           Closing pipe fds here is redundant, since closeDescriptors()
-           would do it anyways, but a little paranoia is a good thing. */
-        /* Note: it is possible for in[0] == 0 */
-        close(in[1]);
-        moveDescriptor(in[0], STDIN_FILENO);
-        close(out[0]);
-        moveDescriptor(out[1], STDOUT_FILENO);
-        close(err[0]);
-        if (redirectErrorStream) {
-            close(err[1]);
-            dup2(STDOUT_FILENO, STDERR_FILENO);
-        } else {
-            moveDescriptor(err[1], STDERR_FILENO);
-        }
-        close(fail[0]);
-        moveDescriptor(fail[1], FAIL_FILENO);
-
-        /* close everything */
-        if (closeDescriptors() == 0) { /* failed,  close the old way */
-            int max_fd = (int)sysconf(_SC_OPEN_MAX);
-            int i;
-            for (i = FAIL_FILENO + 1; i < max_fd; i++)
-                close(i);
-        }
-
-        /* change to the new working directory */
-        if (pdir != NULL && chdir(pdir) < 0)
-            goto WhyCantJohnnyExec;
-
-        if (fcntl(FAIL_FILENO, F_SETFD, FD_CLOEXEC) == -1)
-            goto WhyCantJohnnyExec;
-
-        JDK_execvpe(argv[0], argv, envv);
-
-    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.
-         * 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.
-         */
-        errnum = errno;
-        write(FAIL_FILENO, &errnum, sizeof(errnum));
-        close(FAIL_FILENO);
-        _exit(-1);
-    }
-
     /* parent process */
 
     close(fail[1]); fail[1] = -1; /* See: WhyCantJohnnyExec */
@@ -651,6 +754,10 @@
     (*env)->SetIntField(env, stderr_fd, IO_fd_fdID, err[0]);
 
  Finally:
+#if USE_CLONE
+    free(clone_stack);
+#endif
+
     /* Always clean up the child's side of the pipes */
     closeSafely(in [0]);
     closeSafely(out[1]);
@@ -660,13 +767,14 @@
     closeSafely(fail[0]);
     closeSafely(fail[1]);
 
-    free(argv);
-    free(envv);
-
     releaseBytes(env, prog,     pprog);
     releaseBytes(env, argBlock, pargBlock);
     releaseBytes(env, envBlock, penvBlock);
-    releaseBytes(env, dir,      pdir);
+    releaseBytes(env, dir,      c->pdir);
+
+    free(c->argv);
+    free(c->envv);
+    free(c);
 
     return resultPid;
 
--- a/test/java/lang/ProcessBuilder/Basic.java	Wed Aug 30 15:42:01 2017 +0300
+++ b/test/java/lang/ProcessBuilder/Basic.java	Sun Jun 14 14:23:22 2009 -0700
@@ -254,6 +254,18 @@
         s.write(bytes);         // Might hang!
     }
 
+    static void checkPermissionDenied(ProcessBuilder pb) {
+        try {
+            pb.start();
+            fail("Expected IOException not thrown");
+        } catch (IOException e) {
+            String m = e.getMessage();
+            if (EnglishUnix.is() &&
+                ! matches(m, "Permission denied"))
+                unexpected(e);
+        } catch (Throwable t) { unexpected(t); }
+    }
+
     public static class JavaChild {
         public static void main(String args[]) throws Throwable {
             String action = args[0];
@@ -292,12 +304,10 @@
                 for (final ProcessBuilder pb :
                          new ProcessBuilder[] {pb1, pb2}) {
                     pb.command("true");
-                    r = run(pb.start());
-                    equal(r.exitValue(), True.exitValue());
+                    equal(run(pb).exitValue(), True.exitValue());
 
                     pb.command("false");
-                    r = run(pb.start());
-                    equal(r.exitValue(), False.exitValue());
+                    equal(run(pb).exitValue(), False.exitValue());
                 }
 
                 if (failed != 0) throw new Error("null PATH");
@@ -342,31 +352,82 @@
                         // Can't execute a directory -- permission denied
                         // Report EACCES errno
                         new File("dir1/prog").mkdirs();
-                        try {
-                            pb.start();
-                            fail("Expected IOException not thrown");
-                        } catch (IOException e) {
-                            String m = e.getMessage();
-                            if (EnglishUnix.is() &&
-                                ! matches(m, "Permission denied"))
-                                unexpected(e);
-                        } catch (Throwable t) { unexpected(t); }
+                        checkPermissionDenied(pb);
 
                         // continue searching if EACCES
                         copy("/bin/true", "dir2/prog");
-                        equal(run(pb.start()).exitValue(), True.exitValue());
+                        equal(run(pb).exitValue(), True.exitValue());
                         new File("dir1/prog").delete();
                         new File("dir2/prog").delete();
 
                         new File("dir2/prog").mkdirs();
                         copy("/bin/true", "dir1/prog");
-                        equal(run(pb.start()).exitValue(), True.exitValue());
+                        equal(run(pb).exitValue(), True.exitValue());
 
-                        // Check empty PATH component means current directory
+                        // Check empty PATH component means current directory.
+                        //
+                        // While we're here, let's test different kinds of
+                        // Unix executables, and PATH vs explicit searching.
                         new File("dir1/prog").delete();
                         new File("dir2/prog").delete();
-                        copy("/bin/true", "./prog");
-                        equal(run(pb.start()).exitValue(), True.exitValue());
+                        for (String[] command :
+                                 new String[][] {
+                                     new String[] {"./prog"},
+                                     cmd}) {
+                            pb.command(command);
+                            File prog = new File("./prog");
+                            // "Normal" binaries
+                            copy("/bin/true", "./prog");
+                            equal(run(pb).exitValue(),
+                                  True.exitValue());
+                            copy("/bin/false", "./prog");
+                            equal(run(pb).exitValue(),
+                                  False.exitValue());
+                            prog.delete();
+                            // Interpreter scripts with #!
+                            setFileContents(prog, "#!/bin/true\n");
+                            prog.setExecutable(true);
+                            equal(run(pb).exitValue(),
+                                  True.exitValue());
+                            prog.delete();
+                            setFileContents(prog, "#!/bin/false\n");
+                            prog.setExecutable(true);
+                            equal(run(pb).exitValue(),
+                                  False.exitValue());
+                            // Traditional shell scripts without #!
+                            setFileContents(prog, "exec /bin/true\n");
+                            prog.setExecutable(true);
+                            equal(run(pb).exitValue(),
+                                  True.exitValue());
+                            prog.delete();
+                            setFileContents(prog, "exec /bin/false\n");
+                            prog.setExecutable(true);
+                            equal(run(pb).exitValue(),
+                                  False.exitValue());
+                            prog.delete();
+                        }
+
+                        // Test Unix interpreter scripts
+                        File dir1Prog = new File("dir1/prog");
+                        dir1Prog.delete();
+                        pb.command(new String[] {"prog", "world"});
+                        setFileContents(dir1Prog, "#!/bin/echo hello\n");
+                        checkPermissionDenied(pb);
+                        dir1Prog.setExecutable(true);
+                        equal(run(pb).out(), "hello dir1/prog world\n");
+                        equal(run(pb).exitValue(), True.exitValue());
+                        dir1Prog.delete();
+                        pb.command(cmd);
+
+                        // Test traditional shell scripts without #!
+                        setFileContents(dir1Prog, "/bin/echo \"$@\"\n");
+                        pb.command(new String[] {"prog", "hello", "world"});
+                        checkPermissionDenied(pb);
+                        dir1Prog.setExecutable(true);
+                        equal(run(pb).out(), "hello world\n");
+                        equal(run(pb).exitValue(), True.exitValue());
+                        dir1Prog.delete();
+                        pb.command(cmd);
 
                         // If prog found on both parent and child's PATH,
                         // parent's is used.
@@ -377,10 +438,10 @@
                         copy("/bin/true", "dir1/prog");
                         copy("/bin/false", "dir3/prog");
                         pb.environment().put("PATH","dir3");
-                        equal(run(pb.start()).exitValue(), True.exitValue());
+                        equal(run(pb).exitValue(), True.exitValue());
                         copy("/bin/true", "dir3/prog");
                         copy("/bin/false", "dir1/prog");
-                        equal(run(pb.start()).exitValue(), False.exitValue());
+                        equal(run(pb).exitValue(), False.exitValue());
 
                     } finally {
                         // cleanup
@@ -1170,21 +1231,19 @@
             childArgs.add("OutErr");
             ProcessBuilder pb = new ProcessBuilder(childArgs);
             {
-                ProcessResults r = run(pb.start());
+                ProcessResults r = run(pb);
                 equal(r.out(), "outout");
                 equal(r.err(), "errerr");
             }
             {
                 pb.redirectErrorStream(true);
-                ProcessResults r = run(pb.start());
+                ProcessResults r = run(pb);
                 equal(r.out(), "outerrouterr");
                 equal(r.err(), "");
             }
         } catch (Throwable t) { unexpected(t); }
 
-        if (! Windows.is() &&
-            new File("/bin/true").exists() &&
-            new File("/bin/false").exists()) {
+        if (Unix.is()) {
             //----------------------------------------------------------------
             // We can find true and false when PATH is null
             //----------------------------------------------------------------
@@ -1193,7 +1252,7 @@
                 childArgs.add("null PATH");
                 ProcessBuilder pb = new ProcessBuilder(childArgs);
                 pb.environment().remove("PATH");
-                ProcessResults r = run(pb.start());
+                ProcessResults r = run(pb);
                 equal(r.out(), "");
                 equal(r.err(), "");
                 equal(r.exitValue(), 0);
@@ -1207,7 +1266,7 @@
                 childArgs.add("PATH search algorithm");
                 ProcessBuilder pb = new ProcessBuilder(childArgs);
                 pb.environment().put("PATH", "dir1:dir2:");
-                ProcessResults r = run(pb.start());
+                ProcessResults r = run(pb);
                 equal(r.out(), "");
                 equal(r.err(), "");
                 equal(r.exitValue(), True.exitValue());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/java/lang/ProcessBuilder/BigFork.java	Sun Jun 14 14:23:22 2009 -0700
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2009 Google Inc.  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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+import java.util.*;
+import java.io.*;
+
+/**
+ * A manual test that demonstrates the ability to start a subprocess
+ * on Linux without getting ENOMEM.  Run this test like:
+ *
+ * java -Xmx7000m BigFork
+ *
+ * providing a -Xmx flag suitable for your operating environment.
+ * Here's the bad old behavior:
+ *
+ * ==> java -Xmx7000m -esa -ea BigFork
+ * -------
+ * CommitLimit:   6214700 kB
+ * Committed_AS:  2484452 kB
+ * -------
+ * size=4.6GB
+ * -------
+ * CommitLimit:   6214700 kB
+ * Committed_AS:  7219680 kB
+ * -------
+ * Exception in thread "main" java.io.IOException: Cannot run program "/bin/true": java.io.IOException: error=12, Cannot allocate memory
+ *         at java.lang.ProcessBuilder.start(ProcessBuilder.java:1018)
+ *         at BigFork.main(BigFork.java:79)
+ * Caused by: java.io.IOException: java.io.IOException: error=12, Cannot allocate memory
+ *         at java.lang.UNIXProcess.<init>(UNIXProcess.java:190)
+ *         at java.lang.ProcessImpl.start(ProcessImpl.java:128)
+ *         at java.lang.ProcessBuilder.start(ProcessBuilder.java:1010)
+ *         ... 1 more
+ */
+public class BigFork {
+    static final Random rnd = new Random();
+    static void touchPages(byte[] chunk) {
+        final int pageSize = 4096;
+        for (int i = 0; i < chunk.length; i+= pageSize) {
+            chunk[i] = (byte) rnd.nextInt();
+        }
+    }
+
+    static void showCommittedMemory() throws IOException {
+        BufferedReader r =
+            new BufferedReader(
+                new InputStreamReader(
+                    new FileInputStream("/proc/meminfo")));
+        System.out.println("-------");
+        String line;
+        while ((line = r.readLine()) != null) {
+            if (line.startsWith("Commit")) {
+                System.out.printf("%s%n", line);
+            }
+        }
+        System.out.println("-------");
+    }
+
+    public static void main(String[] args) throws Throwable {
+        showCommittedMemory();
+
+        final int chunkSize = 1024 * 1024 * 100;
+        List<byte[]> chunks = new ArrayList<byte[]>(100);
+        try {
+            for (;;) {
+                byte[] chunk = new byte[chunkSize];
+                touchPages(chunk);
+                chunks.add(chunk);
+            }
+        } catch (OutOfMemoryError e) {
+            chunks.set(0, null);        // Free up one chunk
+            System.gc();
+            int size = chunks.size();
+            System.out.printf("size=%.2gGB%n", (double)size/10);
+
+            showCommittedMemory();
+
+            // Can we fork/exec in our current bloated state?
+            Process p = new ProcessBuilder("/bin/true").start();
+            p.waitFor();
+        }
+    }
+}