Skip to content

Commit 3f228cb

Browse files
Matías Hernándezaoleary
Matías Hernández
authored andcommitted
Fix allowlist token issues
1) Don't accept enqueued notifications with an unexpected token. 2) Ensure allowlist token matches for all parceled and unparceled notifications (by only using the "root notification" one). 3) Simplify cookie usage in allowlist token serialization. 4) Ensure group summary (and any notifications added directly by NMS) have the correct token. Bug: 328254922 Bug: 305695605 Test: atest NotificationManagerServiceTest ParcelTest CloseSystemDialogsTest + manually (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:8a7453435b633282a9445ee01a902f090adc138c) Merged-In: I232e9b74eece745560ed2e762071b48984b3f176 Change-Id: I232e9b74eece745560ed2e762071b48984b3f176
1 parent 93225ba commit 3f228cb

File tree

5 files changed

+474
-17
lines changed

5 files changed

+474
-17
lines changed

core/java/android/app/Notification.java

+37-11
Original file line numberDiff line numberDiff line change
@@ -2565,8 +2565,11 @@ private void readFromParcelImpl(Parcel parcel)
25652565
if (mAllowlistToken == null) {
25662566
mAllowlistToken = processAllowlistToken;
25672567
}
2568-
// Propagate this token to all pending intents that are unmarshalled from the parcel.
2569-
parcel.setClassCookie(PendingIntent.class, mAllowlistToken);
2568+
// Propagate this token to all pending intents that are unmarshalled from the parcel,
2569+
// or keep the one we're already propagating, if that's the case.
2570+
if (!parcel.hasClassCookie(PendingIntent.class)) {
2571+
parcel.setClassCookie(PendingIntent.class, mAllowlistToken);
2572+
}
25702573

25712574
when = parcel.readLong();
25722575
creationTime = parcel.readLong();
@@ -3027,9 +3030,24 @@ public void writeToParcel(Parcel parcel, int flags) {
30273030
});
30283031
}
30293032
try {
3030-
// IMPORTANT: Add marshaling code in writeToParcelImpl as we
3031-
// want to intercept all pending events written to the parcel.
3032-
writeToParcelImpl(parcel, flags);
3033+
boolean mustClearCookie = false;
3034+
if (!parcel.hasClassCookie(Notification.class)) {
3035+
// This is the "root" notification, and not an "inner" notification (including
3036+
// publicVersion or anything else that might be embedded in extras). So we want
3037+
// to use its token for every inner notification (might be null).
3038+
parcel.setClassCookie(Notification.class, mAllowlistToken);
3039+
mustClearCookie = true;
3040+
}
3041+
try {
3042+
// IMPORTANT: Add marshaling code in writeToParcelImpl as we
3043+
// want to intercept all pending events written to the parcel.
3044+
writeToParcelImpl(parcel, flags);
3045+
} finally {
3046+
if (mustClearCookie) {
3047+
parcel.removeClassCookie(Notification.class, mAllowlistToken);
3048+
}
3049+
}
3050+
30333051
synchronized (this) {
30343052
// Must be written last!
30353053
parcel.writeArraySet(allPendingIntents);
@@ -3044,7 +3062,10 @@ public void writeToParcel(Parcel parcel, int flags) {
30443062
private void writeToParcelImpl(Parcel parcel, int flags) {
30453063
parcel.writeInt(1);
30463064

3047-
parcel.writeStrongBinder(mAllowlistToken);
3065+
// Always use the same token as the root notification (might be null).
3066+
IBinder rootNotificationToken = (IBinder) parcel.getClassCookie(Notification.class);
3067+
parcel.writeStrongBinder(rootNotificationToken);
3068+
30483069
parcel.writeLong(when);
30493070
parcel.writeLong(creationTime);
30503071
if (mSmallIcon == null && icon != 0) {
@@ -3400,18 +3421,23 @@ public void setLatestEventInfo(Context context,
34003421
* Sets the token used for background operations for the pending intents associated with this
34013422
* notification.
34023423
*
3403-
* This token is automatically set during deserialization for you, you usually won't need to
3404-
* call this unless you want to change the existing token, if any.
3424+
* Note: Should <em>only</em> be invoked by NotificationManagerService, since this is normally
3425+
* populated by unparceling (and also used there). Any other usage is suspect.
34053426
*
34063427
* @hide
34073428
*/
3408-
public void clearAllowlistToken() {
3409-
mAllowlistToken = null;
3429+
public void overrideAllowlistToken(IBinder token) {
3430+
mAllowlistToken = token;
34103431
if (publicVersion != null) {
3411-
publicVersion.clearAllowlistToken();
3432+
publicVersion.overrideAllowlistToken(token);
34123433
}
34133434
}
34143435

3436+
/** @hide */
3437+
public IBinder getAllowlistToken() {
3438+
return mAllowlistToken;
3439+
}
3440+
34153441
/**
34163442
* @hide
34173443
*/

core/java/android/os/Parcel.java

+22
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,28 @@ public final Object getClassCookie(Class clz) {
782782
return mClassCookies != null ? mClassCookies.get(clz) : null;
783783
}
784784

785+
/** @hide */
786+
public void removeClassCookie(Class clz, Object expectedCookie) {
787+
if (mClassCookies != null) {
788+
Object removedCookie = mClassCookies.remove(clz);
789+
if (removedCookie != expectedCookie) {
790+
Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz
791+
+ ") but instead removed " + removedCookie);
792+
}
793+
} else {
794+
Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz
795+
+ ") but no cookies were present");
796+
}
797+
}
798+
799+
/**
800+
* Whether {@link #setClassCookie} has been called with the specified {@code clz}.
801+
* @hide
802+
*/
803+
public boolean hasClassCookie(Class clz) {
804+
return mClassCookies != null && mClassCookies.containsKey(clz);
805+
}
806+
785807
/** @hide */
786808
public final void adoptClassCookies(Parcel from) {
787809
mClassCookies = from.mClassCookies;

core/tests/coretests/src/android/os/ParcelTest.java

+49
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@
1616

1717
package android.os;
1818

19+
import static com.google.common.truth.Truth.assertThat;
20+
1921
import static org.junit.Assert.assertEquals;
2022
import static org.junit.Assert.assertFalse;
2123
import static org.junit.Assert.assertThrows;
2224
import static org.junit.Assert.assertTrue;
2325

2426
import android.platform.test.annotations.Presubmit;
27+
import android.util.Log;
2528

2629
import androidx.test.runner.AndroidJUnit4;
2730

2831
import org.junit.Test;
2932
import org.junit.runner.RunWith;
3033

34+
import java.util.ArrayList;
35+
3136
@Presubmit
3237
@RunWith(AndroidJUnit4.class)
3338
public class ParcelTest {
@@ -239,4 +244,48 @@ public void testCompareDataInRange_whenNegativeOffset_throws() {
239244
assertThrows(IllegalArgumentException.class, () -> Parcel.compareData(pA, -1, pB, iB, 0));
240245
assertThrows(IllegalArgumentException.class, () -> Parcel.compareData(pA, 0, pB, -1, 0));
241246
}
247+
248+
@Test
249+
public void testClassCookies() {
250+
Parcel p = Parcel.obtain();
251+
assertThat(p.hasClassCookie(ParcelTest.class)).isFalse();
252+
253+
p.setClassCookie(ParcelTest.class, "string_cookie");
254+
assertThat(p.hasClassCookie(ParcelTest.class)).isTrue();
255+
assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo("string_cookie");
256+
257+
p.removeClassCookie(ParcelTest.class, "string_cookie");
258+
assertThat(p.hasClassCookie(ParcelTest.class)).isFalse();
259+
assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo(null);
260+
261+
p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie");
262+
p.recycle();
263+
assertThat(p.getClassCookie(ParcelTest.class)).isNull();
264+
}
265+
266+
@Test
267+
public void testClassCookies_removeUnexpected() {
268+
Parcel p = Parcel.obtain();
269+
270+
assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "not_present"));
271+
272+
p.setClassCookie(ParcelTest.class, "value");
273+
274+
assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "different"));
275+
assertThat(p.getClassCookie(ParcelTest.class)).isNull(); // still removed
276+
277+
p.recycle();
278+
}
279+
280+
private static void assertLogsWtf(Runnable test) {
281+
ArrayList<Log.TerribleFailure> wtfs = new ArrayList<>();
282+
Log.TerribleFailureHandler oldHandler = Log.setWtfHandler(
283+
(tag, what, system) -> wtfs.add(what));
284+
try {
285+
test.run();
286+
} finally {
287+
Log.setWtfHandler(oldHandler);
288+
}
289+
assertThat(wtfs).hasSize(1);
290+
}
242291
}

services/core/java/com/android/server/notification/NotificationManagerService.java

100755100644
+16-2
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ public class NotificationManagerService extends SystemService {
643643

644644
private static final int MY_UID = Process.myUid();
645645
private static final int MY_PID = Process.myPid();
646-
private static final IBinder ALLOWLIST_TOKEN = new Binder();
646+
static final IBinder ALLOWLIST_TOKEN = new Binder();
647647
protected RankingHandler mRankingHandler;
648648
private long mLastOverRateLogTime;
649649
private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
@@ -4398,7 +4398,7 @@ private StatusBarNotification sanitizeSbn(String pkg, int userId,
43984398
// Remove background token before returning notification to untrusted app, this
43994399
// ensures the app isn't able to perform background operations that are
44004400
// associated with notification interactions.
4401-
notification.clearAllowlistToken();
4401+
notification.overrideAllowlistToken(null);
44024402
return new StatusBarNotification(
44034403
sbn.getPackageName(),
44044404
sbn.getOpPkg(),
@@ -6560,6 +6560,15 @@ void enqueueNotificationInternal(final String pkg, final String opPkg, final int
65606560
+ " trying to post for invalid pkg " + pkg + " in user " + incomingUserId);
65616561
}
65626562

6563+
IBinder allowlistToken = notification.getAllowlistToken();
6564+
if (allowlistToken != null && allowlistToken != ALLOWLIST_TOKEN) {
6565+
throw new SecurityException(
6566+
"Unexpected allowlist token received from " + callingUid);
6567+
}
6568+
// allowlistToken is populated by unparceling, so it can be null if the notification was
6569+
// posted from inside system_server. Ensure it's the expected value.
6570+
notification.overrideAllowlistToken(ALLOWLIST_TOKEN);
6571+
65636572
checkRestrictedCategories(notification);
65646573

65656574
// Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE,
@@ -7480,6 +7489,11 @@ protected class EnqueueNotificationRunnable implements Runnable {
74807489
@Override
74817490
public void run() {
74827491
synchronized (mNotificationLock) {
7492+
// allowlistToken is populated by unparceling, so it will be absent if the
7493+
// EnqueueNotificationRunnable is created directly by NMS (as we do for group
7494+
// summaries) instead of via notify(). Fix that.
7495+
r.getNotification().overrideAllowlistToken(ALLOWLIST_TOKEN);
7496+
74837497
final Long snoozeAt =
74847498
mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
74857499
r.getUser().getIdentifier(),

0 commit comments

Comments
 (0)