CVE-2025-48558 AI-generated analysis
platform/frameworks/base, services/core BatteryService (system_server)
BatteryService now resolves the ACTION_REQUEST_SHUTDOWN intent it fires on low battery or over-temperature to an explicit component whose owning uid holds android.permission.SHUTDOWN. Before, it launched the implicit intent from the system process, so any activity with a matching intent-filter could resolve.
Root cause
In services/core/java/com/android/server/BatteryService.java, shutdownIfNoPowerLocked() and shutdownIfOverTempLocked() each posted a Runnable. That Runnable built an implicit Intent(Intent.ACTION_REQUEST_SHUTDOWN) with EXTRA_KEY_CONFIRM=false, EXTRA_REASON (SHUTDOWN_LOW_BATTERY or SHUTDOWN_BATTERY_THERMAL_STATE) and FLAG_ACTIVITY_NEW_TASK, then called mContext.startActivityAsUser(intent, UserHandle.CURRENT) with no component set. Because the intent was implicit and started by system_server, any activity declaring an intent-filter for ACTION_REQUEST_SHUTDOWN could be resolved and launched, with no check that the target held android.permission.SHUTDOWN.
How the fix works
- BatteryService.java:shutdownIfNoPowerLocked() and shutdownIfOverTempLocked(): the Intent is now built inline, with the same action, extras and flags as before. The posted Runnable is replaced by a lambda that calls the new private method startShutdownActivity(intent). The isSystemReady() check moves into that method.
- BatteryService.java:startShutdownActivity(Intent): returns early if !mActivityManagerInternal.isSystemReady(). It gets PackageManagerInternal via LocalServices.getService and returns early (no activity started) if it is null.
- startShutdownActivity: calls pmi.queryIntentActivities(intent, null, 0, Process.myUid(), mActivityManagerInternal.getCurrentUserId()) and walks the ResolveInfo list. Entries with null activityInfo or applicationInfo are skipped. Each remaining entry must pass mContext.checkPermission(android.Manifest.permission.SHUTDOWN, 0, ri.activityInfo.applicationInfo.uid) == PERMISSION_GRANTED; otherwise a Slog.w naming the component is logged and the entry is skipped.
- The first ResolveInfo whose uid holds SHUTDOWN is pinned with intent.setComponent(ri.activityInfo.getComponentName()) and the loop breaks. The intent is then started with mContext.startActivityAsUser(intent, UserHandle.CURRENT) as before, now as an explicit intent.
Completeness
- Both BatteryService trigger paths (low battery via shouldShutdownLocked(), over-temperature via mShutdownBatteryTemperature) go through the single new startShutdownActivity() helper, so the permission check applies to both.
- Fallback gap visible in the code: if resolveInfos is null, or no resolved activity's uid holds SHUTDOWN, intent.setComponent() is never called. The method still falls through to mContext.startActivityAsUser(intent, UserHandle.CURRENT) with the implicit intent. The fix only guarantees an explicit, permission-checked target when at least one SHUTDOWN-holding handler resolves for the current user.
- The permission check uses the owning uid of the resolved activity (pid argument 0). It does not check any other property of the component.
- The query uses flags 0 and the current user id from ActivityManagerInternal. The later start uses UserHandle.CURRENT.
- If PackageManagerInternal is unavailable, the shutdown activity is not started at all (silent early return). This no-op path did not exist before.
Behavior changes
- The shutdown request intent now has an explicit component (the first resolved handler whose uid holds SHUTDOWN) when such a handler exists.
- New Slog.w log line 'Shutdown activity <component> does not have permission android.permission.SHUTDOWN' for each skipped handler.
- If LocalServices has no PackageManagerInternal, the activity launch is skipped entirely.
- The Intent object is now created on the calling thread (inside the *Locked methods) rather than inside the posted Runnable. The isSystemReady() check still runs at execution time on mHandler.
Per major
- Android 14: same fix: frameworks/base e22e656b5e88 in android-security-14.0.0_r22 (cherry-pick of the internal change, Change-Id I6c00b47d424d81712bd9634c31de4b7d2e9cbe31). Patched region fully present (8/8 lines) at android-security-14.0.0_r29.
- Android 15: same fix: frameworks/base 0d30c78c8953 in android-security-15.0.0_r10. Hunk content is identical; only the surrounding import lines differ (CopyOnWriteArraySet). 8/8 lines present at android-security-15.0.0_r17.
- Android 16: same fix: frameworks/base b1c60eeadb2c in android-security-16.0.0_r1. Hunk content is identical; the import context differs (Looper, Objects, CopyOnWriteArraySet). 8/8 lines present at android-security-16.0.0_r8.
- Android 17: not affected (not listed by the bulletin). The patched region is present (8/8 lines) at android-security-17.0.0_r1.
Unknowns
- Whether the Android 13 branch received this change (the bulletin lists 13 as affected; the material has no 13 commit).
- Whether any other system component sends ACTION_REQUEST_SHUTDOWN as an implicit intent; this material only covers BatteryService.
- The concrete consequence of system_server launching a non-system activity in this flow (the bulletin only states EoP, High).