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