Post

Abusing the Companion BAL Exemption for Ads

Abusing the Companion BAL Exemption for Ads

Most Android abuse, in volume, is for ads. Adware accounted for 62% of mobile threat detections in 2025. Most of those detections are the same thing: an app puts a full-screen view on top of whatever the user is doing, long enough to register an ad impression and get paid.

That full-screen view is an Android Activity, the framework’s term for a single UI screen. The app showing it is not in the foreground when the launch happens. The capability to do this from the background is called Background Activity Launch, or BAL, and it is the specific thing Android has been trying to lock down. Without BAL restrictions, any installed app could interrupt whatever the user is doing at any time, with whatever screen it wanted. Android has tightened BAL across five releases. The bypass below depends on what is left after all of them.

Six years of tightening

Background Activity Launch (BAL) was introduced in Android 10 (API 29). The rule: an app in the background cannot start an Activity. Before 10, background launches were unrestricted; after 10, they became the exception.

Android 12 (API 31) closed several indirect launch paths. The “notification trampoline” pattern, where a tap on a notification was quietly routed through a background component that then called startActivity, was blocked. Foreground services (a long-running background task with a persistent notification) could no longer be started from the background. And PendingIntent objects had to declare whether they were mutable. A PendingIntent is a deferred action one app creates and hands to another component, which fires it on the originator’s behalf later; a mutable handoff was a common way to launder activity-start permission.

Android 14 (API 34) closed the implicit-privilege paths. Earlier, an app could effectively share its BAL permission with another app by handing it a PendingIntent, or by letting that app connect to one of its background services. In 14, neither transfer happens by default. The granting app has to set an explicit flag to allow it. Several older bypasses relied on the previous default-on behaviour, and stopped working.

Android 15 removed the last default-on case for PendingIntent creators. Android 16 added a developer-visible runtime warning (via Android’s StrictMode debug system) when a background activity start happens.

After all of that, what remains is a list of explicit exceptions. The official guide enumerates thirteen of them. Twelve are uncontroversial. An app can launch an Activity if it already has a visible window, if a notification it issued was just tapped, if the user granted it overlay permission (SYSTEM_ALERT_WINDOW), if it is bound by the system as an accessibility or autofill service, and a handful of similar cases. The thirteenth is the subject of this post.

The exemption

The thirteenth item, quoted verbatim from the developer guide:

The app is associated with a companion hardware device through the CompanionDeviceManager API. This API lets the app start activities in response to actions that the user performs on a paired device.

Companion Device Manager, or CDM, was added for legitimate use cases. Wear OS watches need to open Activities on the phone when the user taps a watch face. Smart glasses need to bring up controls or captions. Fitness bands send notifications that, when acted on at the band, open the phone’s app. The user gesture occurs on the companion device; the Activity has to appear on the phone. Without an exemption, that flow cannot complete.

So CDM-associated apps are exempt from BAL. The exemption attaches to the application UID, persists for the lifetime of the association, and applies whether the companion device is connected, in range, or, as the proof-of-concept demonstrates, present at all after the initial pairing flow has completed. Internally, it is tracked as REASON_COMPANION_DEVICE_MANAGER in PowerExemptionManager and consulted by BackgroundActivityStartController in the window manager service.

The minimum

A line in the app’s manifest declaring use of the companion-device feature:

1
2
<uses-feature android:name="android.software.companion_device_setup"
              android:required="false" />

A runtime call that asks the system to start a companion-device association. The device filter can be empty, which makes any nearby Bluetooth Low Energy (BLE) advertiser match:

1
2
3
4
5
AssociationRequest request = new AssociationRequest.Builder()
        .addDeviceFilter(new BluetoothLeDeviceFilter.Builder().build())
        .setSingleDevice(false)
        .build();
cdm.associate(request, callback, null);

And one tap from the user, on a system dialog whose wording varies by OEM and version but generally amounts to “choose a device to connect to.” The list shown contains whatever BLE is advertising in range: earbuds, a neighbor’s tracker, a smart scale, a passing fitness band. Any selection works, because nothing was filtered.

After the tap, the UID is BAL-exempt for the lifetime of the association.

What the exemption gives

With the UID flagged, any background execution path can call startActivity():

  • A scheduled AlarmManager wakeup at 3am
  • A BOOT_COMPLETED receiver, run when the device finishes booting
  • A WorkManager job that fires when the phone starts charging
  • An FCM push notification from the app’s backend
  • A foreground service started for any plausible reason

The PoC uses a foreground service because it keeps the process alive long enough for the delayed launch to fire. The same exemption serves a push-triggered BroadcastReceiver just as well, with no notification surfaced at all.

The ad

Once an Activity can be launched from the background, what follows is window configuration. A theme without a title bar. FLAG_KEEP_SCREEN_ON. Immersive flags to hide the system bars. android:excludeFromRecents="true" so the Activity does not appear in the task switcher. The Activity reschedules itself on a timer, so dismissing it brings it back in five seconds.

The platform never restricted any of those flags. BAL targets the call to startActivity() from a background Service, and that call succeeds because of one tap, weeks earlier, on a dialog the user does not remember.

What the user saw

A pairing dialog. The wording is a variation of “Allow this app to access nearby devices?” with a list of advertised devices underneath. The user picked their earbuds, or the first item in the list, or just something to make the dialog go away. The dialog did not mention activities, background behavior, or that the grant would, for the lifetime of the association, place the app on a list of thirteen platform-wide exceptions.

Dismissing the dialog does not record a “deny.” There is no runtime permission gating the call, and no system-enforced backoff. Any time the app’s Activity reaches the foreground, the dialog can be re-presented. That includes user-initiated opens, notification taps, and full-screen-intent notifications fired from a push.

After the user opens the app once, the app can start a foreground service while it is still in the foreground. That service survives the user backgrounding the app, swiping it from recents, or rebooting and re-launching it. From the service, the app can launch Activities, each of which calls associate() and surfaces the dialog again. On Android 13 and later, the user can deny the notifications permission for the app and the foreground service keeps running silently, with no visible notification and nothing in recents to indicate the app is still alive. The dialog reappears minutes or hours after the user thought the app was gone.

A freshly installed app, opened just once, can keep presenting the dialog indefinitely until the user picks something. The user picks eventually, because the alternative is uninstalling.

The problem

The CDM exemption was added deliberately, for a class of legitimate apps (Wear OS watches, smart glasses, automotive projection systems) that need to launch UI in response to off-device events. The exemption serves that audience correctly. The cost of joining that audience is one BLE filter and one tap, and that is where the abuse begins.

A pairing grant currently unlocks background activity launch silently, for the entire lifetime of the association. The dialog itself has no deny-state and no rate limit, so an app can re-present it whenever its Activity reaches the foreground, including from a foreground service that survives the user closing the app. Safer behavior would scope the unlock to the time the device is connected, treat a dismissed dialog as a deny, and place the grant itself behind a second prompt that names what is being granted.

This post is licensed under CC BY 4.0 by the author.