Fix for this issue has appeared as CVE-2025-22441: bulletin patch follow up
ApplicationInfo is structure defining various information about installed app, most notably path to apk file from which resources and code are loaded
Usually it is passed from system to applications, however sometimes there are cases where non-system caller could provide own one. For example in the past that was vulnerability in bindBackupAgent() method, where attacker could pass in parameter own ApplicationInfo object with uid and sourceDir values and they weren't checked against what apps are really installed in system, because that method was meant to be called internally by system_server, but was exposed to adb shell
This time though, I've looked closely at ApplicationInfo field within RemoteViews
RemoteViews is object describing view that can come from another process. This most notably is used for home screen widgets, where app providing widget builds RemoteViews and then it is "applied" within home screen process
Other places where RemoteViews are used are notifications (applied by SystemUI process) and autofill dialogs (provided by autofill service, applied by system_server)
RemoteViews.mApplication field is serialized through Parcel and therefore may come from remote processes and whenever RemoteViews are applied it is used by following method (snippet source):
private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
&& context.getPackageName().equals(mApplication.packageName)) {
return context;
}
try {
LoadedApk.checkAndUpdateApkPaths(mApplication);
return context.createApplicationContext(mApplication,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
}
}
return context;
}Most interesting here is LoadedApk.checkAndUpdateApkPaths() call, as this is static method and will modify some global state (snippet source)
public static void checkAndUpdateApkPaths(ApplicationInfo expectedAppInfo) {
// Get the LoadedApk from the cache
ActivityThread activityThread = ActivityThread.currentActivityThread();
if (activityThread == null) {
Log.e(TAG, "Cannot find activity thread");
return;
}
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ true);
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ false);
}
private static void checkAndUpdateApkPaths(ActivityThread activityThread,
ApplicationInfo expectedAppInfo, boolean cacheWithCode) {
String expectedCodePath = expectedAppInfo.getCodePath();
LoadedApk loadedApk = activityThread.peekPackageInfo(
expectedAppInfo.packageName, /* includeCode= */ cacheWithCode);
// If there is load apk cached, or if the cache is valid, don't do anything.
if (loadedApk == null || loadedApk.getApplicationInfo() == null
|| loadedApk.getApplicationInfo().getCodePath().equals(expectedCodePath)) {
return;
}
// Duplicate framework logic
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(activityThread, expectedAppInfo, oldPaths);
// Force update the LoadedApk instance, which should update the reference in the cache
loadedApk.updateApplicationInfo(expectedAppInfo, oldPaths);
}Lets discuss what is going on in these methods
First we're calling 3-parameter checkAndUpdateApkPaths with cacheWithCode being set to both true and false. The LoadedApk object can be constructed in two modes, either with mIncludeCode being true or false, which SDK-wise maps to Context with CONTEXT_INCLUDE_CODE flag being set or not
That method will first use ActivityThread.peekPackageInfo(), which will return already cached LoadedApk instance, therefore if app didn't previously construct LoadedApk with matching packageName and includeCode, peekPackageInfo() will return null and checkAndUpdateApkPaths() won't do anything
Looking back at RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(), we have context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED) there, the CONTEXT_INCLUDE_CODE flag was not specified (same is the case with contexts passed in argument to that method) and therefore that method will always using LoadedApk with mIncludeCode=false
Therefore, for RemoteViews updating version with code is unnecessary, as RemoteViews are always using Context without code. Commit message just says that there are two places where ApplicationInfo may be cached and I think removing updating version with code wouldn't cause issues here (as checkAndUpdateApkPaths() is only used by AppWidgetHostView and RemoteViews). Counterargument to that though is avoiding possibility of getting various caches out of sync and that in itself while removing call with cacheWithCode=true removes most severe impact of this bug, it doesn't completely fix issue as modification of just resources (instead of code) still might be valuable to attacker
So far presented code is only used for RemoteViews (widgets, notifications, etc), but now we'll be entering LoadedApk.updateApplicationInfo() which is also used for updating running app processes after new split has been installed (for example when using Play Feature Delivery on-demand delivery)
Now untrusted ApplicationInfo object from RemoteViews will be passed to updateApplicationInfo(), lets take a look what that method does (snippet source)
public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
@Nullable List<String> oldPaths) {
if (!setApplicationInfo(aInfo)) {
return;
}Let's take a look at that setApplicationInfo() (snippet source)
private boolean setApplicationInfo(ApplicationInfo aInfo) {
if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
Slog.w(TAG, "New application info for package " + aInfo.packageName
+ " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
+ mApplicationInfo.createTimestamp);
return false;
}
// Snip: assign fields such as mAppDir and mResDir on this object from aInfo
return true;
}createTimestamp field is normally set to SystemClock.uptimeMillis(), but as this object is coming from attacker, this means that attacker can provide future value to prevent further updateApplicationInfo() calls from executing
Returning to updateApplicationInfo() (snippet source)
final List<String> newPaths = new ArrayList<>();
makePaths(mActivityThread, aInfo, newPaths);
final List<String> addedPaths = new ArrayList<>(newPaths.size());
// Snip: populate addedPaths with items that are in newPaths and not in oldPaths (passed in argument)
synchronized (mLock) {
createOrUpdateClassLoaderLocked(addedPaths);List of addedPaths is built: in case of installation of new split oldPaths would contain list of paths that were used before installation of new split and addedPaths would contain list of .apk files that have to be added to existing ClassLoader, however in case of checkAndUpdateApkPaths() oldPaths will be built from exactly same ApplicationInfo object and therefore addedPaths will be empty and attacker won't be able to add new paths to existing ClassLoader here
createOrUpdateClassLoaderLocked() is long method, but only few things are interesting here:
- If
mIncludeCodeisfalse, only thing done is creatingClassLoaderwithout referencing anyapk/dexfiles - Most dangerous thing is creating new
ClassLoaderusing paths that were just set using attacker-controlledApplicationInfo, however that only will happen ifmDefaultClassLoaderisnull, which means that this is firstcreateOrUpdateClassLoaderLocked()on thatLoadedApkinstance (mDefaultClassLoaderfield refers to instance ofClassLoaderused before applyingAppComponentFactory.instantiateClassLoader()) - There is also adding new paths to native library search path of that
ClassLoader, however to exploit that victim app would need to performSystem.loadLibrary()that normally isn't present - And there is adding
apk/dexpaths fromaddedPathsargument to existingClassLoader, however on code path fromRemoteViewsaddedPathswill be empty
Returning to updateApplicationInfo() again, there's still one relevant thing that this method does, replacing LoadedApk.mResources with instance that uses new mResDir value. It should be noted that this is new instance, not updating instances that are already present. Newly created Contexts will return/use this new Resources instance, however any already created Contexts will keep using old Resources
To summarize above sections, whenever victim process applies RemoteViews, this vulnerability allows to:
- Replace
Resources(localization strings, layouts, etc) of newly created Activities within that process - Append to native library search path used by
System.loadLibrary(), however exploiting that would require victim to callSystem.loadLibrary()passing name of library that is normally absent, which is unlikely - Load arbitrary Java code if victim process has used
createPackageContext(CONTEXT_INCLUDE_CODE), but didn't callgetClassLoader()on thatContext, however since loading code is reason to useCONTEXT_INCLUDE_CODEflag, this is also unlikely to happen naturally
Now, while chance to load Java code in this case is unlikely to happen naturally, I was able to trigger it
On modern Android versions, WebView is not part of system, but loaded from normal apk that is defined in system configuration and is either system app or has signature matching one defined within system
Loading of that apk is done by creating new Context, passing Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY flags
Let's now take a look at caller of that method (snippet source)
// Overall snip: try/catch/finally, Trace, logging and timing measurement
webViewContext = getWebViewContextAndSetProvider();
if (android.content.res.Flags.registerResourcePaths()) {
Resources.registerResourcePaths(webViewContext.getPackageName(),
webViewContext.getApplicationInfo());
} else {
// Snip: old resource update path, won't be used on latest Android builds
}
ClassLoader clazzLoader = webViewContext.getClassLoader();getWebViewContextAndSetProvider() is method that creates Context, Resources.registerResourcePaths() is our only chance to introduce delay for us to have checkAndUpdateApkPaths() call in another thread and load arbitrary Java code, as webViewContext.getClassLoader() is the end of window where there's LoadedApk with mIncludeCode being true and mDefaultClassLoader being null
Resources.registerResourcePaths() will reach to appendLibAssetsLocked(), which will iterate over mResourceImpls field of singleton, which contains all resources that were loaded into this process and therefore may contain resources built using ApplicationInfo planted through RemoteViews
My initial idea was to put in ApplicationInfo large number of overlays paths all of which have same hashCode() which would be slow to deduplicate by createNewResourceKeyIfNeeded() call and while when using interpreter (for example when using debugger or within freshly installed app) this was significant delay, when runtime has done optimizations delay introduced by hash collisions wasn't significant, however another slowdown reason has appeared: since these overlay paths didn't point to existing files, for every overlay that failed to load a log message with stack trace was printed and that in practice did lead to slowdown, making exploitation of this race condition feasible
Apps can pass RemoteViews to SystemUI in Notifications. My exploit app requests POST_NOTIFICATIONS permission, which I think is reasonable user interaction, although it might be possible for some notifications to bypass that requirement
Normally SystemUI doesn't use WebView and in fact it cannot even do so, as SystemUI uses device protected storage (that is, storage that is not protected by lock screen credential) and WebView implementation disallows that
This exploit allows me to modify Resources though, so first I've modified layout used by SlicePermissionActivity to include <WebView /> element, then launched that Activity, which triggers WebView initialization on SystemUI main thread
Then I have to get RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths() called on other thread in order to replace path used to load code
Normally posting notification involves multiple threads in SystemUI process, including main thread (so while WebView is being initialized I cannot post another notification), however application of RemoteViews is happening on separate thread and I can suspend that operation by having ImageView within RemoteViews and asking it to load image from my ContentProvider
This exploit loads WebView by replacing layout used in SlicePermissionActivity. If updating code was not possible, attacking SlicePermissionActivity could still be valuable, as attacker could replace layout to fully hide original message and for example show changelog, replace allow button with "Got it" and deny button with empty string making it effectively invisible. While Slices framework is deprecated, there are still Settings slices which can for example change mobile data setting without user interaction
Other important permission prompt within SystemUI is Media Projection confirmation, however that one happened to not be vulnerable as it uses application context for Dialog
Also interesting are Preference fragments, as you can define Intent to be launched by Preference, however in case of SystemUI only preference Activities are ones related to SystemUI Tuner and Demo Mode, but these run in different process than one that shows Notifications