diff options
6 files changed, 285 insertions, 7 deletions
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 0668958b2d5c..50486611a5a9 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1028,4 +1028,14 @@ interface IActivityManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.DEVICE_POWER)") void noteAppRestrictionEnabled(in String packageName, int uid, int restrictionType, boolean enabled, int reason, in String subReason, int source, long threshold); + + /** + * Creates and returns a new IntentCreatorToken that keeps the creatorUid and refreshes key + * fields of the intent passed in. + * + * @param intent The intent with key fields out of sync of the IntentCreatorToken it contains. + * @hide + */ + @EnforcePermission("INTERACT_ACROSS_USERS_FULL") + IBinder refreshIntentCreatorToken(in Intent intent); } diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index cc57dc05d6b1..e271cf4f60ec 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -946,6 +946,34 @@ public class ClipData implements Parcelable { } /** + * Make a clone of ClipData that only contains URIs. This reduces the size of data transfer over + * IPC and only retains important information for the purpose of verifying creator token of an + * Intent. + * @return a copy of ClipData with only URIs remained. + * @hide + */ + public ClipData cloneOnlyUriItems() { + ArrayList<Item> items = null; + final int N = mItems.size(); + for (int i = 0; i < N; i++) { + Item item = mItems.get(i); + if (item.getUri() != null) { + if (items == null) { + items = new ArrayList<>(N); + } + items.add(new Item(item.getUri())); + } else if (item.getIntent() != null) { + if (items == null) { + items = new ArrayList<>(N); + } + items.add(new Item(item.getIntent().cloneForCreatorToken())); + } + } + if (items == null || items.isEmpty()) return null; + return new ClipData(new ClipDescription("", new String[0]), items); + } + + /** * Create a new ClipData holding data of the type * {@link ClipDescription#MIMETYPE_TEXT_PLAIN}. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 02eed1a7553f..d7660172a2f1 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -7970,6 +7970,24 @@ public class Intent implements Parcelable, Cloneable { } /** + * Make a copy of all members important to identify an intent with its creator token. + * @hide + */ + public @NonNull Intent cloneForCreatorToken() { + Intent clone = new Intent() + .setAction(this.mAction) + .setDataAndType(this.mData, this.mType) + .setPackage(this.mPackage) + .setComponent(this.mComponent) + .setFlags(this.mFlags & IMMUTABLE_FLAGS); + if (this.mClipData != null) { + clone.setClipData(this.mClipData.cloneOnlyUriItems()); + } + clone.mCreatorTokenInfo = this.mCreatorTokenInfo; + return clone; + } + + /** * Create an intent with a given action. All other fields (data, type, * class) are null. Note that the action <em>must</em> be in a * namespace because Intents are used globally in the system -- for @@ -11684,7 +11702,7 @@ public class Intent implements Parcelable, Cloneable { Log.w(TAG, "Failure filling in extras", e); } } - mCreatorTokenInfo = other.mCreatorTokenInfo; + fillInCreatorTokenInfo(other.mCreatorTokenInfo, changes); if (mayHaveCopiedUris && mContentUserHint == UserHandle.USER_CURRENT && other.mContentUserHint != UserHandle.USER_CURRENT) { mContentUserHint = other.mContentUserHint; @@ -11692,6 +11710,45 @@ public class Intent implements Parcelable, Cloneable { return changes; } + // keep original creator token and merge nested intent keys. + private void fillInCreatorTokenInfo(CreatorTokenInfo otherCreatorTokenInfo, int changes) { + if (otherCreatorTokenInfo != null && otherCreatorTokenInfo.mNestedIntentKeys != null) { + if (mCreatorTokenInfo == null) { + mCreatorTokenInfo = new CreatorTokenInfo(); + } + ArraySet<NestedIntentKey> otherNestedIntentKeys = + otherCreatorTokenInfo.mNestedIntentKeys; + if (mCreatorTokenInfo.mNestedIntentKeys == null) { + mCreatorTokenInfo.mNestedIntentKeys = new ArraySet<>(otherNestedIntentKeys); + } else { + ArraySet<NestedIntentKey> otherKeys; + if ((changes & FILL_IN_CLIP_DATA) == 0) { + // If clip data is Not filled in from other, do not merge clip data keys. + otherKeys = new ArraySet<>(); + int N = otherNestedIntentKeys.size(); + for (int i = 0; i < N; i++) { + NestedIntentKey key = otherNestedIntentKeys.valueAt(i); + if (key.mType != NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA) { + otherKeys.add(key); + } + } + } else { + // If clip data is filled in from other, remove clip data keys from this + // creatorTokenInfo and then merge every key from the others. + int N = mCreatorTokenInfo.mNestedIntentKeys.size(); + for (int i = N - 1; i >= 0; i--) { + NestedIntentKey key = mCreatorTokenInfo.mNestedIntentKeys.valueAt(i); + if (key.mType == NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA) { + mCreatorTokenInfo.mNestedIntentKeys.removeAt(i); + } + } + otherKeys = otherNestedIntentKeys; + } + mCreatorTokenInfo.mNestedIntentKeys.addAll(otherKeys); + } + } + } + /** * Merge the extras data in this intent with that of other supplied intent using the * strategy specified using {@code extrasMerger}. @@ -12228,6 +12285,7 @@ public class Intent implements Parcelable, Cloneable { private IBinder mCreatorToken; // Stores all extra keys whose values are intents for a top level intent. private ArraySet<NestedIntentKey> mNestedIntentKeys; + } /** diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java index 7bc4abd935b6..fdfb0c34cdeb 100644 --- a/core/tests/coretests/src/android/content/IntentTest.java +++ b/core/tests/coretests/src/android/content/IntentTest.java @@ -19,8 +19,9 @@ package android.content; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; -import android.net.Uri; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; @@ -29,6 +30,7 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.security.Flags; +import android.util.ArraySet; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -40,6 +42,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Build/Install/Run: @@ -51,7 +54,6 @@ import java.util.List; public class IntentTest { private static final String TEST_ACTION = "android.content.IntentTest_test"; private static final String TEST_EXTRA_NAME = "testExtraName"; - private static final Uri TEST_URI = Uri.parse("content://com.example/people"); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -129,4 +131,111 @@ public class IntentTest { } } + @Test + @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT) + public void testFillInCreatorTokenInfo() { + // case 1: intent does not have creatorTokenInfo; fillinIntent contains creatorTokenInfo + Intent intent = new Intent(); + Intent fillInIntent = new Intent(); + fillInIntent.setCreatorToken(new Binder()); + fillInIntent.putExtra("extraKey", new Intent()); + + fillInIntent.collectExtraIntentKeys(); + intent.fillIn(fillInIntent, 0); + + // extra intent keys are merged + assertThat(intent.getExtraIntentKeys()).isEqualTo(fillInIntent.getExtraIntentKeys()); + // but creator token is not overwritten. + assertThat(intent.getCreatorToken()).isNull(); + + + // case 2: Both intent and fillInIntent contains creatorToken, intent's creatorToken is not + // overwritten. + intent = new Intent(); + IBinder creatorToken = new Binder(); + intent.setCreatorToken(creatorToken); + fillInIntent = new Intent(); + fillInIntent.setCreatorToken(new Binder()); + + intent.fillIn(fillInIntent, 0); + + assertThat(intent.getCreatorToken()).isEqualTo(creatorToken); + + + // case 3: Contains duplicate extra keys + intent = new Intent(); + intent.putExtra("key1", new Intent()); + intent.putExtra("key2", new Intent()); + fillInIntent = new Intent(); + fillInIntent.putExtra("key1", new Intent()); + fillInIntent.putExtra("key3", new Intent()); + + intent.collectExtraIntentKeys(); + Set originalIntentKeys = new ArraySet<>(intent.getExtraIntentKeys()); + + fillInIntent.collectExtraIntentKeys(); + intent.fillIn(fillInIntent, 0); + + assertThat(intent.getExtraIntentKeys()).hasSize(3); + assertTrue(intent.getExtraIntentKeys().containsAll(originalIntentKeys)); + assertTrue(intent.getExtraIntentKeys().containsAll(fillInIntent.getExtraIntentKeys())); + + + // case 4: Both contains a mixture of extras and clip data. NOT force to fill in clip data. + intent = new Intent(); + ClipData clipData = ClipData.newIntent("clip", new Intent()); + clipData.addItem(new ClipData.Item(new Intent())); + intent.setClipData(clipData); + intent.putExtra("key1", new Intent()); + intent.putExtra("key2", new Intent()); + fillInIntent = new Intent(); + ClipData fillInClipData = ClipData.newIntent("clip", new Intent()); + fillInClipData.addItem(new ClipData.Item(new Intent())); + fillInClipData.addItem(new ClipData.Item(new Intent())); + fillInIntent.setClipData(fillInClipData); + fillInIntent.putExtra("key1", new Intent()); + fillInIntent.putExtra("key3", new Intent()); + + intent.collectExtraIntentKeys(); + originalIntentKeys = new ArraySet<>(intent.getExtraIntentKeys()); + fillInIntent.collectExtraIntentKeys(); + intent.fillIn(fillInIntent, 0); + + // size is 5 ( 3 extras merged from both + 2 clip data in the original. + assertThat(intent.getExtraIntentKeys()).hasSize(5); + // all keys from original are kept. + assertTrue(intent.getExtraIntentKeys().containsAll(originalIntentKeys)); + // Not all keys from fillInIntent are kept - clip data keys are dropped. + assertFalse(intent.getExtraIntentKeys().containsAll(fillInIntent.getExtraIntentKeys())); + + + // case 5: Both contains a mixture of extras and clip data. Force to fill in clip data. + intent = new Intent(); + clipData = ClipData.newIntent("clip", new Intent()); + clipData.addItem(new ClipData.Item(new Intent())); + clipData.addItem(new ClipData.Item(new Intent())); + clipData.addItem(new ClipData.Item(new Intent())); + intent.setClipData(clipData); + intent.putExtra("key1", new Intent()); + intent.putExtra("key2", new Intent()); + fillInIntent = new Intent(); + fillInClipData = ClipData.newIntent("clip", new Intent()); + fillInClipData.addItem(new ClipData.Item(new Intent())); + fillInClipData.addItem(new ClipData.Item(new Intent())); + fillInIntent.setClipData(fillInClipData); + fillInIntent.putExtra("key1", new Intent()); + fillInIntent.putExtra("key3", new Intent()); + + intent.collectExtraIntentKeys(); + originalIntentKeys = new ArraySet<>(intent.getExtraIntentKeys()); + fillInIntent.collectExtraIntentKeys(); + intent.fillIn(fillInIntent, Intent.FILL_IN_CLIP_DATA); + + // size is 6 ( 3 extras merged from both + 3 clip data in the fillInIntent. + assertThat(intent.getExtraIntentKeys()).hasSize(6); + // all keys from fillInIntent are kept. + assertTrue(intent.getExtraIntentKeys().containsAll(fillInIntent.getExtraIntentKeys())); + // Not all keys from intent are kept - clip data keys are dropped. + assertFalse(intent.getExtraIntentKeys().containsAll(originalIntentKeys)); + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 01daceba5fb9..78dee3169161 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -192,6 +192,7 @@ import static com.android.systemui.shared.Flags.enableHomeDelay; import android.Manifest; import android.Manifest.permission; +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.PermissionMethod; @@ -19228,6 +19229,11 @@ public class ActivityManagerService extends IActivityManager.Stub return mKeyFields.mCreatorPackage; } + @VisibleForTesting + public @NonNull Key getKeyFields() { + return mKeyFields; + } + public static boolean isValid(@NonNull Intent intent) { IBinder binder = intent.getCreatorToken(); IntentCreatorToken token = null; @@ -19271,9 +19277,13 @@ public class ActivityManagerService extends IActivityManager.Stub this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS; ClipData clipData = intent.getClipData(); if (clipData != null) { - this.mClipDataUris = new ArrayList<>(clipData.getItemCount()); - for (int i = 0; i < clipData.getItemCount(); i++) { - this.mClipDataUris.add(clipData.getItemAt(i).getUri()); + clipData = clipData.cloneOnlyUriItems(); + if (clipData != null) { + List<Uri> clipDataUris = new ArrayList<>(); + clipData.collectUris(clipDataUris); + if (!clipDataUris.isEmpty()) { + this.mClipDataUris = clipDataUris; + } } } } @@ -19375,11 +19385,34 @@ public class ActivityManagerService extends IActivityManager.Stub String creatorPackage) { if (IntentCreatorToken.isValid(intent)) return null; IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent); + return createOrGetIntentCreatorToken(intent, key); + } + + /** + * @hide + */ + @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL") + public IBinder refreshIntentCreatorToken(Intent intent) { + refreshIntentCreatorToken_enforcePermission(); + IBinder binder = intent.getCreatorToken(); + if (binder instanceof IntentCreatorToken) { + IntentCreatorToken token = (IntentCreatorToken) binder; + IntentCreatorToken.Key key = new IntentCreatorToken.Key(token.getCreatorUid(), + token.getCreatorPackage(), intent); + return createOrGetIntentCreatorToken(intent, key); + + } else { + throw new IllegalArgumentException("intent does not contain a creator token."); + } + } + + private static IntentCreatorToken createOrGetIntentCreatorToken(Intent intent, + IntentCreatorToken.Key key) { IntentCreatorToken token; synchronized (sIntentCreatorTokenCache) { WeakReference<IntentCreatorToken> ref = sIntentCreatorTokenCache.get(key); if (ref == null || ref.get() == null) { - token = new IntentCreatorToken(creatorUid, creatorPackage, intent); + token = new IntentCreatorToken(key.mCreatorUid, key.mCreatorPackage, intent); sIntentCreatorTokenCache.put(key, token.mRef); } else { token = ref.get(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index d9332ec05697..6defadf44d05 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -89,6 +89,7 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.SyncNotedAppOp; import android.app.backup.BackupAnnotations; +import android.content.ClipData; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -100,6 +101,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ServiceInfo; +import android.graphics.Rect; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; @@ -204,6 +206,16 @@ public class ActivityManagerServiceTest { private static final String TEST_AUTHORITY = "test_authority"; private static final String TEST_MIME_TYPE = "application/test_type"; + private static final Uri TEST_URI = Uri.parse("content://com.example/people"); + private static final int TEST_CREATOR_UID = 12345; + private static final String TEST_CREATOR_PACKAGE = "android.content.testCreatorPackage"; + private static final String TEST_TYPE = "testType"; + private static final String TEST_IDENTIFIER = "testIdentifier"; + private static final String TEST_CATEGORY = "testCategory"; + private static final String TEST_LAUNCH_TOKEN = "testLaunchToken"; + private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PACKAGE, + "TestClass"); + private static final int ALL_SET_FLAG = 0xFFFFFFFF; private static final int[] UID_RECORD_CHANGES = { UidRecord.CHANGE_PROCSTATE, @@ -1414,6 +1426,34 @@ public class ActivityManagerServiceTest { & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); } + @Test + public void testUseCloneForCreatorTokenAndOriginalIntent_createSameIntentCreatorToken() { + Intent testIntent = new Intent(TEST_ACTION1) + .setComponent(TEST_COMPONENT) + .setDataAndType(TEST_URI, TEST_TYPE) + .setIdentifier(TEST_IDENTIFIER) + .addCategory(TEST_CATEGORY); + testIntent.setOriginalIntent(new Intent(TEST_ACTION2)); + testIntent.setSelector(new Intent(TEST_ACTION3)); + testIntent.setSourceBounds(new Rect(0, 0, 100, 100)); + testIntent.setLaunchToken(TEST_LAUNCH_TOKEN); + testIntent.addFlags(ALL_SET_FLAG) + .addExtendedFlags(ALL_SET_FLAG); + ClipData testClipData = ClipData.newHtmlText("label", "text", "<html/>"); + testClipData.addItem(new ClipData.Item(new Intent(TEST_ACTION1))); + testClipData.addItem(new ClipData.Item(TEST_URI)); + testIntent.putExtra(TEST_EXTRA_KEY1, TEST_EXTRA_VALUE1); + + ActivityManagerService.IntentCreatorToken tokenForFullIntent = + new ActivityManagerService.IntentCreatorToken(TEST_CREATOR_UID, + TEST_CREATOR_PACKAGE, testIntent); + ActivityManagerService.IntentCreatorToken tokenForCloneIntent = + new ActivityManagerService.IntentCreatorToken(TEST_CREATOR_UID, + TEST_CREATOR_PACKAGE, testIntent.cloneForCreatorToken()); + + assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields()); + } + private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, long lastNetworkUpdatedProcStateSeq, final long procStateSeqToWait, boolean expectWait) throws Exception { |