Recipe — debugging¶
How to debug rosetta-frida hooks when something goes wrong. The
library's diagnostic surface is structured and exhaustive — most
problems surface as a specific error with a clear message before
they cascade into mystery null/undefined failures.
Trace mode¶
The single most useful debugging tool. Set trace: true on the
session:
Every diagnostic event prints to console.error as a single readable
line:
[rosetta] detect auto: [email protected]
[rosetta] map-load [email protected] schema=4 classes=15
[rosetta] health-check PASS rate=100.0% threshold=80.0% failures=0
[rosetta] com.example.app.IRemoteService$Stub ← aaaa (map)
[rosetta] com.example.app.IRemoteService$Stub.requestTicket ← c (map) (Landroid/os/Bundle;Lbbbb;)V
[rosetta] com.example.app.IServiceCallback ← bbbb (map)
Each line tells you what was resolved, where it came from
(cache, map, override), and (for methods) the picked overload's
signature.
A miss appears as:
Programmatic event subscription¶
Trace mode is for development. For programmatic surfacing —
e.g. failing CI on misses, or aggregating diagnostics to a host
controller — subscribe via rosetta.events:
rosetta.events.onType('resolve', (e) => {
if (e.miss) {
send({ alert: 'unresolved', name: e.name, scope: e.classScope });
}
});
rosetta.events.onType('health-check', (e) => {
if (!e.passed) {
send({ alert: 'health-check-failed', rate: e.rate, failed: e.failedEntries });
}
});
rosetta.events.onType('detect', (e) => {
send({ stage: 'detect', app: e.app, version: e.version, source: e.source });
});
rosetta.events.onType('map-load', (e) => {
send({ stage: 'map-load', classes: e.classCount });
});
Trace and programmatic subscription coexist — the same event reaches both.
Common errors and what they mean¶
ResolveError¶
"rosetta-frida: cannot resolve class
com.example.app.IFoo— not in the map for[email protected]."
The real name isn't in the loaded map. Two common causes:
- Typo in the real name. Check your hook source against the map's class keys.
- Map is incomplete. Add the entry; or use
rosetta initfor a class scaffold and fill it in.
For method/field misses, the error tells you both the class scope
and the missing member name. See
ResolveError for the full
structured fields.
AmbiguousOverloadError¶
"rosetta-frida: 2 overloads exist for
BlobCache.put; passargsto disambiguate."
You used the string form of rosetta.hook on a multi-overload
method. Switch to the object form with args to disambiguate. See
Overloaded methods recipe.
MapValidationError¶
"rosetta-frida: invalid map"
The map file fails the schema check. The error carries issues: { path,
message }[] with one entry per problem:
catch (e) {
if (e instanceof MapValidationError) {
for (const issue of e.issues) {
console.error(` at ${issue.path}: ${issue.message}`);
}
}
}
Run rosetta validate <map> to surface these
on the CLI before the runtime sees them.
MapVersionMismatchError¶
"rosetta-frida: loaded map is for
[email protected]but the running process is[email protected]. Provide a map for3.4.6or passversionMatch: 'fuzzy'."
The detected (app, version) doesn't match the loaded map's. Three
fixes:
- Author a map for the running version. The canonical answer.
- Pass an override:
rosetta.session({ map, version: '3.4.5' })— forces the session to behave as if3.4.5is running. Use only for testing or known-compatible cases. - Enable fuzzy version matching on a registry bundle:
versionMatch: 'fuzzy'. See Multi-version bundle recipe.
HealthCheckFailedError¶
"rosetta-frida: health check failed for
[email protected]— rate=65.0% threshold=80.0%, 4 entry/entries did not resolve."
Health check failed and failurePolicy === 'strict'. The error
carries rate, threshold, and the full failedEntries: string[]
list. Either:
- Audit the failed entries. Each one's obfuscated name either rotated or never existed. Update the map.
- Lower the threshold for legitimate reasons (e.g. some classes
are loaded later by the app):
healthCheckThreshold: 0.5. - Switch to
warnpolicy for development:failurePolicy: 'warn'. The session still emits the event but doesn't throw.
JsonParseError¶
"Invalid JSON: Unexpected token ... (line 12, column 4)"
The strict-JSON source doesn't parse. The error carries line and
column. Find the location, fix the syntax (usually a missing
comma or quote — note that comments and trailing commas are not
valid JSON and surface here too), retry.
MarkerBlockError¶
"rosetta-frida: no rosetta-frida marker block found in bundle"
The bundle doesn't have a marker block. Either:
- The build pipeline didn't embed one. Add the manual marker- wrapping step (see frida-compile integration recipe).
- The minifier stripped the marker. Check that your minifier
preserves
/*! ... */"important" comments.
Other MarkerBlockError cases — payload not valid JSON, no const
__rosetta_map = ... declaration — usually mean someone hand-edited
the compiled bundle. Use rosetta patch instead of editing.
UnresolvedAccessError¶
"rosetta-frida: cannot use sentinel for
com.example.app.IFoo— name is not in the map for[email protected]."
You're in failurePolicy: 'warn' mode and the warning sentinel
escaped into a usage site. The miss was logged earlier; this is the
deferred crash.
Either fix the underlying miss (add the entry to the map) or switch
to 'strict' mode so the failure surfaces at the call site instead
of later.
Sentinels in 'warn' mode¶
In failurePolicy: 'warn', a miss returns a sentinel proxy instead
of throwing immediately. The sentinel records the unresolved name
and throws UnresolvedAccessError only when you actually use it.
Trade-offs:
- Pro. A miss in one hook doesn't take down the rest of the script. The script keeps running; other hooks fire normally.
- Con. The error surfaces later, at the use site, which can be confusing if the miss happened in a different module.
isSentinel(value) lets you check whether something is a sentinel
before using it — useful for adaptive logic:
import { isSentinel } from 'rosetta-frida';
const Stub = rosetta.use('com.example.app.IRemoteService$Stub');
if (isSentinel(Stub)) {
send({ stage: 'skip', reason: 'IRemoteService$Stub not in map' });
return;
}
// ... safe to use Stub ...
Inspecting what the map says¶
When you don't know whether a name is mapped, query tier 3:
try {
const cls = rosetta.map.resolveClass('com.example.app.IRemoteService$Stub');
send({ stage: 'mapped', real: cls.realName, obf: cls.obfName, kind: cls.entry.kind });
} catch (e) {
if (e instanceof ResolveError) {
send({ stage: 'unmapped', name: 'com.example.app.IRemoteService$Stub' });
} else {
throw e;
}
}
Or dump the whole map:
const map = rosetta.map.extract();
send({ stage: 'map-summary', app: map.app, version: map.version, classes: Object.keys(map.classes) });
Inspecting bundles offline¶
Before running a bundle, audit it:
$ npx rosetta inspect hook.bundle.js
[email protected], schema_version 5, 15 classes
$ npx rosetta extract hook.bundle.js -o snapshot.json
$ jq '.classes | keys' snapshot.json
[
"com.example.app.IRemoteService$Stub",
"com.example.app.IServiceCallback",
...
]
If the bundle's app/version doesn't match the device you intend to target, you'll spot it before you attach.
Debugging the health check¶
Verbose mode for the health check — see every failed entry:
rosetta.events.onType('health-check', (e) => {
if (!e.passed) {
for (const name of e.failedEntries) {
send({ stage: 'failed-entry', name });
}
}
});
For each failed entry, look at the map's ClassEntry to see what
was expected:
const failed = ['com.example.app.IFoo'];
for (const name of failed) {
const entry = rosetta.map.extract().classes[name];
send({
stage: 'failed-detail',
name,
obf: entry?.obfuscated,
kind: entry?.kind,
extends: entry?.extends,
});
}
Then jadx the obfuscated class and confirm it's actually the one you expected. If not — obfuscation rotated, update the map.
When the hook itself is wrong¶
Sometimes the map is right but the hook is wrong:
- Frida silently ignores the implementation. Usually a typo in
the real name passed to
rosetta.hook(...). Compare against your map's keys. - The hook fires but on the wrong overload. Inspect at runtime
with
this.<method>.overloadsto confirm you got the one you wanted. rosetta.proceed(...)returnsundefinedwhen you expected a value. You're at the top of the chain and the original method was already replaced (or never existed). Drop to tier 3 and call the wrapper directly.
Where to file bugs¶
Pre-V1.0 release the library lives in a private repo. After release, issues will be tracked on the GitHub project. Include in any report:
- The error message (full, with stack if available).
- The session options you used (omit
mapitself if it's large). - Trace output (
trace: true) up to the point of failure. - A minimal reproducer if you can extract one.
Related¶
- Errors reference — every error class.
- Events reference — every diagnostic event.
- Session API — failure policy, trace mode, health check controls.