From 89dfb988d1834d1ce183697ddeca6eac2af75100 Mon Sep 17 00:00:00 2001 From: Hongyi Zhang Date: Mon, 23 Nov 2020 13:25:30 -0800 Subject: [PATCH] Add a method for wiping flags per package in RescueParty Provide a method for RollbackManager to clear flags when performing rollback. Mapping of packages to namespaces comes from both the runtime records already maintained by RescueParty and a preset DeviceConfig flag of namespace <-> package for Mainline modules(will be pushed from GMScore). Test: atest RescuePartyTest & manually trigger the action on local build Bug: 173120414 Change-Id: I4773a6124b07e3291cacf71dc7b702f84b69008c --- .../java/com/android/server/RescueParty.java | 94 +++++++++++- .../com/android/server/RescuePartyTest.java | 134 ++++++++++++++++++ 2 files changed, 226 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java index e8e1a16d116b2..a1cf8162f0e96 100644 --- a/services/core/java/com/android/server/RescueParty.java +++ b/services/core/java/com/android/server/RescueParty.java @@ -16,6 +16,8 @@ package com.android.server; +import static android.provider.DeviceConfig.Properties; + import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; import android.annotation.NonNull; @@ -36,6 +38,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; +import android.text.TextUtils; import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.Log; @@ -93,6 +96,12 @@ public class RescueParty { static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); @VisibleForTesting static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS; + // The DeviceConfig namespace containing all RescueParty switches. + @VisibleForTesting + static final String NAMESPACE_CONFIGURATION = "configuration"; + @VisibleForTesting + static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG = + "namespace_to_package_mapping"; private static final String NAME = "rescue-party-observer"; @@ -103,8 +112,6 @@ public class RescueParty { "persist.device_config.configuration.disable_rescue_party"; private static final String PROP_DISABLE_FACTORY_RESET_FLAG = "persist.device_config.configuration.disable_rescue_party_factory_reset"; - // The DeviceConfig namespace containing all RescueParty switches. - private static final String NAMESPACE_CONFIGURATION = "configuration"; private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT | ApplicationInfo.FLAG_SYSTEM; @@ -170,6 +177,81 @@ public class RescueParty { })); } + + /** + * Called when {@code RollbackManager} performs Mainline module rollbacks, + * to avoid rolled back modules consuming flag values only expected to work + * on modules of newer versions. + */ + public static void resetDeviceConfigForPackages(List packageNames) { + if (packageNames == null) { + return; + } + Set namespacesToReset = new ArraySet(); + Iterator it = packageNames.iterator(); + RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstanceIfCreated(); + // Get runtime package to namespace mapping if created. + if (rescuePartyObserver != null) { + while (it.hasNext()) { + String packageName = it.next(); + Set runtimeAffectedNamespaces = + rescuePartyObserver.getAffectedNamespaceSet(packageName); + if (runtimeAffectedNamespaces != null) { + namespacesToReset.addAll(runtimeAffectedNamespaces); + } + } + } + // Get preset package to namespace mapping if created. + Set presetAffectedNamespaces = getPresetNamespacesForPackages( + packageNames); + if (presetAffectedNamespaces != null) { + namespacesToReset.addAll(presetAffectedNamespaces); + } + + // Clear flags under the namespaces mapped to these packages. + // Using setProperties since DeviceConfig.resetToDefaults bans the current flag set. + Iterator namespaceIt = namespacesToReset.iterator(); + while (namespaceIt.hasNext()) { + String namespaceToReset = namespaceIt.next(); + Properties properties = new Properties.Builder(namespaceToReset).build(); + try { + DeviceConfig.setProperties(properties); + } catch (DeviceConfig.BadConfigException exception) { + logCriticalInfo(Log.WARN, "namespace " + namespaceToReset + + " is already banned, skip reset."); + } + } + } + + private static Set getPresetNamespacesForPackages(List packageNames) { + Set resultSet = new ArraySet(); + try { + String flagVal = DeviceConfig.getString(NAMESPACE_CONFIGURATION, + NAMESPACE_TO_PACKAGE_MAPPING_FLAG, ""); + String[] mappingEntries = flagVal.split(","); + for (int i = 0; i < mappingEntries.length; i++) { + if (TextUtils.isEmpty(mappingEntries[i])) { + continue; + } + String[] splittedEntry = mappingEntries[i].split(":"); + if (splittedEntry.length != 2) { + throw new RuntimeException("Invalid mapping entry: " + mappingEntries[i]); + } + String namespace = splittedEntry[0]; + String packageName = splittedEntry[1]; + + if (packageNames.contains(packageName)) { + resultSet.add(namespace); + } + } + } catch (Exception e) { + resultSet.clear(); + Slog.e(TAG, "Failed to read preset package to namespaces mapping.", e); + } finally { + return resultSet; + } + } + @VisibleForTesting static long getElapsedRealtime() { return SystemClock.elapsedRealtime(); @@ -469,6 +551,14 @@ public class RescueParty { } } + /** Gets singleton instance. It returns null if the instance is not created yet.*/ + @Nullable + public static RescuePartyObserver getInstanceIfCreated() { + synchronized (RescuePartyObserver.class) { + return sRescuePartyObserver; + } + } + @VisibleForTesting static void reset() { synchronized (RescuePartyObserver.class) { diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java index 9c8f733730a79..f375421043fd5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java @@ -16,6 +16,7 @@ package com.android.server; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; @@ -44,6 +45,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; +import android.util.ArraySet; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; @@ -63,6 +65,7 @@ import org.mockito.stubbing.Answer; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; /** @@ -79,9 +82,11 @@ public class RescuePartyTest { private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue"; private static final String CALLING_PACKAGE1 = "com.package.name1"; private static final String CALLING_PACKAGE2 = "com.package.name2"; + private static final String CALLING_PACKAGE3 = "com.package.name3"; private static final String NAMESPACE1 = "namespace1"; private static final String NAMESPACE2 = "namespace2"; private static final String NAMESPACE3 = "namespace3"; + private static final String NAMESPACE4 = "namespace4"; private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = "persist.device_config.configuration.disable_rescue_party"; private static final String PROP_DISABLE_FACTORY_RESET_FLAG = @@ -89,6 +94,8 @@ public class RescuePartyTest { private MockitoSession mSession; private HashMap mSystemSettingsMap; + //Records the namespaces wiped by setProperties(). + private HashSet mNamespacesWiped; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mMockContext; @@ -119,6 +126,7 @@ public class RescuePartyTest { .spyStatic(PackageWatchdog.class) .startMocking(); mSystemSettingsMap = new HashMap<>(); + mNamespacesWiped = new HashSet<>(); when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver); // Reset observer instance to get new mock context on every run @@ -167,6 +175,16 @@ public class RescuePartyTest { anyBoolean())); doAnswer((Answer) invocationOnMock -> null) .when(() -> DeviceConfig.resetToDefaults(anyInt(), anyString())); + doAnswer((Answer) invocationOnMock -> { + DeviceConfig.Properties properties = invocationOnMock.getArgument(0); + String namespace = properties.getNamespace(); + // record a wipe + if (properties.getKeyset().isEmpty()) { + mNamespacesWiped.add(namespace); + } + return true; + } + ).when(() -> DeviceConfig.setProperties(any(DeviceConfig.Properties.class))); // Mock PackageWatchdog doAnswer((Answer) invocationOnMock -> mMockPackageWatchdog) @@ -450,6 +468,122 @@ public class RescuePartyTest { assertEquals(observer.onBootLoop(5), PackageHealthObserverImpact.USER_IMPACT_HIGH); } + @Test + public void testResetDeviceConfigForPackagesOnlyRuntimeMap() { + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> Settings.Config.registerMonitorCallback(eq(mMockContentResolver), + mMonitorCallbackCaptor.capture())); + + // Record DeviceConfig accesses + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + RemoteCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE1)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE2)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE2)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE3)); + // Fake DeviceConfig value changes + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE1)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE2)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE3)); + + doReturn("").when(() -> DeviceConfig.getString( + eq(RescueParty.NAMESPACE_CONFIGURATION), + eq(RescueParty.NAMESPACE_TO_PACKAGE_MAPPING_FLAG), + eq(""))); + + RescueParty.resetDeviceConfigForPackages(Arrays.asList(new String[]{CALLING_PACKAGE1})); + ArraySet expectedNamespacesWiped = new ArraySet( + Arrays.asList(new String[]{NAMESPACE1, NAMESPACE2})); + assertEquals(mNamespacesWiped, expectedNamespacesWiped); + } + + @Test + public void testResetDeviceConfigForPackagesOnlyPresetMap() { + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> Settings.Config.registerMonitorCallback(eq(mMockContentResolver), + mMonitorCallbackCaptor.capture())); + + String presetMapping = NAMESPACE1 + ":" + CALLING_PACKAGE1 + "," + + NAMESPACE2 + ":" + CALLING_PACKAGE2 + "," + + NAMESPACE3 + ":" + CALLING_PACKAGE1; + doReturn(presetMapping).when(() -> DeviceConfig.getString( + eq(RescueParty.NAMESPACE_CONFIGURATION), + eq(RescueParty.NAMESPACE_TO_PACKAGE_MAPPING_FLAG), + eq(""))); + + RescueParty.resetDeviceConfigForPackages(Arrays.asList(new String[]{CALLING_PACKAGE1})); + ArraySet expectedNamespacesWiped = new ArraySet( + Arrays.asList(new String[]{NAMESPACE1, NAMESPACE3})); + assertEquals(mNamespacesWiped, expectedNamespacesWiped); + } + + @Test + public void testResetDeviceConfigForPackagesBothMaps() { + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> Settings.Config.registerMonitorCallback(eq(mMockContentResolver), + mMonitorCallbackCaptor.capture())); + + // Record DeviceConfig accesses + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + RemoteCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE1)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE2)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE2)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE3)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE3, NAMESPACE4)); + // Fake DeviceConfig value changes + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE1)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE2)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE3)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE4)); + + String presetMapping = NAMESPACE1 + ":" + CALLING_PACKAGE1 + "," + + NAMESPACE2 + ":" + CALLING_PACKAGE2 + "," + + NAMESPACE4 + ":" + CALLING_PACKAGE3; + doReturn(presetMapping).when(() -> DeviceConfig.getString( + eq(RescueParty.NAMESPACE_CONFIGURATION), + eq(RescueParty.NAMESPACE_TO_PACKAGE_MAPPING_FLAG), + eq(""))); + + RescueParty.resetDeviceConfigForPackages( + Arrays.asList(new String[]{CALLING_PACKAGE1, CALLING_PACKAGE2})); + ArraySet expectedNamespacesWiped = new ArraySet( + Arrays.asList(new String[]{NAMESPACE1, NAMESPACE2, NAMESPACE3})); + assertEquals(mNamespacesWiped, expectedNamespacesWiped); + } + + @Test + public void testResetDeviceConfigNoExceptionWhenFlagMalformed() { + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> Settings.Config.registerMonitorCallback(eq(mMockContentResolver), + mMonitorCallbackCaptor.capture())); + + // Record DeviceConfig accesses + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + RemoteCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE1, NAMESPACE1)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE2, NAMESPACE3)); + monitorCallback.sendResult(getConfigAccessBundle(CALLING_PACKAGE3, NAMESPACE4)); + // Fake DeviceConfig value changes + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE1)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE2)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE3)); + monitorCallback.sendResult(getConfigNamespaceUpdateBundle(NAMESPACE4)); + + String invalidPresetMapping = NAMESPACE2 + ":" + CALLING_PACKAGE2 + "," + + NAMESPACE1 + "." + CALLING_PACKAGE2; + doReturn(invalidPresetMapping).when(() -> DeviceConfig.getString( + eq(RescueParty.NAMESPACE_CONFIGURATION), + eq(RescueParty.NAMESPACE_TO_PACKAGE_MAPPING_FLAG), + eq(""))); + + RescueParty.resetDeviceConfigForPackages( + Arrays.asList(new String[]{CALLING_PACKAGE1, CALLING_PACKAGE2})); + ArraySet expectedNamespacesWiped = new ArraySet( + Arrays.asList(new String[]{NAMESPACE1, NAMESPACE3})); + assertEquals(mNamespacesWiped, expectedNamespacesWiped); + } + private void verifySettingsResets(int resetMode, String[] resetNamespaces, HashMap configResetVerifiedTimesMap) { verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,