8221858: Build Better Processes
authorrriggs
Tue, 30 Apr 2019 16:45:29 -0400
changeset 58619 979b58a3bb97
parent 58618 a95e1f6757c7
child 58620 ca5f1bf5a054
8221858: Build Better Processes Reviewed-by: alanb, rhalade, ahgross, darcy
src/java.base/windows/classes/java/lang/ProcessImpl.java
--- a/src/java.base/windows/classes/java/lang/ProcessImpl.java	Tue May 21 08:37:30 2019 +0800
+++ b/src/java.base/windows/classes/java/lang/ProcessImpl.java	Tue Apr 30 16:45:29 2019 -0400
@@ -38,6 +38,7 @@
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
+import java.util.Locale;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
@@ -46,6 +47,8 @@
 import jdk.internal.access.JavaIOFileDescriptorAccess;
 import jdk.internal.access.SharedSecrets;
 import jdk.internal.ref.CleanerFactory;
+import sun.security.action.GetBooleanAction;
+import sun.security.action.GetPropertyAction;
 
 /* This class is for the exclusive use of ProcessBuilder.start() to
  * create new processes.
@@ -209,12 +212,15 @@
 
     private static final int VERIFICATION_CMD_BAT = 0;
     private static final int VERIFICATION_WIN32 = 1;
-    private static final int VERIFICATION_LEGACY = 2;
+    private static final int VERIFICATION_WIN32_SAFE = 2; // inside quotes not allowed
+    private static final int VERIFICATION_LEGACY = 3;
+    // See Command shell overview for documentation of special characters.
+    // https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-xp/bb490954(v=technet.10)
     private static final char ESCAPE_VERIFICATION[][] = {
         // We guarantee the only command file execution for implicit [cmd.exe] run.
         //    http://technet.microsoft.com/en-us/library/bb490954.aspx
         {' ', '\t', '<', '>', '&', '|', '^'},
-
+        {' ', '\t', '<', '>'},
         {' ', '\t', '<', '>'},
         {' ', '\t'}
     };
@@ -231,8 +237,25 @@
             cmdbuf.append(' ');
             String s = cmd[i];
             if (needsEscaping(verificationType, s)) {
-                cmdbuf.append('"').append(s);
+                cmdbuf.append('"');
 
+                if (verificationType == VERIFICATION_WIN32_SAFE) {
+                    // Insert the argument, adding '\' to quote any interior quotes
+                    int length = s.length();
+                    for (int j = 0; j < length; j++) {
+                        char c = s.charAt(j);
+                        if (c == DOUBLEQUOTE) {
+                            int count = countLeadingBackslash(verificationType, s, j);
+                            while (count-- > 0) {
+                                cmdbuf.append(BACKSLASH);   // double the number of backslashes
+                            }
+                            cmdbuf.append(BACKSLASH);       // backslash to quote the quote
+                        }
+                        cmdbuf.append(c);
+                    }
+                } else {
+                    cmdbuf.append(s);
+                }
                 // The code protects the [java.exe] and console command line
                 // parser, that interprets the [\"] combination as an escape
                 // sequence for the ["] char.
@@ -245,8 +268,9 @@
                 // command line parser. The case of the [""] tail escape
                 // sequence could not be realized due to the argument validation
                 // procedure.
-                if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) {
-                    cmdbuf.append('\\');
+                int count = countLeadingBackslash(verificationType, s, s.length());
+                while (count-- > 0) {
+                    cmdbuf.append(BACKSLASH);   // double the number of backslashes
                 }
                 cmdbuf.append('"');
             } else {
@@ -256,26 +280,16 @@
         return cmdbuf.toString();
     }
 
-    private static boolean isQuoted(boolean noQuotesInside, String arg,
-            String errorMessage) {
-        int lastPos = arg.length() - 1;
-        if (lastPos >=1 && arg.charAt(0) == '"' && arg.charAt(lastPos) == '"') {
-            // The argument has already been quoted.
-            if (noQuotesInside) {
-                if (arg.indexOf('"', 1) != lastPos) {
-                    // There is ["] inside.
-                    throw new IllegalArgumentException(errorMessage);
-                }
-            }
-            return true;
-        }
-        if (noQuotesInside) {
-            if (arg.indexOf('"') >= 0) {
-                // There is ["] inside.
-                throw new IllegalArgumentException(errorMessage);
-            }
-        }
-        return false;
+    /**
+     * Return the argument without quotes (1st and last) if present, else the arg.
+     * @param str a string
+     * @return the string without 1st and last quotes
+     */
+    private static String unQuote(String str) {
+        int len = str.length();
+        return (len >= 2 && str.charAt(0) == DOUBLEQUOTE && str.charAt(len - 1) == DOUBLEQUOTE)
+                ? str.substring(1, len - 1)
+                : str;
     }
 
     private static boolean needsEscaping(int verificationType, String arg) {
@@ -286,9 +300,26 @@
 
         // For [.exe] or [.com] file the unpaired/internal ["]
         // in the argument is not a problem.
-        boolean argIsQuoted = isQuoted(
-            (verificationType == VERIFICATION_CMD_BAT),
-            arg, "Argument has embedded quote, use the explicit CMD.EXE call.");
+        String unquotedArg = unQuote(arg);
+        boolean argIsQuoted = !arg.equals(unquotedArg);
+        boolean embeddedQuote = unquotedArg.indexOf(DOUBLEQUOTE) >= 0;
+
+        switch (verificationType) {
+            case VERIFICATION_CMD_BAT:
+                if (embeddedQuote) {
+                    throw new IllegalArgumentException("Argument has embedded quote, " +
+                            "use the explicit CMD.EXE call.");
+                }
+                break;  // break determine whether to quote
+            case VERIFICATION_WIN32_SAFE:
+                if (argIsQuoted && embeddedQuote)  {
+                    throw new IllegalArgumentException("Malformed argument has embedded quote: "
+                            + unquotedArg);
+                }
+                break;
+            default:
+                break;
+        }
 
         if (!argIsQuoted) {
             char testEscape[] = ESCAPE_VERIFICATION[verificationType];
@@ -304,13 +335,13 @@
     private static String getExecutablePath(String path)
         throws IOException
     {
-        boolean pathIsQuoted = isQuoted(true, path,
-                "Executable name has embedded quote, split the arguments");
-
+        String name = unQuote(path);
+        if (name.indexOf(DOUBLEQUOTE) >= 0) {
+            throw new IllegalArgumentException("Executable name has embedded quote, " +
+                    "split the arguments: " + name);
+        }
         // Win32 CreateProcess requires path to be normalized
-        File fileToRun = new File(pathIsQuoted
-            ? path.substring(1, path.length() - 1)
-            : path);
+        File fileToRun = new File(name);
 
         // From the [CreateProcess] function documentation:
         //
@@ -325,13 +356,26 @@
         // sequence:..."
         //
         // In practice ANY non-existent path is extended by [.exe] extension
-        // in the [CreateProcess] funcion with the only exception:
+        // in the [CreateProcess] function with the only exception:
         // the path ends by (.)
 
         return fileToRun.getPath();
     }
 
+    /**
+     * An executable is any program that is an EXE or does not have an extension
+     * and the Windows createProcess will be looking for .exe.
+     * The comparison is case insensitive based on the name.
+     * @param executablePath the executable file
+     * @return true if the path ends in .exe or does not have an extension.
+     */
+    private boolean isExe(String executablePath) {
+        File file = new File(executablePath);
+        String upName = file.getName().toUpperCase(Locale.ROOT);
+        return (upName.endsWith(".EXE") || upName.indexOf('.') < 0);
+    }
 
+    // Old version that can be bypassed
     private boolean isShellFile(String executablePath) {
         String upPath = executablePath.toUpperCase();
         return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT"));
@@ -342,6 +386,21 @@
         return argbuf.append('"').append(arg).append('"').toString();
     }
 
+    // Count backslashes before start index of string.
+    // .bat files don't include backslashes as part of the quote
+    private static int countLeadingBackslash(int verificationType,
+                                             CharSequence input, int start) {
+        if (verificationType == VERIFICATION_CMD_BAT)
+            return 0;
+        int j;
+        for (j = start - 1; j >= 0 && input.charAt(j) == BACKSLASH; j--) {
+            // just scanning backwards
+        }
+        return (start - 1) - j;  // number of BACKSLASHES
+    }
+
+    private static final char DOUBLEQUOTE = '\"';
+    private static final char BACKSLASH = '\\';
 
     private final long handle;
     private final ProcessHandle processHandle;
@@ -358,15 +417,13 @@
         throws IOException
     {
         String cmdstr;
-        SecurityManager security = System.getSecurityManager();
-        boolean allowAmbiguousCommands = false;
-        if (security == null) {
-            allowAmbiguousCommands = true;
-            String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands");
-            if (value != null)
-                allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
-        }
-        if (allowAmbiguousCommands) {
+        final SecurityManager security = System.getSecurityManager();
+        final String value = GetPropertyAction.
+                privilegedGetProperty("jdk.lang.Process.allowAmbiguousCommands",
+                        (security == null ? "true" : "false"));
+        final boolean allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
+
+        if (allowAmbiguousCommands && security == null) {
             // Legacy mode.
 
             // Normalize path if possible.
@@ -413,11 +470,12 @@
             // Quotation protects from interpretation of the [path] argument as
             // start of longer path with spaces. Quotation has no influence to
             // [.exe] extension heuristic.
+            boolean isShell = allowAmbiguousCommands ? isShellFile(executablePath)
+                    : !isExe(executablePath);
             cmdstr = createCommandLine(
-                    // We need the extended verification procedure for CMD files.
-                    isShellFile(executablePath)
-                        ? VERIFICATION_CMD_BAT
-                        : VERIFICATION_WIN32,
+                    // We need the extended verification procedures
+                    isShell ? VERIFICATION_CMD_BAT
+                            : (allowAmbiguousCommands ? VERIFICATION_WIN32 : VERIFICATION_WIN32_SAFE),
                     quoteString(executablePath),
                     cmd);
         }