▶ Attack flow — CVE-2026-0010 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2026-0010Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

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/mediaserver or drmserver (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
    }
}
Root cause: 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: drmserver or mediaserver crash with SIGSEGV or SIGBUS in BnDrmManagerService::onTransact at the LDP/RET epilogue — visible in tombstoned logs under /data/tombstones/.
  • Binder anomaly: Repeated PROCESS_DRM_INFO transactions with oversized data payloads from the same UID in /sys/kernel/debug/binder/stats.
  • SELinux audit: avc: denied entries showing drm UID accessing resources outside its normal domain after a successful escalation attempt.
  • Logcat: DrmManagerService tag emitting unexpected error codes or native crashes in libdrmframework.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.cpp and has no API surface changes.
  • Defense in depth: Audit all other onTransact implementations in frameworks/av/ for the same pattern: readInt32() → use as length argument to read() or readByteArray() 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 drm SELinux domain from spawning new processes or accessing /data/local/tmp to limit post-exploitation impact.