CVE-2026-0015 AI-generated analysis
platform/frameworks/base, AppOps (services/core/java/com/android/server/appop)
AppOpsService now only accepts proxied attribution tags from trusted proxies (flags without OP_FLAG_UNTRUSTED_PROXIED). It also checks that the proxy package really belongs to the proxy uid before treating it as system, stops the start dry run and finish paths from creating per-tag state, and finds the AttributedOp to finish by clientId and trust flag.
Root cause
In AppOpsService.verifyAndGetBypass, a tag not declared in the proxied package was re-checked against the proxy package's manifest (isAttributionInPackage(proxyPkg, attributionTag)), whether or not the proxy was trusted. In the ROOT_UID early return and in a second early return that yields RestrictionBypass.UNRESTRICTED, the tag counted as valid whenever isPackageNullOrSystem(proxyPackageName, proxyUid) held. That check did not verify that the named package belongs to proxyUid. A tag judged valid is used as a key when creating state: in the start path (getOpsLocked with edit=true, including dry run) and in the finish path (getOpLocked, which used edit=true before the fix). The in-code comment describes the concern as abusive apps clogging the appops system with unlimited attribution tags via proxy calls.
How the fix works
- AppOpsService.java:verifyAndGetBypass (full overload): new boolean isProxyTrusted parameter. The proxy-manifest re-check becomes isProxyTrusted && isAttributionInPackage(proxyPkg, attributionTag). The ROOT_UID early return and the UNRESTRICTED early return set isAttributionTagValid = isProxyTrusted && isPackageNullOrSystem(proxyPackageName, proxyUid). The 3-arg overload passes true with no proxy. The proxy overload gains the isProxyTrusted parameter. checkPackage passes (..., true, true).
- AppOpsService.java note and start paths (method names inferred from parameter lists as noteOperationUnchecked and startOperationUnchecked): both compute proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0 and pass it to verifyAndGetBypass. Invalid tags are set to null as before.
- AppOpsService.java start dry run: on Android 14, startOperationUnchecked takes a dryRun parameter and calls getOpsLocked with dryRun ? null : attributionTag. On 15/16, a separate method (taking startIfModeDefault; per the commit message, startOperationDryRun) calls getOpsLocked with a null tag. In both cases the Ops object is created if needed, but nothing about the tag is cached.
- AppOpsService.java finish path (inferred to be finishOperationUnchecked): verifyAndGetBypass is called with isProxyTrusted=true, because trust is unknown at finish time. getOpLocked is called with edit=false, so finish no longer creates Op entries. The AttributedOp is found through the new getAttributedOpWithClientId(op, clientId, attributionTag, hasProxy) instead of op.mAttributions.get(attributionTag).
- AppOpsService.java:getAttributedOpWithClientId (new): with no proxy, returns op.mAttributions.get(tag). With a proxy, it looks in this order: (1) a matching in-progress event for clientId without OP_FLAG_UNTRUSTED_PROXIED under the tag; (2) if the tag is valid in the proxied package per the 3-arg verifyAndGetBypass (a SecurityException counts as invalid), an untrusted event under the tag; (3) an untrusted event under the null tag. Otherwise it returns null.
- AppOpsService.java:isPackageNullOrSystem: after the appId shortcut, returns false if mPackageManagerInternal.getPackageUid(packageName, MATCH_ALL, userId of uid) != uid.
- AttributedOp.java:hasInProgressEvent(Predicate<InProgressStartOpEvent>) (new): checks mPausedInProgressEvents when paused, otherwise mInProgressEvents, and returns whether any event matches the predicate.
Completeness
- The note, start (including dry run) and finish paths in the diff now derive or assume proxy trust and pass it into verifyAndGetBypass. Both early-return shortcuts in verifyAndGetBypass are gated on isProxyTrusted.
- The finish path still assumes a trusted proxy for tag validation. However, it no longer creates Op state (edit=false) and falls back to the null-tag AttributedOp for untrusted events.
- The fix is validation based and introduces no cap on the number of attribution tags. Tags that are still valid (declared in the proxied package or in a trusted proxy) remain bounded only by manifests.
- The material does not show any other path that accepts proxy-supplied tags.
Behavior changes
- For proxy calls carrying OP_FLAG_UNTRUSTED_PROXIED, the tag is now treated as invalid and set to null when it is declared only in the proxy's manifest, or when the call falls into the ROOT_UID or UNRESTRICTED early-return branches.
- The finish path no longer creates an Op if none exists. A finish without a matching start logs 'Operation not found' and returns.
- The start dry run no longer passes the requested tag to getOpsLocked, so it does not cache state for that tag.
- isPackageNullOrSystem returns false when the named package's uid for that user does not match the supplied uid, even if it is a system package.
- A proxied finish now matches the AttributedOp by clientId and trust flag, so it can resolve to the null-tag AttributedOp when the start was recorded there.
Per major
- Android 14: adapted fix: a404da3cbbc6 in android-security-14.0.0_r25 (RESTRICT AUTOMERGE cherry-pick). Dry run is handled through the dryRun parameter (dryRun ? null : attributionTag). Uses the single op.mAttributions map; getAttributedOpWithClientId has no virtualDeviceId. Present at 14.0.0_r29 (8/8 lines).
- Android 15: adapted fix: 22d9cd12ef99 in android-security-15.0.0_r13. Separate start dry-run method patched (getOpsLocked with null tag). Lookups use op.mDeviceAttributedOps keyed by getPersistentId(virtualDeviceId). Present at 15.0.0_r17 (8/8 lines).
- Android 16: adapted fix: a4523e227733 in android-security-16.0.0_r4. Same shape as 15, but keyed by getPersistentDeviceIdForOp(virtualDeviceId, op.op). Present at 16.0.0_r8 (8/8 lines).
- Android 17: not affected (not listed by the bulletin); patched region present at android-security-17.0.0_r1 (8/8 lines).
Unknowns
- How the DoS shows up in practice (memory growth, persisted state size, crash) is not described beyond the in-code comment about clogging AppOps.
- The names of the note, start and finish methods in the Android 14 diff are inferred from parameter lists; the hunk headers show only the class.
- The condition for the second early-return branch in verifyAndGetBypass (the one returning RestrictionBypass.UNRESTRICTED) is not shown.
- Whether the note path creates per-tag state is not shown in the diff.