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 d1def7e626268..763a65f52c12c 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; @@ -1184,6 +1199,8 @@ public class ActivityOptions { mRemoteTransition = opts.getParcelable(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); @@ -1399,6 +1416,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. * @@ -1972,6 +2007,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 bab5a616a5c93..dd753579df1a8 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -88,6 +88,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; @@ -101,6 +104,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; @@ -146,6 +150,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; @@ -987,6 +998,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 { @@ -995,15 +1010,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() @@ -1242,10 +1254,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 @@ -1314,7 +1338,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) { @@ -1389,9 +1435,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 84b10d05eecfc..eb7ca95d1a083 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2077,7 +2077,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