Skip to content

zygisk: make built-in Zygisk work on Meta Quest (lazy per-trust partition zygotes) - #9968

Open
xAstroBoy wants to merge 3 commits into
topjohnwu:masterfrom
xAstroBoy:meta-quest-zygisk-fix
Open

zygisk: make built-in Zygisk work on Meta Quest (lazy per-trust partition zygotes)#9968
xAstroBoy wants to merge 3 commits into
topjohnwu:masterfrom
xAstroBoy:meta-quest-zygisk-fix

Conversation

@xAstroBoy

Copy link
Copy Markdown

Fixes #9940.

Problem

Built-in Zygisk never becomes active on Meta Quest (Horizon OS): the app shows Zygisk: N/A and Zygisk modules (e.g. LSPosed) don't load. Most Quest users flash NeoZygisk (ptrace) as a workaround — but built-in Zygisk can work with a small change.

Root cause

Quest doesn't start the zygote the normal way:

  • ro.zygote = zygote64_stub32; init starts /system/bin/stub_zygote (Meta's "zygote partitioning" launcher), which fork()+execve()s a separate app_process64 zygote per security partition (system/trusted vs untrusted-app), driven by …hzos_security_zygote_partitioning_policy=untrusted_app.
  • The untrusted-app partition zygote — the one that forks 3rd-party apps and the Magisk manager — is spawned lazily, after boot-complete.

magiskd sets ro.dalvik.vm.native.bridge=libzygisk.so during boot, but at boot-complete it clears it again (ZygiskState::reset). On a normal device every zygote has already started by then, so clearing it is harmless. On Quest the untrusted-app partition zygote reads the (now-cleared) property when it finally execves app_process64, never loads libzygisk.so, and 3rd-party apps + the Magisk manager are never injected → Zygisk: N/A. (The trusted partition, spawned during boot, does get injected.)

Fix

daemon.rs — on the boot-complete reset(restore=true) path, reset the crash counter but keep native.bridge set (set_prop) instead of clearing it (restore_prop); only the >3-crash rollback path still clears. The lazily-spawned partition zygotes then load the loader when they start.

hook.cpp — two robustness fixes the loader needs on this device:

  • Wrap hook_zygote_jni()'s JNI locals in PushLocalFrame(64)/PopLocalFrame (and bail if GetEnv returns no env) so leaking locals can't trip ART's Check failed: … non-empty local reference table abort.
  • Make the strdup(ZygoteInit) trigger a substring match + add an idempotency guard so the zygote hooks install exactly once per process.

Verified

On Quest 3 (Android 14): system_server + every app fork is now intercepted (nativeForkAndSpecialize/nativeSpecializeAppProcess), and LSPosed loads end-to-end (its lspd daemon runs). The Magisk app shows Zygisk: Yes.

Happy to adjust — the two hook.cpp items may be cheap insurance rather than strictly required on a clean stock build, but they made the loader robust on Quest.


Supersedes #9941 — resubmitted after an account migration; branch and commits are identical.

@dushane-smith

This comment was marked as spam.

@aviraxp

aviraxp commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

This should be under a prop to only not restore it for affecting devices (to prevent detection).

Push x pop one should be under another commit. The reason we need it is that we are not in JNI frames in hooking context.

@vvb2060
vvb2060 force-pushed the meta-quest-zygisk-fix branch from e64ff0c to fb50a16 Compare July 29, 2026 07:02
@osm0sis
osm0sis force-pushed the meta-quest-zygisk-fix branch from 011262e to ef099a7 Compare August 15, 2026 11:13
…tion zygotes)

Meta Quest (Horizon OS) does not start the zygote the normal way: ro.zygote=
zygote64_stub32 launches /system/bin/stub_zygote, which forks a separate
app_process64 zygote PER security partition (system/trusted vs untrusted-app),
driven by hzos_security_zygote_partitioning_policy. The untrusted-app partition
zygote -- the one that forks 3rd-party apps and the Magisk manager -- is spawned
LAZILY, after boot-complete.

magiskd sets ro.dalvik.vm.native.bridge=libzygisk.so during boot but clears it
again at boot-complete (ZygiskState::reset). On a normal device every zygote has
already started by then, so clearing it is harmless. On Quest the untrusted-app
partition zygote reads the (now cleared) property when it finally execve's
app_process64, never loads libzygisk.so, so 3rd-party apps + the Magisk manager
are never injected -> "Zygisk: N/A".

Fix (daemon.rs): on the boot-complete reset(restore=true) path, reset the crash
counter but KEEP native.bridge set (set_prop) instead of clearing it
(restore_prop); only the >3-crash rollback path still clears. The lazily-spawned
partition zygotes then load the loader when they start.

Also (hook.cpp), robustness fixes the loader needs on this device:
 - Wrap hook_zygote_jni()'s JNI locals in PushLocalFrame(64)/PopLocalFrame so
   leaking locals can't trip ART's "non-empty local reference table" abort, and
   bail if GetEnv returns no env.
 - Make the strdup(ZygoteInit) trigger a substring match + add an idempotency
   guard so the zygote hooks install exactly once per process.

Verified on Quest 3 (Android 14): system_server + every app fork is intercepted
and LSPosed loads end-to-end.
@osm0sis
osm0sis force-pushed the meta-quest-zygisk-fix branch from 92e7281 to de95686 Compare August 19, 2026 02:07
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.

Zygisk doesn't inject on Meta Quest 3 (partitioned zygote64_stub32 / stub_zygote): native.bridge cleared before the untrusted-app partition spawns

3 participants