1 /* 2 * Copyright (c) 1995, 2010, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package java.lang; 27 28 import java.io.IOException; 29 import java.io.File; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.FileDescriptor; 35 import java.io.BufferedInputStream; 36 import java.io.BufferedOutputStream; 37 import java.lang.ProcessBuilder.Redirect; 38 import java.security.AccessController; 39 import java.security.PrivilegedAction; 40 import java.util.ArrayList; 41 import java.util.regex.Matcher; 42 import java.util.regex.Pattern; 43 44 /* This class is for the exclusive use of ProcessBuilder.start() to 45 * create new processes. 46 * 47 * @author Martin Buchholz 48 * @since 1.5 49 */ 50 51 final class ProcessImpl extends Process { 52 private static final sun.misc.JavaIOFileDescriptorAccess fdAccess 53 = sun.misc.SharedSecrets.getJavaIOFileDescriptorAccess(); 54 55 /** 56 * Open a file for writing. If {@code append} is {@code true} then the file 57 * is opened for atomic append directly and a FileOutputStream constructed 58 * with the resulting handle. This is because a FileOutputStream created 59 * to append to a file does not open the file in a manner that guarantees 60 * that writes by the child process will be atomic. 61 */ 62 private static FileOutputStream newFileOutputStream(File f, boolean append) 63 throws IOException 64 { 65 if (append) { 66 String path = f.getPath(); 67 SecurityManager sm = System.getSecurityManager(); 68 if (sm != null) 69 sm.checkWrite(path); 70 long handle = openForAtomicAppend(path); 71 final FileDescriptor fd = new FileDescriptor(); 72 fdAccess.setHandle(fd, handle); 73 return AccessController.doPrivileged( 74 new PrivilegedAction<FileOutputStream>() { 75 public FileOutputStream run() { 76 return new FileOutputStream(fd); 77 } 78 } 79 ); 80 } else { 81 return new FileOutputStream(f); 82 } 83 } 84 85 // System-dependent portion of ProcessBuilder.start() 86 static Process start(String cmdarray[], 87 java.util.Map<String,String> environment, 88 String dir, 89 ProcessBuilder.Redirect[] redirects, 90 boolean redirectErrorStream) 91 throws IOException 92 { 93 String envblock = ProcessEnvironment.toEnvironmentBlock(environment); 94 95 FileInputStream f0 = null; 96 FileOutputStream f1 = null; 97 FileOutputStream f2 = null; 98 99 try { 100 long[] stdHandles; 101 if (redirects == null) { 102 stdHandles = new long[] { -1L, -1L, -1L }; 103 } else { 104 stdHandles = new long[3]; 105 106 if (redirects[0] == Redirect.PIPE) 107 stdHandles[0] = -1L; 108 else if (redirects[0] == Redirect.INHERIT) 109 stdHandles[0] = fdAccess.getHandle(FileDescriptor.in); 110 else { 111 f0 = new FileInputStream(redirects[0].file()); 112 stdHandles[0] = fdAccess.getHandle(f0.getFD()); 113 } 114 115 if (redirects[1] == Redirect.PIPE) 116 stdHandles[1] = -1L; 117 else if (redirects[1] == Redirect.INHERIT) 118 stdHandles[1] = fdAccess.getHandle(FileDescriptor.out); 119 else { 120 f1 = newFileOutputStream(redirects[1].file(), 121 redirects[1].append()); 122 stdHandles[1] = fdAccess.getHandle(f1.getFD()); 123 } 124 125 if (redirects[2] == Redirect.PIPE) 126 stdHandles[2] = -1L; 127 else if (redirects[2] == Redirect.INHERIT) 128 stdHandles[2] = fdAccess.getHandle(FileDescriptor.err); 129 else { 130 f2 = newFileOutputStream(redirects[2].file(), 131 redirects[2].append()); 132 stdHandles[2] = fdAccess.getHandle(f2.getFD()); 133 } 134 } 135 136 return new ProcessImpl(cmdarray, envblock, dir, 137 stdHandles, redirectErrorStream); 138 } finally { 139 // In theory, close() can throw IOException 140 // (although it is rather unlikely to happen here) 141 try { if (f0 != null) f0.close(); } 142 finally { 143 try { if (f1 != null) f1.close(); } 144 finally { if (f2 != null) f2.close(); } 145 } 146 } 147 148 } 149 150 private static class LazyPattern { 151 // Escape-support version: 152 // "(\")((?:\\\\\\1|.)+?)\\1|([^\\s\"]+)"; 153 private static final Pattern PATTERN = 154 Pattern.compile("[^\\s\"]+|\"[^\"]*\""); 155 }; 156 157 /* Parses the command string parameter into the executable name and 158 * program arguments. 159 * 160 * The command string is broken into tokens. The token separator is a space 161 * or quota character. The space inside quotation is not a token separator. 162 * There are no escape sequences. 163 */ 164 private static String[] getTokensFromCommand(String command) { 165 ArrayList<String> matchList = new ArrayList<>(8); 166 Matcher regexMatcher = LazyPattern.PATTERN.matcher(command); 167 while (regexMatcher.find()) 168 matchList.add(regexMatcher.group()); 169 return matchList.toArray(new String[matchList.size()]); 170 } 171 172 private static final int VERIFICATION_CMD_BAT = 0; 173 private static final int VERIFICATION_WIN32 = 1; 174 private static final int VERIFICATION_LEGACY = 2; 175 private static final char ESCAPE_VERIFICATION[][] = { 176 // We guarantee the only command file execution for implicit [cmd.exe] run. 177 // http://technet.microsoft.com/en-us/library/bb490954.aspx 178 {' ', '\t', '<', '>', '&', '|', '^'}, 179 180 {' ', '\t', '<', '>'}, 181 {' ', '\t'} 182 }; 183 184 private static String createCommandLine(int verificationType, 185 final String executablePath, 186 final String cmd[]) 187 { 188 StringBuilder cmdbuf = new StringBuilder(80); 189 190 cmdbuf.append(executablePath); 191 192 for (int i = 1; i < cmd.length; ++i) { 193 cmdbuf.append(' '); 194 String s = cmd[i]; 195 if (needsEscaping(verificationType, s)) { 196 cmdbuf.append('"').append(s); 197 198 // The code protects the [java.exe] and console command line 199 // parser, that interprets the [\"] combination as an escape 200 // sequence for the ["] char. 201 // http://msdn.microsoft.com/en-us/library/17w5ykft.aspx 202 // 203 // If the argument is an FS path, doubling of the tail [\] 204 // char is not a problem for non-console applications. 205 // 206 // The [\"] sequence is not an escape sequence for the [cmd.exe] 207 // command line parser. The case of the [""] tail escape 208 // sequence could not be realized due to the argument validation 209 // procedure. 210 if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) { 211 cmdbuf.append('\\'); 212 } 213 cmdbuf.append('"'); 214 } else { 215 cmdbuf.append(s); 216 } 217 } 218 return cmdbuf.toString(); 219 } 220 221 private static boolean isQuoted(boolean noQuotesInside, String arg, 222 String errorMessage) { 223 int lastPos = arg.length() - 1; 224 if (lastPos >=1 && arg.charAt(0) == '"' && arg.charAt(lastPos) == '"') { 225 // The argument has already been quoted. 226 if (noQuotesInside) { 227 if (arg.indexOf('"', 1) != lastPos) { 228 // There is ["] inside. 229 throw new IllegalArgumentException(errorMessage); 230 } 231 } 232 return true; 233 } 234 if (noQuotesInside) { 235 if (arg.indexOf('"') >= 0) { 236 // There is ["] inside. 237 throw new IllegalArgumentException(errorMessage); 238 } 239 } 240 return false; 241 } 242 243 private static boolean needsEscaping(int verificationType, String arg) { 244 // Switch off MS heuristic for internal ["]. 245 // Please, use the explicit [cmd.exe] call 246 // if you need the internal ["]. 247 // Example: "cmd.exe", "/C", "Extended_MS_Syntax" 248 249 // For [.exe] or [.com] file the unpaired/internal ["] 250 // in the argument is not a problem. 251 boolean argIsQuoted = isQuoted( 252 (verificationType == VERIFICATION_CMD_BAT), 253 arg, "Argument has embedded quote, use the explicit CMD.EXE call."); 254 255 if (!argIsQuoted) { 256 char testEscape[] = ESCAPE_VERIFICATION[verificationType]; 257 for (int i = 0; i < testEscape.length; ++i) { 258 if (arg.indexOf(testEscape[i]) >= 0) { 259 return true; 260 } 261 } 262 } 263 return false; 264 } 265 266 private static String getExecutablePath(String path) 267 throws IOException 268 { 269 boolean pathIsQuoted = isQuoted(true, path, 270 "Executable name has embedded quote, split the arguments"); 271 272 // Win32 CreateProcess requires path to be normalized 273 File fileToRun = new File(pathIsQuoted 274 ? path.substring(1, path.length() - 1) 275 : path); 276 277 // From the [CreateProcess] function documentation: 278 // 279 // "If the file name does not contain an extension, .exe is appended. 280 // Therefore, if the file name extension is .com, this parameter 281 // must include the .com extension. If the file name ends in 282 // a period (.) with no extension, or if the file name contains a path, 283 // .exe is not appended." 284 // 285 // "If the file name !does not contain a directory path!, 286 // the system searches for the executable file in the following 287 // sequence:..." 288 // 289 // In practice ANY non-existent path is extended by [.exe] extension 290 // in the [CreateProcess] funcion with the only exception: 291 // the path ends by (.) 292 293 return fileToRun.getPath(); 294 } 295 296 297 private boolean isShellFile(String executablePath) { 298 String upPath = executablePath.toUpperCase(); 299 return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT")); 300 } 301 302 private String quoteString(String arg) { 303 StringBuilder argbuf = new StringBuilder(arg.length() + 2); 304 return argbuf.append('"').append(arg).append('"').toString(); 305 } 306 307 308 private long handle = 0; 309 private OutputStream stdin_stream; 310 private InputStream stdout_stream; 311 private InputStream stderr_stream; 312 313 private ProcessImpl(String cmd[], 314 final String envblock, 315 final String path, 316 final long[] stdHandles, 317 final boolean redirectErrorStream) 318 throws IOException 319 { 320 String cmdstr; 321 SecurityManager security = System.getSecurityManager(); 322 boolean allowAmbiguousCommands = false; 323 if (security == null) { 324 allowAmbiguousCommands = true; 325 String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands"); 326 if (value != null) 327 allowAmbiguousCommands = !"false".equalsIgnoreCase(value); 328 } 329 if (allowAmbiguousCommands) { 330 // Legacy mode. 331 332 // Normalize path if possible. 333 String executablePath = new File(cmd[0]).getPath(); 334 335 // No worry about internal, unpaired ["], and redirection/piping. 336 if (needsEscaping(VERIFICATION_LEGACY, executablePath) ) 337 executablePath = quoteString(executablePath); 338 339 cmdstr = createCommandLine( 340 //legacy mode doesn't worry about extended verification 341 VERIFICATION_LEGACY, 342 executablePath, 343 cmd); 344 } else { 345 String executablePath; 346 try { 347 executablePath = getExecutablePath(cmd[0]); 348 } catch (IllegalArgumentException e) { 349 // Workaround for the calls like 350 // Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar") 351 352 // No chance to avoid CMD/BAT injection, except to do the work 353 // right from the beginning. Otherwise we have too many corner 354 // cases from 355 // Runtime.getRuntime().exec(String[] cmd [, ...]) 356 // calls with internal ["] and escape sequences. 357 358 // Restore original command line. 359 StringBuilder join = new StringBuilder(); 360 // terminal space in command line is ok 361 for (String s : cmd) 362 join.append(s).append(' '); 363 364 // Parse the command line again. 365 cmd = getTokensFromCommand(join.toString()); 366 executablePath = getExecutablePath(cmd[0]); 367 368 // Check new executable name once more 369 if (security != null) 370 security.checkExec(executablePath); 371 } 372 373 // Quotation protects from interpretation of the [path] argument as 374 // start of longer path with spaces. Quotation has no influence to 375 // [.exe] extension heuristic. 376 cmdstr = createCommandLine( 377 // We need the extended verification procedure for CMD files. 378 isShellFile(executablePath) 379 ? VERIFICATION_CMD_BAT 380 : VERIFICATION_WIN32, 381 quoteString(executablePath), 382 cmd); 383 } 384 385 handle = create(cmdstr, envblock, path, 386 stdHandles, redirectErrorStream); 387 388 java.security.AccessController.doPrivileged( 389 new java.security.PrivilegedAction<Void>() { 390 public Void run() { 391 if (stdHandles[0] == -1L) 392 stdin_stream = ProcessBuilder.NullOutputStream.INSTANCE; 393 else { 394 FileDescriptor stdin_fd = new FileDescriptor(); 395 fdAccess.setHandle(stdin_fd, stdHandles[0]); 396 stdin_stream = new BufferedOutputStream( 397 new FileOutputStream(stdin_fd)); 398 } 399 400 if (stdHandles[1] == -1L) 401 stdout_stream = ProcessBuilder.NullInputStream.INSTANCE; 402 else { 403 FileDescriptor stdout_fd = new FileDescriptor(); 404 fdAccess.setHandle(stdout_fd, stdHandles[1]); 405 stdout_stream = new BufferedInputStream( 406 new FileInputStream(stdout_fd)); 407 } 408 409 if (stdHandles[2] == -1L) 410 stderr_stream = ProcessBuilder.NullInputStream.INSTANCE; 411 else { 412 FileDescriptor stderr_fd = new FileDescriptor(); 413 fdAccess.setHandle(stderr_fd, stdHandles[2]); 414 stderr_stream = new FileInputStream(stderr_fd); 415 } 416 417 return null; }}); 418 } 419 420 public OutputStream getOutputStream() { 421 return stdin_stream; 422 } 423 424 public InputStream getInputStream() { 425 return stdout_stream; 426 } 427 428 public InputStream getErrorStream() { 429 return stderr_stream; 430 } 431 432 public void finalize() { 433 closeHandle(handle); 434 } 435 436 private static final int STILL_ACTIVE = getStillActive(); 437 private static native int getStillActive(); 438 439 public int exitValue() { 440 int exitCode = getExitCodeProcess(handle); 441 if (exitCode == STILL_ACTIVE) 442 throw new IllegalThreadStateException("process has not exited"); 443 return exitCode; 444 } 445 private static native int getExitCodeProcess(long handle); 446 447 public int waitFor() throws InterruptedException { 448 waitForInterruptibly(handle); 449 if (Thread.interrupted()) 450 throw new InterruptedException(); 451 return exitValue(); 452 } 453 private static native void waitForInterruptibly(long handle); 454 455 public void destroy() { terminateProcess(handle); } 456 private static native void terminateProcess(long handle); 457 458 /** 459 * Create a process using the win32 function CreateProcess. 460 * The method is synchronized due to MS kb315939 problem. 461 * All native handles should restore the inherit flag at the end of call. 462 * 463 * @param cmdstr the Windows command line 464 * @param envblock NUL-separated, double-NUL-terminated list of 465 * environment strings in VAR=VALUE form 466 * @param dir the working directory of the process, or null if 467 * inheriting the current directory from the parent process 468 * @param stdHandles array of windows HANDLEs. Indexes 0, 1, and 469 * 2 correspond to standard input, standard output and 470 * standard error, respectively. On input, a value of -1 471 * means to create a pipe to connect child and parent 472 * processes. On output, a value which is not -1 is the 473 * parent pipe handle corresponding to the pipe which has 474 * been created. An element of this array is -1 on input 475 * if and only if it is <em>not</em> -1 on output. 476 * @param redirectErrorStream redirectErrorStream attribute 477 * @return the native subprocess HANDLE returned by CreateProcess 478 */ 479 private static synchronized native long create(String cmdstr, 480 String envblock, 481 String dir, 482 long[] stdHandles, 483 boolean redirectErrorStream) 484 throws IOException; 485 486 /** 487 * Opens a file for atomic append. The file is created if it doesn't 488 * already exist. 489 * 490 * @param file the file to open or create 491 * @return the native HANDLE 492 */ 493 private static native long openForAtomicAppend(String path) 494 throws IOException; 495 496 private static native boolean closeHandle(long handle); 497 }