Skip to content

Upstream - #18

Open
superturtlee wants to merge 6 commits into
superturtlee:upstreamfrom
JingMatrix:master
Open

Upstream #18
superturtlee wants to merge 6 commits into
superturtlee:upstreamfrom
JingMatrix:master

Conversation

@superturtlee

Copy link
Copy Markdown
Owner

No description provided.

JingMatrix and others added 6 commits July 25, 2026 06:32
Gradle 9.6 removed the internal API that AGP 8.13 relied on, forcing the
AGP 9 upgrade and the surrounding build-script changes.

Toolchain:
- Gradle wrapper 9.3.1 -> 9.6.1; AGP 8.13.1 -> 9.3.1; apksig follows AGP.
- Adopt AGP 9 built-in Kotlin: remove org.jetbrains.kotlin.android from all
  Android modules; Kotlin stdlib 2.3.10 -> 2.4.10.
- compileSdk/targetSdk 36 -> 37, build-tools 37.0.0.

Build-script migration:
- Root build configures the shared CommonExtension through its getters, since
  AGP 9 dropped the action-DSL methods on that type.
- daemon generates SignInfo through androidComponents.onVariants and a typed
  task; android.applicationVariants was removed.

Resource generators (replace the rikka autoResConfig / materialthemebuilder
plugins, whose entry points use removed AGP variant APIs):
- buildSrc/GenerateLangListTask scans the translated locales.
- buildSrc/GenerateMaterialThemeTask computes the accent-color theme overlays,
  reusing the materialthemebuilder color library without applying its plugin.

Drop android.nonFinalResIds=false:
AGP 9 enables optimized resource shrinking by default, and that shrinker
requires non-final resource IDs. With isShrinkResources=true the manager's
release build now fails :app:minifyReleaseWithR8 with "Optimized resource
shrinking requires non-final IDs". AGP offers two remedies: make the IDs
non-final, or opt out of optimized shrinking (r8.optimizedResourceShrinking=
false). We take the former, because the false setting turns out to be dead
weight:
- It was added in 348f049 (Aug 2023), an unrelated "show packagename" feature
  commit, as a one-line drop-in beside the now-removed experimental flags
  enableAppCompileTimeRClass / enableNewResourceShrinker.preciseShrinking. Those
  siblings were cleaned up later; this line was simply missed.
- Final IDs are only actually required to use R.* as Java switch/case labels.
  There are zero `case R.*` occurrences in the tree at 348f049 and at every
  commit since, so the flag never protected anything here.
- Non-final IDs are the modern AGP default, so removing the line (rather than
  writing =true) expresses the intent with no config at all.
It also builds smaller: the optimized shrinker trims the release APK from
~3.45 MB to ~3.13 MB (~9%). Verified end to end -- assembleRelease plus a
zygisk installKsuAndReboot run that loads the module and starts lspd on device.

Formatting task:
- Add buildSrc/src/main/kotlin to the format task so the generator sources are
  formatted with the rest of the Kotlin build logic.
- Exclude daemon/**, which is intentionally kept on ktfmt's default (Meta)
  style; formatting it here fought :daemon:ktfmtFormat and flipped the style
  back and forth.

Dependencies:
- AGP/apksig 9.3.1, Kotlin 2.4.10, androidx.core 1.19.0 (dependabot maven group).
- coroutines 1.11.0, okhttp 5.4.0, gson 2.14.0, nav 2.9.8, glide 5.0.9,
  androidx activity/browser/annotation, ktfmt 0.26.0.
- Material kept at 1.12.0; 1.13+ removes the colorPrimary/colorError attrs the
  manager references.
- Submodules fmt and commons-lang bumped; CI action versions bumped
  (actions/checkout 6 -> 7, actions/cache 5 -> 6).
Android 17 (API 37) reshaped the IServiceConnection callback and dropped the
old overload instead of keeping both:

    void connected(in ComponentName name, IBinder service,
                   in @nullable IBinderSession session, boolean dead);

ManagerGuard overrode only the three-argument form, so as soon as system_server
dispatched the new transaction the Stub landed on an abstract method and the
daemon died with AbstractMethodError, taking the manager session down with it.
Only the Xiaomi XSpace workaround binds this connection, which is why the crash
was reported on HyperOS first; the interface change itself ships in stock
Android 17 and is not vendor specific.

Declare both overloads in the IServiceConnection stub and override both in
ManagerGuard, so every supported release finds the method system_server
dispatches. IBinderSession is stubbed as an empty interface, since the type is
only referenced by the descriptor of the new overload.
Brings the framework in line with the API version master vendors, checked
throughout with test modules that assert the documented behaviour and log
pass/fail, on a Pixel 6 running Android 17.

hookClassInitializer never worked: it aborted the process, and once that was
fixed the hook still could not fire, because resolving <clinit> through JNI
runs the initialiser during the lookup. It finds the method from ART's layout
now — the gap the reflected members leave behind — and the hooker runs ahead of
the class's own initialiser.

Also fixed: the interceptor chain could resurrect an exception it had already
suppressed; ExceptionMode.DEFAULT ignored module.prop, so passthrough was
unreachable; getInvoker() threw NPE on an unhooked method; Constructor
.newInstance was hookable; an unhooked constructor dispatched Method.invoke's
id against itself; getArgs() was mutable; a late-injected system server
dispatched onSystemServerStarting into an empty module set; android:ui was
reported to modules as the system package; edit().clear() did nothing, and
preference updates reached every Android user's hooked processes; empty scope
requests never called back; openRemoteFile threw RemoteException where
FileNotFoundException is documented; getScope() repeated a package once per
user; module.prop was not parsed as Properties.

In the manager, one malformed module.prop could blank the entire module list.
The list now shows which API each module targets, and staticScope is enforced
rather than parsed and ignored — in the picker, in the daemon, and by dropping
stale rows at startup.

Two changes modules will notice:

- Invoker.invoke reports the target's exception wrapped in
  InvocationTargetException on every path, as Method#invoke does. A module
  catching the raw exception will stop catching it.
- A module declaring staticScope loses scope entries outside its scope.list.

The <clinit> lookup is measured on one device, one Android version and one
architecture. Its assumptions are re-checked at runtime and it declines rather
than guesses, so an unfamiliar layout degrades to "no static initializer"
rather than misbehaving.
Since #648 the `ServiceManager.addService` call that claims the `serial` name
sat inside the `SDK_INT >= R` branch that exists only for
`registerForNotifications`, so on pre-R nothing claimed it and the Zygisk module
aborted the injection. Keep only `registerForNotifications` behind the check.

`startActivityAsUserWithFeature` is also R-only, and both call sites used it
unconditionally; the `NoSuchMethodError` escaped `onTransact` and killed the
daemon. Route them through `startActivityAsUserCompat`, like
`registerReceiverCompat`.

Closes #773.
An Actions artifact cannot be downloaded without a GitHub account: measured
against this repository, `GET /actions/artifacts/<id>/zip` answers 401 to an
anonymous caller while a release asset answers 206. Testing a canary is the
lowest-friction way for an ordinary user to help, and asking each of them to grant
an OAuth app something first — to work around where the zips happen to live — is a
real cost for a project whose users are careful about what they install. It also
excludes the users who cannot reach GitHub's login page at all, who are exactly
the ones a canary programme loses first.

Each push to master, and each manual run, now attaches the same two zips to a
`canary-<versionCode>` prerelease. Prerelease, so `releases/latest` — which is what
update checks read — keeps pointing at the last stable tag. Artifacts stay: they
also carry mappings and symbols, which are for us rather than for testers.

Five are kept. They are pruned by version code rather than by date, because the
version code is the commit count and therefore monotonic, while dates can be
disordered by a rerun or a revert. The release is deleted and recreated rather
than edited, so re-running the workflow for a commit replaces that build instead
of appending a second copy of every asset to it.

The job gains `contents: write`, which it did not have; everything else in it only
reads.
The parasitic manager lives inside com.android.shell, which has no INTERNET
permission before Android 12, so preAppSpecialize appends the INET group to its
gid array. That satisfies setgroups() and nothing else: once
nativeForkAndSpecialize returns, Zygote#forkAndSpecialize runs
setAllowNetworkingForProcess(containsInetGid(gids)) against the array it passed
in, not the one we substituted, and turns networking off in libnetd_client.
socket() and dns_open_proxy() then return EPERM whatever groups we belong to,
which libcore reports as "Permission denied (missing INTERNET permission?)" —
an unchecked SecurityException that killed the OkHttp dispatcher and the manager
with it, and left the Repository tab empty.

So overwrite the first entry of the caller's array too; it has no further use
once specialization is done. LSPosed did this from the commit that introduced
the parasitic manager, and the line was dropped in the rewrite for the new
Zygisk architecture. Android 12 and later were never affected: com.android.shell
declares INTERNET there, so the array already carries the group.

Fixes #636
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.

3 participants