Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions lib/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Copy link
Copy Markdown
Member

@oleavr oleavr May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed write-up.

Before merging I want to understand the findSymbolByName vs enumerateSymbols split, because in Gum both go through the same gum_elf_module_enumerate_symbols, which already falls back to .gnu_debugdata when .symtab is missing. So in principle they shouldn't disagree.

A few questions:

  1. Which frida / frida-server version on the A16 device? The mini-debuginfo fallback landed in gum 8ed32c4d (Dec 2024), the dynsym fallback in 01eadbff (Mar 2026).
  2. Does findSymbolByName('libart.so', '_ZN3art2gc4Heap22CollectGarbageInternalENS0_9collector6GcTypeENS0_7GcCauseEbj') start working after an
    enumerateSymbols() pass on the same module? That would point at a state/ordering bug in Gum.
  3. readelf -S on the device's libart.so — which of .symtab / .dynsym / .gnu_debugdata are present?

If it's a Gum bug I'd rather fix it there.

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,
Expand Down Expand Up @@ -146,6 +167,9 @@ function _getApi () {
if (address === null) {
address = module.findSymbolByName(name);
}
if (address === null) {
address = resolveDebugdataSymbol(module, name);
}
return address;
},
flavor,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -2209,13 +2231,25 @@ 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');
}
if (copyingPhase !== null) {
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) {
Expand Down