Skip to content

Open an installed manager directly, and survive an unshare without --propagation - #1005

Open
hhhbwc wants to merge 1 commit into
JingMatrix:masterfrom
hhhbwc:manager-entry-and-daemon-startup-fallbacks
Open

hhhbwc wants to merge 1 commit into
JingMatrix:masterfrom
hhhbwc:manager-entry-and-daemon-startup-fallbacks

Conversation

@hhhbwc

@hhhbwc hhhbwc commented Oct 12, 2026

Copy link
Copy Markdown

What

Two robustness fixes in the Magisk module scripts, both for environments that are not a conventional phone. Each keeps today's behaviour as the first choice and only falls back when the environment cannot support it.

zygisk/module/action.sh — prefer the installed manager

The action button currently only knows the injection path: hand the intent to ${INJECTED_PACKAGE_NAME} and let the Zygisk-injected manager answer it. That needs the shell process to host a 2D activity and to receive the injected manager dex. Neither is guaranteed everywhere: some systems never let the shell host an activity at all, and dynamic code loading restrictions can block the dex (#805 fixed one such case on GrapheneOS). Where the injection path cannot work, the action button is a dead end — even though manager.apk ships inside the module and #809 added a standalone install for exactly this situation.

When the manager has been installed as a regular package, launch its activity directly instead. If it has not, the injection path runs unchanged.

zygisk/module/service.sh — start the daemon where unshare is poorer

unshare --propagation slave -m is GNU util-linux syntax. Android ships toybox, and toybox builds that implement only a subset of util-linux reject the flag outright:

$ unshare --propagation slave -m true
unshare: Unknown option propagation

The daemon then never starts, and nothing says so: service.sh has no error path, and the failure only shows up as every Xposed module silently doing nothing. Probe the flag with a throwaway true and fall back to unshare -m, then to running the daemon without a namespace. The full invocation still runs first wherever it is supported.

Testing

Verified on a PICO 4 (Android 10) VR headset, where unshare --propagation is rejected and the shell cannot host the manager:

  • unshare --propagation slave -m true exits 1 with Unknown option propagation; unshare -m true exits 0. Killed vectord, ran the new service.sh, and the daemon came back (VectorDaemon: Successfully injected Vector IPC binder for applications).
  • With manager.apk installed from the module, the action opens org.matrix.vector.manager/.ui.MainActivity directly (force-stopped the manager, ran the action, the process returned). Without the package installed, the injection path still runs.

Notes

…orer

action.sh: the injection path needs ${INJECTED_PACKAGE_NAME} to host a 2D
activity and to receive the injected manager dex, neither of which holds in
every environment. When the manager has been installed as a regular package
-- manager.apk ships inside the module for exactly that -- launch its
activity directly and leave the injection path as the fallback.

service.sh: "unshare --propagation slave -m" is GNU util-linux syntax;
toybox builds that implement only a subset reject the flag outright, which
leaves the daemon dead with nothing anywhere to say so. Probe the flag with
a throwaway command and fall back to "unshare -m", then to running the
daemon without a namespace at all.
@kinghe2023

Copy link
Copy Markdown

Thanks — this matches a failure we hit on a device that is not a conventional phone either, and the diagnosis was exactly as you describe.

Environment: Samsung Galaxy S25 (SM-S931U1), Android 16 / One UI 8, KernelSU v3.2.5 in LKM late-load mode (bootloader locked, root is loaded after boot, so the service stage runs long after boot has completed).

Reproduced on the unmodified script:

$ /system/bin/unshare --propagation slave -m true
unshare: Unknown option 'propagation' (see "unshare --help")
$ echo $?
1

vectord simply never existed, and nothing anywhere said why — the same silent failure you describe.

One suggestion for the fallback chain: before degrading to unshare -m, it is worth probing the busybox that ships with the root solution. On this device KernelSU bundles BusyBox v1.36.1.1 (topjohnwu) and its unshare does implement --propagation:

$ /data/adb/ksu/bin/busybox unshare --propagation slave -m true; echo $?
0

and it really applies it — the namespace becomes a slave of the parent's, instead of a namespace without propagation semantics:

$ /data/adb/ksu/bin/busybox unshare -m sh -c 'grep -c master: /proc/self/mountinfo'
0
$ /data/adb/ksu/bin/busybox unshare --propagation slave -m sh -c 'grep -c master: /proc/self/mountinfo'
76

(Unknown options are still rejected, so the probe stays meaningful: busybox unshare --totally-bogus-option -m true exits 1, and --propagation banana fails with unrecognized: --propagation=banana.)

What we ship — note $MAGISKTMP is not necessarily set when the service stage runs at late-load, so the paths are probed rather than assumed:

find_busybox() {
  for bb in /data/adb/ksu/bin/busybox /data/adb/magisk/busybox /data/adb/ap/bin/busybox; do
    [ -x "$bb" ] && { echo "$bb"; return; }
  done
}

BB="$(find_busybox)"
if [ -n "$BB" ] && "$BB" unshare --propagation slave -m true >/dev/null 2>&1; then
  "$BB" unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" &
elif unshare --propagation slave -m true >/dev/null 2>&1; then
  unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" &
elif unshare -m true >/dev/null 2>&1; then
  unshare -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" &
else
  "$MODDIR/daemon" --system-server-max-retry=3 "$@" &
fi

Two more things from the same debugging session, both consequences of the same "fails without saying so" property:

  1. Record which branch was taken. We append one line to a file (/data/local/tmp/vector-service-trace.log on our side); anything greppable beats nothing, given the failure mode is invisible by construction.
  2. Refuse to start a second daemon. The daemon's own tryLock does not hold across mount namespaces (No injection after a KernelSU emulated soft reboot spawns a duplicate daemon #878), so anything that re-runs service.sh while a daemon is alive gives you two processes fighting over the same proxy service name. Anchored, because other processes have the string in their cmdline:
if pgrep -f '^vectord' >/dev/null 2>&1; then exit 0; fi

Thanks for the --propagation probe in particular — that alone would have saved us a long night.

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.

2 participants