From 26cceb8762f1a46833137529219f09a802a7ab26 Mon Sep 17 00:00:00 2001 From: Louis Chang Date: Thu, 6 Jul 2023 10:36:40 +0000 Subject: [PATCH] Adding unit tests for ActivityOptions The test would fail whenever adding a new type of options in order to make a notice that if any permission should be added to prevent malicious uses. Also adding tests to verify the existing safe options. Bug: 240978635 Test: atest ActivityOptionsTest SafeActivityOptionsTest Change-Id: Ib3363d874a283674afefbfec570ecc271a64afbd --- .../server/wm/SafeActivityOptions.java | 47 +++--- .../server/wm/ActivityOptionsTest.java | 126 ++++++++++++++++ .../server/wm/SafeActivityOptionsTest.java | 134 ++++++++++++++++++ 3 files changed, 287 insertions(+), 20 deletions(-) diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java index c914fa10687f9..fe3094e2edf0e 100644 --- a/services/core/java/com/android/server/wm/SafeActivityOptions.java +++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java @@ -293,26 +293,7 @@ public class SafeActivityOptions { throw new SecurityException(msg); } // Check if the caller is allowed to launch on the specified display area. - final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); - TaskDisplayArea taskDisplayArea = daToken != null - ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; - - // If we do not have a task display area token, check if the launch task display area - // feature id is specified. - if (taskDisplayArea == null) { - final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId(); - if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) { - final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY - ? DEFAULT_DISPLAY : options.getLaunchDisplayId(); - final DisplayContent dc = supervisor.mRootWindowContainer - .getDisplayContent(launchDisplayId); - if (dc != null) { - taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda -> - tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null); - } - } - } - + final TaskDisplayArea taskDisplayArea = getLaunchTaskDisplayArea(options, supervisor); if (aInfo != null && taskDisplayArea != null && !supervisor.isCallerAllowedToLaunchOnTaskDisplayArea(callingPid, callingUid, taskDisplayArea, aInfo)) { @@ -428,6 +409,32 @@ public class SafeActivityOptions { } } + @VisibleForTesting + TaskDisplayArea getLaunchTaskDisplayArea(ActivityOptions options, + ActivityTaskSupervisor supervisor) { + final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); + TaskDisplayArea taskDisplayArea = daToken != null + ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; + if (taskDisplayArea != null) { + return taskDisplayArea; + } + + // If we do not have a task display area token, check if the launch task display area + // feature id is specified. + final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId(); + if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) { + final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY + ? DEFAULT_DISPLAY : options.getLaunchDisplayId(); + final DisplayContent dc = supervisor.mRootWindowContainer + .getDisplayContent(launchDisplayId); + if (dc != null) { + taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda -> + tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null); + } + } + return taskDisplayArea; + } + private boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) { if (atmService.mActiveVoiceInteractionServiceComponent == null) { return false; diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index fe1ea0d99eeb1..f6f3f0324f9c8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -22,11 +22,17 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; import android.app.Activity; import android.app.ActivityManager.RunningTaskInfo; @@ -41,6 +47,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.Log; import android.util.Rational; import android.view.SurfaceControl; import android.window.TaskOrganizer; @@ -48,7 +55,10 @@ import android.window.TaskOrganizer; import androidx.test.filters.MediumTest; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -166,6 +176,122 @@ public class ActivityOptionsTest { } } + /** + * Tests if any unknown key is being used in the ActivityOptions bundle. If so, please review + * if the newly added bundle should be protected with permissions to avoid malicious attacks. + * + * @see SafeActivityOptionsTest#test_getOptions + */ + @Test + public void testActivityOptionsFromBundle() { + // Spy on a bundle that is generated from a basic ActivityOptions. + final ActivityOptions options = ActivityOptions.makeBasic(); + Bundle bundle = options.toBundle(); + spyOn(bundle); + + // Create a new ActivityOptions from the bundle + new ActivityOptions(bundle); + + // Verify the keys that are being used. + final ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); + verify(bundle, atLeastOnce()).getString(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getBoolean(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getParcelable(stringCaptor.capture(), any()); + verify(bundle, atLeastOnce()).getInt(stringCaptor.capture(), anyInt()); + verify(bundle, atLeastOnce()).getBinder(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getBundle(stringCaptor.capture()); + final List keys = stringCaptor.getAllValues(); + final List unknownKeys = new ArrayList<>(); + for (String key : keys) { + switch (key) { + case ActivityOptions.KEY_PACKAGE_NAME: + case ActivityOptions.KEY_LAUNCH_BOUNDS: + case ActivityOptions.KEY_ANIM_TYPE: + case ActivityOptions.KEY_ANIM_ENTER_RES_ID: + case ActivityOptions.KEY_ANIM_EXIT_RES_ID: + case ActivityOptions.KEY_ANIM_IN_PLACE_RES_ID: + case ActivityOptions.KEY_ANIM_BACKGROUND_COLOR: + case ActivityOptions.KEY_ANIM_THUMBNAIL: + case ActivityOptions.KEY_ANIM_START_X: + case ActivityOptions.KEY_ANIM_START_Y: + case ActivityOptions.KEY_ANIM_WIDTH: + case ActivityOptions.KEY_ANIM_HEIGHT: + case ActivityOptions.KEY_ANIM_START_LISTENER: + case ActivityOptions.KEY_SPLASH_SCREEN_THEME: + case ActivityOptions.KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE: + case ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN: + case ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN: + case ActivityOptions.KEY_TRANSIENT_LAUNCH: + case "android:activity.animationFinishedListener": + // KEY_ANIMATION_FINISHED_LISTENER + case "android:activity.animSpecs": // KEY_ANIM_SPECS + case "android:activity.lockTaskMode": // KEY_LOCK_TASK_MODE + case "android:activity.shareIdentity": // KEY_SHARE_IDENTITY + case "android.activity.launchDisplayId": // KEY_LAUNCH_DISPLAY_ID + case "android.activity.callerDisplayId": // KEY_CALLER_DISPLAY_ID + case "android.activity.launchTaskDisplayAreaToken": + // KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN + case "android.activity.launchTaskDisplayAreaFeatureId": + // KEY_LAUNCH_TASK_DISPLAY_AREA_FEATURE_ID + case "android.activity.windowingMode": // KEY_LAUNCH_WINDOWING_MODE + case "android.activity.activityType": // KEY_LAUNCH_ACTIVITY_TYPE + case "android.activity.launchTaskId": // KEY_LAUNCH_TASK_ID + case "android.activity.disableStarting": // KEY_DISABLE_STARTING_WINDOW + case "android.activity.pendingIntentLaunchFlags": + // KEY_PENDING_INTENT_LAUNCH_FLAGS + case "android.activity.alwaysOnTop": // KEY_TASK_ALWAYS_ON_TOP + case "android.activity.taskOverlay": // KEY_TASK_OVERLAY + case "android.activity.taskOverlayCanResume": // KEY_TASK_OVERLAY_CAN_RESUME + case "android.activity.avoidMoveToFront": // KEY_AVOID_MOVE_TO_FRONT + case "android.activity.freezeRecentTasksReordering": + // KEY_FREEZE_RECENT_TASKS_REORDERING + case "android:activity.disallowEnterPictureInPictureWhileLaunching": + // KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING + case "android:activity.applyActivityFlagsForBubbles": + // KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES + case "android:activity.applyMultipleTaskFlagForShortcut": + // KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT + case "android:activity.applyNoUserActionFlagForShortcut": + // KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT + case "android:activity.transitionCompleteListener": + // KEY_TRANSITION_COMPLETE_LISTENER + case "android:activity.transitionIsReturning": // KEY_TRANSITION_IS_RETURNING + case "android:activity.sharedElementNames": // KEY_TRANSITION_SHARED_ELEMENTS + case "android:activity.resultData": // KEY_RESULT_DATA + case "android:activity.resultCode": // KEY_RESULT_CODE + case "android:activity.exitCoordinatorIndex": // KEY_EXIT_COORDINATOR_INDEX + case "android.activity.sourceInfo": // KEY_SOURCE_INFO + case "android:activity.usageTimeReport": // KEY_USAGE_TIME_REPORT + case "android:activity.rotationAnimationHint": // KEY_ROTATION_ANIMATION_HINT + case "android:instantapps.installerbundle": // KEY_INSTANT_APP_VERIFICATION_BUNDLE + case "android:activity.specsFuture": // KEY_SPECS_FUTURE + case "android:activity.remoteAnimationAdapter": // KEY_REMOTE_ANIMATION_ADAPTER + case "android:activity.remoteTransition": // KEY_REMOTE_TRANSITION + case "android:activity.overrideTaskTransition": // KEY_OVERRIDE_TASK_TRANSITION + case "android.activity.removeWithTaskOrganizer": // KEY_REMOVE_WITH_TASK_ORGANIZER + case "android.activity.launchTypeBubble": // KEY_LAUNCHED_FROM_BUBBLE + case "android.activity.splashScreenStyle": // KEY_SPLASH_SCREEN_STYLE + case "android.activity.launchIntoPipParams": // KEY_LAUNCH_INTO_PIP_PARAMS + case "android.activity.dismissKeyguard": // KEY_DISMISS_KEYGUARD + case "android.activity.pendingIntentCreatorBackgroundActivityStartMode": + // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE + case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE + // Existing keys + break; + default: + unknownKeys.add(key); + break; + } + } + + // Report if any unknown key exists. + for (String key : unknownKeys) { + Log.e("ActivityOptionsTests", "Unknown key " + key + " is found. " + + "Please review if the given bundle should be protected with permissions."); + } + assertTrue(unknownKeys.isEmpty()); + } + public static class TrampolineActivity extends Activity { static int sTaskId; diff --git a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java index 24e932f36f803..6c48a6961bc29 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java @@ -16,17 +16,36 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import android.app.ActivityOptions; +import android.content.pm.ActivityInfo; +import android.os.Looper; import android.platform.test.annotations.Presubmit; +import android.view.RemoteAnimationAdapter; +import android.window.RemoteTransition; import android.window.WindowContainerToken; import androidx.test.filters.MediumTest; import org.junit.Test; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; /** * Build/Install/Run: @@ -73,4 +92,119 @@ public class SafeActivityOptionsTest { assertSame(clone.getOriginalOptions().getLaunchRootTask(), token); } + + @Test + public void test_getOptions() { + // Mock everything necessary + MockitoSession mockingSession = mockitoSession() + .mockStatic(ActivityTaskManagerService.class) + .strictness(Strictness.LENIENT) + .startMocking(); + doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission( + any(), anyInt(), anyInt())); + + final LockTaskController lockTaskController = mock(LockTaskController.class); + doReturn(false).when(lockTaskController).isPackageAllowlisted(anyInt(), any()); + + final ActivityTaskManagerService atm = mock(ActivityTaskManagerService.class); + doReturn(lockTaskController).when(atm).getLockTaskController(); + + final ActivityTaskSupervisor taskSupervisor = + new ActivityTaskSupervisor(atm, mock(Looper.class)); + spyOn(taskSupervisor); + doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnDisplay(anyInt(), + anyInt(), anyInt(), any()); + doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnTaskDisplayArea(anyInt(), + anyInt(), any(), any()); + + taskSupervisor.mRecentTasks = mock(RecentTasks.class); + doReturn(false).when(taskSupervisor.mRecentTasks).isCallerRecents(anyInt()); + + // Ensure exceptions are thrown when lack of permissions. + ActivityOptions activityOptions = ActivityOptions.makeBasic(); + try { + activityOptions.setLaunchTaskId(100); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setDisableStartingWindow(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setTransientLaunch(); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setDismissKeyguard(); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchActivityType(ACTIVITY_TYPE_STANDARD); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchedFromBubble(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchDisplayId(DEFAULT_DISPLAY); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLockTaskEnabled(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeCustomTaskAnimation( + getInstrumentation().getContext(), 0, 0, null, null, null); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + RemoteAnimationAdapter remoteAnimationAdapter = mock(RemoteAnimationAdapter.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter, + remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setRemoteAnimationAdapter(remoteAnimationAdapter); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeRemoteTransition(remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setRemoteTransition(remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + verifySecureExceptionThrown(activityOptions, taskSupervisor, + mock(TaskDisplayArea.class)); + } finally { + mockingSession.finishMocking(); + } + } + + private void verifySecureExceptionThrown(ActivityOptions activityOptions, + ActivityTaskSupervisor taskSupervisor) { + verifySecureExceptionThrown(activityOptions, taskSupervisor, null /* mockTda */); + } + + private void verifySecureExceptionThrown(ActivityOptions activityOptions, + ActivityTaskSupervisor taskSupervisor, TaskDisplayArea mockTda) { + SafeActivityOptions safeActivityOptions = new SafeActivityOptions(activityOptions); + if (mockTda != null) { + spyOn(safeActivityOptions); + doReturn(mockTda).when(safeActivityOptions).getLaunchTaskDisplayArea(any(), any()); + } + + boolean isExceptionThrow = false; + final ActivityInfo aInfo = mock(ActivityInfo.class); + try { + safeActivityOptions.getOptions(null, aInfo, null, taskSupervisor); + } catch (SecurityException ex) { + isExceptionThrow = true; + } + assertTrue(isExceptionThrow); + } }