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