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