Security disclosures

Background activity launch from a zero-permission app: six system components that fire an attacker's PendingIntent with their own BAL exemption

Researcher
Ayush Kumar
Program
Android & Google Devices VRP
Android ID
492470616
Reported
2026-03-13

Android ID 492470616 · Reported to the Android & Google Devices VRP, 13 March 2026


1. Summary

Android 15 tightened background activity launch with a dual opt-in: both the creator of a PendingIntent and the sender must agree before the launch is allowed from the background. The intent was to make BAL something two parties consent to rather than one party assert.

Six system components take an attacker-supplied PendingIntent or IntentSender and fire it with an explicit BAL exemption — MODE_BACKGROUND_ACTIVITY_START_ALLOWED, or the stronger MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS. None of them validate the target of that PendingIntent, the identity of its creator, or the component it will start.

The attacker supplies the creator half of the opt-in legitimately, through the public API setPendingIntentCreatorBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED). The system component then supplies the sender half from its own privilege. Both halves are present, so the dual opt-in is satisfied — and neither half was ever a decision about this attacker.

The proof-of-concept app declares zero permissions.


2. Affected surface

Property Detail
Pattern System components firing caller-supplied PendingIntent / IntentSender with a BAL exemption
Surfaces reported 6
Surfaces demonstrated 3, with working exploits
Attacker position Any installed app
Permissions required None — the PoC manifest declares none
User interaction One natural action for most surfaces; none for Surface 5 after a one-time shortcut pin
Source verified at AOSP android-16.0.0_r4

Reproduction device:

AQ3A.240912.001 · 2312DRA50I · Android 15 (SDK 35) · V816 · garnet
PoC package com.poc.balbypass · UID 10633 · no declared permissions

3. The pattern

Every one of the six surfaces is the same four steps:

  1. A system component accepts an attacker-controlled PendingIntent or IntentSender.
  2. It performs no validation of the target, the creator, or the component to be started.
  3. It fires the intent with an explicit BAL exemption — ALLOWED or ALLOW_ALWAYS.
  4. The Android 15 dual opt-in is satisfied without any party deciding about the attacker.

Step 4 is the part worth dwelling on. Dual opt-in is not bypassed by defeating a check; it is bypassed by satisfying both halves honestly. The creator flag is set by the attacker on their own PendingIntent, using a documented public API, exactly as intended. The sender flag comes from a system component exercising a privilege it genuinely holds. Each half is individually correct. The composition is what fails, because nothing in the chain ever asks whether this particular intent, from this particular creator, should be started from the background.

A check that validated the target would break the chain at step 2 regardless of what either opt-in said.

3.1 How the surfaces were found

The pattern was not spotted once and generalised. It came out of a full sweep of MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS across AOSP android-16.0.0_r4 — every non-test call site read, with the data flow traced back to establish where each PendingIntent actually came from.

Audit of ALLOW_ALWAYS Count
Source files with non-test usage 20
Call sites examined 31
Confirmed exploitable 1ShortcutService.injectSendIntentSender()
Potentially exploitable 3 — Bluetooth Settings, SystemUI Controls DetailDialog, GestureLauncherService
Not exploitable 16 — system-created intents, user-interaction gated, or permission-protected

Sixteen of twenty ruled out is the part that matters. ALLOW_ALWAYS is not dangerous in itself, and most uses of it are fine: the system creates the intent, or a signature permission guards the path, or the user has already acted. The defect is specific — it is the handful of sites where the PendingIntent arrives from the caller and is fired anyway.

GestureLauncherService illustrates the line. It fires a wallet app's PendingIntent with ALLOW_ALWAYS, which looks identical in the source — but reaching it requires BIND_QUICK_ACCESS_WALLET_SERVICE, a signature permission, so an ordinary app never gets there. Same exemption, no bug.

3.2 The surfaces demonstrated

Component Exemption Trigger Status
UnarchiveErrorActivity (PackageInstaller) ALLOWED user taps Continue on the system dialog working exploit
Chooser refinement ALLOWED user picks any target on the system share sheet working exploit
ShortcutService.injectSendIntentSender() ALLOW_ALWAYS none, after a one-time shortcut pin working exploit

The submitted report described six surfaces in total; three were built as working exploits and are the ones set out here. The remainder were identified by the same analysis and reported as such rather than demonstrated, and are not claimed as proven results.

Both halves of Android 15’s dual opt-in are satisfied honestly, and the launch still happensAndroid 15 requires two separate opt-ins before a PendingIntent may start an activity from the background: one from the creator of the PendingIntent and one from whoever sends it. An attacking app with zero declared permissions sets the creator opt-in on its own PendingIntent using the documented public API setPendingIntentCreatorBackgroundActivityStartMode with MODE_BACKGROUND_ACTIVITY_START_ALLOWED. It then hands that PendingIntent to a system component, which fires it and supplies the sender opt-in from the privilege it genuinely holds, using MODE_BACKGROUND_ACTIVITY_START_ALLOWED or the stronger ALLOW_ALWAYS. Both opt-ins are individually legitimate, so the dual opt-in gate is satisfied and the background activity launch proceeds. The defect is that no component in the chain validates the target of the PendingIntent, the identity of its creator, or the component it will start, so no party ever makes a decision about this specific attacker. A check on the target would break the chain regardless of what either opt-in said.Attacking appzero declared permissionsbuilds a PendingIntentSystem componentholds the privilegefires the supplied intenthands it overCreator opt-insetPendingIntentCreatorBackgroundActivityStartModedocumented public APISender opt-inALLOWED / ALLOW_ALWAYSprivilege it genuinely holdsAndroid 15 dual opt-inboth halves present → allowedNobody asks: should THIS intent start an activity?no check of target, creator identity, or componentActivity launches from the backgroundphishing surface · permission prompt · task hijack
Nothing here is defeated. The attacker sets the creator opt-in on their own PendingIntent through a documented public API; the system component adds the sender opt-in from privilege it really has. Both halves are individually correct, so the gate passes them — and because no component validates the target, no party ever makes a decision about this attacker. Dual opt-in answers “did both parties agree?”, not “should this intent start an activity?”

4. Surface 5 — a regression of CVE-2023-40079

The strongest of the six, because it is not a new gap but a reopened one.

ShortcutService.injectSendIntentSender() applies MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS. The patch for CVE-2023-40079 (December 2023) set this path to MODE_BACKGROUND_ACTIVITY_START_DENIED. In AOSP android-16.0.0_r4 it reads ALLOW_ALWAYS again.

ALLOW_ALWAYS is the stronger of the two exemptions — it survives conditions under which ALLOWED would not.

Operationally this is the worst case in the report, because after a one-time shortcut pin the attack needs no further interaction at all:

1. Install the APK
2. Tap "Surface 5" → accept the shortcut pin dialog        ← one time, looks ordinary
3. Tap "Run Legacy Replay", send the app to the background
4. Wait ~20s → the attacker's activity launches from the background

Step 2 is a pin prompt, which is a normal thing for an app to ask for and a normal thing for a user to accept. Everything after it is unattended.


5. What it is worth to an attacker

A background activity launch is a primitive, not an end. What it buys:

Credential phishing. An activity launched from the background can present a sign-in screen at a moment the user did not initiate and has no reason to distrust. The PoC renders a deliberately plain fake sign-in page to demonstrate exactly this, and no more.

Permission harvesting. The same primitive can surface a permission dialog at a chosen moment rather than one the user navigated to, where the surrounding context makes the grant look expected.

Task hijacking and overlays. An activity arriving on top of another at a moment of the attacker's choosing is the foundation of both.

The precondition for all of it is the same, and it is very low: one installed app, no permissions, no special privileges.


6. Evidence

System log from the Surface 1 chain, with the exemption visible in the launch record:

23:40:20.626  START u0 {act=com.android.intent.action.UNARCHIVE_ERROR_DIALOG
              cmp=com.google.android.packageinstaller/
                  com.android.packageinstaller.UnarchiveErrorActivity (has extras)}
              from uid 10633 from pid 24248 callingPackage com.poc.balbypass
              (BAL_ALLOW_VISIBLE_WINDOW) result code=0

23:40:21.825  START u0 {cmp=com.poc.balbypass/.BalProofActivity (has extras)}
              from uid 10633 from pid -1 callingPackage com.poc.balbypass
              (BAL_ALLOW_VISIBLE_WINDOW) result code=0

The second record is the one that matters: from pid -1 — the launching process is gone — and the activity starts anyway.

23:40:20.601  === SURFACE 1: UnarchiveErrorActivity BAL bypass ===
23:40:20.604  Created PendingIntent with creator BAL opt-in
23:40:20.626  UnarchiveErrorActivity launched with attacker PendingIntent
23:40:21.825  *** BACKGROUND ATTACK FIRING *** App in background for ~18s

Scope of demonstration. The proof of concept implements three of the six surfaces — Surfaces 1, 2 and 5 — each with a working exploit. The remaining three were identified as following the same code pattern and are described rather than demonstrated. Three working surfaces across independent components is what supports the claim that the pattern is systemic; the undemonstrated three should be read as identified, not proven.


7. Suggested fix

Validate the target before granting the exemption. At each of the six sites, a component about to fire a caller-supplied PendingIntent with a BAL exemption should establish that the target is one it has reason to start — by creator identity, by component, or by the component's own contract — rather than treating an arbitrary supplied intent as if it were its own.

Restore the CVE-2023-40079 patch. ShortcutService.injectSendIntentSender() should return to MODE_BACKGROUND_ACTIVITY_START_DENIED.

More generally: dual opt-in answers did both parties agree? It does not answer should this intent start an activity from the background? Where a privileged component forwards an untrusted intent, the second question is the one that needs an answer.


8. Disclosure timeline

Date Event
2026-03-13 Reported with proof of concept; Android ID 492470616 opened
2026-03-13 Triaged
2026-03-17 Complete Android Studio project supplied on request — 6 source files, no precompiled artifacts, 3 surfaces demonstrated
2026-03-21 Assessed against an existing internal investigation
2026-03-23 Google confirms the canonical report is not tracked for a Security Bulletin, and asks to be consulted before public disclosure
2026-09-12 Draft of this writeup shared for review
2026-09-12 Google reply: page reviewed, no objections and no requested changes

Disclosure status. Published with Google's agreement, after they reviewed this page.

Since our engineering team assessed this issue as having No Security Impact (NSI) and it is not being tracked for a Security Bulletin, we have no objections to you making your post public. We have reviewed the page and do not have any requests for you to change, hold, or remove any of the content.


Reported by Ayush Kumar to the Android & Google Devices Vulnerability Reward Program. Published under coordinated disclosure.