Skip to content

Take the previous daemon down on an emulated soft reboot - #914

Open
H3XDaemon wants to merge 1 commit into
JingMatrix:masterfrom
H3XDaemon:emulated-soft-reboot
Open

Take the previous daemon down on an emulated soft reboot#914
H3XDaemon wants to merge 1 commit into
JingMatrix:masterfrom
H3XDaemon:emulated-soft-reboot

Conversation

@H3XDaemon

Copy link
Copy Markdown

Fixes #878.

ksud soft-reboot does not reboot the kernel. It runs the emulated-soft-reboot stage, stop, post-fs-data, start, and then on_services(), which runs every active module's service.sh a second time. stop never reaches the daemon the previous cycle started, because service.sh detaches it, so from the second soft reboot onwards two daemons are alive at once. They then claim the same proxy service name — one squatting it on startup, one re-claiming it from its death recipient — each latches the other as originService, SystemServerService.onTransact forwards to it unconditionally, and the resulting binder ping-pong overflows a Java stack. The new daemon dies on JNI FatalError ... StackOverflowError, the survivor times out on the bridge, nothing is injected, and Xposed stays off until a real reboot. FileSystem.tryLock() does not prevent it: it returns true while the previous daemon is alive.

That stage is how a module is meant to shut its daemon down, and Zygisk Next uses it (zygiskd exit). This adds the equivalent for Vector.

Two notes on the script. -f is required because app_process sets the nice name in argv only, so comm stays main and pkill vectord matches nothing; the ^ anchor is required or the pattern also matches any shell whose own command line contains the word. The stage is blocking, so it has to stay cheap — this costs about 60 ms, where a /proc scan forking once per pid cost 9.5 s. If you would rather stop the daemon gracefully, kill followed by a short wait and a kill -9 fallback would work too; I kept the plain kill -9 because that is the form I measured.

customize.sh needs the second hunk because it sets SKIPUNZIP=1 and installs from a fixed list, so a new file in zygisk/module/ ships in the zip but never lands in /data/adb/modules/zygisk_vector. The .sha256 sidecar extract() verifies is generated for every staged file by prepareModuleFiles, so no build change is needed. Installed permissions come from set_perm_recursive "$MODPATH" 0 0 0755 0644, which matches how Zygisk Next ships its own copy.

Verified on a Galaxy SM-S9280, Android 16, KernelSU 3.2.5 late-loaded as an LKM, Vector a0ab735e, by scripting the module directory directly on the device: four consecutive soft reboots on one boot, the two without this script left the framework dead as described in #878, and the two with it produced a single vectord, no StackOverflowError, Injected Vector framework into system_server, Successfully injected Vector IPC binder for applications, a working cli status and modules loading into apps again. I have not rebuilt the zip end to end, so the packaging change is reviewed rather than measured. The file here differs from the one I ran in two cosmetic ways: it drops a /dev/kmsg line I used to confirm the stage had fired, and the shebang, since exec_script invokes stage scripts as busybox sh <path> and the other module scripts carry none.

`ksud soft-reboot` does not reboot the kernel. It runs the
`emulated-soft-reboot` stage, `stop`, post-fs-data, `start`, and then
`on_services()`, which runs every active module's service.sh a second
time. `stop` never reaches the daemon the previous cycle started,
because service.sh detaches it, so from the second soft reboot onwards
two daemons are alive at once. They then claim the same proxy service
name, each latches the other as `originService`, and
`SystemServerService.onTransact` forwards to it unconditionally, so the
binder ping-pong overflows a Java stack: the new daemon dies on
`JNI FatalError ... StackOverflowError`, the survivor times out on the
bridge, and nothing is injected.

That stage is how a module is meant to shut its daemon down, and Zygisk
Next uses it. `-f` is required because app_process sets the nice name in
argv only, so comm stays "main" and `pkill vectord` matches nothing, and
the `^` anchor keeps the pattern off any shell whose own command line
contains the word.

customize.sh sets SKIPUNZIP=1 and installs from a fixed list, so the
script has to be named there as well or it ships in the zip without ever
being installed.
@H3XDaemon

Copy link
Copy Markdown
Author

More verification since opening this, from my own runs only.

Script absent, fresh boot, three consecutive soft reboots:

cycle vectord pid injected failed to inject
1 10020 yes
2 10020 yes yes
3 10020 no yes

The pid is frozen across all three — the previous cycle's daemon is still the one running, which is the stop gap. Cycle 1 is clean only because there is no survivor yet, so within one boot the sole variable is whether a survivor existed.

Script present, seven cycles across two boots: one daemon every cycle, injection succeeded every cycle, and the pid changes every cycle instead of freezing. Trace from a copy with a logging line added:

entered pid=5098 path=...:/system/bin:...:/data/adb/ksu/bin
pgrep rc=0 pids=[11685]
procfs scan=[11685 ]
killed [11685] rc=0

procfs scan was a pgrep-free walk of /proc/*/cmdline kept as a cross-check; it agreed, so the pattern is not the fragile part. I dropped it afterwards — about 8 s in a blocking stage, against roughly 60 ms for pgrep.

Two notes for anyone verifying this. A marker written to /dev/kmsg is unreadable afterwards: the soft reboot replays init and the message burst rotates the ring buffer within seconds, so write it to a file. And on my device the framework triggers RescueParty at the fourth or fifth consecutive soft reboot in one boot (persist.sys.boot.reason.history shows reboot,rescueparty, persist.sys.rescue_level 4) — three cycles is enough to show the transition, so there is no reason to run longer.

This removes the duplicate daemon; it does not make every path to "nothing injected" impossible. A report with a single daemon alive and no StackOverflowError would be a different failure, not a regression of this change.

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.

No injection after a KernelSU emulated soft reboot spawns a duplicate daemon

1 participant