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
This commit is contained in:
Andrii Kulian
2022-02-08 12:45:14 -08:00
parent d21447ccd2
commit 97b59a72fa
6 changed files with 231 additions and 9 deletions

View File

@@ -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());
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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 =

View File

@@ -75,6 +75,7 @@ import android.content.pm.ActivityInfo.WindowLayout;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
import android.graphics.Rect;
import android.os.Binder;
import android.os.IBinder;
@@ -84,9 +85,11 @@ import android.platform.test.annotations.Presubmit;
import android.service.voice.IVoiceInteractionSession;
import android.util.Pair;
import android.view.Gravity;
import android.window.TaskFragmentOrganizerToken;
import androidx.test.filters.SmallTest;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
import com.android.server.wm.utils.MockTracker;
@@ -94,6 +97,10 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Tests for the {@link ActivityStarter} class.
*
@@ -1109,7 +1116,7 @@ public class ActivityStarterTests extends WindowTestsBase {
}
@Test
public void testStartActivityInner_inTaskFragment() {
public void testStartActivityInner_inTaskFragment_failsByDefault() {
final ActivityStarter starter = prepareStarter(0, false);
final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setCreateTask(true).build();
@@ -1117,6 +1124,97 @@ public class ActivityStarterTests extends WindowTestsBase {
true /* createdByOrganizer */);
sourceRecord.getTask().addChild(taskFragment, POSITION_TOP);
starter.startActivityInner(
/* r */targetRecord,
/* sourceRecord */ sourceRecord,
/* voiceSession */null,
/* voiceInteractor */ null,
/* startFlags */ 0,
/* doResume */true,
/* options */null,
/* inTask */null,
/* inTaskFragment */ taskFragment,
/* restrictedBgActivity */false,
/* intentGrants */null);
assertFalse(taskFragment.hasChild());
}
@Test
public void testStartActivityInner_inTaskFragment_allowedForSameUid() {
final ActivityStarter starter = prepareStarter(0, false);
final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setCreateTask(true).build();
final TaskFragment taskFragment = new TaskFragment(mAtm, sourceRecord.token,
true /* createdByOrganizer */);
sourceRecord.getTask().addChild(taskFragment, POSITION_TOP);
taskFragment.setTaskFragmentOrganizer(mock(TaskFragmentOrganizerToken.class),
targetRecord.getUid(), "test_process_name");
starter.startActivityInner(
/* r */targetRecord,
/* sourceRecord */ sourceRecord,
/* voiceSession */null,
/* voiceInteractor */ null,
/* startFlags */ 0,
/* doResume */true,
/* options */null,
/* inTask */null,
/* inTaskFragment */ taskFragment,
/* restrictedBgActivity */false,
/* intentGrants */null);
assertTrue(taskFragment.hasChild());
}
@Test
public void testStartActivityInner_inTaskFragment_allowedTrustedCertUid() {
final ActivityStarter starter = prepareStarter(0, false);
final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setCreateTask(true).build();
final TaskFragment taskFragment = new TaskFragment(mAtm, sourceRecord.token,
true /* createdByOrganizer */);
sourceRecord.getTask().addChild(taskFragment, POSITION_TOP);
taskFragment.setTaskFragmentOrganizer(mock(TaskFragmentOrganizerToken.class),
12345, "test_process_name");
AndroidPackage androidPackage = mock(AndroidPackage.class);
doReturn(androidPackage).when(mMockPackageManager).getPackage(eq(12345));
Set<String> certs = new HashSet(Arrays.asList("test_cert1", "test_cert1"));
targetRecord.info.setKnownActivityEmbeddingCerts(certs);
SigningDetails signingDetails = mock(SigningDetails.class);
doReturn(true).when(signingDetails).hasAncestorOrSelfWithDigest(any());
doReturn(signingDetails).when(androidPackage).getSigningDetails();
starter.startActivityInner(
/* r */targetRecord,
/* sourceRecord */ sourceRecord,
/* voiceSession */null,
/* voiceInteractor */ null,
/* startFlags */ 0,
/* doResume */true,
/* options */null,
/* inTask */null,
/* inTaskFragment */ taskFragment,
/* restrictedBgActivity */false,
/* intentGrants */null);
assertTrue(taskFragment.hasChild());
}
@Test
public void testStartActivityInner_inTaskFragment_allowedForUntrustedEmbedding() {
final ActivityStarter starter = prepareStarter(0, false);
final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build();
final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setCreateTask(true).build();
final TaskFragment taskFragment = new TaskFragment(mAtm, sourceRecord.token,
true /* createdByOrganizer */);
sourceRecord.getTask().addChild(taskFragment, POSITION_TOP);
targetRecord.info.flags |= ActivityInfo.FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
starter.startActivityInner(
/* r */targetRecord,
/* sourceRecord */ sourceRecord,