From d82d60ad65ff17e29488ddbb557eaee82f045741 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Oct 2026 13:13:21 +0200 Subject: [PATCH] Bind an empty directory over /data/resource-cache, not a tmpfs 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 156c6ae8 and restored the next day in 3c020a9c 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 https://github.com/JingMatrix/Demo/commit/1f91ee6fc935d0c67f99c46aa43ff9dffa5c35b0 for the check this closes. --- zygisk/module/daemon | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/zygisk/module/daemon b/zygisk/module/daemon index 3b1dec4a5f..79a70d7991 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -25,9 +25,16 @@ if [ "$debug" = "true" ]; then fi fi -# Mount an empty temporary file system over /data/resource-cache to avoid possible overlay conflicts. +# Shadow /data/resource-cache with an empty directory of our own: initializing the framework assets +# in a root app_process forks `idmap2 create-multiple`, which would otherwise write the immutable +# framework idmaps of this boot into the cache every other process shares. Bind an empty directory +# rather than a tmpfs: a bind mount reuses the superblock of /data, so it claims none of the +# kernel-global anonymous device minors, whose gaps are visible from any process on the device. # Note that this script runs inside an isolated mount namespace (via unshare). -mount tmpfs -t tmpfs /data/resource-cache +emptyDir="$dir/.resource-cache" +rm -rf "$emptyDir" +mkdir -p "$emptyDir" +mount -o bind "$emptyDir" /data/resource-cache # Wait for Zygote socket using a polling loop if [ ! -S "/dev/socket/zygote" ]; then