Skip to content

android: Clone replacements into LinearAlloc - #410

Open
pandasauce wants to merge 2 commits into
frida:mainfrom
pandasauce:feature/new-garbage-collector
Open

pandasauce wants to merge 2 commits into
frida:mainfrom
pandasauce:feature/new-garbage-collector

Conversation

@pandasauce

@pandasauce pandasauce commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Background

Java objects, including class objects, live on the managed heap. Android's GC moves them around to defragment memory. After moving something, it must fix every pointer to it.

ART can fix the class pointer inside an ArtMethod only because it knows where those structs are. It allocates them in its own tracked memory area, called LinearAlloc, and walks that area to update the pointers.

Latest ART uses a new moving collector, CMC. It only walks LinearAlloc when it moves things.

Frida's copy lived in ordinary native memory, so the GC never saw it and never updated its class pointer. After a compaction, the Frida copy's class pointer was stale. The next call into the hooked method, or the GC's own walk of a thread's stack, followed the pointer and read garbage. That produced crashes.

Frida already had a workaround: GC hooks that copy the pointer back from the original afterwards. On my device that fix-up ran too late, or not at all. It runs after the GC finishes, and it has to wait for the script lock.

Change summary

When the device uses CMC, Frida asks ART to allocate the copy's memory. It calls an exported ART function that returns a fresh ArtMethod slot in LinearAlloc, then copies the original into it. The GC now knows about the copy and fixes its class pointer like any other method. Other collectors still use the old plain copy.

Caveats:

  • The slot is never freed (LinearAlloc does not expose an API to do so), but is re-used on each re-assignment of .implementation, so we only pay the price once per method
    • An alternative was explored with spinning up a dummy ClassLoader per hook and tearing it down, thus forcing GC to collect the slots, but the cost would outweigh the benefits, and it would gift every RASP a list of all Java hooks
  • It relies on that ART function (CreateCalleeSaveMethod) allocating in LinearAlloc, which holds on my device's libart.so but isn't guaranteed across all devices/ART versions

@pandasauce
pandasauce force-pushed the feature/new-garbage-collector branch from 6536b5b to 739c46f Compare October 6, 2026 12:24
With the new userfaultfd (CMC) collector, ART only updates the
declaring_class_ of ArtMethods allocated in its LinearAlloc. The
replacement clones lived on the native heap, so they went stale on
compaction and crashed DoCall and the GC's own stack walk.

When gUseUserfaultfd is set, allocate the clone with
Runtime::CreateCalleeSaveMethod() on a runnable thread and copy the
original into it. Other collectors keep using Memory.dup().
@pandasauce
pandasauce force-pushed the feature/new-garbage-collector branch from 739c46f to b77771a Compare October 6, 2026 12:26
Without this, we would leak every time .implementation was reassigned.

With this, we only leak the first time a method gets hooked, future
reassignments reuse the existing LinearAlloc slot.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant