From a57d5dd189e4af837aa49cdaa8e58ffd8a904e33 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 | 25 +++++++++++++++++++ 3 files changed, 37 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 f3403de79..84469065f 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 @@ -117,9 +118,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 47176a091..b441ae227 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -143,6 +143,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 47235213e..553587fd2 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,15 +1,18 @@ 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.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.BackupRequester import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import org.koin.core.component.KoinComponent @@ -26,9 +29,12 @@ internal class NotificationBackupObserver( private val nm: BackupNotificationManager by inject() private val metadataManager: MetadataManager by inject() + private val settingsManager: SettingsManager by inject() private var currentPackage: String? = null private var numPackages: Int = 0 + private var errorPackageName: String? = null + init { // Inform the notification manager that a backup has started // and inform about the expected numbers, so it can compute a total. @@ -63,6 +69,16 @@ internal class NotificationBackupObserver( if (isLoggable(TAG, INFO)) { Log.i(TAG, "Completed. Target: $target, status: $status") } + // 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) } @@ -75,6 +91,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")