Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 97b59a72 authored by Andrii Kulian's avatar Andrii Kulian
Browse files

Verify cross-uid activity embedding

By default activities cannot be embedded by non-privileged
processes. Apps can either declare their activities to be available
for untrusted embedding, or limit it to certain trusted hosts.

This CL verifies that an application cannot embed activities that
didn't opt in, and verifies that in untrusted mode the host cannot
request SurfaceControl transactions or position the embedded
container outside of the task bounds.

Bug: 197364677
Test: ActivityEmbeddingCrossUidTests, TaskFragmentOrganizerTest
Change-Id: Ief54c9acae1a9a4152af710f67e7810afdcff9ba
parent d21447cc
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -924,6 +924,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     * Checks if an activity is embedded and its presentation is customized by a
     * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds.
     */
    @Override
    public boolean isActivityEmbedded(@NonNull Activity activity) {
        return mPresenter.isActivityEmbedded(activity.getActivityToken());
    }
+3 −4
Original line number Diff line number Diff line
@@ -2003,8 +2003,8 @@ class ActivityStarter {
     * @param targetTask the target task for launching activity, which could be different from
     *                   the one who hosting the embedding.
     */
    private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, ActivityRecord starting,
            boolean newTask, Task targetTask) {
    private boolean canEmbedActivity(@NonNull TaskFragment taskFragment,
            @NonNull ActivityRecord starting, boolean newTask, Task targetTask) {
        final Task hostTask = taskFragment.getTask();
        if (hostTask == null) {
            return false;
@@ -2016,8 +2016,7 @@ class ActivityStarter {
            return true;
        }

        // Not allowed embedding an activity of another app.
        if (hostUid != starting.getUid()) {
        if (!taskFragment.isAllowedToEmbedActivity(starting)) {
            return false;
        }

+53 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.pm.ActivityInfo.FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
import static android.content.pm.ActivityInfo.FLAG_RESUME_WHILE_PAUSING;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
@@ -95,6 +96,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
import com.android.server.pm.parsing.pkg.AndroidPackage;

import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -102,6 +104,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

@@ -504,6 +507,56 @@ class TaskFragment extends WindowContainer<WindowContainer> {
        return false;
    }

    /**
     * Checks if the organized task fragment is allowed to have the specified activity, which is
     * allowed if an activity allows embedding in untrusted mode, or if the trusted mode can be
     * enabled.
     * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord)
     */
    boolean isAllowedToEmbedActivity(@NonNull ActivityRecord a) {
        if ((a.info.flags & FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING)
                == FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING) {
            return true;
        }

        return isAllowedToEmbedActivityInTrustedMode(a);
    }

    /**
     * Checks if the organized task fragment is allowed to embed activity in fully trusted mode,
     * which means that all transactions are allowed. This is supported in the following cases:
     * <li>the activity belongs to the same app as the organizer host;</li>
     * <li>the activity has declared the organizer host as trusted explicitly via known
     * certificate.</li>
     */
    private boolean isAllowedToEmbedActivityInTrustedMode(@NonNull ActivityRecord a) {
        if (mTaskFragmentOrganizerUid == a.getUid()) {
            // Activities from the same UID can be embedded freely by the host.
            return true;
        }

        Set<String> knownActivityEmbeddingCerts = a.info.getKnownActivityEmbeddingCerts();
        if (knownActivityEmbeddingCerts.isEmpty()) {
            // An application must either declare that it allows untrusted embedding, or specify
            // a set of app certificates that are allowed to embed it in trusted mode.
            return false;
        }

        AndroidPackage hostPackage = mAtmService.getPackageManagerInternalLocked()
                .getPackage(mTaskFragmentOrganizerUid);

        return hostPackage != null && hostPackage.getSigningDetails().hasAncestorOrSelfWithDigest(
                knownActivityEmbeddingCerts);
    }

    /**
     * Checks if all activities in the task fragment are allowed to be embedded in trusted mode.
     * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord)
     */
    boolean isAllowedToBeEmbeddedInTrustedMode() {
        return forAllActivities(this::isAllowedToEmbedActivityInTrustedMode);
    }

    /**
     * Returns the TaskFragment that is being organized, which could be this or the ascendant
     * TaskFragment.
+16 −2
Original line number Diff line number Diff line
@@ -294,14 +294,28 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
        }
    }

    /** Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. */
    /**
     * Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns
     * {@code null} if it doesn't, or if the organizer has activity(ies) embedded in untrusted mode.
     */
    @Nullable
    public RemoteAnimationDefinition getRemoteAnimationDefinition(
            ITaskFragmentOrganizer organizer) {
        synchronized (mGlobalLock) {
            final TaskFragmentOrganizerState organizerState =
                    mTaskFragmentOrganizerState.get(organizer.asBinder());
            return organizerState != null ? organizerState.mRemoteAnimationDefinition : null;
            if (organizerState == null) {
                return null;
            }
            for (TaskFragment tf : organizerState.mOrganizedTaskFragments) {
                if (!tf.isAllowedToBeEmbeddedInTrustedMode()) {
                    // Disable client-driven animations for organizer if at least one of the
                    // embedded task fragments is not embedding in trusted mode.
                    // TODO(b/197364677): replace with a stub or Shell-driven one instead of skip?
                    return null;
                }
            }
            return organizerState.mRemoteAnimationDefinition;
        }
    }

+59 −2
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.server.wm;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
import static android.app.ActivityManager.isStartResultSuccessful;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.WindowContainerTransaction.Change.CHANGE_BOUNDS_TRANSACTION;
import static android.window.WindowContainerTransaction.Change.CHANGE_BOUNDS_TRANSACTION_RECT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
@@ -1224,10 +1226,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
        while (entries.hasNext()) {
            final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next();
            // Only allow to apply changes to TaskFragment that is created by this organizer.
            enforceTaskFragmentOrganized(func, WindowContainer.fromBinder(entry.getKey()),
                    organizer);
            WindowContainer wc = WindowContainer.fromBinder(entry.getKey());
            enforceTaskFragmentOrganized(func, wc, organizer);
            enforceTaskFragmentConfigChangeAllowed(func, wc, entry.getValue(), organizer);
        }

        // TODO(b/197364677): Enforce safety of hierarchy operations in untrusted mode. E.g. one
        // could first change a trusted TF, and then start/reparent untrusted activity there.

        // Hierarchy changes
        final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
        for (int i = hops.size() - 1; i >= 0; i--) {
@@ -1297,6 +1303,57 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
        }
    }

    /**
     * Makes sure that SurfaceControl transactions and the ability to set bounds outside of the
     * parent bounds are not allowed for embedding without full trust between the host and the
     * target.
     * TODO(b/197364677): Allow SC transactions when the client-driven animations are protected from
     * tapjacking.
     */
    private void enforceTaskFragmentConfigChangeAllowed(String func, @Nullable WindowContainer wc,
            WindowContainerTransaction.Change change, ITaskFragmentOrganizer organizer) {
        if (wc == null) {
            Slog.e(TAG, "Attempt to operate on task fragment that no longer exists");
            return;
        }
        // Check if TaskFragment is embedded in fully trusted mode
        if (wc.asTaskFragment().isAllowedToBeEmbeddedInTrustedMode()) {
            // Fully trusted, no need to check further
            return;
        }

        if (change == null) {
            return;
        }
        final int changeMask = change.getChangeMask();
        if ((changeMask & (CHANGE_BOUNDS_TRANSACTION | CHANGE_BOUNDS_TRANSACTION_RECT)) != 0) {
            String msg = "Permission Denial: " + func + " from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
                    + " trying to apply SurfaceControl changes to TaskFragment in non-trusted "
                    + "embedding mode, TaskFragmentOrganizer=" + organizer;
            Slog.w(TAG, msg);
            throw new SecurityException(msg);
        }
        if (change.getWindowSetMask() == 0) {
            // Nothing else to check.
            return;
        }
        WindowConfiguration requestedWindowConfig = change.getConfiguration().windowConfiguration;
        WindowContainer wcParent = wc.getParent();
        if (wcParent == null) {
            Slog.e(TAG, "Attempt to set bounds on task fragment that has no parent");
            return;
        }
        if (!wcParent.getBounds().contains(requestedWindowConfig.getBounds())) {
            String msg = "Permission Denial: " + func + " from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
                    + " trying to apply bounds outside of parent for non-trusted host,"
                    + " TaskFragmentOrganizer=" + organizer;
            Slog.w(TAG, msg);
            throw new SecurityException(msg);
        }
    }

    void createTaskFragment(@NonNull TaskFragmentCreationParams creationParams,
            @Nullable IBinder errorCallbackToken, @NonNull CallerInfo caller) {
        final ActivityRecord ownerActivity =
Loading