Skip to content

Keep XResources out of the classes R8 shares, and stamp saved archives - #851

Open
JingMatrix wants to merge 2 commits into
masterfrom
xresources-shared-synthetics
Open

Keep XResources out of the classes R8 shares, and stamp saved archives#851
JingMatrix wants to merge 2 commits into
masterfrom
xresources-shared-synthetics

Conversation

@JingMatrix

Copy link
Copy Markdown
Owner

Modules loaded but none of their hooks landed in release builds, while debug builds were fine (#847, #848).

Some framework types extend super classes that no dex contains: they are generated on the device, so they can inherit from whichever platform classes it provides. Such a type cannot be resolved until that has happened, and a failed resolution is permanent, so the fragility is transitive — every class naming one acquires it.

R8 spread it beyond what the source shows. Each lambda becomes a class, and classes of the same shape are merged afterwards, so XResources' two lambdas put it inside the shared Function synthetic — the one commons-lang's ClassUtilsX instantiates from its static initialiser, which XposedHelpers.findClass calls. Every findClass in system_server failed from then on.

Both lambdas are now written out long-hand. Keep rules cannot state this invariant, since they govern the classes you write rather than the ones an optimiser invents, so checkXResourcesIsolationRelease reads the optimised dex, resolves names through the mapping file, and fails the build if any class outside resource hooking comes to name one of these types.

Measured on an SM-A145R (Android 15, KernelSU) with NoWakeLock 3.0.10, same scope both sides:

v2.1 3073 this branch
xposed.dummy.XResourcesSuperClass failures 42 0
module reports itself active no yes

Saved bug reports also get one name, built in one place instead of three. Two archives attached to the same report used to be indistinguishable until opened, so deciding which to inspect first meant extracting both; the name says the build type now, and the logs zip records the commit in its comment — the version code is the commit count on master, so branch builds wear numbers they were never built from.

Some framework types extend super classes that no dex contains: they are
generated at runtime, so that they can inherit from whichever platform
classes the device actually provides. A type whose super class does not
yet exist cannot be resolved, and the runtime records the failure rather
than retrying it, so the damage outlives the window that caused it. The
fragility is transitive -- every class naming such a type acquires it.

A whole-program optimiser spreads it beyond what the source shows. Each
lambda is lowered to a class of its own, and classes of the same shape are
merged afterwards, so a lambda written in a fragile class can end up
sharing its class with lambdas from anywhere in the program. The reference
then sits in a class that arbitrary code instantiates, and the
relationship exists only in the optimiser's output.

That is what happened to XResources: its two lambdas put it inside the
shared Function synthetic, which commons-lang's ClassUtilsX instantiates
from its static initialiser, which XposedHelpers.findClass calls. Every
findClass in system_server then failed for the rest of the boot, so no
module's hooks landed there (#847, #848). Both lambdas are written out
long-hand now.

Keep rules cannot state the invariant, since they govern the classes one
writes rather than the ones an optimiser invents, so it is checked where
it is decided. checkXResourcesIsolationRelease reads the optimised dex,
resolves class names through the mapping file, and fails the build if any
class outside resource hooking has come to name one of these types.
Three places produced one and named it three ways: the log panel and the
troubleshooting page formatted a translated string resource, and the root
export built its own name inline. They now share logArchiveName(), which
puts the build type in front of the stamp -- a report from a debug build
explains behaviour a release build does not have, and the file name can
say so without a round trip to ask.

The name is no longer a string resource. It was translated into nineteen
locales, where the only thing the translations could do was disagree.

The extension stays a parameter: the manager writes a zip through SAF,
while the root path shells out to tar, which is what Android ships.

An archive now also records which commit wrote it, since the version code
is the commit count on master and every branch build at the same depth
wears the number of an official build it was never made from. Each format
carries it where that format allows: the log zip in its comment field, the
module backup as a field of its own document, since gzip's comment is not
reachable through GZIPOutputStream. tar has no such slot, so the root
export still says only what its name says.
@JingMatrix JingMatrix linked an issue Aug 2, 2026 that may be closed by this pull request
1 task
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.

Release builds: XPL-EX (eu.faircode.xlua) loads but reports itself inactive

1 participant