ChildProcessLauncher.java revision d3868032626d59662ff73b372b5d584c1d144c53
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 mSandboxedChildConnectionAllocator =
120            new ChildConnectionAllocator(true);
121    private static final ChildConnectionAllocator mPrivilegedChildConnectionAllocator =
122            new ChildConnectionAllocator(false);
123
124    private static boolean mConnectionAllocated = 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 !mConnectionAllocated;
132        mSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass);
133        mPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass);
134    }
135
136    private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) {
137        return inSandbox ?
138                mSandboxedChildConnectionAllocator : mPrivilegedChildConnectionAllocator;
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        mConnectionAllocated = 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> mServiceMap =
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    static ChildProcessConnection mSpareSandboxedConnection = 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 = mServiceMap.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 (mSpareSandboxedConnection == null) {
212                mSpareSandboxedConnection = 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 = mSpareSandboxedConnection;
276                mSpareSandboxedConnection = 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                    mServiceMap.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 = mServiceMap.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 = mServiceMap.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 = mServiceMap.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 = mServiceMap.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    public 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