CVE-2025-48565 AI-generated analysis
platform/frameworks/base, core/java/com/android/internal/app/IntentForwarderActivity (cross-profile intent forwarding), plus PackageManager / ApplicationPackageManager hidden API
IntentForwarderActivity now resolves the intent MIME type once and passes that same string to both the IPackageManager.canForwardTo cross-profile check and the target-user activity resolution (via a new hidden PackageManager.resolveActivityAsUser(Intent, String resolvedType, ...) overload), uses getLaunchedFromPackage() instead of getCallingPackage() for the privileged skip-user-consent check, and (after a follow-up on 14) sanitizes intent selectors (package, component, nested selector) in every sanitizeIntent call.
Root cause
In IntentForwarderActivity.onCreate the cross-profile check canForward -> canForwardInner computed the type with intent.resolveTypeIfNeeded(contentResolver) and passed it to IPackageManager.canForwardTo; afterwards mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId) went through ApplicationPackageManager.resolveActivityAsUser, which independently called intent.resolveTypeIfNeeded(...) again. The commit message states the intent can change its type after the canForward check, so the type authorized for forwarding is not necessarily the type used for resolution in the other user. Additionally, launchResolverActivityWithCorrectTab sanitized only intentReceived and not its selector, and the managed-profile path computed callingPackage via getCallingPackage() for the EXTRA_SKIP_USER_CONFIRMATION privileged-caller decision.
How the fix works
- Commit 042b52a8551f (14.0.0_r24) core/java/android/content/pm/PackageManager.java: adds two hidden overloads resolveActivityAsUser(Intent, String resolvedType, int flags, int userId) and resolveActivityAsUser(Intent, String resolvedType, ResolveInfoFlags, int userId); the javadoc states resolvedType is specified explicitly when the type contained in the intent cannot be trusted, null falls back to Intent#resolveTypeIfNeeded.
- Same commit core/java/android/app/ApplicationPackageManager.java: the resolveActivity(Intent, ResolveInfoFlags) and resolveActivityAsUser overloads now funnel into resolveActivityAsUser(Intent, String, ResolveInfoFlags, int), which calls mPM.resolveIntent with resolvedType when non-null and only falls back to intent.resolveTypeIfNeeded(mContext.getContentResolver()) when it is null.
- Same commit IntentForwarderActivity.onCreate: computes String resolvedType = intentReceived.resolveTypeIfNeeded(getContentResolver()) once, passes it to canForward(...) and then to the new mInjector.resolveActivityAsUser(newIntent, resolvedType, MATCH_DEFAULT_ONLY, targetUserId); canForward and canForwardInner take String resolvedType instead of ContentResolver and no longer call resolveTypeIfNeeded themselves. Injector interface and InjectorImpl gain the resolvedType overload (InjectorImpl calls getPackageManager().resolveActivityAsUser(intent, resolvedType, flags, userId)).
- Same commit IntentForwarderActivity.java managed-profile branch: String callingPackage = getLaunchedFromPackage() replaces getCallingPackage() for the privilegedCallerAskedToSkipUserConsent computation.
- Same commit IntentForwarderActivity.launchResolverActivityWithCorrectTab: adds sanitizeIntent(intentReceived.getSelector()) when a selector is present, before startActivityAsCaller.
- Same commit AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker.hasCrossProfileIntents: adapted to the new canForward signature by passing intent.resolveTypeIfNeeded(mContentResolver).
- Commit 2fadc52d7f18 (14.0.0_r29): test enablement; Injector gains getExecutorService() and isDeviceProvisioned(), onCreate uses mInjector.getExecutorService(), isDeviceProvisioned() delegates to the injector; test manifest adds HIDE_OVERLAY_WINDOWS; inverted assertNull assertions on the captured intents are corrected to assertNotNull.
- Commit 96057a4e7e40 (14.0.0_r29) IntentForwarderActivity.sanitizeIntent: now also clears the selector's package, component and nested selector; it removes the explicit selector-sanitize call added by 042b52a8551f in launchResolverActivityWithCorrectTab and the pre-existing sanitizeIntent(forwardIntent.getSelector()) call in canForward, since every sanitizeIntent call (including launchChooserActivityWithCorrectTab's calls on intentReceived and innerIntent) now covers the selector.
Completeness
- Covers the main forwarding path in onCreate: one resolvedType is used for the root-intent canForwardTo check, the selector canForwardTo check, and the target-user resolveIntent.
- launchResolverActivityWithCorrectTab still calls mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, callingUserId).join() (overload without resolvedType, 14.0.0_r29 lines 444-445), so that resolution still derives the type from the intent; its result selects which user hosts the resolver.
- The selector check in canForward now reuses the root intent's resolvedType rather than resolving the selector's own type.
- After 96057a4e7e40, selector sanitization applies wherever sanitizeIntent is called (canForward copy, resolver path, chooser path including the EXTRA_INTENT inner intent); a nested selector is nulled rather than recursed.
- AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker still resolves the type from the intent at the call site.
- The ApplicationPackageManager fallback (resolvedType == null -> resolveTypeIfNeeded) keeps legacy behavior for all other callers; only callers that pass a resolvedType benefit.
Behavior changes
- New hidden PackageManager API: resolveActivityAsUser(Intent, String, int, int) and resolveActivityAsUser(Intent, String, ResolveInfoFlags, int); the base ResolveInfoFlags variant throws UnsupportedOperationException unless overridden, so PackageManager subclasses reached through the new path must override it.
- Cross-profile forwarding and target-user resolution use a single type string computed in onCreate; the selector is checked with the root intent's type.
- EXTRA_SKIP_USER_CONFIRMATION privileged-caller check uses getLaunchedFromPackage() instead of getCallingPackage().
- Selectors of forwarded, resolver-path and chooser-path intents lose any explicit package, component and nested selector (after 96057a4e7e40).
- IntentForwarderActivity.canForward signature changed (String resolvedType replaces ContentResolver); Injector interface gains resolveActivityAsUser(Intent, String, int, int), getExecutorService() and isDeviceProvisioned().
Per major
- Android 14: same fix: 042b52a8551f (resolvedType plumbing, getLaunchedFromPackage, selector sanitize) in android-security-14.0.0_r24; follow-ups 2fadc52d7f18 (test/Injector refactor) and 96057a4e7e40 (selector sanitization folded into sanitizeIntent) in android-security-14.0.0_r29. 6/8 patched-region lines at r29, plausibly because 96057a4e7e40 removed the explicit selector-sanitize lines (not confirmed by the material).
- Android 15: same fix: bf9e58d7edb5 in android-security-15.0.0_r12, same production-code changes as 14 apart from context (private-profile branch, singleTabOnly); test changes differ slightly. 6/8 lines at 15.0.0_r17; whether a sanitizeIntent follow-up was applied is not shown.
- Android 16: same fix: 28579dff4305 in android-security-16.0.0_r3, same content as 15. 6/8 lines at 16.0.0_r8, same caveat as 15.
- Android 17: resolved by change_id at android-security-17.0.0_r1; the 17 commit shown, 323c8b968a88, is the test/Injector refactor (also removes the IntentForwarderActivityTest exclude filter). 8/8 lines present; whether the sanitizeIntent folding (96057a4e7e40 equivalent) is in 17.0.0_r1 is not shown.
Unknowns
- Whether the sanitizeIntent selector-folding commit (96057a4e7e40) was applied on Android 15, 16 and 17; the material shows it only for 14.
- Which 2 of 8 patched-region lines are absent at the latest 14/15/16 tags.
- Whether launchResolverActivityWithCorrectTab's remaining resolveActivityAsUser call without resolvedType was intentionally left as is.
- Whether the IntentResolver module copy of IntentForwarderActivity is active on Android 14 devices and whether it received a matching fix.
- Exact trigger details beyond the commit message statement that the intent can change its type after the canForward check.