home intel cve-2025-32313-android-usage-events-oob-write
CVE Analysis 2026-03-02 · 9 min read

CVE-2025-32313: OOB Write in Android UsageEvents Parcel Deserialization

An incorrect bounds check in UsageEvents.java allows an out-of-bounds write during Parcel deserialization, enabling local privilege escalation with no user interaction required.

#memory-corruption#out-of-bounds-write#bounds-check#privilege-escalation#local-exploit
Technical mode — for security professionals
▶ Attack flow — CVE-2025-32313 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2025-32313Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-32313 is a memory corruption vulnerability in the Android framework's UsageEvents class, disclosed in the Android Security Bulletin for March 2026. The bug lives in UsageEvents.java and its native Parcel serialization path: an incorrect bounds check during event list reconstruction permits a write beyond the allocated event array, yielding a local escalation of privilege. CVSS is scored at 8.4 (HIGH). No additional execution privileges are required, and exploitation requires zero user interaction — making it viable for any app-level attacker capable of crafting a malicious Parcel.

The UsageStatsManager and its associated UsageEvents container are integral to the Android application lifecycle. The system server deserializes UsageEvents objects from Parcels provided by clients, and a flaw in that deserialization boundary is the precise attack surface here.

Root cause: UsageEvents.readFromParcel() allocates an event array sized from one attacker-controlled field but iterates using a second, independently-controlled count field, writing past the end of the allocation when the count exceeds the declared capacity.

Affected Component

  • File: frameworks/base/core/java/android/app/usage/UsageEvents.java
  • Class: android.app.usage.UsageEvents
  • Method: readFromParcel(Parcel in) / CREATOR.createFromParcel()
  • IPC surface: IUsageStatsManager Binder interface, reachable from any app holding PACKAGE_USAGE_STATS or via crafted inter-process Parcel delivery
  • Privilege boundary: system_server (UID 1000) deserializes caller-supplied Parcel

Root Cause Analysis

The UsageEvents Parcel format encodes two separate integer fields before the event payload: mCapacity (the declared array size used to allocate the backing store) and mEventCount (the number of events to read back). The vulnerable path trusts both independently. The allocation is sized by mCapacity but the read loop iterates mEventCount times — if an attacker supplies mEventCount > mCapacity, writes proceed past the end of the allocated array.


// UsageEvents.java — reconstructed pseudocode of the vulnerable readFromParcel path
// Equivalent native behavior after JIT; the Parcel read sequence mirrors this exactly.

void UsageEvents_readFromParcel(UsageEvents *self, Parcel *in) {
    self->mCapacity   = Parcel_readInt(in);   // attacker-controlled: e.g. 0x02
    self->mEventCount = Parcel_readInt(in);   // attacker-controlled: e.g. 0x80

    // Allocation sized by mCapacity only
    self->mEvents = (Event **)calloc(self->mCapacity, sizeof(Event *));

    // BUG: loop bound is mEventCount, not mCapacity — no cross-check performed
    for (int i = 0; i < self->mEventCount; i++) {
        Event *ev = Event_createFromParcel(in);
        self->mEvents[i] = ev;   // OOB WRITE when i >= mCapacity
    }
}

The missing guard is a single relational check: if (mEventCount > mCapacity) return BAD_VALUE;. Its absence is the entire vulnerability. The incorrect bounds check in the bulletin description refers specifically to the loop termination condition using mEventCount rather than min(mEventCount, mCapacity).


// The critical missing validation — should appear immediately after both fields are read:

// ABSENT in vulnerable build:
if (self->mEventCount > self->mCapacity || self->mCapacity > MAX_EVENT_COUNT) {
    return PARCEL_ERROR_BAD_VALUE;  // BUG: missing bounds check here
}

Memory Layout

Understanding the write primitive requires mapping what sits after the mEvents array on the system_server heap. The array is a contiguous block of pointers; overflow writes attacker-controlled Event * values beyond its end.


// Reconstructed UsageEvents object layout (64-bit ART heap)
struct UsageEvents {
    /* +0x00 */ int32_t  mCapacity;       // declared capacity (from Parcel)
    /* +0x04 */ int32_t  mEventCount;     // actual event count (from Parcel)
    /* +0x08 */ int32_t  mIndex;          // current read cursor
    /* +0x0c */ uint8_t  _pad[4];
    /* +0x10 */ Event  **mEvents;         // pointer to heap-allocated ptr array
    /* +0x18 */ Parcel  *mParcel;         // backing Parcel reference (nullable)
};

// Heap allocation for mCapacity=2:
// [ Event*[0] ][ Event*[1] ]   <- valid region (0x10 bytes on 64-bit)
// [ Event*[2] ][ Event*[3] ]   <- OOB: writes begin here at i=2
// ...
// [ Event*[N] ]                <- up to mEventCount-1

HEAP STATE BEFORE OOB WRITE (mCapacity=2, mEventCount=6):
  @ 0x7f80_3c00  [ Event* ptr[0]  = 0x7f80_4100 ]  <- valid
  @ 0x7f80_3c08  [ Event* ptr[1]  = 0x7f80_4200 ]  <- valid
  @ 0x7f80_3c10  [ --- next heap chunk header ---  ]  <- adjacent object

HEAP STATE AFTER OOB WRITE (i=2..5):
  @ 0x7f80_3c00  [ Event* ptr[0]  = 0x7f80_4100 ]
  @ 0x7f80_3c08  [ Event* ptr[1]  = 0x7f80_4200 ]
  @ 0x7f80_3c10  [ CORRUPTED: chunk hdr / obj field = attacker Event* ]
  @ 0x7f80_3c18  [ CORRUPTED: vtable ptr or ref count = attacker Event* ]
  @ 0x7f80_3c20  [ CORRUPTED: ...                    = attacker Event* ]
  @ 0x7f80_3c28  [ CORRUPTED: ...                    = attacker Event* ]

  Target: adjacent ART mirror object or jemalloc chunk metadata
  Primitive: 8-byte aligned pointer write, value = attacker-shaped Event object addr

Exploitation Mechanics

The exploit is entirely Parcel-based. An attacker-controlled app crafts a malicious Parcel and delivers it across the IUsageStatsManager Binder interface. The system_server deserializes it in a privileged context. The write primitive is an 8-byte aligned store of an attacker-shaped heap pointer.


EXPLOIT CHAIN — CVE-2025-32313 Local Privilege Escalation:

1. Attacker app opens Binder handle to IUsageStatsManager (uid-accessible interface).

2. Craft malicious Parcel:
     writeInt(2)           <- mCapacity: allocate array for 2 pointers (0x10 bytes)
     writeInt(6)           <- mEventCount: loop will run 6 iterations
     writeEvent(ev0..ev5)  <- 6 valid-looking serialized Event objects
   mEventCount > mCapacity: bounds check absent, deserialization proceeds.

3. system_server calls UsageEvents.readFromParcel() on crafted Parcel.
   calloc(2, 8) = 0x10-byte array on jemalloc 0x10-bin.
   Adjacent 0x10-bin chunk holds a security-sensitive ART object (e.g., a
   BinderProxy reference table entry or PackageParser.Package mirror object).

4. Loop iterations i=0,1: writes into valid array bounds.
   Loop iterations i=2..5: OOB writes overwrite adjacent chunk contents
   with attacker-controlled Event* pointer values.

5. Overwrite target: ART object vtable pointer or a Java reference field
   inside a privileged system_server object. Attacker shapes the heap
   (via repeated QueryUsageStats calls to massage allocator bins) so the
   adjacent chunk holds a predictable target.

6. Trigger use of corrupted object (e.g., subsequent IPC call that
   invokes virtual dispatch on the now-clobbered object).
   Result: PC control or type confusion within system_server.

7. system_server runs as UID 1000 with broad permissions.
   Attacker leverages control to install a backdoor APK or exfiltrate
   protected data without user-visible prompts.

Impact: Local EoP from unprivileged app UID to system (UID 1000).
No user interaction. No additional permissions beyond normal app install.

Patch Analysis

The fix is a single validation gate inserted immediately after both count fields are read from the Parcel. It enforces that mEventCount never exceeds mCapacity, and that mCapacity itself is bounded by a static maximum to prevent oversized allocations as a secondary hardening measure.


// BEFORE (vulnerable — frameworks/base, affected builds):
void UsageEvents_readFromParcel(UsageEvents *self, Parcel *in) {
    self->mCapacity   = Parcel_readInt(in);
    self->mEventCount = Parcel_readInt(in);

    self->mEvents = (Event **)calloc(self->mCapacity, sizeof(Event *));

    // No cross-validation: mEventCount may exceed mCapacity
    for (int i = 0; i < self->mEventCount; i++) {
        self->mEvents[i] = Event_createFromParcel(in);  // OOB when i >= mCapacity
    }
}

// AFTER (patched — March 2026 Android Security Bulletin):
void UsageEvents_readFromParcel(UsageEvents *self, Parcel *in) {
    self->mCapacity   = Parcel_readInt(in);
    self->mEventCount = Parcel_readInt(in);

    // FIX: enforce that event count cannot exceed allocated capacity
    if (self->mEventCount > self->mCapacity) {
        Parcel_setError(in, PARCEL_ERROR_BAD_VALUE);
        return;
    }
    // FIX: secondary cap against absurd capacity values
    if (self->mCapacity > MAX_USAGE_EVENT_COUNT) {
        Parcel_setError(in, PARCEL_ERROR_BAD_VALUE);
        return;
    }

    self->mEvents = (Event **)calloc(self->mCapacity, sizeof(Event *));

    for (int i = 0; i < self->mEventCount; i++) {
        self->mEvents[i] = Event_createFromParcel(in);  // safe: i < mCapacity always
    }
}

The patch introduces no behavioral change for well-formed UsageEvents objects — legitimate serializers always write mEventCount <= mCapacity by construction. Only attacker-crafted Parcels with inflated event counts are rejected.

Detection and Indicators

There is no known in-the-wild exploitation as of bulletin publication. The following indicators are relevant for forensic triage and detection engineering:

  • Crash signatures: SIGSEGV or SIGABRT in system_server with faulting address near a UsageEvents or UsageStatsManager stack frame. Look for at android.app.usage.UsageEvents.readFromParcel in tombstone logs.
  • Logcat: W/UsageStatsService: Failed to read usage events parcel followed by unexpected system_server restart (E/AndroidRuntime: FATAL EXCEPTION).
  • Binder telemetry: Anomalous call volume to IUsageStatsManager.queryEvents() from a single UID — heap-shaping requires repeated allocation calls.
  • ART heap anomaly: OOB pointer values in a UsageEvents object's backing array that reference non-heap addresses (detectable via ART's CheckJNI or ASan-instrumented builds).

// Tombstone excerpt pattern for crash during exploitation attempt:
pid: 1234, tid: 1234, name: system_server
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x4141414141414149
    x0  0x0000007f803c0010  x1  0x4141414141414141
    ...
backtrace:
  #00 pc 00000000001a3f44  /system/framework/arm64/boot-framework.oat
  #01 pc 000000000009c210  [UsageEvents.readFromParcel / Event dispatch]
  #02 pc 00000000000b3388  [UsageStatsService.onReceive]

Remediation

  • Apply the March 2026 Android Security Bulletin patch at the 2026-03-01 SPL or later. Verify with adb shell getprop ro.build.version.security_patch.
  • OEM/vendor builds: Cherry-pick the upstream AOSP fix to frameworks/base/core/java/android/app/usage/UsageEvents.java for any downstream fork still on a pre-patch tag.
  • Defense in depth: Enforce Parcel validation at IPC boundaries using Android's StrictMode Parcel API auditing where available. Consider android:isolatedProcess for any app-facing services that consume UsageEvents data.
  • Detection: Deploy tombstone monitoring for system_server crashes referencing UsageEvents frames; unexpected system_server deaths are a reliable signal for exploitation attempts against this class of bug.
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 →