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