How Taobao Adapted libmemunreachable for Zero‑Overhead Release‑Package Memory Leak Detection
This article explains the principles of memory leaks, details the zero‑overhead libmemunreachable detector, walks through its core algorithms and code, and describes the modifications Taobao made to enable reliable leak detection in Android release builds.
Background
As client stability quality deepens, memory quality becomes a prominent issue for Taobao, prompting a systematic memory governance effort and the creation of a dedicated memory project.
This article focuses on one of the project's tools: the memory‑leak analyzer memunreachable .
Memory Leak
A memory leak occurs when dynamically allocated heap memory is not released, leading to wasted resources, slower execution, or crashes.
Detecting leaks in C/C++ is difficult because pointers must be precisely tracked. Tools such as libmemunreachable, kmemleak, and LLVM LeakSanitizer rely on allocation records.
Android’s libmemunreachable is a zero‑overhead native memory leak detector that uses an imprecise mark‑and‑sweep GC to traverse native memory and report unreachable blocks. However, the original tool requires a debug‑configured environment and cannot be used with Taobao’s release packages.
This article examines libmemunreachable’s implementation and Taobao’s adaptations to support release builds.
libmemunreachable Analysis
Basic Principle
In Java GC, objects not reachable from GC roots are reclaimed. GC roots include:
Objects referenced from the VM stack.
Static fields in the method area.
Constants in the method area.
Objects referenced by native methods (JNI).
For C/C++ memory, the model consists of heap, stack, global/static storage (.bss and .data), constant storage (.rodata), and code (.text). libmemunreachable treats stack, .bss, and .data as GC roots and marks heap objects not reachable from these roots as leaks.
libmemunreachable Flowchart
The sequence includes creating a LeakPipe, forking a child process, capturing threads via ptrace, reading /proc/self/maps, classifying mappings, and finally reporting unreachable memory.
Core Code
// MemUnreachable.cpp
bool GetUnreachableMemory(UnreachableMemoryInfo &info, size_t limit) {
int parent_pid = getpid();
int parent_tid = gettid();
Heap heap;
Semaphore continue_parent_sem;
LeakPipe pipe;
PtracerThread thread{[&]() -> int {
// Collection thread
ALOGE("collecting thread info for process %d...", parent_pid);
ThreadCapture thread_capture(parent_pid, heap);
allocator::vector<ThreadInfo> thread_info(heap);
allocator::vector<Mapping> mappings(heap);
allocator::vector<uintptr_t> refs(heap);
if (!thread_capture.CaptureThreads()) { LOGE("CaptureThreads failed"); }
if (!thread_capture.CapturedThreadInfo(thread_info)) { LOGE("CapturedThreadInfo failed"); }
if (!ProcessMappings(parent_pid, mappings)) {
continue_parent_sem.Post(); LOGE("ProcessMappings failed"); return 1; }
thread_capture.ReleaseThread(parent_tid);
continue_parent_sem.Post();
int ret = fork();
if (ret < 0) { return 1; }
else if (ret == 0) {
// Heap walker process
if (!pipe.OpenSender()) { _exit(1); }
MemUnreachable unreachable{parent_pid, heap};
if (!unreachable.CollectAllocations(thread_info, mappings)) { _exit(2); }
size_t num_allocations = unreachable.Allocations();
size_t allocation_bytes = unreachable.AllocationBytes();
allocator::vector<Leak> leaks{heap};
size_t num_leaks = 0; size_t leak_bytes = 0;
bool ok = unreachable.GetUnreachableMemory(leaks, limit, &num_leaks, &leak_bytes);
ok = ok && pipe.Sender().Send(num_allocations);
ok = ok && pipe.Sender().Send(allocation_bytes);
ok = ok && pipe.Sender().Send(num_leaks);
ok = ok && pipe.Sender().Send(leak_bytes);
ok = ok && pipe.Sender().SendVector(leaks);
if (!ok) { _exit(3); }
_exit(0);
} else {
ALOGI("collection thread done");
return 0;
}
}};
// Original thread
{
ScopedDisableMalloc disable_malloc;
thread.Start();
continue_parent_sem.Wait(300s);
}
int ret = thread.Join();
if (ret != 0) { return false; }
if (!pipe.OpenReceiver()) { return false; }
bool ok = true;
ok = ok && pipe.Receiver().Receive(&info.num_allocations);
ok = ok && pipe.Receiver().Receive(&info.allocation_bytes);
ok = ok && pipe.Receiver().Receive(&info.num_leaks);
ok = ok && pipe.Receiver().Receive(&info.leak_bytes);
ok = ok && pipe.Receiver().ReceiveVector(info.leaks);
if (!ok) { return false; }
LOGD("unreachable memory detection done");
LOGD("%zu bytes in %zu allocation%s unreachable out of %zu bytes in %zu allocation%s",
info.leak_bytes, info.num_leaks, plural(info.num_leaks),
info.allocation_bytes, info.num_allocations, plural(info.num_allocations));
return true;
}CaptureThreads (Core Function)
// ThreadCapture.cpp
bool ThreadCaptureImpl::CaptureThreads() {
TidList tids{allocator_};
bool found_new_thread;
do {
if (!ListThreads(tids)) { LOGE("ListThreads failed"); ReleaseThreads(); return false; }
found_new_thread = false;
for (auto it = tids.begin(); it != tids.end(); ++it) {
if (captured_threads_.find(*it) == captured_threads_.end()) {
if (CaptureThread(*it) < 0) { LOGE("CaptureThread(*it) failed"); ReleaseThreads(); return false; }
found_new_thread = true;
}
}
} while (found_new_thread);
return true;
}CaptureThreadInfo (Core Function)
// ThreadCaptureImpl.cpp
bool ThreadCaptureImpl::CapturedThreadInfo(ThreadInfoList &threads) {
threads.clear();
for (auto it = captured_threads_.begin(); it != captured_threads_.end(); ++it) {
ThreadInfo t{0, allocator::vector<uintptr_t>(allocator_), std::pair<uintptr_t, uintptr_t>(0,0)};
if (!PtraceThreadInfo(it->first, t)) { return false; }
threads.push_back(t);
}
return true;
}ProcessMappings (Core Function)
// ProcessMappings.cpp
bool ProcessMappings(pid_t pid, allocator::vector<Mapping> &mappings) {
char map_buffer[1024];
snprintf(map_buffer, sizeof(map_buffer), "/proc/%d/maps", pid);
android::base::unique_fd fd(open(map_buffer, O_RDONLY));
if (fd == -1) {
LOGE("ProcessMappings parent pid failed to open %s: %s", map_buffer, strerror(errno));
snprintf(map_buffer, sizeof(map_buffer), "/proc/self/maps");
fd.reset(open(map_buffer, O_RDONLY));
if (fd == -1) { LOGE("ProcessMappings failed to open %s: %s", map_buffer, strerror(errno)); return false; }
}
LineBuffer line_buf(fd, map_buffer, sizeof(map_buffer));
char *line; size_t line_len;
while (line_buf.GetLine(&line, &line_len)) {
int name_pos; char perms[5]; Mapping mapping{};
if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %4s %*x %*x:%*x %*d %n",
&mapping.begin, &mapping.end, perms, &name_pos) == 3) {
mapping.read = perms[0] == 'r';
mapping.write = perms[1] == 'w';
mapping.execute = perms[2] == 'x';
mapping.priv = perms[3] == 'p';
if ((size_t)name_pos < line_len) { strlcpy(mapping.name, line + name_pos, sizeof(mapping.name)); }
mappings.emplace_back(mapping);
}
}
return true;
}CollectAllocations (Core Function)
// MemUnreachable.cpp
bool MemUnreachable::CollectAllocations(const allocator::vector<ThreadInfo> &threads,
const allocator::vector<Mapping> &mappings) {
ALOGI("searching process %d for allocations", pid_);
allocator::vector<Mapping> heap_mappings{mappings};
allocator::vector<Mapping> anon_mappings{mappings};
allocator::vector<Mapping> globals_mappings{mappings};
allocator::vector<Mapping> stack_mappings{mappings};
if (!ClassifyMappings(mappings, heap_mappings, anon_mappings, globals_mappings, stack_mappings)) { return false; }
for (auto &it : heap_mappings) {
HeapIterate(it, [&](uintptr_t base, size_t size) { heap_walker_.Allocation(base, base + size); });
}
for (auto &it : anon_mappings) {
heap_walker_.Allocation(it.begin, it.end);
}
for (auto &it : globals_mappings) {
heap_walker_.Root(it.begin, it.end);
}
if (!threads.empty()) {
for (auto &thread_it : threads) {
for (auto &it : stack_mappings) {
if (thread_it.stack.first >= it.begin && thread_it.stack.first <= it.end) {
heap_walker_.Root(thread_it.stack.first, it.end);
}
}
heap_walker_.Root(thread_it.regs);
}
} else {
for (auto &it : stack_mappings) { heap_walker_.Root(it.begin, it.end); }
}
ALOGI("searching done");
return true;
}GetUnreachableMemory (Core Function)
// HeapWalker.cpp
void HeapWalker::RecurseRoot(const Range &root) {
std::vector<Range> to_do(1, root);
while (!to_do.empty()) {
Range range = to_do.back();
to_do.pop_back();
ForEachPtrInRange(range, [&](Range &ref_range, AllocationInfo *ref_info) {
if (!ref_info->referenced_from_root) {
ref_info->referenced_from_root = true;
to_do.push_back(ref_range);
}
});
}
}
bool HeapWalker::DetectLeaks() {
for (auto &it : roots_) { RecurseRoot(it); }
Range vals{reinterpret_cast<uintptr_t>(root_vals_.data()),
reinterpret_cast<uintptr_t>(root_vals_.data()) + root_vals_.size() * sizeof(uintptr_t)};
RecurseRoot(vals);
return true;
}
bool MemUnreachable::GetUnreachableMemory(allocator::vector<Leak> &leaks,
size_t limit, size_t *num_leaks, size_t *leak_bytes) {
ALOGI("sweeping process %d for unreachable memory", pid_);
leaks.clear();
if (!heap_walker_.DetectLeaks()) { return false; }
// Data aggregation omitted for brevity
return true;
}Taobao Release Package Improvements
After Android 10, access to private process files such as /proc/pid/maps is restricted, causing libmemunreachable to fail in release builds. Taobao recompiled the library and changed the maps‑reading logic to use /proc/self/maps, which remains accessible to the forked child.
Additional fixes include setting prctl(PR_SET_DUMPABLE, 1) to allow ptrace on release binaries and engineering changes to report leak data through TBRest to EMAS, while gracefully handling missing thread‑register information.
Possible False‑Positive Scenarios
If a heap allocation is referenced via a base‑plus‑offset pointer stored in .bss or .data, the detector may incorrectly label it as a leak because the actual pointer value is not directly reachable.
Conclusion
libmemunreachable employs a child‑process, mark‑and‑sweep approach that imposes virtually no overhead on the main thread, making it an effective zero‑overhead native memory leak detector for Android applications.
References
libmemunacachable documentation: https://android.googlesource.com/platform/system/memory/libmemunreachable/+/master/README.md
Scudo memory allocator: https://source.android.google.cn/docs/security/test/scudo?hl=zh-cn
jemalloc allocator: http://jemalloc.net/
malloc_debug: https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
