Auto-disable apps that cancel the entire backup

This can happen when the app process gets killed while its BackupAgent is running. There are several qcom apps in the wild that have this issue. These are DoSing our backups and are non-free, so we are defending ourselves against them.
This commit is contained in:
Torsten Grote 2024-03-14 11:14:31 -03:00 committed by Chirayu Desai
parent 499126c459
commit c8d21fcf34
3 changed files with 38 additions and 1 deletions

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault
import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
import android.app.Application import android.app.Application
import android.app.backup.BackupManager
import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
@ -147,9 +148,10 @@ open class App : Application() {
} }
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL
const val ANCESTRAL_RECORD_KEY = "@ancestral_record@" const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
const val GLOBAL_METADATA_KEY = "@meta@" const val GLOBAL_METADATA_KEY = "@meta@"
const val ERROR_BACKUP_CANCELLED: Int = BackupManager.ERROR_BACKUP_CANCELLED
// TODO this doesn't work for LineageOS as they do public debug builds // TODO this doesn't work for LineageOS as they do public debug builds
fun isDebugBuild() = Build.TYPE == "userdebug" fun isDebugBuild() = Build.TYPE == "userdebug"

View file

@ -166,6 +166,15 @@ class SettingsManager(private val context: Context) {
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName) fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
/**
* Disables backup for an app. Similar to [onAppBackupStatusChanged].
*/
fun disableBackup(packageName: String) {
if (blacklistedApps.add(packageName)) {
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
}
}
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false) fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)
@UiThread @UiThread

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.ui.notification package com.stevesoltys.seedvault.ui.notification
import android.app.backup.BackupProgress import android.app.backup.BackupProgress
import android.app.backup.BackupTransport.AGENT_ERROR
import android.app.backup.IBackupObserver import android.app.backup.IBackupObserver
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_SYSTEM
@ -8,9 +9,11 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log import android.util.Log
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.isLoggable import android.util.Log.isLoggable
import com.stevesoltys.seedvault.ERROR_BACKUP_CANCELLED
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.worker.BackupRequester import com.stevesoltys.seedvault.worker.BackupRequester
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -27,11 +30,14 @@ internal class NotificationBackupObserver(
private val nm: BackupNotificationManager by inject() private val nm: BackupNotificationManager by inject()
private val metadataManager: MetadataManager by inject() private val metadataManager: MetadataManager by inject()
private val packageService: PackageService by inject() private val packageService: PackageService by inject()
private val settingsManager: SettingsManager by inject()
private var currentPackage: String? = null private var currentPackage: String? = null
private var numPackages: Int = 0 private var numPackages: Int = 0
private var numPackagesToReport: Int = 0 private var numPackagesToReport: Int = 0
private var pmCounted: Boolean = false private var pmCounted: Boolean = false
private var errorPackageName: String? = null
init { init {
// Inform the notification manager that a backup has started // Inform the notification manager that a backup has started
// and inform about the expected numbers of apps. // and inform about the expected numbers of apps.
@ -86,6 +92,17 @@ internal class NotificationBackupObserver(
// should only happen for MAGIC_PACKAGE_MANAGER, but better save than sorry // should only happen for MAGIC_PACKAGE_MANAGER, but better save than sorry
Log.e(TAG, "Error getting ApplicationInfo: ", e) Log.e(TAG, "Error getting ApplicationInfo: ", e)
} }
// Apps that get killed while interacting with their [BackupAgent] cancel the entire backup.
// In order to prevent them from DoSing us, we remember them here to auto-disable them.
// We noticed that the same app behavior can cause a status of
// either AGENT_ERROR or ERROR_BACKUP_CANCELLED, so we need to handle both.
errorPackageName = if (status == AGENT_ERROR || status == ERROR_BACKUP_CANCELLED) {
target
} else {
null // To not disable apps by mistake, we reset it when getting a new non-error result.
}
// often [onResult] gets called right away without any [onUpdate] call // often [onResult] gets called right away without any [onUpdate] call
showProgressNotification(target) showProgressNotification(target)
} }
@ -98,6 +115,15 @@ internal class NotificationBackupObserver(
* as a whole failed. * as a whole failed.
*/ */
override fun backupFinished(status: Int) { override fun backupFinished(status: Int) {
if (status == ERROR_BACKUP_CANCELLED) {
val packageName = errorPackageName
if (packageName == null) {
Log.e(TAG, "Backup got cancelled, but there we have no culprit :(")
} else {
Log.w(TAG, "App $packageName misbehaved, will disable backup for it...")
settingsManager.disableBackup(packageName)
}
}
if (backupRequester.requestNext()) { if (backupRequester.requestNext()) {
if (isLoggable(TAG, INFO)) { if (isLoggable(TAG, INFO)) {
Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status") Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")