1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser; 6 7import android.content.Context; 8import android.util.Log; 9import android.util.SparseIntArray; 10import android.view.Surface; 11 12import java.util.ArrayList; 13import java.util.Map; 14import java.util.concurrent.ConcurrentHashMap; 15 16import org.chromium.base.CalledByNative; 17import org.chromium.base.JNINamespace; 18import org.chromium.base.ThreadUtils; 19import org.chromium.content.app.ChildProcessService; 20import org.chromium.content.app.PrivilegedProcessService; 21import org.chromium.content.app.SandboxedProcessService; 22import org.chromium.content.common.IChildProcessCallback; 23import org.chromium.content.common.IChildProcessService; 24 25/** 26 * This class provides the method to start/stop ChildProcess called by native. 27 */ 28@JNINamespace("content") 29public class ChildProcessLauncher { 30 private static String TAG = "ChildProcessLauncher"; 31 32 private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0; 33 private static final int CALLBACK_FOR_GPU_PROCESS = 1; 34 private static final int CALLBACK_FOR_RENDERER_PROCESS = 2; 35 36 private static final String SWITCH_PROCESS_TYPE = "type"; 37 private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker"; 38 private static final String SWITCH_RENDERER_PROCESS = "renderer"; 39 private static final String SWITCH_GPU_PROCESS = "gpu-process"; 40 41 // The upper limit on the number of simultaneous sandboxed and privileged child service process 42 // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX 43 // classes and PrivilegedProcessServiceX classes declared in this package and defined as 44 // services in the embedding application's manifest file. 45 // (See {@link ChildProcessService} for more details on defining the services.) 46 /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13; 47 /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3; 48 49 private static class ChildConnectionAllocator { 50 // Connections to services. Indices of the array correspond to the service numbers. 51 private ChildProcessConnection[] mChildProcessConnections; 52 53 // The list of free (not bound) service indices. When looking for a free service, the first 54 // index in that list should be used. When a service is unbound, its index is added to the 55 // end of the list. This is so that we avoid immediately reusing the freed service (see 56 // http://crbug.com/164069): the framework might keep a service process alive when it's been 57 // unbound for a short time. If a new connection to the same service is bound at that point, 58 // the process is reused and bad things happen (mostly static variables are set when we 59 // don't expect them to). 60 // SHOULD BE ACCESSED WITH mConnectionLock. 61 private ArrayList<Integer> mFreeConnectionIndices; 62 private final Object mConnectionLock = new Object(); 63 64 private Class<? extends ChildProcessService> mChildClass; 65 private final boolean mInSandbox; 66 67 public ChildConnectionAllocator(boolean inSandbox) { 68 int numChildServices = inSandbox ? 69 MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES; 70 mChildProcessConnections = new ChildProcessConnection[numChildServices]; 71 mFreeConnectionIndices = new ArrayList<Integer>(numChildServices); 72 for (int i = 0; i < numChildServices; i++) { 73 mFreeConnectionIndices.add(i); 74 } 75 setServiceClass(inSandbox ? 76 SandboxedProcessService.class : PrivilegedProcessService.class); 77 mInSandbox = inSandbox; 78 } 79 80 public void setServiceClass(Class<? extends ChildProcessService> childClass) { 81 mChildClass = childClass; 82 } 83 84 public ChildProcessConnection allocate( 85 Context context, ChildProcessConnection.DeathCallback deathCallback) { 86 synchronized(mConnectionLock) { 87 if (mFreeConnectionIndices.isEmpty()) { 88 Log.w(TAG, "Ran out of service." ); 89 return null; 90 } 91 int slot = mFreeConnectionIndices.remove(0); 92 assert mChildProcessConnections[slot] == null; 93 mChildProcessConnections[slot] = new ChildProcessConnection(context, slot, 94 mInSandbox, deathCallback, mChildClass); 95 return mChildProcessConnections[slot]; 96 } 97 } 98 99 public void free(ChildProcessConnection connection) { 100 synchronized(mConnectionLock) { 101 int slot = connection.getServiceNumber(); 102 if (mChildProcessConnections[slot] != connection) { 103 int occupier = mChildProcessConnections[slot] == null ? 104 -1 : mChildProcessConnections[slot].getServiceNumber(); 105 Log.e(TAG, "Unable to find connection to free in slot: " + slot + 106 " already occupied by service: " + occupier); 107 assert false; 108 } else { 109 mChildProcessConnections[slot] = null; 110 assert !mFreeConnectionIndices.contains(slot); 111 mFreeConnectionIndices.add(slot); 112 } 113 } 114 } 115 } 116 117 // Service class for child process. As the default value it uses SandboxedProcessService0 and 118 // PrivilegedProcessService0. 119 private static final ChildConnectionAllocator sSandboxedChildConnectionAllocator = 120 new ChildConnectionAllocator(true); 121 private static final ChildConnectionAllocator sPrivilegedChildConnectionAllocator = 122 new ChildConnectionAllocator(false); 123 124 private static boolean sConnectionAllocated = false; 125 126 // Sets service class for sandboxed service and privileged service. 127 public static void setChildProcessClass( 128 Class<? extends SandboxedProcessService> sandboxedServiceClass, 129 Class<? extends PrivilegedProcessService> privilegedServiceClass) { 130 // We should guarantee this is called before allocating connection. 131 assert !sConnectionAllocated; 132 sSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass); 133 sPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass); 134 } 135 136 private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) { 137 return inSandbox ? 138 sSandboxedChildConnectionAllocator : sPrivilegedChildConnectionAllocator; 139 } 140 141 private static ChildProcessConnection allocateConnection(Context context, 142 boolean inSandbox) { 143 ChildProcessConnection.DeathCallback deathCallback = 144 new ChildProcessConnection.DeathCallback() { 145 @Override 146 public void onChildProcessDied(int pid) { 147 stop(pid); 148 } 149 }; 150 sConnectionAllocated = true; 151 return getConnectionAllocator(inSandbox).allocate(context, deathCallback); 152 } 153 154 private static ChildProcessConnection allocateBoundConnection(Context context, 155 String[] commandLine, boolean inSandbox) { 156 ChildProcessConnection connection = allocateConnection(context, inSandbox); 157 if (connection != null) { 158 connection.start(commandLine); 159 } 160 return connection; 161 } 162 163 private static void freeConnection(ChildProcessConnection connection) { 164 if (connection == null) { 165 return; 166 } 167 getConnectionAllocator(connection.isInSandbox()).free(connection); 168 return; 169 } 170 171 // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. 172 private static final int NULL_PROCESS_HANDLE = 0; 173 174 // Map from pid to ChildService connection. 175 private static Map<Integer, ChildProcessConnection> sServiceMap = 176 new ConcurrentHashMap<Integer, ChildProcessConnection>(); 177 178 // Map from pid to the count of oom bindings. "Oom binding" is a binding that raises the process 179 // oom priority so that it shouldn't be killed by the OS out-of-memory killer under normal 180 // conditions (it can still be killed under drastic memory pressure). 181 private static SparseIntArray sOomBindingCount = new SparseIntArray(); 182 183 // A pre-allocated and pre-bound connection ready for connection setup, or null. 184 private static ChildProcessConnection sSpareSandboxedConnection = null; 185 186 /** 187 * Returns the child process service interface for the given pid. This may be called on 188 * any thread, but the caller must assume that the service can disconnect at any time. All 189 * service calls should catch and handle android.os.RemoteException. 190 * 191 * @param pid The pid (process handle) of the service obtained from {@link #start}. 192 * @return The IChildProcessService or null if the service no longer exists. 193 */ 194 public static IChildProcessService getChildService(int pid) { 195 ChildProcessConnection connection = sServiceMap.get(pid); 196 if (connection != null) { 197 return connection.getService(); 198 } 199 return null; 200 } 201 202 /** 203 * Should be called early in startup so the work needed to spawn the child process can be done 204 * in parallel to other startup work. Must not be called on the UI thread. Spare connection is 205 * created in sandboxed child process. 206 * @param context the application context used for the connection. 207 */ 208 public static void warmUp(Context context) { 209 synchronized (ChildProcessLauncher.class) { 210 assert !ThreadUtils.runningOnUiThread(); 211 if (sSpareSandboxedConnection == null) { 212 sSpareSandboxedConnection = allocateBoundConnection(context, null, true); 213 } 214 } 215 } 216 217 private static String getSwitchValue(final String[] commandLine, String switchKey) { 218 if (commandLine == null || switchKey == null) { 219 return null; 220 } 221 // This format should be matched with the one defined in command_line.h. 222 final String switchKeyPrefix = "--" + switchKey + "="; 223 for (String command : commandLine) { 224 if (command != null && command.startsWith(switchKeyPrefix)) { 225 return command.substring(switchKeyPrefix.length()); 226 } 227 } 228 return null; 229 } 230 231 /** 232 * Spawns and connects to a child process. May be called on any thread. It will not block, but 233 * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is 234 * established. Note this callback will not necessarily be from the same thread (currently it 235 * always comes from the main thread). 236 * 237 * @param context Context used to obtain the application context. 238 * @param commandLine The child process command line argv. 239 * @param file_ids The ID that should be used when mapping files in the created process. 240 * @param file_fds The file descriptors that should be mapped in the created process. 241 * @param file_auto_close Whether the file descriptors should be closed once they were passed to 242 * the created process. 243 * @param clientContext Arbitrary parameter used by the client to distinguish this connection. 244 */ 245 @CalledByNative 246 static void start( 247 Context context, 248 final String[] commandLine, 249 int[] fileIds, 250 int[] fileFds, 251 boolean[] fileAutoClose, 252 final int clientContext) { 253 assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length; 254 FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length]; 255 for (int i = 0; i < fileFds.length; i++) { 256 filesToBeMapped[i] = 257 new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]); 258 } 259 assert clientContext != 0; 260 261 int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS; 262 boolean inSandbox = true; 263 String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE); 264 if (SWITCH_RENDERER_PROCESS.equals(processType)) { 265 callbackType = CALLBACK_FOR_RENDERER_PROCESS; 266 } else if (SWITCH_GPU_PROCESS.equals(processType)) { 267 callbackType = CALLBACK_FOR_GPU_PROCESS; 268 } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) { 269 inSandbox = false; 270 } 271 272 ChildProcessConnection allocatedConnection = null; 273 synchronized (ChildProcessLauncher.class) { 274 if (inSandbox) { 275 allocatedConnection = sSpareSandboxedConnection; 276 sSpareSandboxedConnection = null; 277 } 278 } 279 if (allocatedConnection == null) { 280 allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox); 281 if (allocatedConnection == null) { 282 // Notify the native code so it can free the heap allocated callback. 283 nativeOnChildProcessStarted(clientContext, 0); 284 return; 285 } 286 } 287 final ChildProcessConnection connection = allocatedConnection; 288 Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber()); 289 290 ChildProcessConnection.ConnectionCallbacks connectionCallbacks = 291 new ChildProcessConnection.ConnectionCallbacks() { 292 public void onConnected(int pid, int oomBindingCount) { 293 Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext); 294 if (pid != NULL_PROCESS_HANDLE) { 295 sOomBindingCount.put(pid, oomBindingCount); 296 sServiceMap.put(pid, connection); 297 } else { 298 freeConnection(connection); 299 } 300 nativeOnChildProcessStarted(clientContext, pid); 301 } 302 303 public void onOomBindingAdded(int pid) { 304 if (pid != NULL_PROCESS_HANDLE) { 305 sOomBindingCount.put(pid, sOomBindingCount.get(pid) + 1); 306 } 307 } 308 309 public void onOomBindingRemoved(int pid) { 310 if (pid != NULL_PROCESS_HANDLE) { 311 int count = sOomBindingCount.get(pid, -1); 312 assert count > 0; 313 count--; 314 if (count > 0) { 315 sOomBindingCount.put(pid, count); 316 } else { 317 sOomBindingCount.delete(pid); 318 } 319 } 320 } 321 }; 322 323 // TODO(sievers): Revisit this as it doesn't correctly handle the utility process 324 // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS; 325 326 connection.setupConnection(commandLine, filesToBeMapped, createCallback(callbackType), 327 connectionCallbacks); 328 } 329 330 /** 331 * Terminates a child process. This may be called from any thread. 332 * 333 * @param pid The pid (process handle) of the service connection obtained from {@link #start}. 334 */ 335 @CalledByNative 336 static void stop(int pid) { 337 Log.d(TAG, "stopping child connection: pid=" + pid); 338 ChildProcessConnection connection = sServiceMap.remove(pid); 339 if (connection == null) { 340 LogPidWarning(pid, "Tried to stop non-existent connection"); 341 return; 342 } 343 connection.stop(); 344 freeConnection(connection); 345 } 346 347 /** 348 * Remove the initial child process binding. Child processes are bound with initial binding to 349 * protect them from getting killed before they are put to use. This method allows to remove the 350 * binding once it is no longer needed. 351 */ 352 static void removeInitialBinding(int pid) { 353 ChildProcessConnection connection = sServiceMap.get(pid); 354 if (connection == null) { 355 LogPidWarning(pid, "Tried to remove a binding for a non-existent connection"); 356 return; 357 } 358 connection.removeInitialBinding(); 359 } 360 361 /** 362 * Bind a child process as a high priority process so that it has the same priority as the main 363 * process. This can be used for the foreground renderer process to distinguish it from the the 364 * background renderer process. 365 * 366 * @param pid The process handle of the service connection obtained from {@link #start}. 367 */ 368 static void bindAsHighPriority(int pid) { 369 ChildProcessConnection connection = sServiceMap.get(pid); 370 if (connection == null) { 371 LogPidWarning(pid, "Tried to bind a non-existent connection"); 372 return; 373 } 374 connection.attachAsActive(); 375 } 376 377 /** 378 * Unbind a high priority process which is bound by {@link #bindAsHighPriority}. 379 * 380 * @param pid The process handle of the service obtained from {@link #start}. 381 */ 382 static void unbindAsHighPriority(int pid) { 383 ChildProcessConnection connection = sServiceMap.get(pid); 384 if (connection == null) { 385 LogPidWarning(pid, "Tried to unbind non-existent connection"); 386 return; 387 } 388 connection.detachAsActive(); 389 } 390 391 /** 392 * @return True iff the given service process is protected from the out-of-memory killing, or it 393 * was protected from it when it died. 394 */ 395 static boolean isOomProtected(int pid) { 396 return sOomBindingCount.get(pid) > 0; 397 } 398 399 /** 400 * This implementation is used to receive callbacks from the remote service. 401 */ 402 private static IChildProcessCallback createCallback(final int callbackType) { 403 return new IChildProcessCallback.Stub() { 404 /** 405 * This is called by the remote service regularly to tell us about new values. Note that 406 * IPC calls are dispatched through a thread pool running in each process, so the code 407 * executing here will NOT be running in our main thread -- so, to update the UI, we 408 * need to use a Handler. 409 */ 410 @Override 411 public void establishSurfacePeer( 412 int pid, Surface surface, int primaryID, int secondaryID) { 413 // Do not allow a malicious renderer to connect to a producer. This is only used 414 // from stream textures managed by the GPU process. 415 if (callbackType != CALLBACK_FOR_GPU_PROCESS) { 416 Log.e(TAG, "Illegal callback for non-GPU process."); 417 return; 418 } 419 420 nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID); 421 } 422 423 @Override 424 public Surface getViewSurface(int surfaceId) { 425 // Do not allow a malicious renderer to get to our view surface. 426 if (callbackType != CALLBACK_FOR_GPU_PROCESS) { 427 Log.e(TAG, "Illegal callback for non-GPU process."); 428 return null; 429 } 430 431 return nativeGetViewSurface(surfaceId); 432 } 433 }; 434 }; 435 436 private static void LogPidWarning(int pid, String message) { 437 // This class is effectively a no-op in single process mode, so don't log warnings there. 438 if (pid > 0 && !nativeIsSingleProcess()) { 439 Log.w(TAG, message + ", pid=" + pid); 440 } 441 } 442 443 private static native void nativeOnChildProcessStarted(int clientContext, int pid); 444 private static native Surface nativeGetViewSurface(int surfaceId); 445 private static native void nativeEstablishSurfacePeer( 446 int pid, Surface surface, int primaryID, int secondaryID); 447 private static native boolean nativeIsSingleProcess(); 448} 449