GrapheneOS: InviZible Pro tried to perform DCL via storage
In several versions of Invizible Pro Beta, including most recently 2.5.4, GrapheneOS (currently 2025071900) occasionally notifies that "Invizible Pro tried to perform DCL via storage." This is an exploit protection, restricted by default in GrapheneOS. I usually have DNSCRYPT and TOR enabled in VPN mode, I2P not enabled, Firewall enabled.
I have not found any specific trigger for this, but if this changes I will follow up here. I also have not noticed any interruption to Invizible's operation when the DCL notifications occur; if an interruption occurs, it seems to be temporary. So I do not consider this to be an urgent issue, but I hope this information is helpful.
Thank you for providing and maintaining Invizible.
Invizible Pro tried to perform DCL via storage.
InviZible does not use DCL at all. It is difficult to say whether this is a false positive or something else, as I do not have a phone with GrapheneOS. Perhaps someone else can provide additional information on this issue.
@Gedsh It's VERY rare, i think i've seen it today during:
- Update OS
- Reboot
- After OS is loaded, GrapheneOS saying something "wait for optimization of apps", during that process today i've seen this DCL via storage triggered.
Yes, last 3 updates it was definitely always triggered after update-reboot!
Anyway, InviZible doesn't use DCL at all.
@thestinger Any ideas why it's triggered here, after GrapheneOS update -> reboot?
I think this may be triggered by native code loading content from files, e.g. blocklists. E.g. I think it triggers DCL warnings every time, once a blocklist is configured. (Although the DCL warning doesn't provide sufficient detail in the warning-message for me to confirm this.)
Blocklists really shouldn't involve code from files. Perhaps they implemented it as some kind of plugin that's loaded as code but that's a genuine security weakness, especially if it isn't signed, but even if it is signed it's a way for an attacker to modify code on storage to persist a compromise within the app.
Blocklists really shouldn't involve code from files.
That is clear to me.
Perhaps they implemented it as some kind of plugin that's loaded as code but that's a genuine security weakness, especially if it isn't signed, but even if it is signed it's a way for an attacker to modify code on storage to persist a compromise within the app.
The blocklists are textfiles, essentially. What about file-reading that is done from within subprocess of a native binary (dnscrypt-proxy) that is part of the APK? Is it possible that you cannot distinguish between plain file reads and otherwise, in such cases, because it already involves a native process?
Note: I am not intimately familiar with InviZible code-base, but I'll provide information to the best of my knowledge.
There are zero false positives for either the memory or storage dynamic code loading blocking. If it blocks and shows the notice, it blocked doing it. Many cases of apps doing this are mistakes opening up vulnerabilities and it blocks it happening. For example, an app may accidentally map a file as executable or may not realize they're using a legacy approach to library inclusion instead of mapping it directly from their APK. The still legacy but inferior way is having the package manager extract it and mapping it from there which the app cannot write to, protecting against persistent compromise through the app, but it's not quite as secure as mapping it directly from the APK against OS level attacks.
GrapheneOS has support for blocking dynamic code loading by apps split between a memory protection and a storage protection. We enforce both for all base OS apps with the exception of our Vanadium browser app and WebView. Vanadium has it enforced on a per-process basis via seccomp-bpf. It has an opt-in per-site JIT toggle and an opt-out per-app WebView JIT toggle. For user installed apps, neither are blocked by default but users can change it per-app and can set the global default to blocked. We have an exception system which detects known cases of it being required and overrides the global default being set to enabled for those but users can still force enable the protections.
For the memory protection, it prevents having memory which is both writable and executable combined with preventing making memory which was ever previously writable previously into executable memory including for tmpfs/ashmem/memfd. This prevents in-memory generation or modification of code. This is entirely implemented with SELinux for native code and has no false positives. We also implement it in the class loader for the Android Runtime for Java/Kotlin. An app can bring their own interpreter or could modify their own code to bypass the Java/Kotlin part but the point is protecting apps from themselves.
For the storage protection, it's similarly entirely implemented via SELinux for native code. It blocks the native execution of executables in app data (app_data_file SELinux label) and other app writable storage. It also blocks mapping it as executable, which is how libraries work rather than being run as executable. If an app extracts a library to app data and tries to load it, it will be blocked. If an app accidentally uses PROT_EXEC for mapping a file from app data, that will be blocked. Kotlin/Java class loading in the runtime is also blocked similarly to the in-memory blocking.
The native code blocking is done in the kernel via SELinux and cannot be bypassed by apps. For blocking dynamic Java/Kotlin class loading, it's done within the Android Runtime which runs as part of the app itself. An app can bring their own interpreter which won't be blocked or could bypass the check for Java/Kotlin. The point of the feature is not blocking apps from doing those things, it's protecting them from common vulnerabilities including mistakenly doing dynamic code loading when they don't need it and work without it.
For the storage blocking, the notification shows the file path it blocked from executing, mapping as executable or loading Java/Kotlin classes from which is usually enough to figure out what's happening and why.
Ah, if those are the mechanisms, then I know how to reason about these protections. I will look into this further.
Concerning the warnings: I have seen frequent warnings lately. So, whether or not from blocklists, they are coming from somewhere. There were not such frequent updates.
or may not realize they're using a legacy approach to library inclusion instead of mapping it directly from their APK. The still legacy but inferior way is having the package manager extract it and mapping it from there which the app cannot write to, protecting against persistent compromise through the app, but it's not quite as secure as mapping it directly from the APK against OS level attacks
Thank you! Now it's clear why GrapheneOS thinks InviZible uses DCL. InviZible indeed uses this legacy approach. InviZible libraries are not actually libraries, but binary files. They need to be extracted from the apk to run as executables. This greatly simplifies the development process, as I can use the original implementations from the upstream projects without unnecessary wrappers. Furthermore, it is completely safe, as the libraries are read-only.
Since this is the only solution for running binaries on Android, do you consider this approach to be valid use case that should not trigger a DCL warning?
It's VERY rare, i think i've seen it today during:
Update OS Reboot After OS is loaded, GrapheneOS saying something "wait for optimization of apps", during that process today i've seen this DCL via storage triggered.
Also, InviZible uses https://developer.android.com/topic/performance/baselineprofiles Could GrapheneOS be treating it as DCL?
Since this is the only solution for running binaries on Android, do you consider this approach to be valid use case that should not trigger a DCL warning?
It's not the only way to do it. You can include them via the official approach to include libraries but without the modern non-extract mode enabled so that they're extracted by the package manager rather than the app. This is much more secure because they're extracted to where only the package manager can write to them and the app can read but not write them (apk_data_file SELinux label instead of app_data_file).
Android itself forbids this for target API 29 or higher for executables, but not libraries:
https://developer.android.com/about/versions/10/behavior-changes-10#execute-permission
You can include them as libraries in the APK and enable extractNativeLibs to have the package manager extract them to the library directory where executables can be run from:
https://developer.android.com/guide/topics/manifest/application-element#extractNativeLibs
Also, InviZible uses https://developer.android.com/topic/performance/baselineprofiles Could GrapheneOS be treating it as DCL?
No, that's not related to the DCL features.
You can include them as libraries in the APK and enable extractNativeLibs to have the package manager extract them to the library directory where executables can be run from
That's exactly what InviZible does. Perhaps my explanation above isn't entirely clear. Does GrapheneOS treat this as DCL?
No, it's not DCL as long as the OS package manager is extracting them which is marked as apk_data_file similar to the APK rather than app_data_file which the app can write to. Our DCL protection disallows executing from app_data_file since the app can write to it, not apk_data_file including extracted libraries.
Android API 29+ disallows directly executing stuff in app_data_file itself but still allows mapping it as executable, which we don't allow, so that's why I was mentioned that above. It seemed like the app couldn't be doing that. The issue must be something else.
The issue must be something else.
Then I 'm out of ideas. I insist that InviZible does not use DCL.
The feature doesn't have false positives. It will show the file path in the notification for DCL from storage. No one has provided the full error message they're receiving here.
Here's a message. Tell me if you need anything else. This is after re-enabling Invizible (stable), i.e. with prior configuration preserved:
type: storage_DCL
osVersion: google/raven/raven:16/BP3A.250905.014/2025112101:user/release-keys
package: pan.alexander.tordnscrypt.stable:3261,
targetSdk 35
package: pan.alexander.tordnscrypt.stable:3261
DCL denial type: DENY_EXECUTE_APP_DATA_FILE
The app is trying to execute data labelled as app_data_file. A library extracted by the package manager is not app_data_file but rather apk_data_file. You'll need to determine what it's trying to map as executable from app_data_file. Perhaps it's using mmap with PROT_EXEC by accident or something similar. Trying to load a library from app data will trigger this. It's a real security issue.
Additionally, in system logging. SELinux audit failures, several occurrences: (copied by hand so expect minor changes, at least in spacing) @Gedsh
12-01 16:03:39.803 11256 11256 I auditd : avc=type=1400 audit(0.0:27925):
avc: denied { execute } for comm="sh" name="busybox" dev="dm-57" ino=<number> scontext=u:r:untrusted_app:s0:c26,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c26,c256,c512,c768 tclass=file permissive=0 app=pan.alexander.tordnscrypt.stable
@cobratbq, are you using root mode?
@cobratbq, are you using root mode?
No. VPN. (I don't think I ever even switched to proxy mode.) Some changes to configuration. No root and no access to it.
I was checking the apk and found some busyb.mp3 and some others for dnscrypt and tor. Haven't checked contents yet. Look like they could be audio by size of it. That is, apart from your other native binary assets.
busyb.mp3 is a zip-file containing a 1.5 MB program binary. Same with the other .mp3 files. So there is more than the packaged binaries in /lib/arm64-v8a/lib*.so of which some are executable program-binaries.
No. VPN.
It seems that this happens when InviZible tries to stop its services, such as Tor. Busybox cannot be used in VPN mode, but it can indeed trigger DCL. This is obsolete code. I will try to prevent it from executing in VPN mode, as it actually does nothing.
https://github.com/Gedsh/InviZible/blob/018ccb9a59a68802d61b4cac1e48200e518f5713/tordnscrypt/src/main/java/pan/alexander/tordnscrypt/modules/ModulesKiller.java#L505-L533
busyb.mp3 is a zip-file containing a 1.5 MB program binary
This is required for root mode, as well as for the iptables binary. Root mode needs to use this approach. There's no other way to use app's busybox and iptables in root mode.
But then why do you have to include 'dnscrypt', 'i2pd' and 'tor' in the same way? ~~Right now it seems like everything is shipped twice.~~ Okay, so not shipped twice. Those are additional files to the programs. So then the busybox program is the only program shipped in this indirect way.
@Gedsh can't you just include it in the same manner as lib*.so. You don't have to call on it if it isn't needed. Right?
can't you just include it in the same manner as lib*.so.
Unfortunately, this approach does not work in root mode.
You don't have to call on it if it isn't needed. Right?
Right. It doesn't actually do anything because it's blocked in modern versions of Android. But it can trigger a DCL warning.
You don't have to call on it if it isn't needed. Right?
Right. It doesn't actually do anything because it's blocked in modern versions of Android. But it can trigger a DCL warning.
I mean: you could include busybox in the same way you include dnscrypt-proxy at /lib/arm64-v8a/libdnscrypt-proxy.so instead of hiding it through busyb.mp3? Why not do it this way?