From b77771a75610afb02f48cb444b29edadd6f8809f Mon Sep 17 00:00:00 2001 From: Georgi Boiko Date: Tue, 6 Oct 2026 12:39:43 +0100 Subject: [PATCH 1/2] android: Clone replacements into LinearAlloc 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(). --- lib/android.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/android.js b/lib/android.js index d023166..8dfe357 100644 --- a/lib/android.js +++ b/lib/android.js @@ -291,6 +291,9 @@ function _getApi () { }; }, + // userfaultfd GC for cloneArtMethod() + _ZN3art7Runtime22CreateCalleeSaveMethodEv: ['art::Runtime::CreateCalleeSaveMethod', 'pointer', ['pointer']], + _ZN3art3Dbg14SetJdwpAllowedEb: ['art::Dbg::SetJdwpAllowed', 'void', ['bool']], _ZN3art3Dbg13ConfigureJdwpERKNS_4JDWP11JdwpOptionsE: ['art::Dbg::ConfigureJdwp', 'void', ['pointer']], _ZN3art31InternalDebuggerControlCallback13StartDebuggerEv: ['art::InternalDebuggerControlCallback::StartDebugger', 'void', ['pointer']], @@ -325,6 +328,9 @@ function _getApi () { }, _ZN3art3Dbg15gDebuggerActiveE: function (address) { this.isDebuggerActive = () => !!address.readU8(); + }, + _ZN3art15gUseUserfaultfdE: function (address) { + this.isUserfaultfdGcActive = !!address.readU8(); } }, optionals: new Set([ @@ -343,6 +349,7 @@ function _getApi () { '_ZN3art6mirror6Object5CloneEPNS_6ThreadE', '_ZN3art6mirror6Object5CloneEPNS_6ThreadEm', '_ZN3art6mirror6Object5CloneEPNS_6ThreadEj', + '_ZN3art7Runtime22CreateCalleeSaveMethodEv', '_ZN3art22IndirectReferenceTable3AddEjPNS_6mirror6ObjectE', '_ZN3art22IndirectReferenceTable3AddENS_15IRTSegmentStateENS_6ObjPtrINS_6mirror6ObjectEEE', '_ZN3art2gc4Heap12VisitObjectsEPFvPNS_6mirror6ObjectEPvES5_', @@ -363,6 +370,7 @@ function _getApi () { '_ZN3art3Dbg13ConfigureJdwpERKNS_4JDWP11JdwpOptionsE', '_ZN3art31InternalDebuggerControlCallback13StartDebuggerEv', '_ZN3art3Dbg15gDebuggerActiveE', + '_ZN3art15gUseUserfaultfdE', '_ZN3art15instrumentation15Instrumentation20EnableDeoptimizationEv', '_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEPKc', '_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEv', @@ -3972,7 +3980,22 @@ function cloneArtMethod (method, vm) { return api['art::mirror::Object::Clone'](method, thread); } - return Memory.dup(method, getArtMethodSpec(vm).size); + const artMethodSize = getArtMethodSpec(vm).size; + + if (api.isUserfaultfdGcActive) { + // The CMC GC only updates declaring_class_ of ArtMethods in LinearAlloc, so a clone elsewhere goes stale on + // compaction. CreateCalleeSaveMethod() is exported and allocates its ArtMethod in the runtime's LinearAlloc. + let artMethodClone; + vm.perform(env => { + withRunnableArtThread(vm, env, () => { + artMethodClone = api['art::Runtime::CreateCalleeSaveMethod'](api.artRuntime); + Memory.copy(artMethodClone, method, artMethodSize); + }); + }); + return artMethodClone; + } + + return Memory.dup(method, artMethodSize); } export function deoptimizeMethod (vm, env, method) { From fcdd7186467e8c6dfdcef888208807c9fc0424db Mon Sep 17 00:00:00 2001 From: Georgi Boiko Date: Wed, 14 Oct 2026 15:24:28 +0100 Subject: [PATCH 2/2] android: cache LinearAlloc slots for ART method clones 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. --- lib/android.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/android.js b/lib/android.js index 8dfe357..6b40f24 100644 --- a/lib/android.js +++ b/lib/android.js @@ -108,6 +108,7 @@ let MethodMangler = null; let artController = null; const inlineHooks = []; const patchedClasses = new Map(); +const linearAllocClones = new Map(); const artQuickInterceptors = []; let thunkPage = null; let thunkOffset = 0; @@ -3985,13 +3986,18 @@ function cloneArtMethod (method, vm) { if (api.isUserfaultfdGcActive) { // The CMC GC only updates declaring_class_ of ArtMethods in LinearAlloc, so a clone elsewhere goes stale on // compaction. CreateCalleeSaveMethod() is exported and allocates its ArtMethod in the runtime's LinearAlloc. - let artMethodClone; - vm.perform(env => { - withRunnableArtThread(vm, env, () => { - artMethodClone = api['art::Runtime::CreateCalleeSaveMethod'](api.artRuntime); - Memory.copy(artMethodClone, method, artMethodSize); + const key = method.toString(); + let artMethodClone = linearAllocClones.get(key); + // LinearAlloc never frees, so each method keeps its clone; re-copying it would race with calls still in flight. + if (artMethodClone === undefined) { + vm.perform(env => { + withRunnableArtThread(vm, env, () => { + artMethodClone = api['art::Runtime::CreateCalleeSaveMethod'](api.artRuntime); + Memory.copy(artMethodClone, method, artMethodSize); + }); }); - }); + linearAllocClones.set(key, artMethodClone); + } return artMethodClone; }