Skip to content

Let dex obfuscation be turned off, and say which way it is set - #1063

Closed
JingMatrix wants to merge 1 commit into
masterfrom
fix/dex-obfuscation-switch
Closed

JingMatrix wants to merge 1 commit into
masterfrom
fix/dex-obfuscation-switch

Conversation

@JingMatrix

@JingMatrix JingMatrix commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

enable_dex_obfuscate has a row in modules_config.db and nothing in this repository reads it. The answer lives in DaemonState as a constructor default, !BuildConfig.DEBUG, under a comment saying it is not configurable: a release daemon renames the legacy Xposed API classes in the framework dex and in every module dex, a debug daemon leaves them alone, and no setting in the manager and no key in config set could say otherwise. #1054 asks for the switch by name and reports the CLI rejecting it.

It is worth having because of what the rewrite reaches. obfuscateDexBuffer walks a dex's string table and nothing else, so a module that names one of those prefixes where a dex writer cannot follow keeps the original name. A native library is the case that occurs: the JNI signature strings compiled into it still read Lde/robv/android/xposed/callbacks/XC_LoadPackage$LoadPackageParam;, RegisterNatives finds no method of that shape, and the module loads with its native entry points unbound — No implementation found for void MainHook.initZygote, which is #1054's report. The library is the module's own build output, so nothing in this daemon can fix it. Turning the rewrite off is the way out, and that is what was unreachable.

So the preference is read, once, while ConfigCache is initialized — after applySqliteHelperWorkaround, which has to be in place before the configuration database is opened, and caught, because a daemon that cannot read its own preference must still start. Once, because FileSystem.getPreloadDex caches the framework dex the first time it is asked for: the answer that produced it is the only one the rest of the process may act on, and a module loaded against the other answer would look for class names the framework does not have. A write therefore takes effect on the next boot, and config set says so when the daemon it is talking to is running the other way.

Also here, the diagnosis in #1054 was led astray by a log line. The reporter ruled obfuscation out on both builds because both printed Fetched obfuscation map with 8 entries, and that line cannot distinguish them: the daemon answers with every prefix either way and maps each to itself when obfuscation is off. FetchObfuscationMap compares the pairs and names the configuration, which is the one thing a reader looking at an unbound native method needs to tell apart. It also means "3080 fails, 3116 works" needs no code change to explain it, if one of those artifacts was the Debug build — worth asking before anything is bisected.

Left alone: the identity pairs themselves. Answering with an empty map when obfuscation is off would be tidier and would crash every injected process — SetupEntryClass and HookBridge reach for their entries with std::map::at, and GetXResourcesClassName gives up on a missing key. Also left alone: the manager, which still has no switch for this. A daemon preference with a CLI key is what #1054 needs to work around the defect; a row on the status page is a separate question, and it needs a string in eighteen languages.

Not tested on a device.

`enable_dex_obfuscate` has a row in modules_config.db and nothing has
ever read it. `DaemonState` carried the answer as a constructor default,
`!BuildConfig.DEBUG`, marked as not configurable: a release daemon
renamed the legacy Xposed API classes in the framework dex and in every
module dex, a debug daemon left them alone, and no setting in the
manager and no key in the CLI could say otherwise. The preference is
read now, once, while `ConfigCache` is initialized, and `config get` and
`config set` take `dex-obfuscation` beside the two keys they already
took.

Read once because it has to be. `FileSystem.getPreloadDex` caches the
framework dex the first time it is asked for, so the answer that
produced it is the only one the rest of the process may act on; a module
loaded against the other answer would look for class names the framework
does not have. A write therefore takes effect on the next boot, and the
CLI says so when the daemon it is talking to is running the other way.

What makes the setting worth having is that the rewrite reaches a dex's
string table and nothing else. A module that names one of those prefixes
where a dex writer cannot follow keeps the original name: the JNI
signatures compiled into a module's native library still read
Lde/robv/android/xposed/..., `RegisterNatives` finds no method of that
shape, and the module loads with its native entry points unbound. The
library is the module's own build output, so nothing in this daemon can
fix it; turning the rewrite off is the way out, and that is now
reachable.

Also here: the zygote side's "Fetched obfuscation map with N entries"
said the same thing in both configurations, because the daemon answers
with every prefix either way and maps each to itself when obfuscation is
off. It compares the pairs and names the configuration, which is the one
thing a reader looking at an unbound native method needs to tell apart.

Refs #1054
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.

[Bug] Legacy module dex obfuscation leaves Dex2C JNI signatures unrewritten — RegisterNatives fails on 3080, works on 3116

1 participant