diff --git a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt index aff6724299..d8e4b65196 100644 --- a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt +++ b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistRulesTest.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.app.strictmode import android.os.strictmode.DiskReadViolation +import android.os.strictmode.DiskWriteViolation import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith @@ -98,4 +99,113 @@ class WhitelistRulesTest { ), ) } + + @Test + fun allow_DiskRead_on_MtkBoostFwkIsGameApp() { + assertAllowed( + // @formatter:off + stackTraceElement("java.io.File", "exists"), + stackTraceElement("com.mediatek.boostfwk.utils.Util", "isGameApp"), + stackTraceElement("com.mediatek.boostfwk.utils.TasksUtil", "isGameAPP"), + stackTraceElement("com.mediatek.boostfwk.identify.scroll.ScrollIdentify", "checkAppType"), + stackTraceElement("com.mediatek.boostfwk.identify.scroll.ScrollIdentify", "dispatchScenario"), + // @formatter:on + ) + } + + @Test + fun allow_DiskRead_on_MtkAsyncDrawableCachePutCacheList() { + assertAllowed( + // @formatter:off + stackTraceElement("java.io.File", "exists"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + stackTraceElement("android.content.res.ResourcesImpl", "cacheDrawable"), + // @formatter:on + ) + } + + @Test + fun allow_DiskRead_on_MtkAsyncDrawableCachePutCacheList_OsStatVariant() { + assertAllowed( + // @formatter:off + stackTraceElement("android.system.Os", "stat"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + stackTraceElement("android.content.res.ResourcesImpl", "cacheDrawable"), + // @formatter:on + ) + } + + @Test + fun allow_DiskWrite_on_MtkAsyncDrawableCachePutCacheList() { + assertAllowed( + // @formatter:off + stackTraceElement("libcore.io.IoBridge", "open"), + stackTraceElement("java.io.FileOutputStream", ""), + stackTraceElement("android.app.SharedPreferencesImpl", "createFileOutputStream"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + // @formatter:on + ) + } + + @Test + fun allow_DiskWrite_on_MtkAsyncDrawableCache_FileOutputStreamWrite() { + assertAllowed( + // @formatter:off + stackTraceElement("java.io.FileOutputStream", "write"), + stackTraceElement("com.android.internal.util.FastXmlSerializer", "flushBytes"), + stackTraceElement("com.android.internal.util.FastXmlSerializer", "flush"), + stackTraceElement("com.android.internal.util.FastXmlSerializer", "endDocument"), + stackTraceElement("com.android.internal.util.XmlSerializerWrapper", "endDocument"), + stackTraceElement("com.android.internal.util.XmlUtils", "writeMapXml"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + // @formatter:on + ) + } + + @Test + fun allow_DiskWrite_on_MtkAsyncDrawableCache_FileDelete() { + assertAllowed( + // @formatter:off + stackTraceElement("java.io.UnixFileSystem", "delete"), + stackTraceElement("java.io.File", "delete"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + // @formatter:on + ) + } + + @Test + fun allow_DiskWrite_on_MtkAsyncDrawableCache_OsChmod() { + assertAllowed( + // @formatter:off + stackTraceElement("android.system.Os", "chmod"), + stackTraceElement("android.os.FileUtils", "setPermissions"), + stackTraceElement("android.app.ContextImpl", "setFilePermissionsFromMode"), + stackTraceElement("android.app.SharedPreferencesImpl", "writeToFile"), + stackTraceElement("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + stackTraceElement("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + stackTraceElement("com.mediatek.res.ResOptExtImpl", "putCacheList"), + // @formatter:on + ) + } } diff --git a/app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt b/app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt index 02a30e6e66..9f396072fe 100644 --- a/app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt +++ b/app/src/main/java/com/itsaky/androidide/app/strictmode/WhitelistEngine.kt @@ -1,7 +1,9 @@ package com.itsaky.androidide.app.strictmode import android.os.strictmode.DiskReadViolation +import android.os.strictmode.DiskWriteViolation import androidx.annotation.VisibleForTesting +import com.itsaky.androidide.app.strictmode.FrameMatcher.Companion.anyOf import com.itsaky.androidide.app.strictmode.FrameMatcher.Companion.classAndMethod import android.os.strictmode.Violation as StrictModeViolation @@ -220,6 +222,93 @@ object WhitelistEngine { classAndMethod("com.itsaky.androidide.activities.OnboardingActivity", "checkToolsIsInstalled"), ) } + + rule { + ofType() + allow( + """ + On MediaTek devices, BoostFwk's 'Util.isGameApp' is invoked from ScrollIdentify + (via OverScroller's BoostFwkManagerImpl.perfHint) when a RecyclerView/ViewPager2 + is initialized. It checks for a file's existence, resulting in a DiskReadViolation. + Since we can't control when BoostFwk performs scenario detection, we allow this + violation. + """.trimIndent(), + ) + + matchAdjacentFrames( + classAndMethod("java.io.File", "exists"), + classAndMethod("com.mediatek.boostfwk.utils.Util", "isGameApp"), + classAndMethod("com.mediatek.boostfwk.utils.TasksUtil", "isGameAPP"), + classAndMethod("com.mediatek.boostfwk.identify.scroll.ScrollIdentify", "checkAppType"), + ) + } + + rule { + ofType() + allow( + """ + On MediaTek devices, AsyncDrawableCache hooks into Resources.loadDrawable and + synchronously commits to a SharedPreferences-backed cache. This is triggered by + routine layout inflation and produces a DiskReadViolation. We anchor this rule + with a java.io disk-access frame at the top and the SharedPreferencesImpl + commit frame immediately adjacent to the vendor chain, so it only fires on the + actual prefs-write path inside this vendor caching code. + """.trimIndent(), + ) + + matchAdjacentFramesInOrder( + listOf( + listOf( + anyOf( + classAndMethod("java.io.File", "exists"), + classAndMethod("java.io.FileInputStream", ""), + classAndMethod("java.io.FileOutputStream", ""), + classAndMethod("java.io.RandomAccessFile", ""), + classAndMethod("android.system.Os", "stat"), + ), + ), + listOf( + classAndMethod("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + classAndMethod("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + classAndMethod("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + classAndMethod("com.mediatek.res.ResOptExtImpl", "putCacheList"), + ), + ), + ) + } + + rule { + ofType() + allow( + """ + On MediaTek devices, AsyncDrawableCache hooks into Resources.loadDrawable and + synchronously commits to a SharedPreferences-backed cache. The same path also + produces a DiskWriteViolation when the prefs file is written. We anchor this + rule with a concrete write-path frame at the top and the SharedPreferencesImpl + commit frame immediately adjacent to the vendor chain, so it only fires on the + actual prefs-write path inside this vendor caching code. + """.trimIndent(), + ) + + matchAdjacentFramesInOrder( + listOf( + listOf( + anyOf( + classAndMethod("java.io.FileOutputStream", ""), + classAndMethod("java.io.FileOutputStream", "write"), + classAndMethod("java.io.File", "delete"), + classAndMethod("android.system.Os", "chmod"), + ), + ), + listOf( + classAndMethod("android.app.SharedPreferencesImpl\$EditorImpl", "commit"), + classAndMethod("com.mediatek.res.AsyncDrawableCache", "storeDrawableId"), + classAndMethod("com.mediatek.res.AsyncDrawableCache", "putCacheList"), + classAndMethod("com.mediatek.res.ResOptExtImpl", "putCacheList"), + ), + ), + ) + } } /**