Background activity launch from a zero-permission app: six system components that fire an attacker's PendingIntent with their own BAL exemption
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:
- A system component accepts an attacker-controlled
PendingIntentorIntentSender. - It performs no validation of the target, the creator, or the component to be started.
- It fires the intent with an explicit BAL exemption —
ALLOWEDorALLOW_ALWAYS. - 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 | 1 — ShortcutService.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.
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.