Merge "Add hidden API to allow PendingIntent sender to allow/block BAL"

This commit is contained in:
Ricky Wai
2021-09-23 15:17:36 +00:00
committed by Android (Google) Code Review
7 changed files with 209 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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