From c8d21fcf344af990aeb465e3774f2f2e3046414b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 14 Mar 2024 11:14:31 -0300 Subject: [PATCH] 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. --- .../java/com/stevesoltys/seedvault/App.kt | 4 ++- .../seedvault/settings/SettingsManager.kt | 9 +++++++ .../NotificationBackupObserver.kt | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index a51b5263..372c86fc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.app.Application +import android.app.backup.BackupManager import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL import android.app.backup.IBackupManager 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 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 fun isDebugBuild() = Build.TYPE == "userdebug" diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 786f5781..9584ffea 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -166,6 +166,15 @@ class SettingsManager(private val context: Context) { 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) @UiThread diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 30d93441..c507bd59 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -1,6 +1,7 @@ package com.stevesoltys.seedvault.ui.notification import android.app.backup.BackupProgress +import android.app.backup.BackupTransport.AGENT_ERROR import android.app.backup.IBackupObserver import android.content.Context 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.INFO import android.util.Log.isLoggable +import com.stevesoltys.seedvault.ERROR_BACKUP_CANCELLED import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.worker.BackupRequester import org.koin.core.component.KoinComponent @@ -27,11 +30,14 @@ internal class NotificationBackupObserver( private val nm: BackupNotificationManager by inject() private val metadataManager: MetadataManager by inject() private val packageService: PackageService by inject() + private val settingsManager: SettingsManager by inject() private var currentPackage: String? = null private var numPackages: Int = 0 private var numPackagesToReport: Int = 0 private var pmCounted: Boolean = false + private var errorPackageName: String? = null + init { // Inform the notification manager that a backup has started // 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 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 showProgressNotification(target) } @@ -98,6 +115,15 @@ internal class NotificationBackupObserver( * as a whole failed. */ 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 (isLoggable(TAG, INFO)) { Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")