Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 37880517 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Validate pending intents

PendingIntents used for Bubbles or Direct Reply must
be mutable.

Test: atest
Fixes: 169690799
Change-Id: Ibc87c959a7fb02e5e0a6b937be36ee797506d3c8
parent e57d36d4
Loading
Loading
Loading
Loading
+100 −63
Original line number Diff line number Diff line
@@ -411,6 +411,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;
@@ -1887,7 +1888,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(),
@@ -1909,6 +1910,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;
@@ -2187,7 +2189,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);
@@ -5266,13 +5269,13 @@ 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));
            }
        }
    }

    private String disableNotificationEffects(NotificationRecord record) {
        if (mDisableNotificationEffects) {
@@ -6009,13 +6012,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));
@@ -6024,7 +6031,6 @@ 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)) {
@@ -6055,7 +6061,7 @@ public class NotificationManagerService extends SystemService {
            }

            // limit the number of non-fgs outstanding notificationrecords an app can have
                if (!r.getNotification().isForegroundService()) {
            if (!n.isForegroundService()) {
                int count = getNotificationCountLocked(pkg, userId, id, tag);
                if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                    mUsageStats.registerOverCountQuota(pkg);
@@ -6065,9 +6071,41 @@ public class NotificationManagerService extends SystemService {
                }
            }
        }

        // 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");
                }
            }
        }

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

        synchronized (mNotificationLock) {
        // snoozed apps
        if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
            MetricsLogger.action(r.getLogMaker()
@@ -6089,7 +6127,6 @@ public class NotificationManagerService extends SystemService {
        if (isBlocked(r, mUsageStats)) {
            return false;
        }
        }

        return true;
    }
+169 −4
Original line number Diff line number Diff line
@@ -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);
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -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;