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

Commit eb7f3a60 authored by Valentin Iftime's avatar Valentin Iftime Committed by Android Build Coastguard Worker
Browse files

Enforce persisted snoozed notifications limits

 Prevent DoS attack that causes boot-looping by serializing a huge amount of snoozed notifications:
  - Check snooze limits for persisted notifications
  - Remove persisted group summary notification when in-memory counterpart is removed
  - Prevent unpriviledged API calls that allow 3P apps to snooze notifications with context/criterion

Test: atest SnoozeHelperTest
Test: atest NotificationManagerServiceTest
Bug: 307948424
Bug: 308414141

(cherry picked from commit 965ff2d3)
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:ade22bfdf6698cb97b4edc303e8952d6cc1a2f73)
Merged-In: I3571fa9207b778def652130d3ca840183a9a8414
Change-Id: I3571fa9207b778def652130d3ca840183a9a8414
parent 32792059
Loading
Loading
Loading
Loading
+22 −1
Original line number Diff line number Diff line
@@ -142,13 +142,29 @@ public class SnoozeHelper {

    protected boolean canSnooze(int numberToSnooze) {
        synchronized (mLock) {
            if ((mPackages.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
            if ((mPackages.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
                || (countPersistedNotificationsLocked() + numberToSnooze)
                > CONCURRENT_SNOOZE_LIMIT) {
                return false;
            }
        }
        return true;
    }

    private int countPersistedNotificationsLocked() {
        int numNotifications = 0;
        for (ArrayMap<String, String> persistedWithContext :
                mPersistedSnoozedNotificationsWithContext.values()) {
            numNotifications += persistedWithContext.size();
        }
        for (ArrayMap<String, Long> persistedWithDuration :
                mPersistedSnoozedNotifications.values()) {
            numNotifications += persistedWithDuration.size();
        }
        return numNotifications;
    }


    @NonNull
    protected Long getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key) {
        Long time = null;
@@ -451,6 +467,11 @@ public class SnoozeHelper {
                mPackages.remove(groupSummaryKey);
                mUsers.remove(groupSummaryKey);

                final String trimmedKey = getTrimmedString(groupSummaryKey);
                removeRecordLocked(pkg, trimmedKey, userId, mPersistedSnoozedNotifications);
                removeRecordLocked(pkg, trimmedKey, userId,
                      mPersistedSnoozedNotificationsWithContext);

                if (record != null && !record.isCanceled) {
                    Runnable runnable = () -> {
                        MetricsLogger.action(record.getLogMaker()
+102 −2
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.server.notification;
import static com.android.server.notification.SnoozeHelper.CONCURRENT_SNOOZE_LIMIT;
import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;

import static com.google.common.truth.Truth.assertThat;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
@@ -75,6 +77,16 @@ import java.io.IOException;
public class SnoozeHelperTest extends UiServiceTestCase {
    private static final String TEST_CHANNEL_ID = "test_channel_id";

    private static final String XML_TAG_NAME = "snoozed-notifications";
    private static final String XML_SNOOZED_NOTIFICATION = "notification";
    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
    private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
    private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
    private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
    private static final String XML_SNOOZED_NOTIFICATION_PKG = "pkg";
    private static final String XML_SNOOZED_NOTIFICATION_USER_ID = "user-id";

    @Mock SnoozeHelper.Callback mCallback;
    @Mock AlarmManager mAm;
    @Mock ManagedServices.UserProfiles mUserProfiles;
@@ -328,6 +340,56 @@ public class SnoozeHelperTest extends UiServiceTestCase {
        assertFalse(mSnoozeHelper.canSnooze(1));
    }

    @Test
    public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException {
        final long snoozeTimeout = 1234;
        final String snoozeContext = "ctx";
        // Serialize & deserialize notifications so that only persisted lists are used
        TypedXmlSerializer serializer = Xml.newFastSerializer();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
        serializer.startDocument(null, true);
        serializer.startTag(null, XML_TAG_NAME);
        // Serialize maximum number of timed + context snoozed notifications, half of each
        for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) {
            final boolean timedNotification = i % 2 == 0;
            if (timedNotification) {
                serializer.startTag(null, XML_SNOOZED_NOTIFICATION);
            } else {
                serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
            }
            serializer.attribute(null, XML_SNOOZED_NOTIFICATION_PKG, "pkg");
            serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_USER_ID,
                UserHandle.USER_SYSTEM);
            serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1);
            serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i);
            if (timedNotification) {
                serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout);
                serializer.endTag(null, XML_SNOOZED_NOTIFICATION);
            } else {
                serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext);
                serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
            }
        }
        serializer.endTag(null, XML_TAG_NAME);
        serializer.endDocument();
        serializer.flush();

        TypedXmlPullParser parser = Xml.newFastPullParser();
        parser.setInput(new BufferedInputStream(
                new ByteArrayInputStream(baos.toByteArray())), "utf-8");
        mSnoozeHelper.readXml(parser, 1);
        // Verify that we can't snooze any more notifications
        //  and that the limit is caused by persisted notifications
        assertThat(mSnoozeHelper.canSnooze(1)).isFalse();
        assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse();
        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM,
                "pkg", "key0")).isEqualTo(snoozeTimeout);
        assertThat(
            mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
                "key1")).isEqualTo(snoozeContext);
    }

    @Test
    public void testCancelByApp() throws Exception {
        NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
@@ -601,6 +663,7 @@ public class SnoozeHelperTest extends UiServiceTestCase {

    @Test
    public void repostGroupSummary_repostsSummary() throws Exception {
        final int snoozeDuration = 1000;
        IntArray profileIds = new IntArray();
        profileIds.add(UserHandle.USER_SYSTEM);
        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
@@ -608,10 +671,44 @@ public class SnoozeHelperTest extends UiServiceTestCase {
                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
        NotificationRecord r2 = getNotificationRecord(
                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
        mSnoozeHelper.snooze(r, 1000);
        mSnoozeHelper.snooze(r2, 1000);
        final long snoozeTime = System.currentTimeMillis() + snoozeDuration;
        mSnoozeHelper.snooze(r, snoozeDuration);
        mSnoozeHelper.snooze(r2, snoozeDuration);
        assertEquals(2, mSnoozeHelper.getSnoozed().size());
        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
        // Verify that summary notification was added to the persisted list
        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
                r.getKey())).isAtLeast(snoozeTime);

        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());

        verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false);
        verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false);

        assertEquals(1, mSnoozeHelper.getSnoozed().size());
        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
        // Verify that summary notification was removed from the persisted list
        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
                r.getKey())).isEqualTo(0);
    }

    @Test
    public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception {
        final String snoozeContext = "zzzzz";
        IntArray profileIds = new IntArray();
        profileIds.add(UserHandle.USER_SYSTEM);
        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
        NotificationRecord r = getNotificationRecord(
                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
        NotificationRecord r2 = getNotificationRecord(
                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
        mSnoozeHelper.snooze(r, snoozeContext);
        mSnoozeHelper.snooze(r2, snoozeContext);
        assertEquals(2, mSnoozeHelper.getSnoozed().size());
        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
        // Verify that summary notification was added to the persisted list
        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
            "pkg", r.getKey())).isEqualTo(snoozeContext);

        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());

@@ -620,6 +717,9 @@ public class SnoozeHelperTest extends UiServiceTestCase {

        assertEquals(1, mSnoozeHelper.getSnoozed().size());
        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
        // Verify that summary notification was removed from the persisted list
        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
                "pkg", r.getKey())).isNull();
    }

    @Test