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