Skip to content

Let the parasitic manager load its DEX on GrapheneOS - #805

Merged
JingMatrix merged 2 commits into
masterfrom
fix-graphene-dcl
Aug 1, 2026
Merged

Let the parasitic manager load its DEX on GrapheneOS#805
JingMatrix merged 2 commits into
masterfrom
fix-graphene-dcl

Conversation

@JingMatrix

@JingMatrix JingMatrix commented Jul 30, 2026

Copy link
Copy Markdown
Owner

On GrapheneOS, the "Restrict dynamic code loading" (memory DCL) exploit-protection setting is immutable and enabled for system apps. The parasitic manager runs inside com.android.shell, a system app, so the manager's DEX cannot be loaded and the manager fails to start.

This change hooks a single method in system_server, AswRestrictMemoryDynCodeLoading.getImmutableValue, and returns false for com.android.shell. All other apps retain GrapheneOS's original behaviour.

The choice of method and return value follows from the GrapheneOS source:

  • AswRestrictMemoryDynCodeLoading.getImmutableValue determines the per-app verdict and returns true for non-allowlisted system apps, which is what blocks the shell.
  • AppSwitch.get is the value consumed by enforcement. A non-null result from getImmutableValue takes precedence over both the user toggle and the default, so returning false forces the setting to allowed regardless of the user's configuration. The three possible results are false (allowed), true (restricted), and null (user-configurable).

GrapheneOS is identified solely by the presence of the class. On any other ROM, findClass throws ClassNotFoundError and the hook does nothing.

The scope is intentionally narrow:

  • Only com.android.shell is affected, and only in system_server, where the value is computed. No other app's verdict changes.
  • No new build variable and no daemon change are required, as the host package is already exposed as BuildConfig.InjectedPackageName.
  • Modules are excluded. On GrapheneOS a normal app already exposes a user-configurable DCL toggle, so a module can be permitted without a patch. Automatically allowing enabled scopes would require the scope list inside system_server and can be addressed separately.

This supersedes #711 with a smaller footprint and incorporates the review feedback there: the GRAPHENE_SETTINGS_PACKAGE_NAME build variable is dropped, and the hook resides in its own class. The technique was originally developed by @Enovale in #711 and in discussion #340.

GrapheneOS ships a "Restrict dynamic code loading" (memory DCL)
exploit-protection setting that is immutable and enabled for system
apps. The parasitic manager runs inside the host package
com.android.shell (a system app), so GrapheneOS forbids it from loading
the manager's DEX and the manager never starts.

Hook AswRestrictMemoryDynCodeLoading.getImmutableValue in system_server
and return false (allowed) for that single host package, deferring to
the original verdict for every other app. GrapheneOS is detected by the
class being present, so the hook is a no-op on every other system.

The scope is intentionally narrow: only the manager host is affected.
Module apps are not, since a normal app already exposes the DCL toggle
as user-configurable on GrapheneOS and can be allowed without a patch.

Co-authored-by: Enovale <17408285+Enovale@users.noreply.github.com>
@JingMatrix

Copy link
Copy Markdown
Owner Author

This one's a small, GrapheneOS-only change and I'd like to confirm it on a real device before merging. @Enovale @raxod502 @User10987654321 @Shidapu @m-doescode — if any of you have a few minutes, could you give it a try?

The build is on the PR CI run here: https://github.com/JingMatrix/Vector/actions/runs/30518058782 — the artifacts are at the bottom (you'll need to be signed in to GitHub). Grab Vector-v2.0-3054-Release and flash it as a Zygisk module in Magisk or KernelSU. There's also a Debug artifact if you'd rather capture a log.

Ideally test on a clean GrapheneOS with no other modules enabled, so we know the manager opening is down to this change and nothing else. I'm mainly checking that the manager opens normally; if you use modules, they should load once you allow their DCL toggle.

A quick "works" or "doesn't", with your GrapheneOS version, would help a lot. Thanks!

@m-doescode

m-doescode commented Jul 30, 2026

Copy link
Copy Markdown

sadly not :(

Tapping the notification crashes the created activity (ANR prompt appears)

This is the error:

signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'JNI FatalError called: (org.matrix.vector.manager) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:2112: selinux_android_setcontext(2000, 0, "platform:privapp:targetSdkVersion=36:partition=system:complete", "org.matrix.vector.manager", selinux_flags: "198") failed'

NOTE: This PR may in fact fix the DCL issue, however there was a new breakage in the form of #786 on later builds. I'm unsure if this fix can solve that, but I don't know enough to say

@JingMatrix

Copy link
Copy Markdown
Owner Author

@m-doescode Please upload full logs for me to investigate deeper.

@m-doescode

Copy link
Copy Markdown

Sure, will the /data/adb/lspd/log/ directory suffice?

log.zip

@raxod502

Copy link
Copy Markdown

(Un)fortunately, I no longer use GrapheneOS due to exactly problems like this! I migrated to /e/OS a couple of weeks ago and am not looking back.

I could install it on a test device if it is really necessary, but that would take many hours to get everything configured, so I'd rather avoid it unless it's the only way forward and the results would be really valuable. Looks like we've got at least one other active GOS user though 🤞

@Enovale

Enovale commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't tried this build yet, but from looking at your description and the code I believe this will be inadequate for GrapheneOS. Users are not allowed to reconfigure the DCL restriction for system apps, so any module that modifies system apps will be unusable. For example: https://github.com/Xposed-Modules-Repo/com.github.dan.nostoragerestrict

The only "low-scope" way to fix this is to allow all system apps to be user-configurable by patching the Settings package as I did in my PR and GrapheneFixer.

@m-doescode

Copy link
Copy Markdown

The only "low-scope" way to fix this is to allow all system apps to be user-configurable by patching the Settings package as I did in my PR and GrapheneFixer.

I think it's fair that upstream Vector only modifies as much as necessary such that the manager can be accessed at all. Then, for modules that need to patch system apps on Graphene, the GrapheneFixer module may be used instead. I understand though if you prefer this was upstreamed instead since I know otherwise it would put the burden of work on you.

Ultimately I'll leave the decision up to JingMatrix, but a built-in setting for "unlocking DCL options" in settings would be really nice.

@Enovale

Enovale commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

It's not really about the burden of work on me, I just feel that if you wish to make Vector work on GrapheneOS, it should work on GrapheneOS. Like I'll maintain the only fix for this if I have to but I just feel like users of GrapheneOS deserve a little better than "slightly less annoying than using the non-parasitic manager to install GrapheneFixer"

@JingMatrix

Copy link
Copy Markdown
Owner Author

NeoZygisk matches Zygote's native methods by exact JNI signature. GrapheneOS reshuffled those signatures in branch 17: extraLongArgs moved from the last parameter to the first in both nativeForkAndSpecialize and nativeSpecializeAppProcess, and nativeForkAndSpecialize also gained a useFifoUi argument.

NeoZygisk's current entries encode the earlier (GrapheneOS 16) layout, so on Android 17 those descriptors no longer resolve. The hook loop skips them silently, leaving only nativeForkSystemServer (signature unchanged) hooked; both app-specialize entrypoints stay unhooked.

The manager is specialized from the USAP pool via nativeSpecializeAppProcess. With that method unhooked, the module's pre-specialize callback never runs, so the process name is never masked back to com.android.shell and the SELinux context set aborts (uid 2000, name org.matrix.vector.manager) — consistent with the [J-first frame in the backtrace and the shell-domain rule in seapp_contexts.

I'll implement a fix of NeoZygisk for GrapheneOS 17.

@m-doescode

m-doescode commented Jul 30, 2026

Copy link
Copy Markdown

I see! Thank you so much for your work. I'll keep an eye out for the NeoZygisk update.

EDIT:
Fantastic! I tried the update and now Vector is fixed! Thank you!

As for this patch, unfortunately I am still getting a DCL error popup. I'll upload the logs here in a second, please wait.

Crash report:
Error in Shell 753c9edaf347.txt

Lspd logs (release):
log.zip

@JingMatrix

Copy link
Copy Markdown
Owner Author

Thanks for the logs — that's a different GrapheneOS check than this PR was closing. GrapheneOS splits "Restrict dynamic code loading" into three separate restrictions (memory, storage, WebView); the earlier build only cleared memory. You're now hitting the storage one: the manager's DEX is transplanted into the shell process from a /proc/self/fd/N handle, which isn't on GrapheneOS's storage allow-list (/apex, /system, /data/app, …), so binding aborts with SecurityException: DCL via storage. It only surfaced now because the NeoZygisk fix let the parasite get far enough to load its DEX.

I've pushed a commit that clears all three DCL flags for the shell host at once (hooking DynCodeLoading.getAppBindFlags instead of the single memory setting). Could you grab the fresh build and try again — ideally on a clean GrapheneOS with no modules, just to confirm the manager opens from the notification? Artifacts are at the bottom of this run (sign in to GitHub to download): https://github.com/JingMatrix/Vector/actions/runs/30606099206 — flash Vector-…-Release as a Zygisk module. A quick works/doesn't with your GrapheneOS version would help.

@m-doescode

Copy link
Copy Markdown

Unfortunately this one crashes as well, but this time it crashes during startup (after SystemUI loads, as opposed to when tapping the notification)

log.zip

(For the record, /data/adb/lspd/config does exist, so a I'm thinking it's a permission error)

@JingMatrix

Copy link
Copy Markdown
Owner Author

@m-doescode Were you using the correct NeoZygisk build (this CI: https://github.com/JingMatrix/NeoZygisk/actions/runs/30606180030) ?
You make ensure that Zygisk is active before testing Vector.

@m-doescode

m-doescode commented Jul 31, 2026

Copy link
Copy Markdown

My bad, I did not realize there was an update.

Unfortunately still, the same error occurs, no change in logs. Zygisk is active, but then it crashes (likely as a result of a bug in the Vector build)

@JingMatrix

JingMatrix commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Please flash both the debug builds of NeoZygisk (CI of master) and Vector (CI of this PR).

Since the last logs aren't informative at all, our goal now is to get the most of logs. You can read #123 for installing manager directly, so that to get logs generated by the manager. Before exporting the logs, please open parasitic manager to trigger the errors of course.

@m-doescode

m-doescode commented Jul 31, 2026

Copy link
Copy Markdown

Ah alright. I'll install the debug build

Strangely after installing the debug build of NeoZygisk the boot crash no longer happens. However, the DCL error with the parasitic manager via notification still occurs. I have tapped on it and saved the logs through the manager apk

vector-logs-20260731-120404.zip

(NeoZygisk cb7e89c debug, Vector abe0bfd debug)

The memory-only hook let the manager's DEX map into com.android.shell,
but GrapheneOS enforces dynamic code loading through two channels, both
fed by the same per-app verdict, and clearing memory alone covers only
part of each:

  - The ART DexFile checks (DynCodeLoading.getAppBindFlags) still
    rejected the manager's transplanted /proc/self/fd DEX as "DCL via
    storage".
  - The kernel grapheneos_flags written at zygote specialize kept
    DENY_EXECMEM/DENY_EXECMOD set, so LSPlant/Dobby could not make its
    inline-hook trampoline executable and the process took a SIGSEGV
    (TSEC_FLAG_DENY_EXECMEM: op denied). The manager's isolated WebView
    process likewise kept DENY_EXECMEM, disabling Chromium's JIT.

Both getAppBindFlags and SELinuxFlags.{get,getForWebViewProcess} read
AswRestrict{Memory,Storage,WebView}DynCodeLoading.get(), which honours a
non-null getImmutableValue. Extend the existing memory hook to the
storage and WebView switches so the verdict is cleared at the shared
source, neutralising both channels for every DCL category at once.

The manager host ends up as an ordinary DCL-allowed app: the DexFile
checks are off and the exec* SELinux flags cleared, while ptrace denial,
hardened_malloc and MTE stay intact, and every other app keeps
GrapheneOS's verdict. Verified against the GrapheneOS 14, 16 and 17
branches; a no-op on releases without these classes.
@JingMatrix

Copy link
Copy Markdown
Owner Author

Thanks — those debug logs pinned it down. The good news first: the storage check this PR was originally about is now cleared. Your log shows the manager's DEX mapping in fine (Framework ClassLoader created successfully, no more "DCL via storage"). The crash just moved one layer deeper.

GrapheneOS enforces "Restrict dynamic code loading" through two separate paths that both read the same per-app setting: the ART DexFile checks (which the last build cleared, so the DEX now loads), and a kernel flag written at process startup that stops a process from making memory executable — that's the TSEC_FLAG_DENY_EXECMEM: op denied line, and the SIGSEGV right after. The manager needs executable memory for its hooking engine, so it aborted the moment it tried to hook. The same flag would also have disabled the JIT in the manager's in-app browser.

The previous build only cleared the DexFile path. I've pushed one that clears the setting at its source for the shell host — memory, storage and WebView categories together — so both paths see "allowed". Nothing changes for any other app.

Could you give it one more go? Same setup as before (debug NeoZygisk from master + debug Vector from this PR) — you can keep the NeoZygisk build you already have and just reflash Vector. Fresh artifacts are at the bottom of this run once it finishes: https://github.com/JingMatrix/Vector/actions/runs/30691636408 — open the parasitic manager from the notification to trigger it, and if anything still fails, the manager-side logs like last time would be perfect. A quick works/doesn't with your GrapheneOS version would help a lot. Thanks again for the patient testing!

@ZingyAwesome

Copy link
Copy Markdown

Working using the release version of the linked actions build on GOS 2026072901.
One note, if you have the old "LSPosed" home screen shortcut that will not work, you have to open Vector via Magisk/KSU action button and create a new "Vector" home screen shortcut.

@m-doescode

Copy link
Copy Markdown

No trouble at all! I'm glad to be of help :)

Fortunately, it finally works! Thank you so much for your help and efforts, and for getting this fixed so quickly.

Good luck!

@JingMatrix
JingMatrix merged commit b5bce6a into master Aug 1, 2026
1 check passed
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.

Manager crashes on GrapheneOS Parasitic manager fails to open on GrapheneOS

5 participants