Merge "Validate pending intents"

This commit is contained in:
TreeHugger Robot
2020-10-12 19:21:54 +00:00
committed by Android (Google) Code Review
3 changed files with 271 additions and 67 deletions

View File

@@ -421,6 +421,7 @@ public class NotificationManagerService extends SystemService {
private IActivityManager mAm;
private ActivityTaskManagerInternal mAtm;
private ActivityManager mActivityManager;
private ActivityManagerInternal mAmi;
private IPackageManager mPackageManager;
private PackageManager mPackageManagerClient;
AudioManager mAudioManager;
@@ -1897,7 +1898,7 @@ public class NotificationManagerService extends SystemService {
DevicePolicyManagerInternal dpm, IUriGrantsManager ugm,
UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
NotificationHistoryManager historyManager, StatsManager statsManager,
TelephonyManager telephonyManager) {
TelephonyManager telephonyManager, ActivityManagerInternal ami) {
mHandler = handler;
Resources resources = getContext().getResources();
mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -1919,6 +1920,7 @@ public class NotificationManagerService extends SystemService {
mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
mCompanionManager = companionManager;
mActivityManager = activityManager;
mAmi = ami;
mDeviceIdleManager = getContext().getSystemService(DeviceIdleManager.class);
mDpm = dpm;
mUm = userManager;
@@ -2197,7 +2199,8 @@ public class NotificationManagerService extends SystemService {
new NotificationHistoryManager(getContext(), handler),
mStatsManager = (StatsManager) getContext().getSystemService(
Context.STATS_MANAGER),
getContext().getSystemService(TelephonyManager.class));
getContext().getSystemService(TelephonyManager.class),
LocalServices.getService(ActivityManagerInternal.class));
publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -5239,8 +5242,8 @@ public class NotificationManagerService extends SystemService {
if (!summaries.containsKey(pkg)) {
// Add summary
final ApplicationInfo appInfo =
adjustedSbn.getNotification().extras.getParcelable(
Notification.EXTRA_BUILDER_APPLICATION_INFO);
adjustedSbn.getNotification().extras.getParcelable(
Notification.EXTRA_BUILDER_APPLICATION_INFO);
final Bundle extras = new Bundle();
extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
final String channelId = notificationRecord.getChannel().getId();
@@ -5276,11 +5279,11 @@ public class NotificationManagerService extends SystemService {
notificationRecord.getIsAppImportanceLocked());
summaries.put(pkg, summarySbn.getKey());
}
}
if (summaryRecord != null && checkDisqualifyingFeatures(userId, MY_UID,
summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord,
true)) {
mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord, isAppForeground));
if (summaryRecord != null && checkDisqualifyingFeatures(userId, MY_UID,
summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord,
true)) {
mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord, isAppForeground));
}
}
}
@@ -6019,13 +6022,17 @@ public class NotificationManagerService extends SystemService {
+ " cannot post for pkg " + targetPkg + " in user " + userId);
}
public boolean hasFlag(final int flags, final int flag) {
return (flags & flag) != 0;
}
/**
* Checks if a notification can be posted. checks rate limiter, snooze helper, and blocking.
*
* Has side effects.
*/
private boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,
boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,
NotificationRecord r, boolean isAutogroup) {
Notification n = r.getNotification();
final String pkg = r.getSbn().getPackageName();
final boolean isSystemNotification =
isUidSystemOrPhone(uid) || ("android".equals(pkg));
@@ -6034,71 +6041,101 @@ public class NotificationManagerService extends SystemService {
// Limit the number of notifications that any given package except the android
// package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemNotification && !isNotificationFromListener) {
synchronized (mNotificationLock) {
final int callingUid = Binder.getCallingUid();
if (mNotificationsByKey.get(r.getSbn().getKey()) == null
&& isCallerInstantApp(callingUid, userId)) {
// Ephemeral apps have some special constraints for notifications.
// They are not allowed to create new notifications however they are allowed to
// update notifications created by the system (e.g. a foreground service
// notification).
throw new SecurityException("Instant app " + pkg
+ " cannot create notifications");
}
final int callingUid = Binder.getCallingUid();
if (mNotificationsByKey.get(r.getSbn().getKey()) == null
&& isCallerInstantApp(callingUid, userId)) {
// Ephemeral apps have some special constraints for notifications.
// They are not allowed to create new notifications however they are allowed to
// update notifications created by the system (e.g. a foreground service
// notification).
throw new SecurityException("Instant app " + pkg
+ " cannot create notifications");
}
// rate limit updates that aren't completed progress notifications
if (mNotificationsByKey.get(r.getSbn().getKey()) != null
&& !r.getNotification().hasCompletedProgress()
&& !isAutogroup) {
// rate limit updates that aren't completed progress notifications
if (mNotificationsByKey.get(r.getSbn().getKey()) != null
&& !r.getNotification().hasCompletedProgress()
&& !isAutogroup) {
final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
if (appEnqueueRate > mMaxPackageEnqueueRate) {
mUsageStats.registerOverRateQuota(pkg);
final long now = SystemClock.elapsedRealtime();
if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
+ ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
mLastOverRateLogTime = now;
}
return false;
final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
if (appEnqueueRate > mMaxPackageEnqueueRate) {
mUsageStats.registerOverRateQuota(pkg);
final long now = SystemClock.elapsedRealtime();
if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
+ ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
mLastOverRateLogTime = now;
}
return false;
}
}
// limit the number of non-fgs outstanding notificationrecords an app can have
if (!r.getNotification().isForegroundService()) {
int count = getNotificationCountLocked(pkg, userId, id, tag);
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
mUsageStats.registerOverCountQuota(pkg);
Slog.e(TAG, "Package has already posted or enqueued " + count
+ " notifications. Not showing more. package=" + pkg);
return false;
}
// limit the number of non-fgs outstanding notificationrecords an app can have
if (!n.isForegroundService()) {
int count = getNotificationCountLocked(pkg, userId, id, tag);
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
mUsageStats.registerOverCountQuota(pkg);
Slog.e(TAG, "Package has already posted or enqueued " + count
+ " notifications. Not showing more. package=" + pkg);
return false;
}
}
}
synchronized (mNotificationLock) {
// snoozed apps
if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
MetricsLogger.action(r.getLogMaker()
.setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
.setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
mNotificationRecordLogger.log(
NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,
r);
if (DBG) {
Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
// bubble or inline reply that's immutable?
if (n.getBubbleMetadata() != null
&& n.getBubbleMetadata().getIntent() != null
&& hasFlag(mAmi.getPendingIntentFlags(
n.getBubbleMetadata().getIntent().getTarget()),
PendingIntent.FLAG_IMMUTABLE)) {
throw new IllegalArgumentException(r.getKey() + " Not posted."
+ " PendingIntents attached to bubbles must be mutable");
}
if (n.actions != null) {
for (Notification.Action action : n.actions) {
if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
&& hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
PendingIntent.FLAG_IMMUTABLE)) {
throw new IllegalArgumentException(r.getKey() + " Not posted."
+ " PendingIntents attached to actions with remote"
+ " inputs must be mutable");
}
mSnoozeHelper.update(userId, r);
handleSavePolicyFile();
return false;
}
}
if (r.getSystemGeneratedSmartActions() != null) {
for (Notification.Action action : r.getSystemGeneratedSmartActions()) {
if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
&& hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
PendingIntent.FLAG_IMMUTABLE)) {
throw new IllegalArgumentException(r.getKey() + " Not posted."
+ " PendingIntents attached to contextual actions with remote inputs"
+ " must be mutable");
}
}
}
// snoozed apps
if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
MetricsLogger.action(r.getLogMaker()
.setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
.setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
mNotificationRecordLogger.log(
NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,
r);
if (DBG) {
Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
}
mSnoozeHelper.update(userId, r);
handleSavePolicyFile();
return false;
}
// blocked apps
if (isBlocked(r, mUsageStats)) {
return false;
}
// blocked apps
if (isBlocked(r, mUsageStats)) {
return false;
}
return true;

View File

@@ -43,6 +43,9 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -110,6 +113,7 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IIntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
@@ -248,6 +252,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
Resources mResources;
@Mock
RankingHandler mRankingHandler;
@Mock
ActivityManagerInternal mAmi;
@Mock
IIntentSender pi1;
private static final int MAX_POST_DELAY = 1000;
@@ -392,7 +401,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
DeviceIdleInternal deviceIdleInternal = mock(DeviceIdleInternal.class);
when(deviceIdleInternal.getNotificationAllowlistDuration()).thenReturn(3000L);
ActivityManagerInternal activityManagerInternal = mock(ActivityManagerInternal.class);
LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
@@ -403,7 +411,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
LocalServices.removeServiceForTest(DeviceIdleInternal.class);
LocalServices.addService(DeviceIdleInternal.class, deviceIdleInternal);
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
LocalServices.addService(ActivityManagerInternal.class, activityManagerInternal);
LocalServices.addService(ActivityManagerInternal.class, mAmi);
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
@@ -477,7 +485,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mGroupHelper, mAm, mAtm, mAppUsageStats,
mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal,
mAppOpsManager, mUm, mHistoryManager, mStatsManager,
mock(TelephonyManager.class));
mock(TelephonyManager.class), mAmi);
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
mService.setAudioManager(mAudioManager);
@@ -674,7 +682,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon);
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.addAction(new Notification.Action.Builder(null, "test", null).build());
if (extender != null) {
nb.extend(extender);
}
@@ -810,6 +819,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
PendingIntent pendingIntent = mock(PendingIntent.class);
Intent intent = mock(Intent.class);
when(pendingIntent.getIntent()).thenReturn(intent);
when(pendingIntent.getTarget()).thenReturn(pi1);
ActivityInfo info = new ActivityInfo();
info.resizeMode = RESIZE_MODE_RESIZEABLE;
@@ -7134,4 +7144,159 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
inOrder.verify(parent).recordDismissalSentiment(anyInt());
inOrder.verify(child).recordDismissalSentiment(anyInt());
}
@Test
public void testImmutableBubbleIntent() throws Exception {
when(mAmi.getPendingIntentFlags(pi1))
.thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
NotificationRecord r = generateMessageBubbleNotifRecord(true,
mTestNotificationChannel, 7, "testImmutableBubbleIntent", null, false);
try {
mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
waitForIdle();
fail("Allowed a bubble with an immutable intent to be posted");
} catch (IllegalArgumentException e) {
// good
}
}
@Test
public void testMutableBubbleIntent() throws Exception {
when(mAmi.getPendingIntentFlags(pi1))
.thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
NotificationRecord r = generateMessageBubbleNotifRecord(true,
mTestNotificationChannel, 7, "testMutableBubbleIntent", null, false);
mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(r.getSbn().getPackageName());
assertEquals(1, notifs.length);
}
@Test
public void testImmutableDirectReplyActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
NotificationRecord r = generateMessageBubbleNotifRecord(false,
mTestNotificationChannel, 7, "testImmutableDirectReplyActionIntent", null, false);
try {
mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
waitForIdle();
fail("Allowed a direct reply with an immutable intent to be posted");
} catch (IllegalArgumentException e) {
// good
}
}
@Test
public void testMutableDirectReplyActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
NotificationRecord r = generateMessageBubbleNotifRecord(false,
mTestNotificationChannel, 7, "testMutableDirectReplyActionIntent", null, false);
mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(r.getSbn().getPackageName());
assertEquals(1, notifs.length);
}
@Test
public void testImmutableDirectReplyContextualActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
ArrayList<Notification.Action> extraAction = new ArrayList<>();
RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build();
PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply",
inputIntent).addRemoteInput(remoteInput)
.build();
extraAction.add(replyAction);
Bundle signals = new Bundle();
signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
r.getUser());
r.addAdjustment(adjustment);
r.applyAdjustments();
try {
mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
r.getSbn().getTag(), r,false);
fail("Allowed a contextual direct reply with an immutable intent to be posted");
} catch (IllegalArgumentException e) {
// good
}
}
@Test
public void testMutableDirectReplyContextualActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT);
when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
ArrayList<Notification.Action> extraAction = new ArrayList<>();
RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build();
PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply",
inputIntent).addRemoteInput(remoteInput)
.build();
extraAction.add(replyAction);
Bundle signals = new Bundle();
signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
r.getUser());
r.addAdjustment(adjustment);
r.applyAdjustments();
mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
r.getSbn().getTag(), r,false);
}
@Test
public void testImmutableActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
mBinderService.enqueueNotificationWithTag(PKG, PKG, r.getSbn().getTag(),
r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId());
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(r.getSbn().getPackageName());
assertEquals(1, notifs.length);
}
@Test
public void testImmutableContextualActionIntent() throws Exception {
when(mAmi.getPendingIntentFlags(any(IIntentSender.class)))
.thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT);
when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
ArrayList<Notification.Action> extraAction = new ArrayList<>();
extraAction.add(new Notification.Action(0, "hello", null));
Bundle signals = new Bundle();
signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction);
Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "",
r.getUser());
r.addAdjustment(adjustment);
r.applyAdjustments();
mService.checkDisqualifyingFeatures(r.getUserId(), r.getUid(), r.getSbn().getId(),
r.getSbn().getTag(), r,false);
}
}

View File

@@ -32,6 +32,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.IUriGrantsManager;
@@ -154,7 +155,8 @@ public class RoleObserverTest extends UiServiceTestCase {
mock(DevicePolicyManagerInternal.class), mock(IUriGrantsManager.class),
mock(UriGrantsManagerInternal.class),
mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class),
mock(StatsManager.class), mock(TelephonyManager.class));
mock(StatsManager.class), mock(TelephonyManager.class),
mock(ActivityManagerInternal.class));
} catch (SecurityException e) {
if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
throw e;