From f1f080fd362d03a5996290fb91155522b5d44914 Mon Sep 17 00:00:00 2001 From: Ricky Wai Date: Tue, 31 Aug 2021 15:59:07 +0000 Subject: [PATCH] Add hidden API to allow PendingIntent sender to allow/block BAL For example, AlarmManagerService uses it to block all PendingIntent BAL, so no apps can use alarm manager api to start activity from background. The current default behavior is allowing PendingIntent to use caller's foreground visibility to determine if BAL can be launched. We may want to change this behavior becomes disallow by default, but it may need androidx changes as SliceView is using PendingIntent.send() to launch apps and it should allow apps to start BAL activity using Slice API. For legacy app (targetSDK < T), even the flag is allowed, it won't use caller's BAL permission to bypass BAL checking, to match the original behavior. Next step will be removing @hide and formalize the api. Bug: 192341120 Test: atest BackgroundActivityLaunchTest Test: Test AlarmManager API, PendingIntent won't cause BAL. Change-Id: I89588473f0f028078adb9230caacb18ed4d6c504 --- .../server/alarm/AlarmManagerService.java | 29 ++++++++- core/java/android/app/ActivityOptions.java | 36 +++++++++++ core/java/android/app/BroadcastOptions.java | 35 ++++++++++ .../android/server/wm/ActivityStarter.java | 62 +++++++++++++++--- .../server/wm/ActivityTaskManagerService.java | 2 +- .../com/android/server/wm/AppTaskImpl.java | 2 +- .../server/alarm/AlarmManagerServiceTest.java | 64 ++++++++++++++++--- 7 files changed, 209 insertions(+), 21 deletions(-) diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 95728081bdb97..2b56e4f7d0158 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -56,6 +56,7 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; import android.app.AlarmManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; @@ -307,6 +308,7 @@ public class AlarmManagerService extends SystemService { BroadcastOptions mOptsWithFgs = BroadcastOptions.makeBasic(); BroadcastOptions mOptsWithoutFgs = BroadcastOptions.makeBasic(); BroadcastOptions mOptsTimeBroadcast = BroadcastOptions.makeBasic(); + ActivityOptions mActivityOptsRestrictBal = ActivityOptions.makeBasic(); // TODO(b/172085676): Move inside alarm store. private final SparseArray mNextAlarmClockForUser = @@ -1610,6 +1612,10 @@ public class AlarmManagerService extends SystemService { @Override public void onStart() { mInjector.init(); + mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); + mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); + mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false); + mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); mMetricsHelper = new MetricsHelper(getContext(), mLock); mListenerDeathRecipient = new IBinder.DeathRecipient() { @@ -4339,7 +4345,16 @@ public class AlarmManagerService extends SystemService { for (int i = 0; i < triggerList.size(); i++) { Alarm alarm = triggerList.get(i); try { - alarm.operation.send(); + // Disallow AlarmManager to start random background activity. + final Bundle bundle; + if (alarm.operation.isActivity()) { + bundle = mActivityOptsRestrictBal.toBundle(); + } else { + bundle = null; + } + alarm.operation.send(/* context */ null, /* code */0, /* intent */ + null, /* onFinished */null, /* handler */ + null, /* requiredPermission */ null, bundle); } catch (PendingIntent.CanceledException e) { if (alarm.repeatInterval > 0) { // This IntentSender is no longer valid, but this @@ -4901,9 +4916,19 @@ public class AlarmManagerService extends SystemService { mSendCount++; try { + final Bundle bundle; + if (alarm.mIdleOptions != null) { + bundle = alarm.mIdleOptions; + } else { + if (alarm.operation.isActivity()) { + bundle = mActivityOptsRestrictBal.toBundle(); + } else { + bundle = null; + } + } alarm.operation.send(getContext(), 0, mBackgroundIntent.putExtra(Intent.EXTRA_ALARM_COUNT, alarm.count), - mDeliveryTracker, mHandler, null, alarm.mIdleOptions); + mDeliveryTracker, mHandler, null, bundle); } catch (PendingIntent.CanceledException e) { if (alarm.repeatInterval > 0) { // This IntentSender is no longer valid, but this diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 76f873185267a..4079135c4d566 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -166,6 +166,14 @@ public class ActivityOptions { */ public static final String KEY_SPLASH_SCREEN_THEME = "android.activity.splashScreenTheme"; + /** + * PendingIntent caller allows activity start even if PendingIntent creator is in background. + * This only works if the PendingIntent caller is allowed to start background activities, + * for example if it's in the foreground, or has BAL permission. + * @hide + */ + public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED = + "android.pendingIntent.backgroundActivityAllowed"; /** * Callback for when the last frame of the animation is played. * @hide @@ -380,6 +388,12 @@ public class ActivityOptions { /** @hide */ public static final int ANIM_REMOTE_ANIMATION = 13; + /** + * Default value for KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED. + * @hide + **/ + public static final boolean PENDING_INTENT_BAL_ALLOWED_DEFAULT = true; + private String mPackageName; private Rect mLaunchBounds; private int mAnimationType = ANIM_UNDEFINED; @@ -431,6 +445,7 @@ public class ActivityOptions { private String mSplashScreenThemeResName; @SplashScreen.SplashScreenStyle private int mSplashScreenStyle; + private boolean mPendingIntentBalAllowed = PENDING_INTENT_BAL_ALLOWED_DEFAULT; private boolean mRemoveWithTaskOrganizer; private boolean mLaunchedFromBubble; private boolean mTransientLaunch; @@ -1185,6 +1200,8 @@ public class ActivityOptions { KEY_REMOTE_TRANSITION)); mOverrideTaskTransition = opts.getBoolean(KEY_OVERRIDE_TASK_TRANSITION); mSplashScreenThemeResName = opts.getString(KEY_SPLASH_SCREEN_THEME); + mPendingIntentBalAllowed = opts.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + PENDING_INTENT_BAL_ALLOWED_DEFAULT); mRemoveWithTaskOrganizer = opts.getBoolean(KEY_REMOVE_WITH_TASK_ORGANIZER); mLaunchedFromBubble = opts.getBoolean(KEY_LAUNCHED_FROM_BUBBLE); mTransientLaunch = opts.getBoolean(KEY_TRANSIENT_LAUNCH); @@ -1400,6 +1417,24 @@ public class ActivityOptions { return mSplashScreenStyle; } + /** + * Set PendingIntent activity is allowed to be started in the background if the caller + * can start background activities. + * @hide + */ + public void setPendingIntentBackgroundActivityLaunchAllowed(boolean allowed) { + mPendingIntentBalAllowed = allowed; + } + + /** + * Get PendingIntent activity is allowed to be started in the background if the caller + * can start background activities. + * @hide + */ + public boolean isPendingIntentBackgroundActivityLaunchAllowed() { + return mPendingIntentBalAllowed; + } + /** * Sets whether the activity is to be launched into LockTask mode. * @@ -1973,6 +2008,7 @@ public class ActivityOptions { if (mSplashScreenThemeResName != null && !mSplashScreenThemeResName.isEmpty()) { b.putString(KEY_SPLASH_SCREEN_THEME, mSplashScreenThemeResName); } + b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); if (mRemoveWithTaskOrganizer) { b.putBoolean(KEY_REMOVE_WITH_TASK_ORGANIZER, mRemoveWithTaskOrganizer); } diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index bd7162c1bf3b6..4e19abfa0d5bc 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -43,6 +43,7 @@ public class BroadcastOptions { private int mMaxManifestReceiverApiLevel = Build.VERSION_CODES.CUR_DEVELOPMENT; private boolean mDontSendToRestrictedApps = false; private boolean mAllowBackgroundActivityStarts; + private boolean mPendingIntentBalAllowed = ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT; /** * How long to temporarily put an app on the power allowlist when executing this broadcast @@ -78,6 +79,16 @@ public class BroadcastOptions { private static final String KEY_DONT_SEND_TO_RESTRICTED_APPS = "android:broadcast.dontSendToRestrictedApps"; + /** + * PendingIntent caller allows activity start even if PendingIntent creator is in background. + * This only works if the PendingIntent caller is allowed to start background activities, + * for example if it's in the foreground, or has BAL permission. + * TODO: Merge it with ActivityOptions. + * @hide + */ + public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED = + "android.pendingIntent.backgroundActivityAllowed"; + /** * Corresponds to {@link #setBackgroundActivityStartsAllowed}. */ @@ -130,6 +141,8 @@ public class BroadcastOptions { mDontSendToRestrictedApps = opts.getBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, false); mAllowBackgroundActivityStarts = opts.getBoolean(KEY_ALLOW_BACKGROUND_ACTIVITY_STARTS, false); + mPendingIntentBalAllowed = opts.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT); } /** @@ -300,6 +313,26 @@ public class BroadcastOptions { return mAllowBackgroundActivityStarts; } + /** + * Set PendingIntent activity is allowed to be started in the background if the caller + * can start background activities. + * TODO: Merge it with ActivityOptions. + * @hide + */ + public void setPendingIntentBackgroundActivityLaunchAllowed(boolean allowed) { + mPendingIntentBalAllowed = allowed; + } + + /** + * Get PendingIntent activity is allowed to be started in the background if the caller + * can start background activities. + * TODO: Merge it with ActivityOptions. + * @hide + */ + public boolean isPendingIntentBackgroundActivityLaunchAllowed() { + return mPendingIntentBalAllowed; + } + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) @@ -328,6 +361,8 @@ public class BroadcastOptions { if (mAllowBackgroundActivityStarts) { b.putBoolean(KEY_ALLOW_BACKGROUND_ACTIVITY_STARTS, true); } + // TODO: Add API for BroadcastOptions and have a shared base class with ActivityOptions. + b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); return b.isEmpty() ? null : b; } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f6757f599d7c8..37c77f7e4b9e2 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -89,6 +89,9 @@ import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.ProfilerInfo; import android.app.WaitResult; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.IIntentSender; import android.content.Intent; @@ -102,6 +105,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Process; @@ -147,6 +151,13 @@ class ActivityStarter { private static final String TAG_USER_LEAVING = TAG + POSTFIX_USER_LEAVING; private static final int INVALID_LAUNCH_MODE = -1; + /** + * Feature flag to protect PendingIntent being abused to start background activity. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + static final long ENABLE_PENDING_INTENT_BAL_OPTION = 192341120L; + private final ActivityTaskManagerService mService; private final RootWindowContainer mRootWindowContainer; private final ActivityTaskSupervisor mSupervisor; @@ -988,6 +999,10 @@ class ActivityStarter { abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid, callingPackage); + // Merge the two options bundles, while realCallerOptions takes precedence. + ActivityOptions checkedOptions = options != null + ? options.getOptions(intent, aInfo, callerApp, mSupervisor) : null; + boolean restrictedBgActivity = false; if (!abort) { try { @@ -996,15 +1011,12 @@ class ActivityStarter { restrictedBgActivity = shouldAbortBackgroundActivityStart(callingUid, callingPid, callingPackage, realCallingUid, realCallingPid, callerApp, request.originatingPendingIntent, request.allowBackgroundActivityStart, - intent); + intent, checkedOptions); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - // Merge the two options bundles, while realCallerOptions takes precedence. - ActivityOptions checkedOptions = options != null - ? options.getOptions(intent, aInfo, callerApp, mSupervisor) : null; if (request.allowPendingRemoteAnimationRegistryLookup) { checkedOptions = mService.getActivityStartController() .getPendingRemoteAnimationRegistry() @@ -1243,10 +1255,22 @@ class ActivityStarter { return activity != null && packageName.equals(activity.getPackageName()); } + private static boolean isPendingIntentBalAllowedByCaller(ActivityOptions activityOptions) { + if (activityOptions == null) { + return ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT; + } + final Bundle options = activityOptions.toBundle(); + if (options == null) { + return ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT; + } + return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT); + } + boolean shouldAbortBackgroundActivityStart(int callingUid, int callingPid, final String callingPackage, int realCallingUid, int realCallingPid, WindowProcessController callerApp, PendingIntentRecord originatingPendingIntent, - boolean allowBackgroundActivityStart, Intent intent) { + boolean allowBackgroundActivityStart, Intent intent, ActivityOptions checkedOptions) { // don't abort for the most important UIDs final int callingAppId = UserHandle.getAppId(callingUid); if (callingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID @@ -1315,7 +1339,29 @@ class ActivityStarter { ? isCallingUidPersistentSystemProcess : (realCallingAppId == Process.SYSTEM_UID) || realCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; - if (realCallingUid != callingUid) { + + // If caller a legacy app, we won't check if caller has BAL permission. + final boolean isPiBalOptionEnabled = CompatChanges.isChangeEnabled( + ENABLE_PENDING_INTENT_BAL_OPTION, callingUid); + + // Legacy behavior allows to use caller foreground state to bypass BAL restriction. + final boolean balAllowedByPiSender = + isPendingIntentBalAllowedByCaller(checkedOptions); + + if (balAllowedByPiSender && realCallingUid != callingUid) { + if (isPiBalOptionEnabled) { + if (ActivityManager.checkComponentPermission( + android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, + realCallingUid, -1, true) + == PackageManager.PERMISSION_GRANTED) { + if (DEBUG_ACTIVITY_STARTS) { + Slog.d(TAG, "Activity start allowed: realCallingUid (" + realCallingUid + + ") has BAL permission."); + } + return false; + } + } + // don't abort if the realCallingUid has a visible window // TODO(b/171459802): We should check appSwitchAllowed also if (realCallingUidHasAnyVisibleWindow) { @@ -1390,9 +1436,9 @@ class ActivityStarter { // If we don't have callerApp at this point, no caller was provided to startActivity(). // That's the case for PendingIntent-based starts, since the creator's process might not be // up and alive. If that's the case, we retrieve the WindowProcessController for the send() - // caller, so that we can make the decision based on its state. + // caller if caller allows, so that we can make the decision based on its state. int callerAppUid = callingUid; - if (callerApp == null) { + if (callerApp == null && balAllowedByPiSender) { callerApp = mService.getProcessController(realCallingPid, realCallingUid); callerAppUid = realCallingUid; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 19cf14f70e0a3..30b543183f638 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2068,7 +2068,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final ActivityStarter starter = getActivityStartController().obtainStarter( null /* intent */, "moveTaskToFront"); if (starter.shouldAbortBackgroundActivityStart(callingUid, callingPid, callingPackage, -1, - -1, callerApp, null, false, null)) { + -1, callerApp, null, false, null, null)) { if (!isBackgroundActivityStartsEnabled()) { return; } diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index 7f0adcacc9513..81de82c85cb11 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -108,7 +108,7 @@ class AppTaskImpl extends IAppTask.Stub { final ActivityStarter starter = mService.getActivityStartController().obtainStarter( null /* intent */, "moveToFront"); if (starter.shouldAbortBackgroundActivityStart(callingUid, callingPid, - callingPackage, -1, -1, callerApp, null, false, null)) { + callingPackage, -1, -1, callerApp, null, false, null, null)) { if (!mService.isBackgroundActivityStartsEnabled()) { return; } diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 16afef57d31e8..6d72d52b26661 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -109,6 +109,7 @@ import static org.mockito.Mockito.times; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; import android.app.AlarmManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; @@ -550,15 +551,24 @@ public class AlarmManagerServiceTest { FLAG_STANDALONE, null, null, TEST_CALLING_UID, TEST_CALLING_PACKAGE, null, 0); } - private PendingIntent getNewMockPendingIntent() { - return getNewMockPendingIntent(TEST_CALLING_UID, TEST_CALLING_PACKAGE); + return getNewMockPendingIntent(false); + } + + private PendingIntent getNewMockPendingIntent(boolean isActivity) { + return getNewMockPendingIntent(TEST_CALLING_UID, TEST_CALLING_PACKAGE, isActivity); } private PendingIntent getNewMockPendingIntent(int creatorUid, String creatorPackage) { + return getNewMockPendingIntent(creatorUid, creatorPackage, false); + } + + private PendingIntent getNewMockPendingIntent(int creatorUid, String creatorPackage, + boolean isActivity) { final PendingIntent mockPi = mock(PendingIntent.class, Answers.RETURNS_DEEP_STUBS); when(mockPi.getCreatorUid()).thenReturn(creatorUid); when(mockPi.getCreatorPackage()).thenReturn(creatorPackage); + when(mockPi.isActivity()).thenReturn(isActivity); return mockPi; } @@ -2801,21 +2811,57 @@ public class AlarmManagerServiceTest { anyString())); } - @Test - public void idleOptionsSentOnExpiration() throws Exception { + private void optionsSentOnExpiration(boolean isActivity, Bundle idleOptions) + throws Exception { final long triggerTime = mNowElapsedTest + 5000; - final PendingIntent alarmPi = getNewMockPendingIntent(); - final Bundle idleOptions = new Bundle(); - idleOptions.putChar("TEST_CHAR_KEY", 'x'); - idleOptions.putInt("TEST_INT_KEY", 53); + final PendingIntent alarmPi = getNewMockPendingIntent(isActivity); setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, 0, alarmPi, 0, 0, TEST_CALLING_UID, idleOptions); mNowElapsedTest = mTestTimer.getElapsed(); mTestTimer.expire(); + ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), - any(), any(Handler.class), isNull(), eq(idleOptions)); + any(), any(Handler.class), isNull(), bundleCaptor.capture()); + if (idleOptions != null) { + assertEquals(idleOptions, bundleCaptor.getValue()); + } else { + if (isActivity) { + assertFalse("BAL flag needs to be false in alarm manager", + bundleCaptor.getValue().getBoolean( + ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, + true)); + } else { + assertNull(bundleCaptor.getValue()); + } + } + } + + @Test + public void activityIdleOptionsSentOnExpiration() throws Exception { + final Bundle idleOptions = new Bundle(); + idleOptions.putChar("TEST_CHAR_KEY", 'x'); + idleOptions.putInt("TEST_INT_KEY", 53); + optionsSentOnExpiration(true, idleOptions); + } + + @Test + public void broadcastIdleOptionsSentOnExpiration() throws Exception { + final Bundle idleOptions = new Bundle(); + idleOptions.putChar("TEST_CHAR_KEY", 'x'); + idleOptions.putInt("TEST_INT_KEY", 53); + optionsSentOnExpiration(false, idleOptions); + } + + @Test + public void emptyActivityOptionsSentOnExpiration() throws Exception { + optionsSentOnExpiration(true, null); + } + + @Test + public void emptyBroadcastOptionsSentOnExpiration() throws Exception { + optionsSentOnExpiration(false, null); } @Test