Welcome to the Native Heap
If you are just getting into Android internals, you probably view malloc() as a magic black box: you ask for memory, and the system hands you a pointer. But as an internals developer, you need to understand that the physical reality behind that abstract C pointer dictates the entire security and performance profile of the OS.
In 2018, the Android native heap was largely a story about understanding the differences between two allocators: Doug Lea’s dlmalloc and Android’s jemalloc. But that landscape has changed massively. Android 11 made Scudo the default native allocator for all devices except those with low-memory configurations.
To debug a native crash, a memory leak, or a use-after-free (UAF) vulnerability today, you need to upgrade your mental model from “how does the system find free memory?” to “how does the system prevent corrupted memory from becoming a usable exploit?”.
Let’s walk through exactly how we got here, and what you need to know to navigate the modern Android heap.
Generation I: dlmalloc and the Variable Chunk
To understand why Scudo exists, you have to understand what it fixed. The first major era of Android used dlmalloc, which is a variable-sized chunk allocator.
When an application requested memory, dlmalloc created a chunk. Crucially, the allocator metadata (like the chunk’s size, allocation flags, and information about the previous chunk) lived right next to the user’s allocated data.
This “boundary tag” design allowed dlmalloc to perform efficient coalescing. If chunk A was freed, and chunk B next to it was also free, the allocator would combine them into one larger chunk to fight fragmentation. It kept track of these free chunks using multiple structures; the Android implementation historically used 32 small bins and 32 tree bins, managed via bitmaps.
The Security Flaw: Because allocator metadata sat directly between application objects, a simple buffer overflow could easily corrupt chunk sizes or free-list links. Attackers routinely turned this metadata corruption into powerful arbitrary memory-write primitives.
Generation II & III: jemalloc and the Size Class Paradigm
To scale better and fix some of dlmalloc’s metadata exposure, Android moved to jemalloc. Instead of treating the heap as a sequence of arbitrary chunks, jemalloc organized memory hierarchically into arenas, chunks, runs, regions, and thread caches.
If you asked for 17 bytes, jemalloc wouldn’t build a 17-byte object; it mapped your request into a predefined size class and gave you a block (perhaps 32 bytes) from a homogeneous region.
The Trade-off: By packing similar-sized objects into regions, allocator metadata no longer sat between every user object. However, this meant application objects were now directly contiguous. A linear overflow would seamlessly corrupt a neighboring application object rather than allocator metadata, which attackers quickly adapted to. Furthermore, jemalloc relied heavily on thread caches. If you freed an object and quickly requested the same size again, you were almost guaranteed to get the exact same memory address back, making use-after-free (UAF) bugs incredibly reliable to exploit.
The Insight: Do not assume “Android uses Scudo everywhere.” Current AOSP source code explicitly retains jemalloc 5 (
libjemalloc5) for low-memory (“svelte”) configurations. You must always verify the build configuration of the specific device you are debugging.
Generation IV: Scudo and the Hardened Heap
With Android 11, the engineering philosophy shifted dramatically. Scudo was introduced as a hardened user-space allocator specifically designed to provide resilience against heap-based vulnerabilities. It operates as an active mitigation layer.
Scudo splits allocations into two domains:
The Primary Allocator: Handles small and medium allocations by carving reserved regions (currently described as 256 MiB regions on 64-bit Android) into identical size-class blocks.
The Secondary Allocator: Handles large allocations by mapping them directly via the operating system and surrounding them with unmapped guard pages. If a large buffer overflows, it hits the guard page and safely crashes the process instead of silently corrupting neighboring data.
Weaponizing Entropy: Randomization and Quarantine
Old allocators were deterministic; modern Scudo is deliberately chaotic.
Scudo randomizes region offsets, block ordering, and cache assignments to destroy predictable address patterns. Instead of handing out blocks in a linear sequence (B0, B1, B2), the Primary allocator actively shuffles available blocks.
Even more importantly, Scudo introduces a quarantine.
Android configures Scudo with both global and thread-local quarantines, defaulting to 256 KiB for 64-bit configurations and 64 KiB for 32-bit configurations. When you call free(), the memory is held in quarantine and is not immediately reusable. This shatters the immediate free → malloc → same address loop, forcing an attacker to control the allocator state perfectly to defeat the quarantine delay, size-class restrictions, and randomization all at once.
The Hardware Frontier: Memory Tagging and GWP-ASan
The most profound shift you will face as a modern Android developer is that allocator security is no longer just a software concept. It is now hardware-enforced via ARM’s Memory Tagging Extension (MTE).
MTE assigns a tag to both the pointer and the physical memory granule. Android implements this directly through Scudo, which explicitly updates memory tags on every malloc and free.
If Scudo frees a block (Tag A) and eventually reuses it for a new allocation (Tag B), the old, stale pointer still holds Tag A. When that stale pointer is used, the hardware detects the tag mismatch and triggers a fault. A stale pointer can now be invalid even when the underlying virtual address has been legitimately reused.
Clarifying the Debugging Ecosystem
As a developer, you need to understand the difference between the tools at your disposal:
Scudo: Always-on allocator mitigation focused on heap integrity, quarantine, and layout randomization.
GWP-ASan: A sampled memory-error detector integrated via headers/wrappers alongside Scudo in Bionic. It runs in production to catch actual memory errors with low overhead.
ASan / HWASan: Heavyweight development and testing instrumentation meant to comprehensively find memory bugs before release.
A Pragmatic Methodology for Modern Analysis
When you read older exploit writeups, you will see assumptions that are fundamentally false today.
malloc(x); malloc(x);does not guarantee adjacent objects anymore.free(x); malloc(x);does not guarantee the same address.Corrupting metadata is no longer the easiest path to an exploit because Scudo’s integrity checks will often detect it and abort the process.
If you are investigating a native memory crash today, you must follow this sequence:
Identify the Allocator: Check the Android release, ABI, and build config (Scudo vs. low-memory jemalloc).
Identify the Path: Did it go to the Primary (small/medium) or Secondary (large) allocator?
Determine the Size Class: Calculate the allocator’s rounding behavior to find the actual block size.
Determine Context: Which thread, cache, or arena was active?
Determine Reuse Behavior: Account for the quarantine size limits and block shuffling.
Identify Defenses: Did a Scudo integrity check, a guard page, GWP-ASan, or an MTE tag fault catch the bug?
Reason About Layout: Only after answering all of the above can you attempt to reason about physical memory adjacency.
Down the Rabbit Hole
To master Android internals, you have to accept that memory safety emerges from a layered defense. The C/C++ object interacts with Bionic, which routes through Scudo, which manages quarantines and tags, which are finally enforced by ARM hardware. Dive into the actual source code to see how these theories are implemented in practice.
Android Open Source Project — Scudo: Android’s current documentation describing Scudo’s deployment, role, and configuration.
LLVM/Scudo Allocator Architecture: Describes the core Primary/Secondary architecture, thread-specific data, randomization, and block shuffling.
AOSP Bionic MTE Documentation: Documents the precise integration between Scudo and the ARM Memory Tagging Extension.
Android Native-Memory Debugging: Explicitly lists heapprofd, malloc debugging, libmemunreachable, and sanitizer approaches.