Add more secure method of vold app data isolation (needs testing - enabled as default), try to fix the corrupted config issue

This commit is contained in:
frknkrc44 2025-09-19 21:28:43 +03:00
commit 2a00fe9b13
No known key found for this signature in database
GPG key ID: 784AE1CC0DCFD34F
16 changed files with 163 additions and 51 deletions

View file

@ -44,7 +44,6 @@ jobs:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
run: |
echo officialBuild=true >> local.properties
echo buildWithGitSuffix=true >> local.properties
echo storePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> local.properties
echo keyAlias='${{ secrets.ALIAS }}' >> local.properties
echo keyPassword='${{ secrets.ALIAS_KEY_PASSWORD }}' >> local.properties

View file

@ -30,7 +30,7 @@ jobs:
- name: Prepare env
run: |
echo buildWithGitSuffix=true >> local.properties
touch local.properties
mkdir -p ~/.config/.android
ln -s ~/.config/.android ~/.android

View file

@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatDelegate
import icu.nullptr.hidemyapplist.receiver.AppChangeReceiver
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.service.PrefManager
import icu.nullptr.hidemyapplist.ui.util.makeToast
import icu.nullptr.hidemyapplist.ui.util.showToast
import icu.nullptr.hidemyapplist.util.ConfigUtils.Companion.getLocale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -29,7 +29,7 @@ class MyApp : Application() {
super.onCreate()
hmaApp = this
if (!filesDir.absolutePath.startsWith("/data/user/0/")) {
makeToast(R.string.do_not_dual)
showToast(R.string.do_not_dual)
exitProcess(0)
}
AppChangeReceiver.register(this)

View file

@ -4,7 +4,7 @@ import android.os.Build
import android.util.Log
import icu.nullptr.hidemyapplist.common.JsonConfig
import icu.nullptr.hidemyapplist.hmaApp
import icu.nullptr.hidemyapplist.ui.util.makeToast
import icu.nullptr.hidemyapplist.ui.util.showToast
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.common.BuildConfig
import java.io.File
@ -19,25 +19,36 @@ object ConfigManager {
val configFile = File("${hmaApp.filesDir.absolutePath}/config.json")
fun init() {
if (!configFile.exists()) {
configFile.writeText(JsonConfig().toString())
val configFileIsNew = !configFile.exists()
if (configFileIsNew) {
config = JsonConfig()
configFile.writeText(config.toString())
}
runCatching {
config = JsonConfig.parse(configFile.readText())
if (!configFileIsNew) config = JsonConfig.parse(configFile.readText())
val configVersion = config.configVersion
if (configVersion < BuildConfig.MIN_BACKUP_VERSION) throw RuntimeException("Config version too old")
config.configVersion = BuildConfig.CONFIG_VERSION
}.onSuccess {
saveConfig()
}.onFailure {
makeToast(R.string.config_damaged)
throw RuntimeException("Config file too old or damaged", it)
}.onFailure { catch ->
runCatching {
config = JsonConfig.parse(ServiceClient.readConfig() ?: throw RuntimeException("Service config is unavailable"))
config.configVersion = BuildConfig.CONFIG_VERSION
showToast(R.string.home_restore_config)
}.onSuccess {
saveConfig()
}.onFailure {
showToast(R.string.config_damaged)
throw RuntimeException("Config file too old or damaged", catch)
}
}
}
private fun saveConfig() {
val text = config.toString()
ServiceClient.writeConfig(text)
configFile.writeText(text)
ServiceClient.syncConfig(text)
}
var detailLog: Boolean
@ -84,6 +95,13 @@ object ConfigManager {
saveConfig()
}
var skipSystemAppDataIsolation: Boolean
get() = config.skipSystemAppDataIsolation
set(value) {
config.skipSystemAppDataIsolation = value
saveConfig()
}
fun importConfig(json: String) {
config = JsonConfig.parse(json)
config.configVersion = BuildConfig.CONFIG_VERSION

View file

@ -91,8 +91,10 @@ object ServiceClient : IHMAService, IBinder.DeathRecipient {
override fun getPackagesForPreset(presetName: String) =
getServiceLegacy()?.getPackagesForPreset(presetName)
override fun syncConfig(json: String) {
getServiceLegacy()?.syncConfig(json)
override fun readConfig() = getServiceLegacy()?.readConfig()
override fun writeConfig(json: String) {
getServiceLegacy()?.writeConfig(json)
}
override fun stopService(cleanEnv: Boolean) {

View file

@ -15,9 +15,9 @@ import icu.nullptr.hidemyapplist.hmaApp
import icu.nullptr.hidemyapplist.service.PrefManager
import icu.nullptr.hidemyapplist.service.ServiceClient
import icu.nullptr.hidemyapplist.ui.adapter.LogAdapter
import icu.nullptr.hidemyapplist.ui.util.makeToast
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.ui.util.showToast
import kotlinx.coroutines.launch
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentLogsBinding
@ -36,14 +36,14 @@ class LogsFragment : Fragment(R.layout.fragment_logs) {
registerForActivityResult(ActivityResultContracts.CreateDocument("text/x-log")) save@{ uri ->
if (uri == null) return@save
if (logCache.isNullOrEmpty()) {
makeToast(R.string.logs_empty)
showToast(R.string.logs_empty)
return@save
}
hmaApp.contentResolver.openOutputStream(uri).use { output ->
if (output == null) makeToast(R.string.home_export_failed)
if (output == null) showToast(R.string.home_export_failed)
else output.write(logCache!!.toByteArray())
}
makeToast(R.string.logs_saved)
showToast(R.string.logs_saved)
}
private fun updateLogs() {

View file

@ -22,9 +22,9 @@ import icu.nullptr.hidemyapplist.service.PrefManager
import icu.nullptr.hidemyapplist.service.ServiceClient
import icu.nullptr.hidemyapplist.ui.activity.AboutActivity
import icu.nullptr.hidemyapplist.ui.util.enabledString
import icu.nullptr.hidemyapplist.ui.util.makeToast
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.ui.util.showToast
import icu.nullptr.hidemyapplist.util.ConfigUtils.Companion.getLocale
import icu.nullptr.hidemyapplist.util.LangList
import icu.nullptr.hidemyapplist.util.SuUtils
@ -100,6 +100,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
"bypassRiskyPackageWarning" -> PrefManager.bypassRiskyPackageWarning
"appDataIsolation" -> ConfigManager.altAppDataIsolation
"voldAppDataIsolation" -> ConfigManager.altVoldAppDataIsolation
"skipSystemAppDataIsolation" -> ConfigManager.skipSystemAppDataIsolation
"disableActivityLaunchProtection" -> ConfigManager.disableActivityLaunchProtection
"forceMountData" -> ConfigManager.forceMountData
else -> throw IllegalArgumentException("Invalid key: $key")
@ -127,6 +128,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
"disableActivityLaunchProtection" -> ConfigManager.disableActivityLaunchProtection = value
"appDataIsolation" -> ConfigManager.altAppDataIsolation = value
"voldAppDataIsolation" -> ConfigManager.altVoldAppDataIsolation = value
"skipSystemAppDataIsolation" -> ConfigManager.skipSystemAppDataIsolation = value
else -> throw IllegalArgumentException("Invalid key: $key")
}
}
@ -148,7 +150,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
setPreferencesFromResource(R.xml.settings_data_isolation, rootKey)
findPreference<SwitchPreferenceCompat>("appDataIsolation")?.let {
it.summary = getString(R.string.settings_need_reboot) + "\n" +
it.summary = getString(R.string.settings_need_reboot) + "\n\n" +
getString(
R.string.settings_default_value,
CommonUtils.isAppDataIsolationEnabled.enabledString(resources)
@ -156,7 +158,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
}
findPreference<SwitchPreferenceCompat>("voldAppDataIsolation")?.let {
it.summary = getString(R.string.settings_need_reboot) + "\n" +
it.summary = getString(R.string.settings_need_reboot) + "\n\n" +
getString(
R.string.settings_default_value,
CommonUtils.isVoldAppDataIsolationEnabled.enabledString(resources)
@ -263,15 +265,15 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
.setMessage(R.string.settings_is_clean_env_summary)
.setPositiveButton(R.string.yes) { _, _ ->
ServiceClient.stopService(true)
makeToast(R.string.settings_stop_system_service)
showToast(R.string.settings_stop_system_service)
}
.setNegativeButton(R.string.no) { _, _ ->
ServiceClient.stopService(false)
makeToast(R.string.settings_stop_system_service)
showToast(R.string.settings_stop_system_service)
}
.setNeutralButton(android.R.string.cancel, null)
.show()
} else makeToast(R.string.home_xposed_service_off)
} else showToast(R.string.home_xposed_service_off)
true
}
@ -281,8 +283,8 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
.setMessage(R.string.settings_is_clean_env_summary)
.setPositiveButton(android.R.string.ok) { _, _ ->
val result = SuUtils.execPrivileged("rm -rf /data/misc/hide_my_applist*")
if (result) makeToast(R.string.settings_force_clean_env_toast_successful)
else makeToast(R.string.settings_permission_denied)
if (result) showToast(R.string.settings_force_clean_env_toast_successful)
else showToast(R.string.settings_permission_denied)
}
.setNegativeButton(android.R.string.cancel, null)
.show()

View file

@ -4,10 +4,10 @@ import android.widget.Toast
import androidx.annotation.StringRes
import icu.nullptr.hidemyapplist.hmaApp
fun makeToast(@StringRes resId: Int) {
fun showToast(@StringRes resId: Int) {
Toast.makeText(hmaApp, resId, Toast.LENGTH_SHORT).show()
}
fun makeToast(text: CharSequence) {
fun showToast(text: CharSequence) {
Toast.makeText(hmaApp, text, Toast.LENGTH_SHORT).show()
}

View file

@ -22,9 +22,9 @@ import icu.nullptr.hidemyapplist.ui.util.ThemeUtils.attrDrawable
import icu.nullptr.hidemyapplist.ui.util.ThemeUtils.getColor
import icu.nullptr.hidemyapplist.ui.util.ThemeUtils.homeItemBackgroundColor
import icu.nullptr.hidemyapplist.ui.util.ThemeUtils.themeColor
import icu.nullptr.hidemyapplist.ui.util.makeToast
import icu.nullptr.hidemyapplist.ui.util.navigate
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.ui.util.showToast
import org.frknkrc44.hma_oss.BuildConfig
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentHomeBinding
@ -43,11 +43,11 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
if (uri == null) return@backup
ConfigManager.configFile.inputStream().use { input ->
hmaApp.contentResolver.openOutputStream(uri).use { output ->
if (output == null) makeToast(R.string.home_export_failed)
if (output == null) showToast(R.string.home_export_failed)
else input.copyTo(output)
}
}
makeToast(R.string.home_exported)
showToast(R.string.home_exported)
}
private val restoreSAFLauncher =
@ -58,7 +58,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
.openInputStream(uri)?.reader().use { it?.readText() }
?: throw IOException(getString(R.string.home_import_file_damaged))
ConfigManager.importConfig(backup)
makeToast(R.string.home_import_successful)
showToast(R.string.home_import_successful)
}.onFailure {
it.printStackTrace()
MaterialAlertDialogBuilder(requireContext())

View file

@ -4,12 +4,10 @@
<SwitchPreferenceCompat
android:icon="@drawable/ic_outline_layers_24"
android:key="appDataIsolation"
android:summary="@string/settings_need_reboot"
android:title="@string/settings_app_data_isolation" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_outline_layers_24"
android:icon="@drawable/outline_sd_storage_24"
android:key="voldAppDataIsolation"
android:summary="@string/settings_need_reboot"
android:title="@string/settings_vold_app_data_isolation" />
<Preference
android:key="info"
@ -17,6 +15,16 @@
android:summary="@string/settings_overwrite_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_vold_app_data_isolation"
android:dependency="voldAppDataIsolation">
<SwitchPreferenceCompat
android:key="skipSystemAppDataIsolation"
android:summary="@string/settings_need_reboot"
android:title="@string/app_exclude_system_apps" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_module">
<SwitchPreferenceCompat
android:icon="@drawable/outline_storage_24"

View file

@ -34,16 +34,29 @@ fun getUncommittedSuffix(): String {
val gitHasUncommittedSuffix = getUncommittedSuffix()
val gitCommitCount = "git rev-list HEAD --count".execute().toInt()
val gitCommitHash = "git rev-parse --verify --short HEAD".execute()
// 432 is the count of commits before license changed
val gitCommitCountAfterOss = gitCommitCount - 432
val minSdkVer by extra(29)
val targetSdkVer by extra(36)
val appVerCode = gitCommitCount + 0x6f7373
val appVerCode = gitCommitCount + 0x6f7373 // commit count + 0xOSS
val appVerName by extra("oss-${gitCommitCountAfterOss}${gitHasUncommittedSuffix}")
/*
* configVerCode, serviceVerCode and minBackupVerCode is used by other build.gradle.kts files
*
* DO NOT REMOVE THESE LINES
*/
@Suppress("unused")
val configVerCode by extra(93)
@Suppress("unused")
val serviceVerCode by extra(98)
@Suppress("unused")
val minBackupVerCode by extra(65)
val androidSourceCompatibility = JavaVersion.VERSION_21
@ -62,8 +75,6 @@ fun Project.configureBaseExtension() {
targetSdk = targetSdkVer
versionCode = appVerCode
versionName = appVerName
if (localProperties.getProperty("buildWithGitSuffix").toBoolean())
versionNameSuffix = ".r${gitCommitCount}.${gitCommitHash}"
consumerProguardFiles("proguard-rules.pro")
}

View file

@ -4,7 +4,7 @@ interface IHMAService {
void stopService(boolean cleanEnv) = 0;
void syncConfig(String json) = 1;
void writeConfig(String json) = 1;
int getServiceVersion() = 2;
@ -17,4 +17,6 @@ interface IHMAService {
void handlePackageEvent(String eventType, String packageName) = 6;
String[] getPackagesForPreset(String presetName) = 7;
String readConfig() = 8;
}

View file

@ -19,6 +19,7 @@ data class JsonConfig(
var disableActivityLaunchProtection: Boolean = false,
var altAppDataIsolation: Boolean = false,
var altVoldAppDataIsolation: Boolean = false,
var skipSystemAppDataIsolation: Boolean = true,
val templates: MutableMap<String, Template> = mutableMapOf(),
val scope: MutableMap<String, AppConfig> = mutableMapOf()
) {

View file

@ -2,7 +2,6 @@ package icu.nullptr.hidemyapplist.common
import android.content.pm.ApplicationInfo
import java.util.zip.ZipFile
import kotlin.text.contains
object RiskyPackageUtils {
private const val GMS_PROP = "\u0000c\u0000o\u0000m\u0000.\u0000g\u0000o\u0000o\u0000g\u0000l\u0000e\u0000.\u0000a\u0000n\u0000d\u0000r\u0000o\u0000i\u0000d\u0000.\u0000g\u0000m\u0000s\u0000."

View file

@ -44,7 +44,7 @@ class HMAService(val pms: IPackageManager) : IHMAService.Stub() {
private val configLock = Any()
private val loggerLock = Any()
private val systemApps = mutableSetOf<String>()
val systemApps = mutableSetOf<String>()
private val frameworkHooks = mutableSetOf<IFrameworkHook>()
val executor: ExecutorService = Executors.newSingleThreadExecutor()
@ -264,18 +264,23 @@ class HMAService(val pms: IPackageManager) : IHMAService.Stub() {
}
}
override fun syncConfig(json: String) {
override fun writeConfig(json: String) {
synchronized(configLock) {
configFile.writeText(json)
val newConfig = JsonConfig.parse(json)
if (newConfig.configVersion != BuildConfig.CONFIG_VERSION) {
logW(TAG, "Sync config: version mismatch, need reboot")
return
runCatching {
val newConfig = JsonConfig.parse(json)
if (newConfig.configVersion != BuildConfig.CONFIG_VERSION) {
logW(TAG, "Sync config: version mismatch, need reboot")
return
}
config = newConfig
configFile.writeText(json)
frameworkHooks.forEach(IFrameworkHook::onConfigChanged)
}.onSuccess {
logD(TAG, "Config synced")
}.onFailure {
return@synchronized
}
config = newConfig
frameworkHooks.forEach(IFrameworkHook::onConfigChanged)
}
logD(TAG, "Config synced")
}
override fun getServiceVersion() = BuildConfig.SERVICE_VERSION
@ -305,4 +310,6 @@ class HMAService(val pms: IPackageManager) : IHMAService.Stub() {
override fun getPackagesForPreset(presetName: String) =
AppPresets.instance.getPresetByName(presetName)?.packages?.toTypedArray()
override fun readConfig() = config.toString()
}

View file

@ -8,7 +8,9 @@ import com.github.kyuubiran.ezxhelper.utils.hookAfter
import com.github.kyuubiran.ezxhelper.utils.hookBefore
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import icu.nullptr.hidemyapplist.common.Utils
import icu.nullptr.hidemyapplist.xposed.HMAService
import icu.nullptr.hidemyapplist.xposed.logD
import icu.nullptr.hidemyapplist.xposed.logE
import icu.nullptr.hidemyapplist.xposed.logI
@ -79,6 +81,29 @@ class AppDataIsolationHook(private val service: HMAService): IFrameworkHook {
hooks += it
}
findMethodOrNull(
"com.android.server.am.ProcessList"
) {
name == "needsStorageDataIsolation"
}?.hookAfter { param ->
if (service.config.altVoldAppDataIsolation && service.config.skipSystemAppDataIsolation) {
val app = param.args.find { it.javaClass.simpleName == "ProcessRecord" }
val uid = XposedHelpers.getIntField(app, "uid")
val apps = Utils.binderLocalScope {
service.pms.getPackagesForUid(uid)
} ?: return@hookAfter
val isSystemApp = service.systemApps.any { apps.contains(it) }
logD(
TAG,
"@needsStorageDataIsolation $uid - ${apps.contentToString()} isSystemApp: $isSystemApp"
)
if (isSystemApp) param.result = false
}
}?.let {
hooks += it
}
findMethodOrNull(
"com.android.server.StorageManagerService"
) {
@ -117,6 +142,44 @@ class AppDataIsolationHook(private val service: HMAService): IFrameworkHook {
}?.let {
hooks += it
}
findMethodOrNull(
"com.android.server.StorageManagerService"
) {
name == "remountAppStorageDirs"
}?.hookBefore { param ->
if (service.config.altVoldAppDataIsolation && service.config.skipSystemAppDataIsolation) {
val pidPkgMap = param.args[0] as java.util.Map<*, *>
val userId = param.args[1] as Int
val keysToRemove = mutableSetOf<Any>()
for (entry in pidPkgMap.entrySet()) {
val pid = entry.key
val packageName = entry.value as String
val apps = Utils.binderLocalScope {
val uid = Utils.getPackageUidCompat(service.pms, packageName, 0L, userId)
service.pms.getPackagesForUid(uid)
} ?: continue
for (app in apps) {
if (app in service.systemApps) {
logD(
TAG,
"@remountAppStorageDirs SYSTEM $pid - $packageName is marked to remove"
)
keysToRemove += pid
break
}
}
}
keysToRemove.forEach { pidPkgMap.remove(it) }
}
}?.let {
hooks += it
}
}
override fun unload() {