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

Commit 965ff2d3 authored by Valentin Iftime's avatar Valentin Iftime
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

Change-Id: I3571fa9207b778def652130d3ca840183a9a8414
parent f8e2f788
Loading
Loading
Loading
Loading
+7 −1
Original line number Original line Diff line number Diff line
@@ -118,7 +118,10 @@ public final class SnoozeHelper {


    protected boolean canSnooze(int numberToSnooze) {
    protected boolean canSnooze(int numberToSnooze) {
        synchronized (mLock) {
        synchronized (mLock) {
            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
                    || (mPersistedSnoozedNotifications.size()
                    + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze)
                    > CONCURRENT_SNOOZE_LIMIT) {
                return false;
                return false;
            }
            }
        }
        }
@@ -357,6 +360,9 @@ public final class SnoozeHelper {


            if (groupSummaryKey != null) {
            if (groupSummaryKey != null) {
                NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
                NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
                String trimmedKey = getTrimmedString(groupSummaryKey);
                mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
                mPersistedSnoozedNotifications.remove(trimmedKey);


                if (record != null && !record.isCanceled) {
                if (record != null && !record.isCanceled) {
                    Runnable runnable = () -> {
                    Runnable runnable = () -> {
+96 −2
Original line number Original line Diff line number Diff line
@@ -19,6 +19,7 @@ import static com.android.server.notification.SnoozeHelper.CONCURRENT_SNOOZE_LIM
import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;
import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;


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

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNotNull;
@@ -73,6 +74,14 @@ import java.io.IOException;
public class SnoozeHelperTest extends UiServiceTestCase {
public class SnoozeHelperTest extends UiServiceTestCase {
    private static final String TEST_CHANNEL_ID = "test_channel_id";
    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";

    @Mock SnoozeHelper.Callback mCallback;
    @Mock SnoozeHelper.Callback mCallback;
    @Mock AlarmManager mAm;
    @Mock AlarmManager mAm;
    @Mock ManagedServices.UserProfiles mUserProfiles;
    @Mock ManagedServices.UserProfiles mUserProfiles;
@@ -315,6 +324,53 @@ public class SnoozeHelperTest extends UiServiceTestCase {
        assertFalse(mSnoozeHelper.canSnooze(1));
        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.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
    @Test
    public void testCancelByApp() throws Exception {
    public void testCancelByApp() throws Exception {
        NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
        NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
@@ -611,6 +667,7 @@ public class SnoozeHelperTest extends UiServiceTestCase {


    @Test
    @Test
    public void repostGroupSummary_repostsSummary() throws Exception {
    public void repostGroupSummary_repostsSummary() throws Exception {
        final int snoozeDuration = 1000;
        IntArray profileIds = new IntArray();
        IntArray profileIds = new IntArray();
        profileIds.add(UserHandle.USER_SYSTEM);
        profileIds.add(UserHandle.USER_SYSTEM);
        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
@@ -618,10 +675,44 @@ public class SnoozeHelperTest extends UiServiceTestCase {
                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
        NotificationRecord r2 = getNotificationRecord(
        NotificationRecord r2 = getNotificationRecord(
                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
        mSnoozeHelper.snooze(r, 1000);
        final long snoozeTime = System.currentTimeMillis() + snoozeDuration;
        mSnoozeHelper.snooze(r2, 1000);
        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().size());
        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").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());
        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());


@@ -630,6 +721,9 @@ public class SnoozeHelperTest extends UiServiceTestCase {


        assertEquals(1, mSnoozeHelper.getSnoozed().size());
        assertEquals(1, mSnoozeHelper.getSnoozed().size());
        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").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
    @Test