From 759ab0c8ed404b2b71d9295e362991be5a01963f Mon Sep 17 00:00:00 2001 From: Kolektori <256073454+Kolektori@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:04:50 +0400 Subject: [PATCH] android: Add .gnu_debugdata fallback for stripped libart on Android 16 Android 16 ships libart.so without .symtab, so findExportByName and findSymbolByName miss internal ART symbols. Parse .gnu_debugdata via enumerateSymbols() and cache the result per module. Also attach the GC synchronize-on-leave hook to MarkCompact::RunPhases for Android 16's Concurrent Mark Compact collector, and fall back to ConcurrentCopying::RunPhases when CopyingPhase is inlined. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/android.js | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/android.js b/lib/android.js index 11daebe..626bb3b 100644 --- a/lib/android.js +++ b/lib/android.js @@ -12,6 +12,27 @@ import VM from './vm.js'; const jsizeSize = 4; const pointerSize = Process.pointerSize; +// Fallback for libart.so on Android 16, which is stripped (no .symtab) +// but carries a .gnu_debugdata section. enumerateSymbols() parses that; +// findExportByName / findSymbolByName do not. +const debugdataSymbolCache = new WeakMap(); +function resolveDebugdataSymbol (module, name) { + let byName = debugdataSymbolCache.get(module); + if (byName === undefined) { + byName = new Map(); + try { + for (const sym of module.enumerateSymbols()) { + if (!sym.address.isNull() && !byName.has(sym.name)) { + byName.set(sym.name, sym.address); + } + } + } catch (_) { + } + debugdataSymbolCache.set(module, byName); + } + return byName.get(name) ?? null; +} + const { readU32, readPointer, @@ -146,6 +167,9 @@ function _getApi () { if (address === null) { address = module.findSymbolByName(name); } + if (address === null) { + address = resolveDebugdataSymbol(module, name); + } return address; }, flavor, @@ -2137,9 +2161,8 @@ function instrumentArtMethodInvocationFromInterpreter () { function instrumentArtGarbageCollection () { const api = getApi(); - const art = api.module; - const gc = art.findSymbolByName('_ZN3art2gc4Heap22CollectGarbageInternalENS0_9collector6GcTypeENS0_7GcCauseEbj'); + const gc = api.find('_ZN3art2gc4Heap22CollectGarbageInternalENS0_9collector6GcTypeENS0_7GcCauseEbj'); if (gc === null) { return; } @@ -2159,9 +2182,8 @@ function instrumentArtFixupStaticTrampolines () { ['_ZN3art11ClassLinker26VisiblyInitializedCallback29AdjustThreadVisibilityCounterEPNS_6ThreadEl', '7f0f00f9 : 1ffcffff'], ]; const api = getApi(); - const art = api.module; for (const [name, pattern] of patterns) { - const base = art.findSymbolByName(name); + const base = api.find(name); if (base === null) { continue; } @@ -2209,6 +2231,10 @@ function ensureArtKnowsHowToHandleReplacementMethods (vm) { const api = getApi(); if (apiLevel > 28) { copyingPhase = api.find('_ZN3art2gc9collector17ConcurrentCopying12CopyingPhaseEv'); + if (copyingPhase === null) { + // CopyingPhase can be inlined; RunPhases is the enclosing entry. + copyingPhase = api.find('_ZN3art2gc9collector17ConcurrentCopying9RunPhasesEv'); + } } else if (apiLevel > 22) { copyingPhase = api.find('_ZN3art2gc9collector17ConcurrentCopying12MarkingPhaseEv'); } @@ -2216,6 +2242,14 @@ function ensureArtKnowsHowToHandleReplacementMethods (vm) { Interceptor.attach(copyingPhase, artController.hooks.Gc.copyingPhase); } + // Android 16 defaults to Concurrent Mark Compact; attach the same + // synchronize-on-leave callback so entrypoints stay consistent across + // CMC cycles. See frida-java-bridge#387. + const markCompactRunPhases = api.find('_ZN3art2gc9collector11MarkCompact9RunPhasesEv'); + if (markCompactRunPhases !== null) { + Interceptor.attach(markCompactRunPhases, artController.hooks.Gc.copyingPhase); + } + let runFlip = null; runFlip = api.find('_ZN3art6Thread15RunFlipFunctionEPS0_'); if (runFlip === null) {