
Writeup and exploit for CVE-2025-22441: Privilege escalation from installed app to SystemUI process on Android due to pass of untrusted ApplicationInfo to LoadedApk
Fix for this issue has appeared as CVE-2025-22441: bulletin patch follow up
ApplicationInfo aroundApplicationInfo 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
LoadedApk.updateApplicationInfo()