Repository navigation
Bind an empty directory over /data/resource-cache, not a tmpfs - #1030
Merged
Merged
Conversation
The daemon has shadowed /data/resource-cache since LSPosed/LSPosed#1627, whose only stated reason was "to avoid overlay conflict". The mechanism was never recorded, which is why the line was dropped in 156c6ae and restored the next day in 3c020a9 on a suspicion nobody could confirm. It is this. The daemon is a root app_process that initializes framework assets, and AssetManager.createSystemAssetsInZygoteLocked asks OverlayConfig for the immutable framework idmaps; that call forks `idmap2 create-multiple`, which writes into kIdmapCacheDir, that is /data/resource-cache, under no lock. The zygote does the same during preload, before it creates /dev/socket/zygote, so a daemon starting alongside it can truncate the files the zygote is writing. On Android 10 and earlier the same entry point instead read /data/resource-cache/overlays.list and threw IllegalStateException when a listed idmap failed to load, so a list left by the previous boot could take the daemon down. Shadowing the directory keeps the daemon out of both. A tmpfs pays for that with a global trace. A filesystem with no block device draws its dev_t from one kernel-global pool: get_anon_bdev() returns the lowest free id and holds it until the superblock dies, so the live set is dense from below and a minor missing from a process's own mountinfo is not a free slot but a superblock alive elsewhere, including one in a private mount namespace that no per-namespace check can reach. A bind mount allocates nothing, since it reuses the source superblock. Bind an empty directory of our own instead, recreated on every start so that nothing accumulates in it. See JingMatrix/Demo@1f91ee6 for the check this closes.
LxyaoX
added a commit
to LxyaoX/Vekta
that referenced
this pull request
Oct 25, 2026
Sync 69 upstream commits. Conflict resolutions: - build.gradle.kts: keep fork minSdk 31 / arm64-only / tag filter, adopt upstream NDK r30 - gradle wrapper: keep 9.8.2 with matching wrapper jar (upstream moved to 9.8.0) - gradle/libs.versions.toml: keep fork (kotlin 2.4.21, m3 1.5.0-beta01, navigationevent 1.2.0 are newer than upstream; remaining bumps already identical) - .gitmodules: keep fork (libxposed submodules stay removed, upstream mirror URLs not applicable) - zygisk/module/daemon: keep fork JDWP/Vekta tags, adopt upstream bind-mount fix for /data/resource-cache (JingMatrix#1030) - README.md: keep Vekta branding, adopt upstream libxposed mirror link Co-authored-by: JingMatrix <[email protected]>
LxyaoX
added a commit
to LxyaoX/Vekta
that referenced
this pull request
Oct 25, 2026
Carry over the upstream deltas for files that both sides changed and the fork keeps its own version of: - build.gradle.kts: NDK r30 (30.0.16248370) - README.md: libxposed API mirror link - zygisk/module/daemon: bind-mount an empty directory over /data/resource-cache instead of tmpfs (JingMatrix#1030) Co-authored-by: JingMatrix <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/data/resource-cachehas been shadowed in the daemon's mount namespace since LSPosed/LSPosed#1627 of February 2022, whose entire justification was "to avoid overlay conflict". Nobody wrote down what that conflict was, which is why the line was dropped in 156c6ae and restored the next day in 3c020a9 on a suspicion that could not be confirmed. This records the reason and keeps the shadowing, but pays for it with a mount that leaves nothing to count from outside the namespace.The daemon is a root
app_process, and it initializes framework assets twice over: once throughActivityThread.systemMain(), which builds a system context, and once through theAssetManagerbehindFileSystem.resources, whose public constructor andResources.getSystem()both land increateSystemAssetsInZygoteLocked. On Android 11 and later that method asksOverlayConfigfor the immutable framework idmaps. The comment oncreateImmutableFrameworkIdmapsInZygotesays only the zygote may invoke it, and nothing enforces that: it forksidmap2 create-multiple, which writes intokIdmapCacheDir, that is/data/resource-cache, through a plainstd::ofstreamwith no lock held.The zygote reaches the same code from
preload()by way ofResources.preloadResources, and only creates/dev/socket/zygoteafter preloading has returned. A daemon running alongside it therefore derives the same idmap paths and can truncate the file the zygote is writing, which is the overlay conflict of #1627; the up-to-date check that normally makes the second writer a no-op is no help while the first writer is still mid-file. On Android 10 and earlier the same entry point instead read/data/resource-cache/overlays.listand raisedIllegalStateExceptionwhen a listed idmap failed to load, so a list left behind by the previous boot could take the daemon down before it started.That window has been narrow since LSPosed/LSPosed#1748 added the wait on the zygote socket, and narrower still now that
service.shis the only launcher. The daemon nevertheless forksidmap2on every start and writes whatever the directory it is shown happens to lack, so the shadowing stays.What does not stay is the tmpfs. A filesystem with no block device draws its
dev_tfrom one kernel-global pool:get_anon_bdev()returns the lowest free id and holds it untilkill_anon_super()releases it. The live set is dense from below, so a minor missing from a process's own mountinfo is not a free slot but a superblock that is alive and invisible from there, including one mounted only in a daemon's private namespace, which no per-namespace check can reach. A tmpfs over/data/resource-cacheis exactly such a superblock, and the gap it leaves is readable by any app on the device: JingMatrix/Demo@1f91ee6. A bind mount allocates nodev_tat all, since it reuses the superblock of its source, so binding an empty directory of our own hides the cache just as well while allocating nothing. The directory is wiped and recreated on every start, so the idmaps regenerated inside it do not accumulate, and the daemon keeps its framework overlays as it did before.The change is one file,
zygisk/module/daemon:mount tmpfs -t tmpfs /data/resource-cachebecomes a bind mount of$dir/.resource-cache, with the reason for the mount written above it.