Skip to content

libxposed API 102 support: hot reload, detach and atomic hook replacement - #757

Merged
JingMatrix merged 13 commits into
JingMatrix:masterfrom
HSSkyBoy:master
Aug 3, 2026
Merged

libxposed API 102 support: hot reload, detach and atomic hook replacement#757
JingMatrix merged 13 commits into
JingMatrix:masterfrom
HSSkyBoy:master

Conversation

@HSSkyBoy

@HSSkyBoy HSSkyBoy commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

libxposed 102.0.0 adds hot reload — replacing a module's code inside a running process — plus three smaller things it needs to work: detach(), so one entry class can stop receiving lifecycle callbacks while its siblings carry on; hook ids and replaceHook(), so a hooker can be swapped atomically; and a rule that modules targeting 102 may no longer reach de.robv.android.xposed. It also brings autoHotReload in module.prop, and getRunningTargets() and hotReloadModule() for module apps.

The motivation is iteration cost. Changing one line in a module means force-stopping every process it is injected into, and if it hooks SystemUI or system_server that means a reboot — a minute per attempt, with whatever state you were reproducing a bug in gone with it. Reloading in place takes twenty to forty milliseconds. The rest follows: without atomic replacement a migration has to unhook and re-hook, leaving the method briefly unhooked, and the legacy API is global static state with no notion of generations, so a module that can still reach it leaves things behind that a reload cannot clean up.

For developers this is the edit-test loop, and state survives the swap through setSavedInstanceState(). For users, autoHotReload=true means an update reaches apps already running the module instead of leaving them on old code until they are next killed — offered rather than imposed, since onHotReloading() may refuse. It is not a way to push settings; remote preferences remain that.

The daemon already knows which modules it handed to which process, which is all getRunningTargets() needs, so a target is recorded as a side effect of answering rather than registered by the process. That matters for system_server, which loads its modules before the daemon's module cache exists; the channel the daemon calls back through is attached during bootstrap, carrying no module identity, for the same reason. Reloads are asynchronous with a timeout the daemon owns, since the callee runs module code and binder has none of its own. A new generation is built in full before the old one is touched; old code is then frozen, its handles captured, and onHotReloading() asked. A refusal returns FAILED with a null message, which the API reserves for exactly that, and every other failure carries a diagnostic. The swap commits before onHotReloaded() runs, because the API releases the old generation after that callback returns or throws. Replacement happens natively, swapping the record inside the callback map under the lock the per-call snapshot already takes, so a call in flight is answered by the generation it started with. Cached targets are thawed first, or a frozen process looks exactly like a module refusing.

Most of this is IPC, so the interfaces it touches have moved to org.matrix.vector.ipc and now carry documentation: what each call is for, which process it runs in, and the constraints that are otherwise invisible — that IFrameworkService numbers its transactions implicitly and may only be appended to, and that attachProcessChannel cannot be oneway, since an asynchronous binder call arrives with a calling pid of zero. The names follow: IProcessChannel is the channel the daemon calls back through, ModuleCode a module's dex with the policy read from its module.prop.

The conformance harness sits on api102-harness rather than here, since it tests a device rather than a tree. It ran against v2.1 (3076) on an Android 15 device, in Debug and Release, so the legacy-API rule was checked with dex obfuscation off and on. system_server is a target and reloads in 21 ms without restarting; a frozen target thaws, reloads in 33 ms and is re-frozen; saved state crosses the swap; a superseded handle can neither replace again nor unhook its replacement; a call already inside the chain is answered by the generation it started with; a two-entry module answers UNSUPPORTED without running module code; and autoHotReload reloads every target unprompted.

Left for later, in order: manager-service's AIDL out of org.lsposed.lspd, together with the -keep rule in the manager's proguard config; the org.lsposed.lspd.util.Utils logger; the daemon-side implementation class names, which no longer match their interfaces; and /data/adb/lspd, on-disk state and so a migration rather than a rename.

Description rewritten by @JingMatrix.

@HSSkyBoy

Copy link
Copy Markdown
Contributor Author

重新提交的 #743

HSSkyBoy added a commit to HSSkyBoy/Vector that referenced this pull request Jun 21, 2026
@HSSkyBoy

Copy link
Copy Markdown
Contributor Author

API 102 contains far more changes than just module hot reload.
So should we revise this PR title to reflect full API 102 adaptation instead of only highlighting hot-reload?

@3gf8jv4dv 3gf8jv4dv mentioned this pull request Jun 21, 2026
@RMDycz

RMDycz commented Jun 23, 2026

Copy link
Copy Markdown

大概还要多久正式构建?

@HSSkyBoy

Copy link
Copy Markdown
Contributor Author

大概还要多久正式构建?

你在這問也沒用啊,看JingMatrix什麼時候處理該PR
https://t.me/NPatch/974
你可以先試試這個

@m-doescode

Copy link
Copy Markdown

Would this apply to all modules or just ones that make use of this API? Sorry, I'm kind of a newbie. I'd find this really useful if so as I'm trying to write my own Xposed module but it's hard having to reboot every time

@JingMatrix JingMatrix linked an issue Jul 25, 2026 that may be closed by this pull request
@JingMatrix

Copy link
Copy Markdown
Owner

Review assisted by an automated pass over the libxposed 102.0.0 javadoc and AIDL. Every quote below was checked against the 102.0.0 tags, and every runtime result was measured on a real device — but the analysis is machine-assisted and I am flagging that rather than hiding it.

Thanks for the work on this — the hot reload machinery genuinely works, and I want to say that up front because most of what follows is criticism.

I merged this onto current master (clean), built it, flashed it to a Pixel 6 on Android 17, and drove it with a purpose-built API 102 module across five generations plus a separate hook target app. I then went through the libxposed 102.0.0 javadoc and AIDL against the implementation. Everything below quotes the specification verbatim; where the spec is silent I say so rather than dressing a design opinion up as a conformance problem.

The harness is on the api102-harness branch, so every measurement below can be reproduced. It drives the whole contract from adb — refusal, exceptions with and without a message, old-classloader saved state, hook registration from frozen old code, a throw from onHotReloaded, concurrent requests — and reports the target's pid and process age so a reload can be shown not to have restarted it.

The vendored xposed/libxposed and services/libxposed sources are byte-identical to the upstream 102.0.0 tags, so the spec being checked is the one you actually ship.

What conforms

Verified on device:

  • The reload cycle itself. Target detected STALE after a module update, reload swaps the generation in 70–110 ms, same pid, getOldHookHandles() replaced with new hookers. HotReloadedParam reports the right process.
  • onHotReloading returning false produces FAILED with a null message, matching HotReloadResult: "When the old module refuses reload by returning false from onHotReloading, HotReloadResult#message() is null."
  • setSavedInstanceState rejects an object created by the old module classloader with IllegalArgumentException — the exact type HotReloadingParam documents.
  • Concurrent requests answer IN_PROGRESS; a dead target disappears from getRunningTargets(). Target ids are non-reusing and framework-assigned, matching "Opaque identifier assigned by the framework".
  • Restricting hot reload to single-entry modules is exactly what package-info.java requires: "Hot reload is supported only for modules that declare exactly one Java entry class."
  • No lifecycle callback is replayed for the new generation, and the old generation stays strongly reachable across onHotReloaded.
  • freezeHooks() runs immediately before the handle list is captured, satisfying "Before the old hook handle list is captured, the framework freezes old code".
  • getApiVersion() is not overridden on the XposedInterface side; attachFramework(base, detachImpl) is called only by the framework, once per entry.
  • No classloader leak. Twenty reloads grew Dalvik Other from 16 MB to 64 MB PSS, and a forced GC returned it to 13.9 MB — below the starting baseline.

Contradicts the documentation

1. system_server is hooked by the module but never becomes a hot reload target. IXposedService.aidl:

Returns running processes currently hooked by this module.

With the test module scoped to system and to an ordinary app, the framework log shows the module loading into system_server and instantiating its entry:

[ 01:42:58.956  1000: 1653: 1653 D/VectorModuleManager ] Loading module org.matrix.hrmodule
[ 01:42:58.966  1000: 1653: 1653 V/VectorModuleManager ] Loading class class org.matrix.hrmodule.ModuleMain
[ 01:42:58.967  1000: 1653: 1653 D/VectorModuleManager ] Loaded module org.matrix.hrmodule successfully.

and getRunningTargets() returns one entry, the app process. system_server is never listed and can never be reloaded. The cause is a startup ordering problem: registerHotReloadTarget begins with ConfigCache.getModuleByPackage(...) ?: throw RemoteException("Unknown module: ..."), which is a bare state.modules[packageName] lookup with no ensureCacheReady(). When system_server loads modules, performCacheUpdate has not run — it returns immediately while packageManager is null, and PMS is published later on the very system_server thread that is currently inside loadModules. So the lookup misses, the registration throws, and VectorServiceClient swallows it (runCatching { ... }.getOrNull() ?: -1L). Nothing retries and nothing logs it. This is the target for which not rebooting matters most, so I would treat it as the headline defect rather than an edge case.

2. Hot reload of a frozen process is reported as a module refusal. HotReloadResult:

When the old module refuses reload by returning false from onHotReloading, HotReloadResult#message() is null. When reload fails because of an exception, the message contains a framework-provided diagnostic string.

Freezing the target's cgroup and requesting a reload gives status=FAILED message=null afterMs=1, with onHotReloading never invoked — byte-identical to a genuine refusal. Unfreeze and bring the process forward, and the same request reaches the module normally (afterMs=9). Same pid throughout; the only variable is the freeze state.

This is the common case, not a corner: a module's targets are usually cached background processes, and Android freezes those. The isBinderAlive check in ApplicationService.hotReloadTarget does not help — a frozen process's binder is alive, so PROCESS_DIED is not reported either — and the target is left stuck at TARGET_STATE_FAILED. The daemon already drives AMS elsewhere (ModuleService.sendBinder forges a provider call), so thawing the target for the duration of the transaction looks feasible; at minimum the frozen case must not be reported as a refusal.

The same contract is broken more generally: ModuleService.kt:180 and InjectedModuleService.kt:80 pass throwable.message straight through, so a module throwing new IllegalStateException() also yields FAILED with a null message. Measured. XposedModuleInterface is explicit that this path must carry "a framework-provided diagnostic message". Wrap the throwable class and message, and guarantee non-null on the exception path.

3. openRemoteFile is documented read-only and this makes it writable. XposedInterface:

Open a file in the module's shared data directory. The file is opened in read-only mode.

VectorContext.openRemoteFile is that method, and it is a bare pass-through to ILSPInjectedModuleService.openRemoteFile, which this PR changes from MODE_READ_ONLY to MODE_CREATE or MODE_READ_WRITE (InjectedModuleService.kt:137). The neighbouring method reinforces the intent: "Gets remote preferences stored in Xposed framework. Note that those are read-only in hooked apps." Two consequences: the documented @throws FileNotFoundException If the file does not exist becomes unreachable, and a hooked app process gets write access to the module's data directory. Measured: from org.matrix.hrtarget (uid 10320, module app is uid 10323) module code created and wrote /data/adb/lspd/modules/0/<module>/files/written_by_hooked_app.txt. The module-app-facing XposedService#openRemoteFile is separately documented as "The file will be created if not exists." — the PR collapses both onto one AIDL method and picks the module-app semantics for both. They need to stay distinct.

4. hotReloadModule is meant to enqueue, and this runs the whole reload inside the call. IXposedService.aidl:

Implementations should validate and enqueue the request promptly, then report completion through the callback.

ApplicationService.hotReloadTarget makes a blocking binder call into the target and only invokes the callback afterwards, so the module app's XposedService.hotReloadModule() blocks for the entire reload. Measured: a 4000 ms sleep in onHotReloading blocked the caller for the full 4 s. A module app calling this from the main thread will ANR, a daemon binder thread is pinned for the duration, and there is no timeout.

5. detach() does not do what its javadoc says. XposedInterfaceWrapper:

After this method is called, the framework removes its reference to the entry instance and will no longer invoke any lifecycle callbacks (such as XposedModuleInterface#onPackageLoaded, XposedModuleInterface#onHotReloading, etc.) on the entry instance that called this method.

onHotReloading is named explicitly. The detach implementation is VectorLifecycleManager.detach(moduleInstance), which only removes from activeModules — and activeModules gates onPackageLoaded, onPackageReady and onSystemServerStarting only. VectorModuleManager.hotReloadModule dispatches onHotReloading and onHotReloaded by iterating oldState.entries directly, so a detached entry still receives both. Separately, "the framework removes its reference to the entry instance" is not honoured: moduleStates keeps a strong reference to every entry, detached or not. detach() is new in 102, so this is in scope for this PR.

6. A module-thrown SecurityException collides with the AIDL's reserved meaning. IXposedService.aidl:

@throws SecurityException if the target id is invalid or no longer belongs to this module

ModuleService.kt:172 and InjectedModuleService.kt:76 do if (throwable is SecurityException) throw throwable. Your own checks in ApplicationService.hotReloadTarget raise SecurityException for exactly the two documented cases, which is right — but this rethrow also catches a SecurityException raised by module code in onHotReloading, arriving over binder from the target. The module app then sees a raw SecurityException meaning "invalid target id" and never gets its callback, where the spec requires FAILED. Filter on the two conditions you raise yourself, not on the exception type.

7. Hook replacement is documented atomic and is not. HookBuilder.setId:

A new hook with the same id in the same module on the executable will replace the old one atomically, and the old hook handle will be invalid.

and HookHandle.replaceHook: "Atomically replaces this hook with a new hooker and returns the new hook handle." Both paths do installRecord(new) before uninstallRecord(old). The native side keys on IsSameObject and holds both in the callback multimap in between, and both report isActive(), so an invocation that snapshots inside that window runs both hookers. Deactivating the old record first would at least turn a duplicate into a gap; doing it properly needs an atomic swap in hook_bridge.cpp.

8. In-flight calls are supposed to be snapshot-based. Same javadoc:

The hook chain is snapshot based. Replacing or adding a hook while a call is running does not affect that in-flight call.

onHotReloading repeats it: "In-flight hook calls keep using the hook chain snapshot that was active when they started." VectorNativeHooker.callback filters by isActive() at snapshot time, which is correct. But VectorChain.internalProceed re-checks record.isActive() at every node as the call walks the chain, so a hook unhooked or replaced mid-call is dropped from a call that was already running. That is what the sentence forbids, and hot reload is exactly when it happens.

That skip path has a second defect: return nextChain.internalProceed(...) bypasses executeDownstream, so the node never records downstreamResult/downstreamThrowable. Its parent then sees proceedCalled == true with a null downstreamThrowable, takes the "crashed after proceed" branch in handleInterceptorException, and returns null instead of rethrowing a genuine downstream exception. Wrapping the skip in executeDownstream { } fixes that half.

Documented but not implemented

9. autoHotReload does not exist. package-info.java lists it as an API 102 module.prop key:

autoHotReload (boolean, API 102+) - whether app updates should automatically trigger hot reloading. App-update hot reloading still proceeds only when onHotReloading() returns true.

and onHotReloading names the same trigger: "reloading is triggered through the service, or by app updating if autoHotReload is set to true in module.prop". There is not one occurrence of autoHotReload anywhere outside the vendored spec, and FileSystem.loadModule reads only targetApiVersion. Half of the documented trigger set is missing.

10. The 102 behaviour change for legacy APIs is not enforced. XposedInterface.API_102, under "Behavior changes: Modules targeting 102 or higher":

Libxposed modules can not call legacy de.robv.android.xposed APIs.

targetApi is used only to choose MODERN vs LEGACY loading (FileSystem.kt:189-197); nothing stops a module declaring targetApiVersion=102 from calling de.robv.

11. The clear half of the preference diff is emitted but never consumed. The daemon-side parsing matches RemotePreferences.Editor.buildCommitBundle() key for key, including the new clear handling — that part is right. But this PR also makes deleteRemotePreferences push Bundle{clear=true} to hooked processes (ModuleService.kt:219, InjectedModuleService.kt:129), and the receiver, VectorRemotePreferences.onUpdate, only inspects "delete" and "put". So prefs.edit().clear() and deleteRemotePreferences(group) are no-ops in every hooked process: stale keys survive, no OnSharedPreferenceChangeListener fires, and the daemon's database silently diverges from the hooked process until the app restarts.

Not covered by the specification

Design and security concerns. The docs say nothing either way, so treat these as my opinion rather than as breaches.

12. ILSPInjectedModuleService becomes a full read-write control surface with no caller check. On master it is a deliberately read-only subset. This PR adds getScope, requestScope, removeScope, updateRemotePreferences, deleteRemotePreferences, deleteRemoteFile and hotReloadModule. ModuleService guards every equivalent with ensureModule() (calling uid must equal the module's appId); InjectedModuleService has no equivalent anywhere. The spec never states an authentication requirement, so this is not a doc violation — but it means any code in a process the module is injected into can rewrite that module's preferences, drop its scope, delete its files and force reloads. The doc's posture is at least suggestive: "Note that those are read-only in hooked apps."

13. VectorXposedService injected into hooked processes is a separate feature. XposedProvider is where the spec puts the service binder, i.e. the module app. Reflecting into the package-private XposedServiceHelper.onBinderReceived from VectorModuleManager.injectXposedService puts a full XposedService inside every hooked process. That is what actually makes remote preferences writable there — via upstream's writable RemotePreferences, not via XposedInterface, which correctly still throws UnsupportedOperationException from edit(). Whatever its merits, it is unrelated to hot reload and deserves its own PR and its own security discussion.

14. onHotReloaded throwing leaves the process migrated but reported as failed. newStateCommitted is set before the callback runs, so the rollback the PR description promises never executes. Measured: the service reported FAILED, getRunningTargets() reported state=FAILED loadedVersionCode=3, and the process was already running generation 4 with generation-4 hooks. loadedVersionCode is documented as "only a diagnostic value", so this is not a doc breach, but the framework's own bookkeeping contradicts reality.

15. Old code can still register hooks during onHotReloading. The freeze starts after the callback returns. Measured: a hook(...).intercept(...) from old code inside onHotReloading succeeded and the stray hook appeared in getOldHookHandles() (2 → 3). The doc only requires freeze-before-capture, which you do, so this is not a violation — but the sentence's stated purpose is "so further hook registrations from old code fail", and it is worth confirming the intent upstream. Related: the freeze gates intercept() only, not replaceHook(), and keys on the hooker's classloader, so a Hooker whose class comes from elsewhere slips through.

16. The daemon's RELOADING check is a TOCTOU. if (state == RELOADING) ... state = RELOADING is not atomic. In practice VectorModuleManager.hotReloadModule is @Synchronized on a process-wide monitor, stronger than per-target, so this cannot interleave two reloads — the second just queues and runs a full redundant reload reported as SUCCEEDED. Low impact, but AtomicInteger.compareAndSet is a one-line fix.

17. Reporting UNSUPPORTED when a new generation cannot be built. package-info.java says frameworks "may also report hot reload as unsupported when they cannot provide a valid new module generation" — "may", so FAILED is permitted. Still, UNSUPPORTED carries more information when instantiateEntries comes back short.

18. getRunningTargets() hides multi-entry modules entirely. No target is registered unless moduleClassNames.size == 1, so such a module gets an empty list rather than targets that answer UNSUPPORTED. getRunningTargets is documented as "Returns running processes currently hooked by this module", not "hot-reloadable processes".

19. shared/libxposed-annotation duplicates a published artifact. io.github.libxposed:annotation:1.0.0 is on Maven Central and is what both submodules' own build files use (compileOnly(libxposedAnnotation)). Hand-copying the classes into the same package is what the -dontwarn and the "duplicate annotation classes in zygisk dex merge" commit are working around. Replace the module with compileOnly("io.github.libxposed:annotation:1.0.0").

Pre-existing, not this PR

Found while checking the chain against the spec. Worth separate issues rather than scope creep here.

  • ExceptionMode.PROTECTIVE recovery can rethrow an already-suppressed exception. executeDownstream stores a node's own hooker exception in downstreamThrowable, and handleInterceptorException never clears it when it recovers. With two DEFAULT hooks where the inner one throws before proceed() and the outer one throws after, the parent's nextChain.downstreamThrowable?.let { throw it } rethrows the inner hook's already-suppressed exception into the app, where the doc requires the proceeded value: "if the exception is thrown after proceed, the framework will return the value / exception proceeded as the result." Deterministic, no concurrency needed. VectorChain.kt is unchanged here by this PR.
  • ExceptionMode.DEFAULT is hard-wired to protective. The doc says "Follows the global exception mode configured in module.prop", and package-info.java names the key: exceptionMode (string) [protective|passthrough]. It is never read.
  • Chain.getArgs() returns args.toList(), a mutable ArrayList, against "The returned list is immutable."
  • intercept() rejects Method.invoke but not Constructor#newInstance, which the @throws IllegalArgumentException clause names explicitly.
  • The daemon parses module.prop with a naive split("="); package-info.java specifies Java Properties format. The manager app already does this correctly, so it is only the daemon side.

Nits

  • New methods are inserted mid-interface in ILSPInjectedModuleService.aidl, renumbering requestRemotePreferences, openRemoteFile and getRemoteFileList. Harmless while everything ships in one zip, but upstream pins ids explicitly (= 1, = 10) for exactly this reason.
  • HotReloadTargetInfo publishes this into hotReloadTargets from its own init; ProcessInfo.binderDied removes targets without unlinking their death recipients.
  • InjectedModuleService.requestScope puts the if (callback != null) test inside the loop and reports nothing when it is null. The AIDL method is oneway, so a throw would be silent too.
  • Hot reload targets are never unregistered when a module is disabled or uninstalled.
  • The reload path skips the "Xposed API classes are compiled into $pkg" integrity check that loadModule performs when it builds the first classloader.

Recommendation

To be direct rather than leave it hanging: this PR will most likely be closed. I intend to do the API 102 and hot reload work myself later, and I will work from the checklist above.

The reason is that the branch reads as mostly AI-generated, and this particular feature does not survive that level of attention. The review above is largely evidence of it:

  • The PR description asserts behaviour the code does not have. "On failure: rollback, unhook new entries, unfreeze hooks"newStateCommitted is set before onHotReloaded runs, so that rollback never executes (item 14). "includes state locking" — the RELOADING check is a plain check-then-set (item 16).
  • It claims the finalized API 102, but detach() does not honour its own javadoc, autoHotReload appears nowhere in the tree, and the 102 legacy-API behaviour change is unenforced (items 5, 9, 10). All three are stated plainly in package-info.java and XposedInterfaceWrapper, which suggests the specification was not read end to end.
  • shared/libxposed-annotation hand-reimplements io.github.libxposed:annotation:1.0.0 — an artifact on Maven Central that is exactly what both submodules' own build files consume (item 19) — and then needed a follow-up commit to fix the duplicate-class fallout it caused.
  • Most importantly, the feature does not work where it matters. Hot reload never applies to system_server, and silently fails for any frozen target; both surface to the module app as an ordinary refusal (items 1 and 2). Neither is visible from reading the diff. They only appear when you run it on a device against a real module, which is the part that was not done.

Bundling an unrelated privilege change on top of that settles it — the full read-write ILSPInjectedModuleService and the injected XposedService, neither needed for hot reload, neither carrying the uid check that guards every equivalent in ModuleService (items 12 and 13).

I have flagged at the top that this review is machine-assisted as well, so to be fair about the distinction: the difference I care about is not the tool, it is that every claim above was checked against the specification text and then run on a device.

Thanks for raising the topic regardless. API 102 hot reload is worth having, and I would not have prioritised it this soon otherwise. Please leave the branch up as a reference.

@HSSkyBoy HSSkyBoy changed the title feat: Implement finalized libxposed API 102 hot reloading Rewrite libxposed API 102 support: hot reload, detach and atomic hook replacement Jul 26, 2026
@HSSkyBoy HSSkyBoy changed the title Rewrite libxposed API 102 support: hot reload, detach and atomic hook replacement libxposed API 102 support: hot reload, detach and atomic hook replacement Jul 26, 2026
@HSSkyBoy

Copy link
Copy Markdown
Contributor Author

The branch has been replaced with the rewrite. It is 11 commits on top of current master rather than the older base the first attempt used, and the original branch is preserved unchanged at HSSkyBoy/Vector@api102-old (b186e336).

Since the objection to the first attempt was that its claims had not been checked against a running device, everything below was measured. Two caveats about the measurements, stated up front:

  • The device is a OnePlus CPH2691 on Android 16, not a Pixel 6 on Android 17. Some of the review's findings are platform-sensitive and the numbers will differ.
  • The api102-harness module was extended locally for four of the checks below (a legacy-API probe, a second entry class, a detaching entry). Those additions are not on the harness branch, so those four rows are not reproducible from it as-is. The rest use the flags it already ships.

Measured against the review

# Finding Measured now
1 system_server never becomes a target getRunningTargets() returns [0] system pid=3750 uid=1000 after boot. It is also reloaded in place, not merely listed — see item 9.
2 Frozen target reported as a refusal Wrote 1 to the target's cgroup.freeze, confirmed the probe stopped answering, then requested a reload: onHotReloading ran, SUCCEEDED afterMs=78, and the freeze state was 1 again afterwards.
2 Exception without a message is indistinguishable from a refusal --ez throwNullMsg gives FAILED message=java.lang.IllegalStateException: no message. A genuine refusal still gives FAILED message=null.
4 hotReloadModule blocks the caller With --el sleepMs 4000 the dispatch returned at 18:42:00.095 and the callback arrived at 18:42:04.163 (afterMs=4069).
5 detach() does not stop the callbacks it names A detaching entry logged no onPackageLoaded/onPackageReady/onHotReloading while a second, non-detached entry in the same module received them. Calling it twice does not throw. With the sole entry detached, a reload answers UNSUPPORTED message=Every entry of org.matrix.hrmodule has detached in this process — not the refusal encoding, since no module code ran.
6 Module-thrown SecurityException collides with the AIDL's meaning --ez secEx gives FAILED message=java.lang.SecurityException: module code threw SecurityException. SecurityException is now raised only for the two conditions the AIDL reserves it for.
9 autoHotReload does not exist Installing a generation with autoHotReload=true reloaded both targets with no service request: system and org.matrix.hrtarget went [V1] onHotReloading extras=null[V3] onHotReloaded. extras is null, as the javadoc says it is for app-update triggers.
10 The 102 legacy-API behaviour change is unenforced From a module declaring targetApiVersion=102: Class.forName("de.robv.android.xposed.XposedBridge")ClassNotFoundException. Enforced in the module classloader, so reflection is covered rather than only direct references.
12 ILSPInjectedModuleService becomes a read-write surface Not carried over. It still has four read-only methods and openRemoteFile is still MODE_READ_ONLY, so item 3 does not arise either.
13 XposedService injected into hooked processes Not carried over. The harness's own probe from inside the hooked process reports no service.
14 onHotReloaded throwing leaves the process migrated but reported failed --ez throwOnReloaded gives FAILED, and afterwards state=FAILED loadedVersionCode=1 with the process still on the old generation (HOOKED-V1), same pid, still serving. The framework log says kept the previous generation.
15 Old code can still register hooks during onHotReloading --ez frozenHook now gets IllegalStateException: This module generation has been retired by a hot reload and cannot register hooks, and getOldHookHandles() stays at 2 rather than growing to 3. The freeze keys on the generation, not on the hooker's classloader.
16 The RELOADING check is a TOCTOU Three concurrent requests give one SUCCEEDED and two IN_PROGRESS, and exactly one reload cycle runs — not a second redundant one reported as SUCCEEDED.
17 / 18 Multi-entry modules are hidden A two-entry module still appears in getRunningTargets(), and a reload of it answers UNSUPPORTED with a message.
19 shared/libxposed-annotation duplicates a published artifact Gone. io.github.libxposed:annotation:1.0.0 is consumed as a compileOnly dependency, which also removes the duplicate-class workaround the first attempt needed.

Two more, not in the review but in the same area: TARGET_STATE_STALE was never reported by the first attempt either, so the documented "notice STALE, then request a reload" loop had nothing to notice. Installing a newer generation now moves targets to STALE and a successful reload clears it. minApiVersion is read as well — package-info lists it as required and the daemon ignored it.

What running it found that reading the diff did not

Your point about this stands, so it is worth being specific about where it applied here.

getOldHookHandles() returned two handles on the first reload of a process and none on every reload after it. Everything compiled, the first reload looked perfect, and the hooks kept working — the list was simply empty from the second generation onward. The cause was that the reload dropped the module's hook tracking once the old handles were captured, which is wrong for a design where replaceHook swaps the hooker inside a record that stays installed. The consequence is worse than a missing list: the default onHotReloaded unhooks exactly the handles it is given, so an empty list means the retired hookers, and the classloader behind them, are never released. Fixed in 5ae6eb42; four consecutive reloads now report two handles each.

The same applied to the cgroup layout. This device exposes cgroup.freeze at both the uid and the per-pid level, and the uid-level file read 1 while the process was demonstrably responsive. Reasoning from that would have led to "fix" a path precedence that was already correct; measuring first showed the per-pid file is the one that means anything here.

Not verified

  • Item 7 (atomic replacement) and item 8 (in-flight snapshot). Both are implemented — replacement is a volatile write to the single already-registered record, so the native callback multimap never holds two, and the chain freezes its hooker list when the root chain is built rather than reading it per node. Neither is verified: observing them needs a concurrency harness that races a replacement against an in-flight call, which I did not build. I am not claiming these from the code alone.
  • 80c16bcf, the last commit, is not verified on device. The commit before it tried to read the version off the ApplicationInfo that PackageParser produces; measured, that stays zero, so system_server's target had no comparable generation and would have reported STALE forever. 80c16bcf takes it from the first cache update that knows it instead. That change is built but was not flashed.

Left alone

#794 owns the API 101 findings, including the clear half of the preference diff and everything in the "Pre-existing" section. None of it is touched here, so the two should not collide beyond the module.prop parsing, which deliberately mirrors yours — Properties, tolerating a malformed file, leading-digit integers matching extractIntPart — so resolving in your favour is a no-op.

hookClassInitializer(), the late-injected system_server case, and invokeOriginalMethod on a Constructor receiver are left where you put them.

I am not asking for this to be merged as-is; the reasonable next step is whichever commits you want in whatever order suits the work you had planned. Splitting them so each stands alone was the point.

@JingMatrix

Copy link
Copy Markdown
Owner

@HSSkyBoy Please resolve all conflicts with the master.

@HSSkyBoy

Copy link
Copy Markdown
Contributor Author

Please resolve all conflicts with the master.

Completed

@AbdulrahmanAlamodi

Copy link
Copy Markdown

@HSSkyBoy can you rebase this with latest changes

JingMatrix added a commit to HSSkyBoy/Vector that referenced this pull request Aug 3, 2026
…c hook replacement)

Takes HSSkyBoy's branch as the base rather than reimplementing it. The head it
was reviewed at is not the head it has now: seventeen commits later the targets
are derived from the daemon's process registry rather than registered by the
injected process, so system_server is a target by construction, and a cached
target is thawed before the transaction instead of being reported as a module
refusal. Both of the device-only defects the earlier review found are fixed
there.

Resolved: master's libs.versions.toml no longer carries hiddenapibypass or a
pinned kotlin-stdlib, so only libxposed-annotation is taken from the branch; the
lifecycle manager keeps both detach() and master's Q gate on onPackageLoaded.
xposed/libxposed moves on to 39cac08, one commit past the 102.0.0 tag, which
requires a module to declare at least one Java entry.
JingMatrix added a commit to HSSkyBoy/Vector that referenced this pull request Aug 3, 2026
Two throwaway apps for exercising the libxposed API 102 hot reload
contract from adb: a single-entry module that hooks one class in a
target app, and a target that reports the hooked values along with its
pid and process age, so a reload can be shown not to have restarted it.

The module's generation is baked in at build time, so the log states
unambiguously which generation of code answers after a reload. Extras
passed through HotReloadingParam drive the documented failure paths:
refusal, exceptions with and without a message, a SecurityException,
old-classloader saved state, hook registration from frozen old code,
and a throw from onHotReloaded.

Written while reviewing JingMatrix#757. The README records what it found, notably
that system_server never becomes a hot reload target and that a frozen
target is reported to the module app as a module refusal.

(cherry picked from commit dd3e297)
HSSkyBoy and others added 10 commits August 3, 2026 14:24
…cement

Adds the whole API 102 surface: hot reloading a module generation in place,
XposedInterfaceWrapper#detach, hook ids with atomic replacement, and the rule
that modules targeting 102 or higher cannot reach the legacy de.robv API.

Hot reload targets are derived from the daemon's existing process registry
rather than registered by the injected process, so system_server - whose
modules load long before that cache exists - is a target by construction.
getRunningTargets() reports every hooked process, including modules that
cannot be reloaded, which answer UNSUPPORTED rather than being hidden.
hotReloadModule() validates and enqueues, then reports through the callback.
A frozen target is thawed for the transaction instead of surfacing as FAILED
with a null message, which the API reserves for a module refusal.

Squashed from the branch this PR carried (head f02468f, seventeen commits)
so that the pull request is a linear series against master with no merge
commits and none of master's own history in it. The corrections that follow
rewrite parts of this, and reviewing them against a moving base was the
problem this solves.
The interface releases the old generation "after this callback returns or
throws", so a throw is not a signal to undo the reload. Rolling back could not
have honoured that anyway: by the time onHotReloaded throws, the new code may
already have called replaceHook, and unhooking only what was added since leaves
the restored old entries running new hookers - a half-swapped state with no name.

So the swap is committed first and a throw is reported as FAILED with the
exception's diagnostic, which is exactly what the API describes: the process runs
new code that migrated some of its hooks and not others.

That makes 'did the reload succeed' and 'did the generation change' two different
questions, so HotReloadOutcome now answers both. Without the second one the
daemon would keep reporting the old loadedVersionCode for a target that is
demonstrably running the new code, and getRunningTargets() would be lying about
which generation is loaded.
The interface promises that replacing a hook is atomic and that a call already
in flight keeps the chain it started with. Mutating the hooker inside the
installed record gets the first for free - the native map never sees two records
- but pays for the second by having VectorChain freeze an Array(hooks.size) of
hookers at the start of every hooked call. That is an allocation on the hottest
path in the framework, for a property only hot reload needs.

HookBridge.replaceCallback swaps the jobject inside the callback multimap under
the same lock callbackSnapshot takes, so a snapshot sees exactly one of the two,
and a snapshot taken earlier keeps working because it copied the reference into a
Java array of its own. The record goes back to being immutable, the per-call
array goes away, and in-flight isolation falls out of the existing design.

It also fixes a defect by construction. A superseded handle could still unhook
the hook that replaced it - unhook() checked only whether the record was
installed, never whether this handle still owned it - although the interface says
such a handle 'is no longer valid'. Now the record it holds is no longer in the
map, so IsSameObject finds nothing and there is nothing to cancel.

Hook identity moves into VectorHookHandle and VectorHookRegistry: one live handle
per registration, ids scoped to (module, executable, id) under a per-module lock
that the reload also takes to freeze old code, so a registration racing the
freeze either lands before it and is handed to the successor, or fails.

And setId now keeps the caller's priority and exception mode. Only the
handle-based replaceHook is specified to inherit them; registering a new hook
that happens to carry the same id is a new hook.
RegisterNativeLib appends to a list the dlopen hook walks without stopping at the
first match, so a name recorded twice means native_init runs twice for one
library. Two paths were producing duplicates: loadModule called
recordNativeEntrypoint after buildGeneration had already done it, and every hot
reload records the same module's names again for the new generation.

Deduplicating natively fixes both, and is the only place that can - the spec is
explicit that the framework does not dlclose or call JNI_OnUnload across a hot
reload, so there is no removal to pair a re-registration with.

Also drops minApiVersion from PreLoadedApk. It was parsed and stored and never
read: the API puts that check on the module through getApiVersion(), and the
manager reads module.prop itself for what it displays.
API 102 forbids a module targeting 102 or higher from reaching
de.robv.android.xposed, and the module class loader is the only place that can be
enforced - it is the one chokepoint direct linkage and Class.forName both funnel
through. But it is handed a name, and the name is not the one in the source.

daemon/src/main/jni/obfuscation.cpp rewrites Lde/robv/android/xposed/ - in the
framework dex and in every module dex - to a fresh random string on every boot.
So on a build with dex obfuscation on, which is every release build, a module's
type reference resolves to a name the literal test never matched, and the rule
was simply not enforced there.

The prefixes now come from the same map the rest of the framework reads, and
cover AndroidAppHelper and the XResources family as well as the package: those
are part of the same legacy surface and the same signature table, and guarding
only the package left the legacy resource API reachable.
The reload transaction ran the old code's onHotReloading and the new code's
onHotReloaded synchronously, so a daemon thread was pinned for as long as the
module cared to take - and binder has no timeout of its own. A module that never
returned left its target in RELOADING for the life of the process, and every
later request answered IN_PROGRESS forever. Only process death recovered it.

So the request becomes oneway, the outcome comes back through
IHotReloadOutcomeCallback, and the daemon waits on it for thirty seconds. The
process side hops off the incoming binder thread too, since holding one of the
app's own binder threads for the length of a module's onHotReloading is the same
mistake one process along.

PROCESS_DIED now comes from the heartbeat registry rather than from the exception
type. DeadObjectException does not mean the process died: a frozen but perfectly
alive target fails a transaction exactly the same way, and reporting that as
'died' is a claim the module app has no way to check. The registry knows, because
it is driven by a DeathRecipient.

A target that is still frozen after the thaw attempt is reported at once rather
than after the timeout, and with a message saying so - the timeout would have
been indistinguishable from a module that hung.
ensureModule() proves only that the caller shares the module's app id. The same
module installed for two users is two module apps with two sets of preferences,
so without a user check the copy in user 10 could enumerate and reload user 0's
processes - which the AIDL reserves SecurityException for.

System uids stay addressable from every user rather than being scoped to user 0.
system_server runs once for the whole device and carries a module enabled in any
user, so the strict reading would make it unreachable from every secondary
user's copy of the module.
The four paths this probed - under /sys/fs/cgroup/apps/ and /sys/fs/cgroup/system/
- do not exist on SM-A145R running Android 15, which uses /uid_<uid>/pid_<pid>
with nothing above it. freezeFile() returned null there, so thaw() did nothing,
isFrozen() was always false, and a cached target could not be hot reloaded at all.
Nothing said so: the reload just failed later, for a different-looking reason.

The 0:: line of /proc/<pid>/cgroup is the process's own cgroup v2 path relative
to the mount point, which is the kernel's own answer and holds whatever the
layout. Only the process's own group is used; the uid-level group holds every
process of the app and thawing there moves processes this has no business
touching.

The restore re-reads before writing. The framework's app compaction owns this
state too, and if it has thawed the process meanwhile - because the user brought
the app to the foreground - freezing it again from here would stop a process the
system believes is running.

A device with no cgroup v2 freezer at all is an ordinary answer rather than a
failure; minSdk is 27 and it does not exist across that range.
…does

The AIDL is the framework's real API between three security domains, and it was
the least documented thing in the tree: no comments, LSPosed-inherited names that
mean nothing here, and at least one name that was actively wrong.

That last one is not a style complaint. registerHotReloadTarget reads as
'register a target', so the first attempt at hot reload had the injected process
pass a module name, which made the daemon look the module up - and system_server
loads its modules before that cache exists, so system_server could never become a
target. The method hands over a channel; it now says so.

  ILSPApplicationService      -> IFrameworkService
  IDaemonService              -> IVectorDaemon
  ILSPSystemServerService     -> ISystemServerBootstrap
  IHotReloadTarget            -> IProcessChannel      (a channel, not a target)
  IHotReloadOutcomeCallback   -> IHotReloadResultReceiver
  ILSPInjectedModuleService   -> IModuleService
  Module                      -> LoadedModule
  PreLoadedApk                -> ModuleCode           (dexes, entries and policy;
                                                       not 'an apk' in any sense)
  registerHotReloadTarget()   -> attachProcessChannel()
  requestApplicationService() -> attachProcess()
  heartBeat                   -> processLifeToken

Every file now carries prose on what each method is for, who may call it, which
process it runs in, and the constraints that were otherwise oral tradition: that
IFrameworkService numbers its transactions implicitly so methods may only be
appended; that a null message on a hot reload outcome is reserved for a module
refusal and nothing else may claim it; that the life token exists only for
linkToDeath and that letting it be collected looks exactly like dying; and that
attachProcessChannel must not be made oneway, because the binder driver records
the sending thread only for synchronous transactions and an async call therefore
arrives with getCallingPid() == 0, which fails the (uid, pid) authentication with
no symptom beyond every later reload answering UNSUPPORTED.

IRemotePreferenceCallback and IModuleService came along even though remote
preferences are not part of 102, because LoadedModule carries the latter and
leaving them behind would have left the new package importing back into the old
one for no reason.

Deliberately not org.matrix.vector.service: that package is already the zygisk
bridge's, and it is one of the three prefixes daemon/src/main/jni/obfuscation.cpp
rewrites. Landing the AIDL there would have started obfuscating it - harmless for
the daemon-to-process interfaces, fatal for anything the manager APK also
compiles, since the manager is a separate APK the daemon's obfuscator never sees
and the binder descriptors would stop matching. org.matrix.vector.ipc is in no
prefix, so what is obfuscated does not change.

manager-service keeps org.lsposed.lspd for now, as does the Utils logger: neither
has anything to do with API 102.
…s using

Names, now that the package move put the inconsistencies next to each other:

  getModulesList / getLegacyModulesList  -> getModules / getLegacyModules
  getRemoteFileList                      -> getRemoteFileNames  (it returns names)
  IHotReloadResultReceiver.onHotReloadOutcome
                        -> IHotReloadOutcomeReceiver.onOutcome
                           (Result and Outcome for one concept)
  IRemotePreferenceCallback.onUpdate(map) -> onRemotePreferencesChanged(diff)
                           (it is a diff, and the old name said nothing about
                            what had updated)
  LoadedModule.file                      -> LoadedModule.code  (its type is
                                                                ModuleCode)

Two structural changes rather than renames.

ISystemServerBootstrap is deleted. Nothing ever held it as an interface: the only
implementer is SystemServerService, nothing calls Stub.asInterface, and the
zygisk side reaches it by transacting BRIDGE_TRANSACTION_CODE on the hijacked
service name, which onTransact answers before super ever sees it. The generated
dispatch table was unreachable and the descriptor was checked by nobody.
SystemServerService is a plain Binder now and says why, so the next person does
not add the interface back.

requestInjectedManagerBinder(out List<IBinder>) becomes openManagerApk() and
requestManagerService(). It was two unrelated results in one call, one of them
through an out-parameter, and the caller read binderList[0] without checking - so
a process that was not granted the manager service got an
IndexOutOfBoundsException swallowed by an outer catch, which is a confusing way
to spell 'no'. Splitting it lets the caller stop before opening an APK it has no
use for, and puts the side effect where it can be documented: asking for the
service is what makes this process the manager's host, which is a claim rather
than a query.
@JingMatrix
JingMatrix force-pushed the master branch 2 times, most recently from 0e06d8a to c7c5a29 Compare August 3, 2026 12:41
Both were found by reading and then confirmed on hardware, and both had the same
shape: the framework reported success for something that had not happened.

addressableBy() tested `uid < PER_USER_RANGE`, meaning every uid in user 0 rather
than the AID_* uids the carve-out was for. A module app in a secondary user could
therefore enumerate and hot reload every user-0 process running that module -
the exact cross-user access the commit that added the check says it prevents. On
the test device a copy of the harness module installed only for user 10 listed
uids 10135 and 10136 and reloaded a user-0 app that was not installed for user 10
at all. The boundary is FIRST_APPLICATION_UID; system_server keeps its carve-out,
which is what the check existed for.

buildGeneration() returned a Generation whose entry list was empty when every
entry class failed to instantiate, and no caller treated that as failure. The
initial load reported the module loaded. A hot reload was worse: it committed the
empty generation, never called onHotReloaded, never unhooked the old hooks,
answered HOT_RELOAD_SUCCEEDED, and left the target wedged, since the committed
generation had no live entry for any later reload to hand over to. Reproduced
with a module whose constructor throws only inside the target process: the daemon
reported SUCCEEDED and loadedVersionCode 8 while the process went on running V7's
hookers, and every later reload answered UNSUPPORTED.

Also corrects documentation the same review found to be wrong, which matters
because this branch's claim is that it documents things - a confidently wrong
comment is worse than none:

  - The dedup comment said the dlopen hook walks the library list without a break,
    so a duplicate would call native_init twice. It does break at the first match.
    The list never shrinking is the real reason to dedup, and now what it says.
  - Two comments said postStartManager decides here that this process hosts the
    manager. It is `pid == managerPid`, a comparison; the decision was taken when
    the daemon launched the manager.
  - IFrameworkService claimed every call is authenticated. isLogMuted is not, and
    deliberately so.
  - IProcessChannel claimed to be the only interface the daemon calls into a
    process on. IRemotePreferenceCallback, in the same package, is another.
  - HotReloadOutcome.message said null means a refusal. Success is null too; what
    is reserved is FAILED with a null message.
  - LoadedModule.versionCode and ModuleCode.targetApiVersion described values the
    daemon does not always produce.
  - The AIDL rename had leaked "LoadedModule" into two English diagnostics, one of
    which reaches the module app as HotReloadResult.message().
  - daemon, zygisk and legacy READMEs still named IDaemonService,
    ILSPApplicationService and getLegacyModulesList.
setModuleScope normalises a framework scope row to user 0 whoever asked for it,
because system_server is one process for the whole device and a module in a work
profile hooking it is hooking the same one as everyone else. removeModuleScope
did not normalise - it refused outright for any user but 0 - so a module outside
user 0 could take system scope and never give it back.

The bad path is IXposedService.removeScope, which returns void: the module asked,
nothing happened, and it was told nothing. Through the CLI it is at least visible
as "removed 0 apps". On a device:

  cli scope rm org.matrix.hrmodule system/11  ->  removed 0 apps
  cli scope rm org.matrix.hrmodule system/0   ->  removed 1 apps

Normalising on the way out, the same way the write normalises on the way in, is
all it needed.

The notification-approval path had the mirror image of the same confusion: it
tested whether the scope was already granted by comparing the requesting user
against the stored row, which for "system" is always 0, so the test never matched.
Every approval appended a duplicate and rewrote the whole scope table.
setModuleScope's normalisation and CONFLICT_IGNORE collapsed it again, so nothing
was corrupted - the check was simply dead for the one package it matters most for.

Also corrects why addressableBy carves out the AID_* uids. The reason is not that
system_server is special; it is that a module in any user may hold this scope and
the row records none of them, so nothing downstream can tell which user asked, and
every user holding the module is equally entitled to the one generation loaded
there.
A module is one package and one APK for the whole device, so the
configuration keys it by package alone: one enabled flag, one scope set,
and a user id on each scope row naming which instance of the target app
it points at. Nothing checked that the module itself existed for that
user, so a row was expanded on the strength of the target resolving and
the module went wherever it pointed. Reproduced on a device: a module
installed for user 11 alone loaded into and hooked a user 0 app.

The rebuild now records which users hold each module and refuses to
expand a row into a user that does not. The framework is exempt, and has
to be: system_server is one process for the whole device belonging to no
user, its row is stored under user 0 whoever asked for it, and every
user holding the module is equally entitled to it.

Which users hold it has to be asked separately. MATCH_ALL_FLAGS carries
MATCH_ANY_USER and MATCH_UNINSTALLED_PACKAGES, so getPackageInfoCompat
answers for a user that does not hold the package - deliberately,
because answering for every user is what distinguishes "no user has this
any more", which deletes the configuration, from "not in this user",
which must not. Nor does the uid in the answer help: the ApplicationInfo
is generated for the user asked about, so a module held only by users 11
and 12 still reports 10136 for user 0. isPackageAvailable is the
per-user installed state and answers correctly, and hidden counts as
held so a locked private space keeps its modules.

Two things the same scenario exposed. A holder now wins the
ApplicationInfo, so the data directory the module is handed is one that
exists - which means appId is read from a secondary user's uid, and it
is stored modulo the user range because every reader compares it against
someUid % PER_USER_RANGE. Otherwise a module held only by user 11 would
fail its own authentication in ensureModule and never be sent its
binder. And the system_server path read the module's uid off
/data/user_de/0, which such a module does not have, so it started life
with an app id of -1 and data paths pointing at nothing; it now looks
for the directory that exists.

getScope now answers with the caller's user plus the framework row.
requestScope asks for the caller's user and removeScope gives back the
caller's user, so returning every row showed a copy in user 11 packages
in user 0 it could neither have asked for nor give back.

The self-scope and legacy-self-scope expansions walked every user on the
device to build uids that could never start a process. They walk the
users holding the module instead.
@JingMatrix
JingMatrix merged commit abae837 into JingMatrix:master Aug 3, 2026
1 check passed
JingMatrix added a commit that referenced this pull request Aug 4, 2026
Finishes the namespace move #757 started, doing the three things it deferred: the manager's interface, the Utils logger, and the daemon's implementation class names. /data/adb/lspd stays as it is — on-disk state wants a migration, not a rename.

ILSPManagerService and its parcelables become IManagerService, IFrameworkInstallReceiver, ScopeEntry and DeviceUser under org.matrix.vector.ipc. org.lsposed.lspd.util.Utils becomes org.matrix.vector.util, with its inner Log promoted to top level. ApplicationService and ModuleService take the names of the interfaces they implement.

45 methods become 39: three were dead, and getUnloadableModules plus getModuleLoadState collapse into one call returning a map, which drops the placeholder reason a lost transaction used to report as a missing APK. The hand-written transaction ids go too — they kept a number stable rather than a meaning, and getProtocolVersion, declared first and so transaction zero in every revision, guards that at the handshake instead.

The interface name is the binder descriptor, so a separately installed manager stops working until it is updated. It now says so rather than drawing blank screens.

Seven unrelated defects come with it, each its own commit: a discarded insert() result, a health flag latched before the work it reports, an unbounded wait on a binder thread, and four smaller ones.
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.

API 102 support

5 participants