From 8db5a37c651cba4908cb59e85e00994c5ca5061e Mon Sep 17 00:00:00 2001 From: hhhbwc <1926185276@qq.com> Date: Mon, 12 Oct 2026 08:11:15 +0800 Subject: [PATCH 1/3] Prefer an installed manager, and start the daemon where unshare is poorer 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. --- zygisk/module/action.sh | 11 +++++++++++ zygisk/module/service.sh | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/zygisk/module/action.sh b/zygisk/module/action.sh index c3ea8c1a34..58b7d206d4 100644 --- a/zygisk/module/action.sh +++ b/zygisk/module/action.sh @@ -1,4 +1,15 @@ MANAGER_PACKAGE_NAME="@MANAGER_PACKAGE_NAME@" INJECTED_PACKAGE_NAME="@INJECTED_PACKAGE_NAME@" +# The manager also ships inside this module as manager.apk and may have been +# installed as a regular package. When it has, launching its activity directly +# is more reliable than the injection path below, which needs +# ${INJECTED_PACKAGE_NAME} to start a 2D activity at all and to receive the +# injected manager dex -- neither holds in every environment (some systems +# never let the shell host an activity, and dynamic code loading restrictions +# can block the dex the way GrapheneOS does, see #805). +if pm path "$MANAGER_PACKAGE_NAME" >/dev/null 2>&1; then + am start -n "$MANAGER_PACKAGE_NAME/.ui.MainActivity" >/dev/null 2>&1 && exit 0 +fi + am start -c "${MANAGER_PACKAGE_NAME}.LAUNCH_MANAGER" "${INJECTED_PACKAGE_NAME}/.BugreportWarningActivity" diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh index e49f8859b2..4f2cd362ff 100644 --- a/zygisk/module/service.sh +++ b/zygisk/module/service.sh @@ -1,6 +1,16 @@ -# Extract the directory path and change directory +# Extract the directory path and change directory MODDIR="${0%/*}" cd "$MODDIR" || exit 1 -# Start the daemon directly in the background within a private mount namespace -unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & +# Start the daemon directly in the background within a private mount namespace. +# Not every environment's unshare understands --propagation: toybox builds that +# implement only a subset of util-linux 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 progressively simpler invocations. +if 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 From 578686ac15950bf8fcae4061ee9600b276e95e48 Mon Sep 17 00:00:00 2001 From: hhhbwc <1926185276@qq.com> Date: Wed, 21 Oct 2026 02:26:15 +0800 Subject: [PATCH 2/3] Keep the daemon's tmpfs out of the host, and start it only once unshare -m on a toybox that rejects --propagation still succeeds, but the new namespace keeps the parent's shared propagation type, so the daemon's tmpfs over /data/resource-cache propagates back and hides the real cache from every process. Probe the root solution's busybox unshare first, since it usually implements --propagation; then make the tree private inside a plain unshare -m; and only when neither works run without that mount. The service stage had no way to say which path it took, and a daemon that never started left no trace at all. Record the chosen path in /data/local/tmp/vector-service.log and mirror it to logcat. Finally, refuse to start a second daemon when one is alive. KernelSU's emulated soft reboot replays service.sh without killing detached processes, and tryLock() does not turn the second one away (#878); the two then claim the same proxy service and break injection. --- zygisk/module/daemon | 7 ++-- zygisk/module/service.sh | 75 +++++++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/zygisk/module/daemon b/zygisk/module/daemon index 3b1dec4a5f..8cc8fb4f5d 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -26,8 +26,11 @@ if [ "$debug" = "true" ]; then fi # Mount an empty temporary file system over /data/resource-cache to avoid possible overlay conflicts. -# Note that this script runs inside an isolated mount namespace (via unshare). -mount tmpfs -t tmpfs /data/resource-cache +# Only when service.sh has established a mount namespace that cannot propagate this mount back to the +# host: without one it would hide the real resource cache from every process on the device. +if [ "${VECTOR_MOUNTNS_ISOLATED:-0}" = 1 ]; then + mount tmpfs -t tmpfs /data/resource-cache +fi # Wait for Zygote socket using a polling loop if [ ! -S "/dev/socket/zygote" ]; then diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh index 4f2cd362ff..2085c5ecb3 100644 --- a/zygisk/module/service.sh +++ b/zygisk/module/service.sh @@ -2,15 +2,72 @@ MODDIR="${0%/*}" cd "$MODDIR" || exit 1 -# Start the daemon directly in the background within a private mount namespace. -# Not every environment's unshare understands --propagation: toybox builds that -# implement only a subset of util-linux 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 progressively simpler invocations. -if unshare --propagation slave -m true >/dev/null 2>&1; then - unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & +# The service stage has no console, and the daemon cannot log anything before it +# exists, so a start that fails is invisible by construction. Keep one line per +# boot, and mirror it to logcat when log is around as well. +TRACE_LOG=/data/local/tmp/vector-service.log +trace() { + printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null)" "$*" >>"$TRACE_LOG" 2>/dev/null + command -v log >/dev/null 2>&1 && log -p i -t VectorService "$*" >/dev/null 2>&1 +} + +# A replayed service stage (KernelSU's emulated soft reboot) leaves the old +# daemon alive, and tryLock() in the daemon does not reliably turn the second +# one away (#878): the two then claim the same proxy service and break +# injection. The survivor handles a system_server restart through its binder +# death recipient, so leaving it alone is the repair; a second daemon is what +# breaks injection. +daemon_running() { + command -v pgrep >/dev/null 2>&1 && pgrep -f '^vectord' >/dev/null 2>&1 && return 0 + command -v pidof >/dev/null 2>&1 && pidof vectord >/dev/null 2>&1 && return 0 + return 1 +} + +if daemon_running; then + trace "vectord already running, not starting a second one" + exit 0 +fi + +# toybox builds that implement only a subset of util-linux reject +# --propagation outright, which leaves the daemon dead with nothing anywhere to +# say so. The root solution usually ships a fuller busybox, so prefer its +# unshare, then the system one, and only then a namespace whose propagation +# type cannot be set. +find_busybox() { + for bb in "${MAGISKTMP:-/nonexistent}/busybox" \ + "${MAGISKTMP:-/nonexistent}/.magisk/busybox" \ + /data/adb/ksu/bin/busybox \ + /data/adb/magisk/busybox \ + /data/adb/ap/bin/busybox \ + /system/xbin/busybox; do + [ -f "$bb" ] && [ -x "$bb" ] && { printf '%s\n' "$bb"; return 0; } + done + bb="$(command -v busybox 2>/dev/null)" && [ -f "$bb" ] && [ -x "$bb" ] && printf '%s\n' "$bb" +} +BB="$(find_busybox)" +DAEMON="$MODDIR/daemon" + +# The daemon mounts a tmpfs over /data/resource-cache, which is only safe when +# this namespace cannot propagate mounts back to the host. +# VECTOR_MOUNTNS_ISOLATED records that service.sh has established exactly that. +if [ -n "$BB" ] && "$BB" unshare --propagation slave -m true >/dev/null 2>&1; then + trace "starting vectord with $BB unshare --propagation slave -m" + VECTOR_MOUNTNS_ISOLATED=1 "$BB" unshare --propagation slave -m "$DAEMON" --system-server-max-retry=3 "$@" & +elif unshare --propagation slave -m true >/dev/null 2>&1; then + trace "starting vectord with unshare --propagation slave -m" + VECTOR_MOUNTNS_ISOLATED=1 unshare --propagation slave -m "$DAEMON" --system-server-max-retry=3 "$@" & +elif unshare -m /system/bin/sh -c ' + mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null + ' >/dev/null 2>&1; then + trace "starting vectord with unshare -m and a private mount tree" + VECTOR_MOUNTNS_ISOLATED=1 unshare -m /system/bin/sh -c ' + mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null || exit 1 + exec "$0" "$@" + ' "$DAEMON" --system-server-max-retry=3 "$@" & elif unshare -m true >/dev/null 2>&1; then - unshare -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & + trace "starting vectord with unshare -m (propagation unchanged, mount skipped)" + unshare -m "$DAEMON" --system-server-max-retry=3 "$@" & else - "$MODDIR/daemon" --system-server-max-retry=3 "$@" & + trace "starting vectord without a mount namespace (resource-cache mount skipped)" + "$DAEMON" --system-server-max-retry=3 "$@" & fi From 7beab8ff52cadb2593495111faf2af54ed9295a4 Mon Sep 17 00:00:00 2001 From: hhhbwc <1926185276@qq.com> Date: Wed, 21 Oct 2026 08:01:31 +0800 Subject: [PATCH 3/3] Verify the mount tree is actually private, since toybox's mount lies toybox's mount accepts --make-rprivate, exits 0, and changes nothing. The probe therefore reported a private namespace while /data stayed shared, and the daemon's tmpfs would have propagated to the host on a device whose service stage runs with shared mounts. Check that no mount is still shared before exporting VECTOR_MOUNTNS_ISOLATED, and if the conversion does not take effect, still run the daemon in the namespace -- just without the tmpfs mount, rather than not starting it at all. Verified on a PICO 4 (Android 10): the corrected probe exits 1 under toybox, the Magisk busybox path is selected, and the daemon's resource-cache tmpfs is absent from PID 1's namespace. --- zygisk/module/service.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/zygisk/module/service.sh b/zygisk/module/service.sh index 2085c5ecb3..c726aa5970 100644 --- a/zygisk/module/service.sh +++ b/zygisk/module/service.sh @@ -57,11 +57,16 @@ elif unshare --propagation slave -m true >/dev/null 2>&1; then trace "starting vectord with unshare --propagation slave -m" VECTOR_MOUNTNS_ISOLATED=1 unshare --propagation slave -m "$DAEMON" --system-server-max-retry=3 "$@" & elif unshare -m /system/bin/sh -c ' - mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null + mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null || exit 1 + ! grep -q " shared:" /proc/self/mountinfo ' >/dev/null 2>&1; then trace "starting vectord with unshare -m and a private mount tree" - VECTOR_MOUNTNS_ISOLATED=1 unshare -m /system/bin/sh -c ' - mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null || exit 1 + unshare -m /system/bin/sh -c ' + if mount --make-rprivate / 2>/dev/null || mount --make-rslave / 2>/dev/null; then + if ! grep -q " shared:" /proc/self/mountinfo; then + export VECTOR_MOUNTNS_ISOLATED=1 + fi + fi exec "$0" "$@" ' "$DAEMON" --system-server-max-retry=3 "$@" & elif unshare -m true >/dev/null 2>&1; then