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