From ec065a8333fff9f39d697877bee2663e39cbdb1f Mon Sep 17 00:00:00 2001 From: Rhed Jao Date: Fri, 7 May 2021 21:32:23 +0800 Subject: [PATCH] Throttle the repeated silent installation requests To avoid a non-system installer repeatedly silent updates and 'denial-of-service' against another app on the device. This CL tracks for all the silent updated installs where the `requireUserAction' is not required. And fall back to user action required if a repeated silent update is requested within the throttle time. Bug: 185878964 Test: atest SilentUpdateHostsideTests Test: atest StagingManagerTest Test: atest PackageInstallerSessionTest Change-Id: I4ea287bf66d8661eec2676b604f9109f6f9add28 Merged-In: I4ea287bf66d8661eec2676b604f9109f6f9add28 --- .../android/content/pm/IPackageInstaller.aidl | 2 + .../server/pm/PackageInstallerService.java | 27 +++- .../server/pm/PackageInstallerSession.java | 45 ++++-- .../server/pm/PackageManagerShellCommand.java | 35 +++++ .../android/server/pm/SilentUpdatePolicy.java | 146 ++++++++++++++++++ .../android/server/pm/StagingManagerTest.java | 29 ++-- .../pm/PackageInstallerSessionTest.java | 5 +- 7 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 services/core/java/com/android/server/pm/SilentUpdatePolicy.java diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index d7c257affd430..32fc74f60d155 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -61,4 +61,6 @@ interface IPackageInstaller { void setPermissionsResult(int sessionId, boolean accepted); void bypassNextStagedInstallerCheck(boolean value); + + void setAllowUnlimitedSilentUpdates(String installerPackageName); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index b6f5e999be27c..62aca3da01a16 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -21,6 +21,7 @@ import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -194,6 +195,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements @GuardedBy("mSessions") private final SparseBooleanArray mLegacySessions = new SparseBooleanArray(); + /** Policy for allowing a silent update. */ + private final SilentUpdatePolicy mSilentUpdatePolicy = new SilentUpdatePolicy(); + private static final FilenameFilter sStageFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { @@ -409,7 +413,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements try { session = PackageInstallerSession.readFromXml(in, mInternalCallback, mContext, mPm, mInstallThread.getLooper(), mStagingManager, - mSessionsDir, this); + mSessionsDir, this, mSilentUpdatePolicy); } catch (Exception e) { Slog.e(TAG, "Could not read session", e); continue; @@ -755,10 +759,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements originatingPackageName, requestedInstallerPackageName, installerAttributionTag); session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, - mInstallThread.getLooper(), mStagingManager, sessionId, userId, callingUid, - installSource, params, createdMillis, 0L, stageDir, stageCid, null, null, false, - false, false, false, null, SessionInfo.INVALID_ID, false, false, false, - SessionInfo.STAGED_SESSION_NO_ERROR, ""); + mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId, + userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, + null, null, false, false, false, false, null, SessionInfo.INVALID_ID, + false, false, false, SessionInfo.STAGED_SESSION_NO_ERROR, ""); synchronized (mSessions) { mSessions.put(sessionId, session); @@ -1087,6 +1091,17 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements mBypassNextStagedInstallerCheck = value; } + /** + * Set an installer to allow for the unlimited silent updates. + */ + @Override + public void setAllowUnlimitedSilentUpdates(@Nullable String installerPackageName) { + if (!isCalledBySystemOrShell(Binder.getCallingUid())) { + throw new SecurityException("Caller not allowed to unlimite silent updates"); + } + mSilentUpdatePolicy.setAllowUnlimitedSilentUpdates(installerPackageName); + } + private static int getSessionCount(SparseArray sessions, int installerUid) { int count = 0; @@ -1369,8 +1384,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements pw.println("Legacy install sessions:"); pw.increaseIndent(); pw.println(mLegacySessions.toString()); + pw.println(); pw.decreaseIndent(); } + mSilentUpdatePolicy.dump(pw); } class InternalCallback { diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 83eb09334efbe..1da80f4b1f772 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -270,6 +270,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final PackageManagerService mPm; private final Handler mHandler; private final PackageSessionProvider mSessionProvider; + private final SilentUpdatePolicy mSilentUpdatePolicy; /** * Note all calls must be done outside {@link #mLock} to prevent lock inversion. */ @@ -995,8 +996,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } public PackageInstallerSession(PackageInstallerService.InternalCallback callback, - Context context, PackageManagerService pm, - PackageSessionProvider sessionProvider, Looper looper, StagingManager stagingManager, + Context context, PackageManagerService pm, PackageSessionProvider sessionProvider, + SilentUpdatePolicy silentUpdatePolicy, Looper looper, StagingManager stagingManager, int sessionId, int userId, int installerUid, @NonNull InstallSource installSource, SessionParams params, long createdMillis, long committedMillis, File stageDir, String stageCid, InstallationFile[] files, @@ -1009,6 +1010,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mContext = context; mPm = pm; mSessionProvider = sessionProvider; + mSilentUpdatePolicy = silentUpdatePolicy; mHandler = new Handler(looper, mHandlerCallback); mStagingManager = stagingManager; @@ -2384,6 +2386,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } // else, we'll wait until we parse to determine if we need to } + boolean silentUpdatePolicyEnforceable = false; synchronized (mLock) { if (mRelinquished) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, @@ -2408,12 +2411,28 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { extractNativeLibraries( mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs()); - if (userActionRequirement == USER_ACTION_PENDING_APK_PARSING - && (result.getTargetSdk() < Build.VERSION_CODES.Q)) { - sendPendingUserActionIntent(); - return null; + if (userActionRequirement == USER_ACTION_PENDING_APK_PARSING) { + if (result.getTargetSdk() < Build.VERSION_CODES.Q) { + sendPendingUserActionIntent(); + return null; + } + if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED) { + silentUpdatePolicyEnforceable = true; + } } } + } + if (silentUpdatePolicyEnforceable) { + if (!mSilentUpdatePolicy.isSilentUpdateAllowed( + getInstallerPackageName(), getPackageName())) { + // Fall back to the non-silent update if a repeated installation is invoked within + // the throttle time. + sendPendingUserActionIntent(); + return null; + } + mSilentUpdatePolicy.track(getInstallerPackageName(), getPackageName()); + } + synchronized (mLock) { return makeVerificationParamsLocked(); } } @@ -4569,7 +4588,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @NonNull PackageInstallerService.InternalCallback callback, @NonNull Context context, @NonNull PackageManagerService pm, Looper installerThread, @NonNull StagingManager stagingManager, @NonNull File sessionsDir, - @NonNull PackageSessionProvider sessionProvider) + @NonNull PackageSessionProvider sessionProvider, + @NonNull SilentUpdatePolicy silentUpdatePolicy) throws IOException, XmlPullParserException { final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID); final int userId = in.getAttributeInt(null, ATTR_USER_ID); @@ -4743,10 +4763,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { InstallSource installSource = InstallSource.create(installInitiatingPackageName, installOriginatingPackageName, installerPackageName, installerAttributionTag); - return new PackageInstallerSession(callback, context, pm, sessionProvider, installerThread, - stagingManager, sessionId, userId, installerUid, installSource, params, - createdMillis, committedMillis, stageDir, stageCid, fileArray, checksumsMap, - prepared, committed, destroyed, sealed, childSessionIdsArray, parentSessionId, - isReady, isFailed, isApplied, stagedSessionErrorCode, stagedSessionErrorMessage); + return new PackageInstallerSession(callback, context, pm, sessionProvider, + silentUpdatePolicy, installerThread, stagingManager, sessionId, userId, + installerUid, installSource, params, createdMillis, committedMillis, stageDir, + stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed, + childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, + stagedSessionErrorCode, stagedSessionErrorMessage); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 853b6a7c946ad..a69caa914846a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -304,6 +304,8 @@ class PackageManagerShellCommand extends ShellCommand { return runLogVisibility(); case "bypass-staged-installer-check": return runBypassStagedInstallerCheck(); + case "allow-unlimited-silent-updates": + return runAllowUnlimitedSilentUpdates(); default: { Boolean domainVerificationResult = mDomainVerificationShell.runCommand(this, cmd); @@ -2978,6 +2980,33 @@ class PackageManagerShellCommand extends ShellCommand { } } + private int runAllowUnlimitedSilentUpdates() { + final PrintWriter pw = getOutPrintWriter(); + String opt; + boolean reset = false; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--reset": + reset = true; + break; + default: + pw.println("Error: Unknown option: " + opt); + return -1; + } + } + + final String installerPackageName = reset ? null : getNextArgRequired(); + try { + mInterface.getPackageInstaller().setAllowUnlimitedSilentUpdates(installerPackageName); + } catch (RemoteException e) { + pw.println("Failure [" + + e.getClass().getName() + " - " + + e.getMessage() + "]"); + return -1; + } + return 1; + } + private static String checkAbiArgument(String abi) { if (TextUtils.isEmpty(abi)) { throw new IllegalArgumentException("Missing ABI argument"); @@ -3792,6 +3821,12 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --enable: turn on debug logging (default)"); pw.println(" --disable: turn off debug logging"); pw.println(""); + pw.println(" allow-unlimited-silent-updates (--reset | )"); + pw.println(" Allows unlimited silent updated installation requests from the installer"); + pw.println(" without the throttle time."); + pw.println(" --reset: clear the allowed installer and tracks of silent updates in"); + pw.println(" the system."); + pw.println(""); mDomainVerificationShell.printHelp(pw); pw.println(""); Intent.printIntentArgsHelp(pw , ""); diff --git a/services/core/java/com/android/server/pm/SilentUpdatePolicy.java b/services/core/java/com/android/server/pm/SilentUpdatePolicy.java new file mode 100644 index 0000000000000..117acab6d0798 --- /dev/null +++ b/services/core/java/com/android/server/pm/SilentUpdatePolicy.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.Pair; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.IndentingPrintWriter; + +import java.util.concurrent.TimeUnit; + +/** + * Tracks for the installer package name and installing app package name of silent updates. + * This class is used to throttle repeated silent updates of the same installer and application + * in the {@link PackageInstallerSession}. + */ +public class SilentUpdatePolicy { + // A throttle time to prevent the installer from silently updating the same app repeatedly. + private static final long SILENT_UPDATE_THROTTLE_TIME_MS = TimeUnit.SECONDS.toMillis(30); + + // Map to the uptime timestamp for each installer and app of the silent update. + @GuardedBy("mSilentUpdateInfos") + private final ArrayMap, Long> mSilentUpdateInfos = new ArrayMap<>(); + + // An installer allowed for the unlimited silent updates within the throttle time + @GuardedBy("mSilentUpdateInfos") + private String mAllowUnlimitedSilentUpdatesInstaller; + + /** + * Checks if the silent update is allowed by the given installer and app package name. + * + * @param installerPackageName The installer package name to check + * @param packageName The package name which is installing + * @return true if the silent update is allowed. + */ + public boolean isSilentUpdateAllowed(@Nullable String installerPackageName, + @NonNull String packageName) { + if (installerPackageName == null) { + // Always true for the installer from Shell + return true; + } + final long lastSilentUpdatedMs = getTimestampMs(installerPackageName, packageName); + return SystemClock.uptimeMillis() - lastSilentUpdatedMs > SILENT_UPDATE_THROTTLE_TIME_MS; + } + + /** + * Adding track for the installer package name and installing app of a silent update. This is + * used to determine whether a silent update is allowed. + * + * @param installerPackageName The installer package name + * @param packageName The package name which is installing + */ + public void track(@Nullable String installerPackageName, @NonNull String packageName) { + if (installerPackageName == null) { + // No need to track the installer from Shell. + return; + } + synchronized (mSilentUpdateInfos) { + if (mAllowUnlimitedSilentUpdatesInstaller != null + && mAllowUnlimitedSilentUpdatesInstaller.equals(installerPackageName)) { + return; + } + final long uptime = SystemClock.uptimeMillis(); + pruneLocked(uptime); + + final Pair key = Pair.create(installerPackageName, packageName); + mSilentUpdateInfos.put(key, uptime); + } + } + + /** + * Set an installer to allow for the unlimited silent updates. Reset the tracker if the + * installer package name is null. + */ + void setAllowUnlimitedSilentUpdates(@Nullable String installerPackageName) { + synchronized (mSilentUpdateInfos) { + if (installerPackageName == null) { + mSilentUpdateInfos.clear(); + } + mAllowUnlimitedSilentUpdatesInstaller = installerPackageName; + } + } + + private void pruneLocked(long uptime) { + final int size = mSilentUpdateInfos.size(); + for (int i = size - 1; i >= 0; i--) { + final long lastSilentUpdatedMs = mSilentUpdateInfos.valueAt(i); + if (uptime - lastSilentUpdatedMs > SILENT_UPDATE_THROTTLE_TIME_MS) { + mSilentUpdateInfos.removeAt(i); + } + } + } + + /** + * Get the timestamp by the given installer and app package name. {@code -1} is returned if not + * exist. + */ + private long getTimestampMs(@NonNull String installerPackageName, @NonNull String packageName) { + final Pair key = Pair.create(installerPackageName, packageName); + final Long timestampMs; + synchronized (mSilentUpdateInfos) { + timestampMs = mSilentUpdateInfos.get(key); + } + return timestampMs != null ? timestampMs : -1; + } + + void dump(IndentingPrintWriter pw) { + synchronized (mSilentUpdateInfos) { + if (mSilentUpdateInfos.isEmpty()) { + return; + } + pw.println("Last silent updated Infos:"); + pw.increaseIndent(); + final int size = mSilentUpdateInfos.size(); + for (int i = 0; i < size; i++) { + final Pair key = mSilentUpdateInfos.keyAt(i); + if (key == null) { + continue; + } + pw.printPair("installerPackageName", key.first); + pw.printPair("packageName", key.second); + pw.printPair("silentUpdatedMillis", mSilentUpdateInfos.valueAt(i)); + pw.println(); + } + pw.decreaseIndent(); + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 195cc010c9a7e..7d628be571a97 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -16,6 +16,20 @@ package com.android.server.pm; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + import android.apex.ApexSessionInfo; import android.content.Context; import android.content.IntentSender; @@ -32,8 +46,6 @@ import com.android.internal.content.PackageHelper; import com.android.internal.os.BackgroundThread; import com.android.internal.util.Preconditions; -import static com.google.common.truth.Truth.assertThat; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -52,18 +64,6 @@ import java.util.Arrays; import java.util.List; import java.util.function.Predicate; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.testng.Assert.assertThrows; - @Presubmit @RunWith(JUnit4.class) public class StagingManagerTest { @@ -472,6 +472,7 @@ public class StagingManagerTest { /* context */ null, /* pm */ null, /* sessionProvider */ null, + /* silentUpdatePolicy */ null, /* looper */ BackgroundThread.getHandler().getLooper(), /* stagingManager */ null, /* sessionId */ sessionId, diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java index 5719dd2b0288d..6c4ae6fae2d2e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -29,8 +29,6 @@ import android.util.AtomicFile; import android.util.Slog; import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; -import android.util.TypedXmlPullParser; -import android.util.TypedXmlSerializer; import android.util.Xml; import androidx.test.runner.AndroidJUnit4; @@ -165,6 +163,7 @@ public class PackageInstallerSessionTest { /* context */null, /* pm */ mMockPackageManagerInternal, /* sessionProvider */ null, + /* silentUpdatePolicy */ null, /* looper */ BackgroundThread.getHandler().getLooper(), /* stagingManager */ null, /* sessionId */ sessionId, @@ -239,7 +238,7 @@ public class PackageInstallerSessionTest { session = PackageInstallerSession.readFromXml(in, null, null, mMockPackageManagerInternal, BackgroundThread.getHandler().getLooper(), null, - mTmpDir, null); + mTmpDir, null, null); ret.add(session); } catch (Exception e) { Slog.e("PackageInstallerSessionTest", "Exception ", e);