Generate CreatorToken

Collect extra intent keys on the client side and generated CreatorToken
on the system server side

Bug: 363016571
Test: ActivityManagerService#testAddCreatorToken
Flag: android.security.prevent_intent_redirect
Change-Id: Ibc6d461e74d96b12a9af990d8653f66fcfa26c14
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index d318812..3bd121a 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1323,4 +1323,12 @@
      */
     public abstract void killApplicationSync(String pkgName, int appId, int userId,
             String reason, int exitInfoReason);
+
+    /**
+     * Add a creator token for all embedded intents (stored as extra) of the given intent.
+     *
+     * @param intent The given intent
+     * @hide
+     */
+    public abstract void addCreatorToken(Intent intent);
 }
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 3714e5d..393ec8c 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -1094,6 +1094,9 @@
             @Nullable String requiredPermission, @Nullable Bundle options)
             throws CanceledException {
         try {
+            if (intent != null) {
+                intent.collectExtraIntentKeys();
+            }
             String resolvedType = intent != null ?
                     intent.resolveTypeIfNeeded(context.getContentResolver())
                     : null;
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index b421339..ff0bb25 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -1149,7 +1149,7 @@
         for (int i = 0; i < size; i++) {
             final Item item = mItems.get(i);
             if (item.mIntent != null) {
-                item.mIntent.prepareToLeaveProcess(leavingPackage);
+                item.mIntent.prepareToLeaveProcess(leavingPackage, false);
             }
             if (item.mUri != null && leavingPackage) {
                 if (StrictMode.vmFileUriExposureEnabled()) {
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 044178c..c23bcab 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -12227,6 +12227,8 @@
 
     /**
      * Collects keys in the extra bundle whose value are intents.
+     * With these keys collected on the client side, the system server would only unparcel values
+     * of these keys and create IntentCreatorToken for them.
      * @hide
      */
     public void collectExtraIntentKeys() {
@@ -12589,22 +12591,29 @@
      */
     @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToLeaveProcess(boolean leavingPackage) {
+        prepareToLeaveProcess(leavingPackage, true);
+    }
+
+    /**
+     * @hide
+     */
+    void prepareToLeaveProcess(boolean leavingPackage, boolean isTopLevel) {
         setAllowFds(false);
 
         if (mSelector != null) {
-            mSelector.prepareToLeaveProcess(leavingPackage);
+            mSelector.prepareToLeaveProcess(leavingPackage, false);
         }
         if (mClipData != null) {
             mClipData.prepareToLeaveProcess(leavingPackage, getFlags());
         }
         if (mOriginalIntent != null) {
-            mOriginalIntent.prepareToLeaveProcess(leavingPackage);
+            mOriginalIntent.prepareToLeaveProcess(leavingPackage, false);
         }
 
         if (mExtras != null && !mExtras.isParcelled()) {
             final Object intent = mExtras.get(Intent.EXTRA_INTENT);
             if (intent instanceof Intent) {
-                ((Intent) intent).prepareToLeaveProcess(leavingPackage);
+                ((Intent) intent).prepareToLeaveProcess(leavingPackage, false);
             }
         }
 
@@ -12678,6 +12687,10 @@
                 StrictMode.onUnsafeIntentLaunch(this);
             }
         }
+
+        if (isTopLevel) {
+            collectExtraIntentKeys();
+        }
     }
 
     /**
diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java
index ca6d86a..f406927 100644
--- a/core/java/android/content/IntentSender.java
+++ b/core/java/android/content/IntentSender.java
@@ -288,6 +288,9 @@
             @Nullable Executor executor, @Nullable OnFinished onFinished)
             throws SendIntentException {
         try {
+            if (intent != null) {
+                intent.collectExtraIntentKeys();
+            }
             String resolvedType = intent != null ?
                     intent.resolveTypeIfNeeded(context.getContentResolver())
                     : null;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 35323d6..5ca72ab 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -130,6 +130,7 @@
 import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES;
 import static android.provider.Settings.Global.DEBUG_APP;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
+import static android.security.Flags.preventIntentRedirect;
 import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -420,6 +421,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.MemInfoReader;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.BootReceiver;
 import com.android.server.DeviceIdleInternal;
@@ -2420,6 +2422,7 @@
         mBroadcastController = new BroadcastController(mContext, this, mBroadcastQueue);
         mComponentAliasResolver = new ComponentAliasResolver(this);
         mApplicationSharedMemoryReadOnlyFd = null;
+        sCreatorTokenCacheCleaner = new Handler(mHandlerThread.getLooper());
     }
 
     // Note: This method is invoked on the main thread but may need to attach various
@@ -2526,6 +2529,7 @@
         mPendingStartActivityUids = new PendingStartActivityUids();
         mTraceErrorLogger = new TraceErrorLogger();
         mComponentAliasResolver = new ComponentAliasResolver(this);
+        sCreatorTokenCacheCleaner = new Handler(mHandlerThread.getLooper());
         try {
             mApplicationSharedMemoryReadOnlyFd =
                     ApplicationSharedMemory.getInstance().getReadOnlyFileDescriptor();
@@ -5532,6 +5536,7 @@
     public int sendIntentSender(IApplicationThread caller, IIntentSender target,
             IBinder allowlistToken, int code, Intent intent, String resolvedType,
             IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+        addCreatorToken(intent);
         if (target instanceof PendingIntentRecord) {
             final PendingIntentRecord originalRecord = (PendingIntentRecord) target;
 
@@ -13610,6 +13615,7 @@
             throws TransactionTooLargeException {
         enforceNotIsolatedCaller("startService");
         enforceAllowedToStartOrBindServiceIfSdkSandbox(service);
+        addCreatorToken(service);
         if (service != null) {
             // Refuse possible leaked file descriptors
             if (service.hasFileDescriptors()) {
@@ -13871,6 +13877,7 @@
 
         validateServiceInstanceName(instanceName);
 
+        addCreatorToken(service);
         try {
             if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                 final ComponentName cn = service.getComponent();
@@ -17148,6 +17155,7 @@
                 Slog.v(TAG_SERVICE,
                         "startServiceInPackage: " + service + " type=" + resolvedType);
             }
+            addCreatorToken(service);
             final long origId = Binder.clearCallingIdentity();
             ComponentName res;
             try {
@@ -17973,6 +17981,11 @@
                         userId, reason, exitInfoReason);
             }
         }
+
+        @Override
+        public void addCreatorToken(Intent intent) {
+            ActivityManagerService.this.addCreatorToken(intent);
+        }
     }
 
     long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
@@ -19105,6 +19118,7 @@
     private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>>
             sIntentCreatorTokenCache = new ConcurrentHashMap<>();
 
+    private static Handler sCreatorTokenCacheCleaner;
     /**
      * A binder token used to keep track of which app created the intent. This token can be used to
      * defend against intent redirect attacks. It stores uid of the intent creator and key fields of
@@ -19112,13 +19126,16 @@
      *
      * @hide
      */
+    @VisibleForTesting
     public static final class IntentCreatorToken extends Binder {
         @NonNull
         private final Key mKeyFields;
+        private final WeakReference<IntentCreatorToken> mRef;
 
         public IntentCreatorToken(int creatorUid, Intent intent) {
             super();
             this.mKeyFields = new Key(creatorUid, intent);
+            mRef = new WeakReference<>(this);
         }
 
         public int getCreatorUid() {
@@ -19136,6 +19153,26 @@
                     new Key(token.mKeyFields.mCreatorUid, intent));
         }
 
+        @Override
+        protected void finalize() throws Throwable {
+            try {
+                sCreatorTokenCacheCleaner.sendMessage(PooledLambda.obtainMessage(
+                        IntentCreatorToken::completeFinalize, this));
+            } finally {
+                super.finalize();
+            }
+        }
+
+        private void completeFinalize() {
+            synchronized (sIntentCreatorTokenCache) {
+                WeakReference<IntentCreatorToken> current = sIntentCreatorTokenCache.get(
+                        mKeyFields);
+                if (current == mRef) {
+                    sIntentCreatorTokenCache.remove(mKeyFields);
+                }
+            }
+        }
+
         private static class Key {
             private Key(int creatorUid, Intent intent) {
                 this.mCreatorUid = creatorUid;
@@ -19178,9 +19215,57 @@
             @Override
             public int hashCode() {
                 return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
-                        mFlags,
-                        mClipDataUris);
+                        mFlags, mClipDataUris);
             }
         }
     }
+
+    /**
+     * Add a creator token for all embedded intents (stored as extra) of the given intent.
+     *
+     * @param intent The given intent
+     * @hide
+     */
+    public void addCreatorToken(@Nullable Intent intent) {
+        if (!preventIntentRedirect()) return;
+
+        if (intent == null || intent.getExtraIntentKeys() == null) return;
+        for (String key : intent.getExtraIntentKeys()) {
+            try {
+                Intent extraIntent = intent.getParcelableExtra(key, Intent.class);
+                if (extraIntent == null) {
+                    Slog.w(TAG, "The key {" + key
+                            + "} does not correspond to an intent in the extra bundle.");
+                    continue;
+                }
+                Slog.wtf(TAG, "A creator token is added to an intent.");
+                IBinder creatorToken = createIntentCreatorToken(extraIntent);
+                if (creatorToken != null) {
+                    extraIntent.setCreatorToken(creatorToken);
+                }
+            } catch (Exception e) {
+                Slog.wtf(TAG,
+                        "Something went wrong when trying to add creator token for embedded "
+                                + "intents of intent: ."
+                                + intent, e);
+            }
+        }
+    }
+
+    private IBinder createIntentCreatorToken(Intent intent) {
+        if (IntentCreatorToken.isValid(intent)) return null;
+        int creatorUid = getCallingUid();
+        IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, intent);
+        IntentCreatorToken token;
+        synchronized (sIntentCreatorTokenCache) {
+            WeakReference<IntentCreatorToken> ref = sIntentCreatorTokenCache.get(key);
+            if (ref == null || ref.get() == null) {
+                token = new IntentCreatorToken(creatorUid, intent);
+                sIntentCreatorTokenCache.put(key, token.mRef);
+            } else {
+                token = ref.get();
+            }
+        }
+        return token;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index c1e859d..a6b650f 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -442,6 +442,8 @@
             throw new IllegalArgumentException("File descriptors passed in Intent");
         }
 
+        mService.mAmInternal.addCreatorToken(resultData);
+
         final ActivityRecord r;
         synchronized (mGlobalLock) {
             r = ActivityRecord.isInRootTaskLocked(token);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index de95d1b..93f428b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1228,6 +1228,7 @@
             String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo,
             String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo,
             Bundle bOptions) {
+        mAmInternal.addCreatorToken(intent);
         return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType,
                 resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions,
                 UserHandle.getCallingUserId());
@@ -1240,6 +1241,11 @@
         assertPackageMatchesCallingUid(callingPackage);
         final String reason = "startActivities";
         enforceNotIsolatedCaller(reason);
+        if (intents != null) {
+            for (Intent intent : intents) {
+                mAmInternal.addCreatorToken(intent);
+            }
+        }
         userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, reason);
         // TODO: Switch to user app stacks here.
         return getActivityStartController().startActivities(caller, -1, 0, -1, callingPackage,
@@ -1269,6 +1275,7 @@
             @Nullable String callingFeatureId, Intent intent, String resolvedType,
             IBinder resultTo, String resultWho, int requestCode, int startFlags,
             ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
+        mAmInternal.addCreatorToken(intent);
         final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions);
 
         assertPackageMatchesCallingUid(callingPackage);
@@ -1323,6 +1330,7 @@
             }
             // Remove existing mismatch flag so it can be properly updated later
             fillInIntent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH);
+            mAmInternal.addCreatorToken(fillInIntent);
         }
 
         if (!(target instanceof PendingIntentRecord)) {
@@ -1352,6 +1360,9 @@
         if (intent != null && intent.hasFileDescriptors()) {
             throw new IllegalArgumentException("File descriptors passed in Intent");
         }
+
+        mAmInternal.addCreatorToken(intent);
+
         SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions);
 
         synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index d699af8..4d17ed2 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -160,6 +160,7 @@
             Intent intent, String resolvedType, Bundle bOptions) {
         checkCallerOrSystemOrRoot();
         mService.assertPackageMatchesCallingUid(callingPackage);
+        mService.mAmInternal.addCreatorToken(intent);
 
         int callingUser = UserHandle.getCallingUserId();
         Task task;
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 809e13c..6ccc037 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -29,6 +29,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.Intent.FILL_IN_ACTION;
 import static android.os.PowerExemptionManager.REASON_DENIED;
 import static android.os.UserHandle.USER_ALL;
 import static android.util.DebugUtils.valueToString;
@@ -1300,6 +1301,45 @@
                 .containsExactly(new Pair<>(USER_ID, 42));
     }
 
+    @Test
+    @RequiresFlagsEnabled(android.security.Flags.FLAG_PREVENT_INTENT_REDIRECT)
+    public void testAddCreatorToken() {
+        Intent intent = new Intent();
+        Intent extraIntent = new Intent("EXTRA_INTENT_ACTION");
+        intent.putExtra("EXTRA_INTENT0", extraIntent);
+
+        intent.collectExtraIntentKeys();
+        mAms.addCreatorToken(intent);
+
+        ActivityManagerService.IntentCreatorToken token =
+                (ActivityManagerService.IntentCreatorToken) extraIntent.getCreatorToken();
+        assertThat(token).isNotNull();
+        assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.security.Flags.FLAG_PREVENT_INTENT_REDIRECT)
+    public void testAddCreatorTokenForFillingIntent() {
+        Intent intent = new Intent();
+        Intent extraIntent = new Intent("EXTRA_INTENT_ACTION");
+        intent.putExtra("EXTRA_INTENT0", extraIntent);
+        Intent fillinIntent = new Intent();
+        Intent fillinExtraIntent = new Intent("FILLIN_EXTRA_INTENT_ACTION");
+        fillinIntent.putExtra("FILLIN_EXTRA_INTENT0", fillinExtraIntent);
+
+        fillinIntent.collectExtraIntentKeys();
+        intent.fillIn(fillinIntent, FILL_IN_ACTION);
+
+        mAms.addCreatorToken(fillinIntent);
+
+        fillinExtraIntent = intent.getParcelableExtra("FILLIN_EXTRA_INTENT0", Intent.class);
+
+        ActivityManagerService.IntentCreatorToken token =
+                (ActivityManagerService.IntentCreatorToken) fillinExtraIntent.getCreatorToken();
+        assertThat(token).isNotNull();
+        assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid());
+    }
+
     private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq,
             long lastNetworkUpdatedProcStateSeq,
             final long procStateSeqToWait, boolean expectWait) throws Exception {