CVE-2025-22431 AI-generated analysis
platform/frameworks/base, AppOps (services/core/java/com/android/server/appop/AppOpsService.java)
AppOpsService stops treating a proxy-supplied attribution tag as valid when the proxied package is a non-app uid (shell, system, etc.) unless the proxy is a system app or absent, requires the next AttributionSource in a chain to also be trusted, and re-checks remaining restrictions before resuming a paused op when one restriction is lifted.
Root cause
In AppOpsService.verifyAndGetBypass, when the target packageName resolved to a non-app uid (com.android.shell special case, or any uid returned by resolveUid), the method unconditionally returned PackageVerificationResult(RestrictionBypass.UNRESTRICTED, isAttributionTagValid=true). In the proxy note/start paths, where the attribution tag comes from the proxy app, a non-system proxy could therefore get arbitrary, unregistered attribution tags accepted whenever the proxied side was one of these exempt uids. The added code comment says the fix prevents "abusive apps clogging the appops system with unlimited attribution tags via proxy calls". Separately, isCallerAndAttributionTrusted returned true when the first AttributionSource was trusted, without checking attributionSource.getNext(). The restriction-change path also resumed a paused AttributedOp (attrOp.resume()) without checking whether other restrictions still applied.
How the fix works
- AppOpsService.java:verifyAndGetBypass (all three overloads): a new int proxyUid parameter is threaded through. The 3-arg overload and checkPackage pass Process.INVALID_UID, and the two note/start call sites that passed proxyPackageName now also pass proxyUid.
- AppOpsService.java:verifyAndGetBypass (6-arg): in the non-app-uid branch (pkgUid renamed to nonAppUid), after the appId-mismatch SecurityException check, the method computes proxyIsSystemAppOrNull. It is true if proxyPackageName == null. Otherwise, if UserHandle.getAppId(proxyUid) >= Process.FIRST_APPLICATION_UID, it is set to mPackageManagerInternal.isSystemPackage(proxyPackageName). The returned PackageVerificationResult now uses this value for isAttributionTagValid instead of a hard-coded true. The call sites shown set attributionTag to null when isAttributionTagValid is false. The rest of the method keeps `int pkgUid = nonAppUid;`.
- AppOpsService.java:isCallerAndAttributionTrusted: the early `return true` now also requires attributionSource.getNext() == null || attributionSource.getNext().isTrusted(mContext). Otherwise the method falls through to the UPDATE_APP_OPS_STATS permission check.
- AppOpsService.java (restriction-change handling around attrOp.pause()/resume()): in the else-if (attrOp.isPaused()) branch, the code gets bypass via verifyAndGetBypass(uid, ops.packageName, attrOp.tag).bypass and only calls attrOp.resume() if !isOpRestrictedLocked(uid, code, ops.packageName, attrOp.tag, bypass, false).
- AppOpsService.java: resolveUid is renamed to resolveNonAppUid, and its call sites (checkPackage, isSpecialPackage, a packageUid resolution path that falls back to getPackageUid, verifyAndGetBypass) and the javadoc references are updated. The rename itself does not change logic.
Completeness
- The attribution-tag restriction applies only in the non-app-uid branch of verifyAndGetBypass. The regular app-package branch is not changed by this diff.
- Proxy-supplied tags are still accepted without registration when proxyPackageName is null, when the proxy appId is below Process.FIRST_APPLICATION_UID, or when mPackageManagerInternal.isSystemPackage(proxyPackageName) is true.
- The new check relies on the proxyPackageName and proxyUid that callers pass in. The diff does not show verifyAndGetBypass checking that proxyPackageName belongs to proxyUid.
- isCallerAndAttributionTrusted checks only one level of getNext(). Deeper chain elements are not checked in this diff.
- The resume re-check calls isOpRestrictedLocked with the bypass from verifyAndGetBypass. On Android 14 it passes no device id; on Android 15 it passes Context.DEVICE_ID_DEFAULT.
Behavior changes
- For proxy note/start operations from non-system apps that target non-app uids, the proxy-supplied attribution tag is now set to null instead of being accepted.
- Callers passing a trusted AttributionSource whose next source is not trusted now need UPDATE_APP_OPS_STATS to pass isCallerAndAttributionTrusted.
- When a restriction is removed, a paused op stays paused if any other restriction still applies, instead of being resumed unconditionally.
- resolveUid is renamed to resolveNonAppUid (private static, internal only).
Per major
- Android 14: same fix: commit 79211e094a73 in android-security-14.0.0_r19 (cherry-pick, Change-Id I6a7b0a24359097c0ea2f52cc69637d929a931b4f). At android-security-14.0.0_r29, 6/8 lines of the patched region are present.
- Android 15: adapted fix: commit be02cee3a50c in android-security-15.0.0_r7. It is the same change plus proxyUid threaded into startOperationDryRun and its call in the proxy start path, removal of the `boolean wasNull` line, and isOpRestrictedLocked called with Context.DEVICE_ID_DEFAULT. At android-security-15.0.0_r17, 6/8 lines of the patched region are present.
- Android 16: not affected per the bulletin. At android-security-16.0.0_r8, 6/8 lines of the patched region are present.
- Android 17: not affected per the bulletin. At android-security-17.0.0_r1, 6/8 lines of the patched region are present.
Unknowns
- The material does not show the full proxy entry points (noteProxyOperation/startProxyOperation) or how a non-system proxy supplies a proxied AttributionSource for a non-app uid, so the exact trigger path is not confirmed.
- The material does not show the names of the methods enclosing the changed note/start call sites.
- The commit marks tests as 'upcoming'; it includes no test change.
- Whether the 6/8-line match in later tags reflects the Android 15 variant or further refactoring is not shown.