blob: db997d8d1d7954355ece594b740385f3d037e045 [file] [log] [blame]
/*
* Copyright (C) 2016 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 static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
import static android.content.pm.PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE;
import static android.content.pm.SigningDetails.CertCapabilities.SHARED_USER_ID;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDWR;
import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME;
import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH;
import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION;
import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING;
import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX;
import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX;
import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
import static com.android.server.pm.PackageManagerService.TAG;
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageInfoLite;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackagePartitions;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
import android.content.pm.parsing.ApkLiteParseUtils;
import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.os.Binder;
import android.os.Build;
import android.os.Debug;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Process;
import android.os.SystemProperties;
import android.os.incremental.IncrementalManager;
import android.os.incremental.IncrementalStorage;
import android.os.incremental.V4Signature;
import android.os.incremental.V4Signature.HashingInfo;
import android.os.storage.DiskInfo;
import android.os.storage.VolumeInfo;
import android.service.pm.PackageServiceDumpProto;
import android.stats.storage.StorageEnums;
import android.system.ErrnoException;
import android.system.Os;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Base64;
import android.util.Log;
import android.util.LogPrinter;
import android.util.Printer;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.content.InstallLocationUtils;
import com.android.internal.content.NativeLibraryHelper;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.HexDump;
import com.android.server.EventLogTags;
import com.android.server.IntentResolver;
import com.android.server.LocalManagerRegistry;
import com.android.server.Watchdog;
import com.android.server.am.ActivityManagerUtils;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.dex.PackageDexUsage;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.component.ParsedMainComponent;
import com.android.server.pm.resolution.ComponentResolverApi;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.zip.GZIPInputStream;
/**
* Class containing helper methods for the PackageManagerService.
*
* {@hide}
*/
public class PackageManagerServiceUtils {
private static final long MAX_CRITICAL_INFO_DUMP_SIZE = 3 * 1000 * 1000; // 3MB
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
// Skip APEX which doesn't have a valid UID
public static final Predicate<PackageStateInternal> REMOVE_IF_APEX_PKG =
pkgSetting -> pkgSetting.getPkg().isApex();
public static final Predicate<PackageStateInternal> REMOVE_IF_NULL_PKG =
pkgSetting -> pkgSetting.getPkg() == null;
// This is a horrible hack to workaround b/240373119, specifically for fixing the T branch.
// A proper fix should be implemented in master instead.
public static final ThreadLocal<Boolean> DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS =
ThreadLocal.withInitial(() -> false);
/**
* Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)}
* when the package attempting to join the sharedUserId is a new install.
*/
public static final int SHARED_USER_ID_JOIN_TYPE_INSTALL = 0;
/**
* Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)}
* when the package attempting to join the sharedUserId is an update.
*/
public static final int SHARED_USER_ID_JOIN_TYPE_UPDATE = 1;
/**
* Type used with {@link #canJoinSharedUserId(String, SigningDetails, SharedUserSetting, int)}
* when the package attempting to join the sharedUserId is a part of the system image.
*/
public static final int SHARED_USER_ID_JOIN_TYPE_SYSTEM = 2;
@IntDef(prefix = { "TYPE_" }, value = {
SHARED_USER_ID_JOIN_TYPE_INSTALL,
SHARED_USER_ID_JOIN_TYPE_UPDATE,
SHARED_USER_ID_JOIN_TYPE_SYSTEM,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SharedUserIdJoinType {}
/**
* Components of apps targeting Android T and above will stop receiving intents from
* external callers that do not match its declared intent filters.
*
* When an app registers an exported component in its manifest and adds an <intent-filter>,
* the component can be started by any intent - even those that do not match the intent filter.
* This has proven to be something that many developers find counterintuitive.
* Without checking the intent when the component is started, in some circumstances this can
* allow 3P apps to trigger internal-only functionality.
*/
@ChangeId
@Disabled /* Revert enforcement: b/274147456 */
private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188;
/**
* The initial enabled state of the cache before other checks are done.
*/
private static final boolean DEFAULT_PACKAGE_PARSER_CACHE_ENABLED = true;
/**
* Whether to skip all other checks and force the cache to be enabled.
*
* Setting this to true will cause the cache to be named "debug" to avoid eviction from
* build fingerprint changes.
*/
private static final boolean FORCE_PACKAGE_PARSED_CACHE_ENABLED = false;
/**
* Returns the registered PackageManagerLocal instance, or else throws an unchecked error.
*/
public static @NonNull PackageManagerLocal getPackageManagerLocal() {
try {
return LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal.class);
} catch (ManagerNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* Checks if the package was inactive during since <code>thresholdTimeinMillis</code>.
* Package is considered active, if:
* 1) It was active in foreground.
* 2) It was active in background and also used by other apps.
*
* If it doesn't have sufficient information about the package, it return <code>false</code>.
*/
public static boolean isUnusedSinceTimeInMillis(long firstInstallTime, long currentTimeInMillis,
long thresholdTimeinMillis, PackageDexUsage.PackageUseInfo packageUseInfo,
long latestPackageUseTimeInMillis, long latestForegroundPackageUseTimeInMillis) {
if (currentTimeInMillis - firstInstallTime < thresholdTimeinMillis) {
return false;
}
// If the app was active in foreground during the threshold period.
boolean isActiveInForeground = (currentTimeInMillis
- latestForegroundPackageUseTimeInMillis)
< thresholdTimeinMillis;
if (isActiveInForeground) {
return false;
}
// If the app was active in background during the threshold period and was used
// by other packages.
boolean isActiveInBackgroundAndUsedByOtherPackages = ((currentTimeInMillis
- latestPackageUseTimeInMillis)
< thresholdTimeinMillis)
&& packageUseInfo.isAnyCodePathUsedByOtherApps();
return !isActiveInBackgroundAndUsedByOtherPackages;
}
/**
* Returns the canonicalized path of {@code path} as per {@code realpath(3)}
* semantics.
*/
public static String realpath(File path) throws IOException {
try {
return Os.realpath(path.getAbsolutePath());
} catch (ErrnoException ee) {
throw ee.rethrowAsIOException();
}
}
/**
* Verifies that the given string {@code isa} is a valid supported isa on
* the running device.
*/
public static boolean checkISA(String isa) {
for (String abi : Build.SUPPORTED_ABIS) {
if (VMRuntime.getInstructionSet(abi).equals(isa)) {
return true;
}
}
return false;
}
public static long getLastModifiedTime(AndroidPackage pkg) {
final File srcFile = new File(pkg.getPath());
if (!srcFile.isDirectory()) {
return srcFile.lastModified();
}
final File baseFile = new File(pkg.getBaseApkPath());
long maxModifiedTime = baseFile.lastModified();
for (int i = pkg.getSplitCodePaths().length - 1; i >=0; --i) {
final File splitFile = new File(pkg.getSplitCodePaths()[i]);
maxModifiedTime = Math.max(maxModifiedTime, splitFile.lastModified());
}
return maxModifiedTime;
}
private static File getSettingsProblemFile() {
File dataDir = Environment.getDataDirectory();
File systemDir = new File(dataDir, "system");
File fname = new File(systemDir, "uiderrors.txt");
return fname;
}
public static void dumpCriticalInfo(ProtoOutputStream proto) {
final File file = getSettingsProblemFile();
final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE;
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
if (skipSize > 0) {
in.skip(skipSize);
}
String line = null;
while ((line = in.readLine()) != null) {
if (line.contains("ignored: updated version")) continue;
proto.write(PackageServiceDumpProto.MESSAGES, line);
}
} catch (IOException ignored) {
}
}
public static void dumpCriticalInfo(PrintWriter pw, String msg) {
final File file = getSettingsProblemFile();
final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE;
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
if (skipSize > 0) {
in.skip(skipSize);
}
String line = null;
while ((line = in.readLine()) != null) {
if (line.contains("ignored: updated version")) continue;
if (msg != null) {
pw.print(msg);
}
pw.println(line);
}
} catch (IOException ignored) {
}
}
public static void logCriticalInfo(int priority, String msg) {
Slog.println(priority, TAG, msg);
EventLogTags.writePmCriticalInfo(msg);
try {
File fname = getSettingsProblemFile();
FileOutputStream out = new FileOutputStream(fname, true);
PrintWriter pw = new FastPrintWriter(out);
SimpleDateFormat formatter = new SimpleDateFormat();
String dateString = formatter.format(new Date(System.currentTimeMillis()));
pw.println(dateString + ": " + msg);
pw.close();
FileUtils.setPermissions(
fname.toString(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IROTH,
-1, -1);
} catch (java.io.IOException e) {
}
}
/** Enforces that if the caller is shell, it does not have the provided user restriction. */
public static void enforceShellRestriction(
UserManagerInternal userManager, String restriction, int callingUid, int userHandle) {
if (callingUid == Process.SHELL_UID) {
if (userHandle >= 0
&& userManager.hasUserRestriction(
restriction, userHandle)) {
throw new SecurityException("Shell does not have permission to access user "
+ userHandle);
} else if (userHandle < 0) {
Slog.e(PackageManagerService.TAG, "Unable to check shell permission for user "
+ userHandle + "\n\t" + Debug.getCallers(3));
}
}
}
/**
* Enforces that the caller must be either the system process or the phone process.
* If not, throws a {@link SecurityException}.
*/
public static void enforceSystemOrPhoneCaller(String methodName, int callingUid) {
if (callingUid != Process.PHONE_UID && callingUid != Process.SYSTEM_UID) {
throw new SecurityException(
"Cannot call " + methodName + " from UID " + callingUid);
}
}
/**
* Derive the value of the {@code cpuAbiOverride} based on the provided
* value.
*/
public static String deriveAbiOverride(String abiOverride) {
if (NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
return null;
}
return abiOverride;
}
/**
* Compares two sets of signatures. Returns:
* <br />
* {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
* <br />
* {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
* <br />
* {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
* <br />
* {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
* <br />
* {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
*/
public static int compareSignatures(Signature[] s1, Signature[] s2) {
if (s1 == null) {
return s2 == null
? PackageManager.SIGNATURE_NEITHER_SIGNED
: PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
}
if (s2 == null) {
return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
}
if (s1.length != s2.length) {
return PackageManager.SIGNATURE_NO_MATCH;
}
// Since both signature sets are of size 1, we can compare without HashSets.
if (s1.length == 1) {
return s1[0].equals(s2[0]) ?
PackageManager.SIGNATURE_MATCH :
PackageManager.SIGNATURE_NO_MATCH;
}
ArraySet<Signature> set1 = new ArraySet<Signature>();
for (Signature sig : s1) {
set1.add(sig);
}
ArraySet<Signature> set2 = new ArraySet<Signature>();
for (Signature sig : s2) {
set2.add(sig);
}
// Make sure s2 contains all signatures in s1.
if (set1.equals(set2)) {
return PackageManager.SIGNATURE_MATCH;
}
return PackageManager.SIGNATURE_NO_MATCH;
}
/**
* Returns true if the signature set of the package is identical to the specified signature
* set or if the signing details of the package are unknown.
*/
public static boolean comparePackageSignatures(PackageSetting pkgSetting,
Signature[] signatures) {
final SigningDetails signingDetails = pkgSetting.getSigningDetails();
return signingDetails == SigningDetails.UNKNOWN
|| compareSignatures(signingDetails.getSignatures(), signatures)
== PackageManager.SIGNATURE_MATCH;
}
/**
* Used for backward compatibility to make sure any packages with
* certificate chains get upgraded to the new style. {@code existingSigs}
* will be in the old format (since they were stored on disk from before the
* system upgrade) and {@code scannedSigs} will be in the newer format.
*/
private static boolean matchSignaturesCompat(String packageName,
PackageSignatures packageSignatures, SigningDetails parsedSignatures) {
ArraySet<Signature> existingSet = new ArraySet<Signature>();
for (Signature sig : packageSignatures.mSigningDetails.getSignatures()) {
existingSet.add(sig);
}
ArraySet<Signature> scannedCompatSet = new ArraySet<Signature>();
for (Signature sig : parsedSignatures.getSignatures()) {
try {
Signature[] chainSignatures = sig.getChainSignatures();
for (Signature chainSig : chainSignatures) {
scannedCompatSet.add(chainSig);
}
} catch (CertificateEncodingException e) {
scannedCompatSet.add(sig);
}
}
// make sure the expanded scanned set contains all signatures in the existing one
if (scannedCompatSet.equals(existingSet)) {
// migrate the old signatures to the new scheme
packageSignatures.mSigningDetails = parsedSignatures;
return true;
} else if (parsedSignatures.hasPastSigningCertificates()) {
// well this sucks: the parsed package has probably rotated signing certificates, but
// we don't have enough information to determine if the new signing certificate was
// blessed by the old one
logCriticalInfo(Log.INFO, "Existing package " + packageName + " has flattened signing "
+ "certificate chain. Unable to install newer version with rotated signing "
+ "certificate.");
}
return false;
}
private static boolean matchSignaturesRecover(
String packageName,
SigningDetails existingSignatures,
SigningDetails parsedSignatures,
@SigningDetails.CertCapabilities int flags) {
String msg = null;
try {
if (parsedSignatures.checkCapabilityRecover(existingSignatures, flags)) {
logCriticalInfo(Log.INFO, "Recovered effectively matching certificates for "
+ packageName);
return true;
}
} catch (CertificateException e) {
msg = e.getMessage();
}
logCriticalInfo(Log.INFO,
"Failed to recover certificates for " + packageName + ": " + msg);
return false;
}
/**
* Make sure the updated priv app is signed with the same key as the original APK file on the
* /system partition.
*
* <p>The rationale is that {@code disabledPkg} is a PackageSetting backed by xml files in /data
* and is not tamperproof.
*/
private static boolean matchSignatureInSystem(@NonNull String packageName,
@NonNull SigningDetails signingDetails, PackageSetting disabledPkgSetting) {
if (signingDetails.checkCapability(
disabledPkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| disabledPkgSetting.getSigningDetails().checkCapability(
signingDetails,
SigningDetails.CertCapabilities.ROLLBACK)) {
return true;
} else {
logCriticalInfo(Log.ERROR, "Updated system app mismatches cert on /system: " +
packageName);
return false;
}
}
/** Default is to not use fs-verity since it depends on kernel support. */
private static final int FSVERITY_DISABLED = 0;
/** Standard fs-verity. */
private static final int FSVERITY_ENABLED = 2;
/** Returns true if standard APK Verity is enabled. */
static boolean isApkVerityEnabled() {
return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
|| SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED)
== FSVERITY_ENABLED;
}
/** Returns true to force apk verification if the package is considered privileged. */
static boolean isApkVerificationForced(@Nullable PackageSetting ps) {
// TODO(b/154310064): re-enable.
return false;
}
/**
* Verifies that signatures match.
* @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
* @throws PackageManagerException if the signatures did not match.
*/
public static boolean verifySignatures(PackageSetting pkgSetting,
@Nullable SharedUserSetting sharedUserSetting,
PackageSetting disabledPkgSetting, SigningDetails parsedSignatures,
boolean compareCompat, boolean compareRecover, boolean isRollback)
throws PackageManagerException {
final String packageName = pkgSetting.getPackageName();
boolean compatMatch = false;
if (pkgSetting.getSigningDetails().getSignatures() != null) {
// Already existing package. Make sure signatures match
boolean match = parsedSignatures.checkCapability(
pkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| pkgSetting.getSigningDetails().checkCapability(
parsedSignatures,
SigningDetails.CertCapabilities.ROLLBACK);
if (!match && compareCompat) {
match = matchSignaturesCompat(packageName, pkgSetting.getSignatures(),
parsedSignatures);
compatMatch = match;
}
if (!match && compareRecover) {
match = matchSignaturesRecover(
packageName,
pkgSetting.getSigningDetails(),
parsedSignatures,
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| matchSignaturesRecover(
packageName,
parsedSignatures,
pkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.ROLLBACK);
}
if (!match && isApkVerificationForced(disabledPkgSetting)) {
match = matchSignatureInSystem(packageName, pkgSetting.getSigningDetails(),
disabledPkgSetting);
}
if (!match && isRollback) {
// Since a rollback can only be initiated for an APK previously installed on the
// device allow rolling back to a previous signing key even if the rollback
// capability has not been granted.
match = pkgSetting.getSigningDetails().hasAncestorOrSelf(parsedSignatures);
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Existing package " + packageName
+ " signatures do not match newer version; ignoring!");
}
}
// Check for shared user signatures
if (sharedUserSetting != null
&& sharedUserSetting.getSigningDetails() != SigningDetails.UNKNOWN) {
// Already existing package. Make sure signatures match. In case of signing certificate
// rotation, the packages with newer certs need to be ok with being sharedUserId with
// the older ones. We check to see if either the new package is signed by an older cert
// with which the current sharedUser is ok, or if it is signed by a newer one, and is ok
// with being sharedUser with the existing signing cert.
boolean match = canJoinSharedUserId(packageName, parsedSignatures, sharedUserSetting,
pkgSetting.getSigningDetails().getSignatures() != null
? SHARED_USER_ID_JOIN_TYPE_UPDATE : SHARED_USER_ID_JOIN_TYPE_INSTALL);
if (!match && compareCompat) {
match = matchSignaturesCompat(
packageName, sharedUserSetting.signatures, parsedSignatures);
}
if (!match && compareRecover) {
match =
matchSignaturesRecover(packageName,
sharedUserSetting.signatures.mSigningDetails,
parsedSignatures,
SigningDetails.CertCapabilities.SHARED_USER_ID)
|| matchSignaturesRecover(packageName,
parsedSignatures,
sharedUserSetting.signatures.mSigningDetails,
SigningDetails.CertCapabilities.SHARED_USER_ID);
compatMatch |= match;
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"Package " + packageName
+ " has no signatures that match those in shared user "
+ sharedUserSetting.name + "; ignoring!");
}
// If the lineage of this package diverges from the lineage of the sharedUserId then
// do not allow the installation to proceed.
if (!parsedSignatures.hasCommonAncestor(
sharedUserSetting.signatures.mSigningDetails)) {
throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"Package " + packageName + " has a signing lineage "
+ "that diverges from the lineage of the sharedUserId");
}
}
return compatMatch;
}
/**
* Returns whether the package {@code packageName} can join the sharedUserId based on the
* settings in {@code sharedUserSetting}.
* <p>
* A sharedUserId maintains a shared {@link SigningDetails} containing the full lineage and
* capabilities for each package in the sharedUserId. A package can join the sharedUserId if
* its current signer is the same as the shared signer, or if the current signer of either
* is in the signing lineage of the other with the {@link
* SigningDetails.CertCapabilities#SHARED_USER_ID} capability granted to that previous signer
* in the lineage. In the case of a key compromise, an app signed with a lineage revoking
* this capability from a previous signing key can still join the sharedUserId with another
* app signed with this previous key if the joining app is being updated; however, a new
* install will not be allowed until all apps have rotated off the key with the capability
* revoked.
*
* @param packageName the name of the package seeking to join the sharedUserId
* @param packageSigningDetails the {@code SigningDetails} of the package seeking to join the
* sharedUserId
* @param sharedUserSetting the {@code SharedUserSetting} for the sharedUserId {@code
* packageName} is seeking to join
* @param joinType the type of join (install, update, system, etc)
* @return true if the package seeking to join the sharedUserId meets the requirements
*/
public static boolean canJoinSharedUserId(@NonNull String packageName,
@NonNull SigningDetails packageSigningDetails,
@NonNull SharedUserSetting sharedUserSetting, @SharedUserIdJoinType int joinType) {
SigningDetails sharedUserSigningDetails = sharedUserSetting.getSigningDetails();
boolean capabilityGranted =
packageSigningDetails.checkCapability(sharedUserSigningDetails, SHARED_USER_ID)
|| sharedUserSigningDetails.checkCapability(packageSigningDetails,
SHARED_USER_ID);
// If the current signer for either the package or the sharedUserId is the current signer
// of the other or in the lineage of the other with the SHARED_USER_ID capability granted,
// then a system and update join type can proceed; an install join type is not allowed here
// since the sharedUserId may contain packages that are signed with a key untrusted by
// the new package.
if (capabilityGranted && joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL) {
return true;
}
// If the package is signed with a key that is no longer trusted by the sharedUserId, then
// the join should not be allowed unless this is a system join type; system packages can
// join the sharedUserId as long as they share a common lineage.
if (!capabilityGranted && sharedUserSigningDetails.hasAncestor(packageSigningDetails)) {
if (joinType == SHARED_USER_ID_JOIN_TYPE_SYSTEM) {
return true;
}
return false;
}
// If the package is signed with a rotated key that no longer trusts the sharedUserId key,
// then allow system and update join types to rotate away from an untrusted key; install
// join types are not allowed since a new package that doesn't trust a previous key
// shouldn't be allowed to join until all packages in the sharedUserId have rotated off the
// untrusted key.
if (!capabilityGranted && packageSigningDetails.hasAncestor(sharedUserSigningDetails)) {
if (joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL) {
return true;
}
return false;
}
// If the capability is not granted and the package signatures are not an ancestor
// or descendant of the sharedUserId signatures, then do not allow any join type to join
// the sharedUserId since there are no common signatures.
if (!capabilityGranted) {
return false;
}
// At this point this is a new install with the capability granted; ensure the current
// packages in the sharedUserId are all signed by a key trusted by the new package.
final ArraySet<PackageStateInternal> susPackageStates =
(ArraySet<PackageStateInternal>) sharedUserSetting.getPackageStates();
if (packageSigningDetails.hasPastSigningCertificates()) {
for (PackageStateInternal shUidPkgSetting : susPackageStates) {
SigningDetails shUidSigningDetails = shUidPkgSetting.getSigningDetails();
// The capability check only needs to be performed against the package if it is
// signed with a key that is in the lineage of the package being installed.
if (packageSigningDetails.hasAncestor(shUidSigningDetails)) {
if (!packageSigningDetails.checkCapability(shUidSigningDetails,
SigningDetails.CertCapabilities.SHARED_USER_ID)) {
Slog.d(TAG, "Package " + packageName
+ " revoked the sharedUserId capability from the"
+ " signing key used to sign "
+ shUidPkgSetting.getPackageName());
return false;
}
}
}
}
return true;
}
/**
* Extract native libraries to a target path
*/
public static int extractNativeBinaries(File dstCodePath, String packageName) {
final File libraryRoot = new File(dstCodePath, LIB_DIR_NAME);
NativeLibraryHelper.Handle handle = null;
try {
handle = NativeLibraryHelper.Handle.create(dstCodePath);
return NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
null /*abiOverride*/, false /*isIncremental*/);
} catch (IOException e) {
logCriticalInfo(Log.ERROR, "Failed to extract native libraries"
+ "; pkg: " + packageName);
return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
} finally {
IoUtils.closeQuietly(handle);
}
}
/**
* Remove native libraries of a given package
*/
public static void removeNativeBinariesLI(PackageSetting ps) {
if (ps != null) {
NativeLibraryHelper.removeNativeBinariesLI(ps.getLegacyNativeLibraryPath());
}
}
/**
* Wait for native library extraction to be done in IncrementalService
*/
public static void waitForNativeBinariesExtractionForIncremental(
ArraySet<IncrementalStorage> incrementalStorages) {
if (incrementalStorages.isEmpty()) {
return;
}
try {
// Native library extraction may take very long time: each page could potentially
// wait for either 10s or 100ms (adb vs non-adb data loader), and that easily adds
// up to a full watchdog timeout of 1 min, killing the system after that. It doesn't
// make much sense as blocking here doesn't lock up the framework, but only blocks
// the installation session and the following ones.
Watchdog.getInstance().pauseWatchingCurrentThread("native_lib_extract");
for (int i = 0; i < incrementalStorages.size(); ++i) {
IncrementalStorage storage = incrementalStorages.valueAtUnchecked(i);
storage.waitForNativeBinariesExtraction();
}
} finally {
Watchdog.getInstance().resumeWatchingCurrentThread("native_lib_extract");
}
}
/**
* Decompress files stored in codePath to dstCodePath for a certain package.
*/
public static int decompressFiles(String codePath, File dstCodePath, String packageName) {
final File[] compressedFiles = getCompressedFiles(codePath);
int ret = PackageManager.INSTALL_SUCCEEDED;
try {
makeDirRecursive(dstCodePath, 0755);
for (File srcFile : compressedFiles) {
final String srcFileName = srcFile.getName();
final String dstFileName = srcFileName.substring(
0, srcFileName.length() - COMPRESSED_EXTENSION.length());
final File dstFile = new File(dstCodePath, dstFileName);
ret = decompressFile(srcFile, dstFile);
if (ret != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.ERROR, "Failed to decompress"
+ "; pkg: " + packageName
+ ", file: " + dstFileName);
break;
}
}
} catch (ErrnoException e) {
logCriticalInfo(Log.ERROR, "Failed to decompress"
+ "; pkg: " + packageName
+ ", err: " + e.errno);
}
return ret;
}
public static int decompressFile(File srcFile, File dstFile) throws ErrnoException {
if (DEBUG_COMPRESSION) {
Slog.i(TAG, "Decompress file"
+ "; src: " + srcFile.getAbsolutePath()
+ ", dst: " + dstFile.getAbsolutePath());
}
final AtomicFile atomicFile = new AtomicFile(dstFile);
FileOutputStream outputStream = null;
try (
InputStream fileIn = new GZIPInputStream(new FileInputStream(srcFile))
) {
outputStream = atomicFile.startWrite();
FileUtils.copy(fileIn, outputStream);
// Flush anything in buffer before chmod, because any writes after chmod will fail.
outputStream.flush();
Os.fchmod(outputStream.getFD(), 0644);
atomicFile.finishWrite(outputStream);
return PackageManager.INSTALL_SUCCEEDED;
} catch (IOException e) {
logCriticalInfo(Log.ERROR, "Failed to decompress file"
+ "; src: " + srcFile.getAbsolutePath()
+ ", dst: " + dstFile.getAbsolutePath());
atomicFile.failWrite(outputStream);
}
return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
}
public static File[] getCompressedFiles(String codePath) {
final File stubCodePath = new File(codePath);
final String stubName = stubCodePath.getName();
// The layout of a compressed package on a given partition is as follows :
//
// Compressed artifacts:
//
// /partition/ModuleName/foo.gz
// /partation/ModuleName/bar.gz
//
// Stub artifact:
//
// /partition/ModuleName-Stub/ModuleName-Stub.apk
//
// In other words, stub is on the same partition as the compressed artifacts
// and in a directory that's suffixed with "-Stub".
int idx = stubName.lastIndexOf(STUB_SUFFIX);
if (idx < 0 || (stubName.length() != (idx + STUB_SUFFIX.length()))) {
return null;
}
final File stubParentDir = stubCodePath.getParentFile();
if (stubParentDir == null) {
Slog.e(TAG, "Unable to determine stub parent dir for codePath: " + codePath);
return null;
}
final File compressedPath = new File(stubParentDir, stubName.substring(0, idx));
final File[] files = compressedPath.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(COMPRESSED_EXTENSION);
}
});
if (DEBUG_COMPRESSION && files != null && files.length > 0) {
Slog.i(TAG, "getCompressedFiles[" + codePath + "]: " + Arrays.toString(files));
}
return files;
}
public static boolean compressedFileExists(String codePath) {
final File[] compressedFiles = getCompressedFiles(codePath);
return compressedFiles != null && compressedFiles.length > 0;
}
/**
* Parse given package and return minimal details.
*/
public static PackageInfoLite getMinimalPackageInfo(Context context, PackageLite pkg,
String packagePath, int flags, String abiOverride) {
final PackageInfoLite ret = new PackageInfoLite();
if (packagePath == null || pkg == null) {
Slog.i(TAG, "Invalid package file " + packagePath);
ret.recommendedInstallLocation = InstallLocationUtils.RECOMMEND_FAILED_INVALID_APK;
return ret;
}
final File packageFile = new File(packagePath);
final long sizeBytes;
try {
sizeBytes = InstallLocationUtils.calculateInstalledSize(pkg, abiOverride);
} catch (IOException e) {
if (!packageFile.exists()) {
ret.recommendedInstallLocation = InstallLocationUtils.RECOMMEND_FAILED_INVALID_URI;
} else {
ret.recommendedInstallLocation = InstallLocationUtils.RECOMMEND_FAILED_INVALID_APK;
}
return ret;
}
final PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_INVALID);
sessionParams.appPackageName = pkg.getPackageName();
sessionParams.installLocation = pkg.getInstallLocation();
sessionParams.sizeBytes = sizeBytes;
sessionParams.installFlags = flags;
final int recommendedInstallLocation;
try {
recommendedInstallLocation = InstallLocationUtils.resolveInstallLocation(context,
sessionParams);
} catch (IOException e) {
throw new IllegalStateException(e);
}
ret.packageName = pkg.getPackageName();
ret.splitNames = pkg.getSplitNames();
ret.versionCode = pkg.getVersionCode();
ret.versionCodeMajor = pkg.getVersionCodeMajor();
ret.baseRevisionCode = pkg.getBaseRevisionCode();
ret.splitRevisionCodes = pkg.getSplitRevisionCodes();
ret.installLocation = pkg.getInstallLocation();
ret.verifiers = pkg.getVerifiers();
ret.recommendedInstallLocation = recommendedInstallLocation;
ret.multiArch = pkg.isMultiArch();
ret.debuggable = pkg.isDebuggable();
ret.isSdkLibrary = pkg.isIsSdkLibrary();
return ret;
}
/**
* Calculate estimated footprint of given package post-installation.
*
* @return -1 if there's some error calculating the size, otherwise installed size of the
* package.
*/
public static long calculateInstalledSize(String packagePath, String abiOverride) {
final File packageFile = new File(packagePath);
try {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
input.reset(), packageFile, /* flags */ 0);
if (result.isError()) {
throw new PackageManagerException(result.getErrorCode(),
result.getErrorMessage(), result.getException());
}
return InstallLocationUtils.calculateInstalledSize(result.getResult(), abiOverride);
} catch (PackageManagerException | IOException e) {
Slog.w(TAG, "Failed to calculate installed size: " + e);
return -1;
}
}
/**
* Checks whenever downgrade of an app is permitted.
*
* @param installFlags flags of the current install.
* @param isAppDebuggable if the currently installed version of the app is debuggable.
* @return {@code true} if downgrade is permitted according to the {@code installFlags} and
* {@code applicationFlags}.
*/
public static boolean isDowngradePermitted(int installFlags, boolean isAppDebuggable) {
// If installed, the package will get access to data left on the device by its
// predecessor. As a security measure, this is permitted only if this is not a
// version downgrade or if the predecessor package is marked as debuggable and
// a downgrade is explicitly requested.
//
// On debuggable platform builds, downgrades are permitted even for
// non-debuggable packages to make testing easier. Debuggable platform builds do
// not offer security guarantees and thus it's OK to disable some security
// mechanisms to make debugging/testing easier on those builds. However, even on
// debuggable builds downgrades of packages are permitted only if requested via
// installFlags. This is because we aim to keep the behavior of debuggable
// platform builds as close as possible to the behavior of non-debuggable
// platform builds.
//
// In case of user builds, downgrade is permitted only for the system server initiated
// sessions. This is enforced by INSTALL_ALLOW_DOWNGRADE flag parameter.
final boolean downgradeRequested =
(installFlags & PackageManager.INSTALL_REQUEST_DOWNGRADE) != 0;
if (!downgradeRequested) {
return false;
}
final boolean isDebuggable = Build.IS_DEBUGGABLE || isAppDebuggable;
if (isDebuggable) {
return true;
}
return (installFlags & PackageManager.INSTALL_ALLOW_DOWNGRADE) != 0;
}
/**
* Copy package to the target location.
*
* @param packagePath absolute path to the package to be copied. Can be
* a single monolithic APK file or a cluster directory
* containing one or more APKs.
* @return returns status code according to those in
* {@link PackageManager}
*/
public static int copyPackage(String packagePath, File targetDir) {
if (packagePath == null) {
return PackageManager.INSTALL_FAILED_INVALID_URI;
}
try {
final File packageFile = new File(packagePath);
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
input.reset(), packageFile, /* flags */ 0);
if (result.isError()) {
Slog.w(TAG, "Failed to parse package at " + packagePath);
return result.getErrorCode();
}
final PackageLite pkg = result.getResult();
copyFile(pkg.getBaseApkPath(), targetDir, "base.apk");
if (!ArrayUtils.isEmpty(pkg.getSplitNames())) {
for (int i = 0; i < pkg.getSplitNames().length; i++) {
copyFile(pkg.getSplitApkPaths()[i], targetDir,
"split_" + pkg.getSplitNames()[i] + ".apk");
}
}
return PackageManager.INSTALL_SUCCEEDED;
} catch (IOException | ErrnoException e) {
Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
}
}
private static void copyFile(String sourcePath, File targetDir, String targetName)
throws ErrnoException, IOException {
if (!FileUtils.isValidExtFilename(targetName)) {
throw new IllegalArgumentException("Invalid filename: " + targetName);
}
Slog.d(TAG, "Copying " + sourcePath + " to " + targetName);
final File targetFile = new File(targetDir, targetName);
final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(),
O_RDWR | O_CREAT, 0644);
Os.chmod(targetFile.getAbsolutePath(), 0644);
FileInputStream source = null;
try {
source = new FileInputStream(sourcePath);
FileUtils.copy(source.getFD(), targetFd);
} finally {
IoUtils.closeQuietly(source);
}
}
/**
* Recursively create target directory
*/
public static void makeDirRecursive(File targetDir, int mode) throws ErrnoException {
final Path targetDirPath = targetDir.toPath();
final int directoriesCount = targetDirPath.getNameCount();
File currentDir;
for (int i = 1; i <= directoriesCount; i++) {
currentDir = targetDirPath.subpath(0, i).toFile();
if (currentDir.exists()) {
continue;
}
Os.mkdir(currentDir.getAbsolutePath(), mode);
Os.chmod(currentDir.getAbsolutePath(), mode);
}
}
/**
* Returns a string that's compatible with the verification root hash extra.
* @see PackageManager#EXTRA_VERIFICATION_ROOT_HASH
*/
@NonNull
public static String buildVerificationRootHashString(@NonNull String baseFilename,
@Nullable String[] splitFilenameArray) {
final StringBuilder sb = new StringBuilder();
final String baseFilePath =
baseFilename.substring(baseFilename.lastIndexOf(File.separator) + 1);
sb.append(baseFilePath).append(":");
final byte[] baseRootHash = getRootHash(baseFilename);
if (baseRootHash == null) {
sb.append("0");
} else {
sb.append(HexDump.toHexString(baseRootHash));
}
if (splitFilenameArray == null || splitFilenameArray.length == 0) {
return sb.toString();
}
for (int i = splitFilenameArray.length - 1; i >= 0; i--) {
final String splitFilename = splitFilenameArray[i];
final String splitFilePath =
splitFilename.substring(splitFilename.lastIndexOf(File.separator) + 1);
final byte[] splitRootHash = getRootHash(splitFilename);
sb.append(";").append(splitFilePath).append(":");
if (splitRootHash == null) {
sb.append("0");
} else {
sb.append(HexDump.toHexString(splitRootHash));
}
}
return sb.toString();
}
/**
* Returns the root has for the given file.
* <p>Otherwise, returns {@code null} if the root hash could not be found or calculated.
* <p>NOTE: This currently only works on files stored on the incremental file system. The
* eventual goal is that this hash [among others] can be retrieved for any file.
*/
@Nullable
private static byte[] getRootHash(String filename) {
try {
final byte[] baseFileSignature =
IncrementalManager.unsafeGetFileSignature(filename);
if (baseFileSignature == null) {
throw new IOException("File signature not present");
}
final V4Signature signature =
V4Signature.readFrom(baseFileSignature);
if (signature.hashingInfo == null) {
throw new IOException("Hashing info not present");
}
final HashingInfo hashInfo =
HashingInfo.fromByteArray(signature.hashingInfo);
if (ArrayUtils.isEmpty(hashInfo.rawRootHash)) {
throw new IOException("Root has not present");
}
return ApkChecksums.verityHashForFile(new File(filename), hashInfo.rawRootHash);
} catch (IOException e) {
Slog.i(TAG, "Could not obtain verity root hash", e);
}
return null;
}
public static boolean isSystemApp(PackageStateInternal ps) {
return (ps.getFlags() & ApplicationInfo.FLAG_SYSTEM) != 0;
}
public static boolean isUpdatedSystemApp(PackageStateInternal ps) {
return (ps.getFlags() & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
}
// Static to give access to ComputeEngine
public static void applyEnforceIntentFilterMatching(
PlatformCompat compat, ComponentResolverApi resolver,
List<ResolveInfo> resolveInfos, boolean isReceiver,
Intent intent, String resolvedType, int filterCallingUid) {
if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return;
final Printer logPrinter = DEBUG_INTENT_MATCHING
? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM)
: null;
for (int i = resolveInfos.size() - 1; i >= 0; --i) {
final ComponentInfo info = resolveInfos.get(i).getComponentInfo();
// Do not enforce filter matching when the caller is system, root, or the same app
if (ActivityManager.checkComponentPermission(null, filterCallingUid,
info.applicationInfo.uid, false) == PackageManager.PERMISSION_GRANTED) {
continue;
}
final ParsedMainComponent comp;
if (info instanceof ActivityInfo) {
if (isReceiver) {
comp = resolver.getReceiver(info.getComponentName());
} else {
comp = resolver.getActivity(info.getComponentName());
}
} else if (info instanceof ServiceInfo) {
comp = resolver.getService(info.getComponentName());
} else {
// This shall never happen
throw new IllegalArgumentException("Unsupported component type");
}
if (comp == null || comp.getIntents().isEmpty()) {
continue;
}
// Only enforce filter matching if target app's target SDK >= T
final boolean enforce = compat.isChangeEnabledInternal(
ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, info.applicationInfo);
boolean match = false;
for (int j = 0, size = comp.getIntents().size(); j < size; ++j) {
IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter();
if (IntentResolver.intentMatchesFilter(intentFilter, intent, resolvedType)) {
match = true;
break;
}
}
if (!match) {
ActivityManagerUtils.logUnsafeIntentEvent(
UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH,
filterCallingUid, intent, resolvedType, enforce);
if (enforce) {
Slog.w(TAG, "Intent does not match component's intent filter: " + intent);
Slog.w(TAG, "Access blocked: " + comp.getComponentName());
if (DEBUG_INTENT_MATCHING) {
Slog.v(TAG, "Component intent filters:");
comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " "));
Slog.v(TAG, "-----------------------------");
}
resolveInfos.remove(i);
}
}
}
}
/**
* Do NOT use for intent resolution filtering. That should be done with
* {@link DomainVerificationManagerInternal#filterToApprovedApp(Intent, List, int, Function)}.
*
* @return if the package is approved at any non-zero level for the domain in the intent
*/
public static boolean hasAnyDomainApproval(
@NonNull DomainVerificationManagerInternal manager,
@NonNull PackageStateInternal pkgSetting, @NonNull Intent intent,
@PackageManager.ResolveInfoFlagsBits long resolveInfoFlags, @UserIdInt int userId) {
return manager.approvalLevelForDomain(pkgSetting, intent, resolveInfoFlags, userId)
> DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE;
}
/**
* Update given intent when being used to request {@link ResolveInfo}.
*/
public static Intent updateIntentForResolve(Intent intent) {
if (intent.getSelector() != null) {
intent = intent.getSelector();
}
if (DEBUG_PREFERRED) {
intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
}
return intent;
}
public static String arrayToString(int[] array) {
StringBuilder stringBuilder = new StringBuilder(128);
stringBuilder.append('[');
if (array != null) {
for (int i = 0; i < array.length; i++) {
if (i > 0) stringBuilder.append(", ");
stringBuilder.append(array[i]);
}
}
stringBuilder.append(']');
return stringBuilder.toString();
}
/**
* Given {@code targetDir}, returns {@code targetDir/~~[randomStrA]/[packageName]-[randomStrB].}
* Makes sure that {@code targetDir/~~[randomStrA]} directory doesn't exist.
* Notice that this method doesn't actually create any directory.
*
* @param targetDir Directory that is two-levels up from the result directory.
* @param packageName Name of the package whose code files are to be installed under the result
* directory.
* @return File object for the directory that should hold the code files of {@code packageName}.
*/
public static File getNextCodePath(File targetDir, String packageName) {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
File firstLevelDir;
do {
random.nextBytes(bytes);
String firstLevelDirName = RANDOM_DIR_PREFIX
+ Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
firstLevelDir = new File(targetDir, firstLevelDirName);
} while (firstLevelDir.exists());
random.nextBytes(bytes);
String dirName = packageName + RANDOM_CODEPATH_PREFIX + Base64.encodeToString(bytes,
Base64.URL_SAFE | Base64.NO_WRAP);
final File result = new File(firstLevelDir, dirName);
if (DEBUG && !Objects.equals(tryParsePackageName(result.getName()), packageName)) {
throw new RuntimeException(
"codepath is off: " + result.getName() + " (" + packageName + ")");
}
return result;
}
static String tryParsePackageName(@NonNull String codePath) throws IllegalArgumentException {
int packageNameEnds = codePath.indexOf(RANDOM_CODEPATH_PREFIX);
if (packageNameEnds == -1) {
throw new IllegalArgumentException("Not a valid package folder name");
}
return codePath.substring(0, packageNameEnds);
}
/**
* Gets the type of the external storage a package is installed on.
* @param packageVolume The storage volume of the package.
* @param packageIsExternal true if the package is currently installed on
* external/removable/unprotected storage.
* @return {@link StorageEnums#UNKNOWN} if the package is not stored externally or the
* corresponding {@link StorageEnums} storage type value if it is.
* corresponding {@link StorageEnums} storage type value if it is.
*/
public static int getPackageExternalStorageType(VolumeInfo packageVolume,
boolean packageIsExternal) {
if (packageVolume != null) {
DiskInfo disk = packageVolume.getDisk();
if (disk != null) {
if (disk.isSd()) {
return StorageEnums.SD_CARD;
}
if (disk.isUsb()) {
return StorageEnums.USB;
}
if (packageIsExternal) {
return StorageEnums.OTHER;
}
}
}
return StorageEnums.UNKNOWN;
}
/**
* Enforces that only the system UID or root's UID or shell's UID can call
* a method exposed via Binder.
*
* @param message used as message if SecurityException is thrown
* @throws SecurityException if the caller is not system or shell
*/
public static void enforceSystemOrRootOrShell(String message) {
if (!isSystemOrRootOrShell()) {
throw new SecurityException(message);
}
}
/**
* Check if the Binder caller is system UID, root's UID, or shell's UID.
*/
public static boolean isSystemOrRootOrShell() {
return isSystemOrRootOrShell(Binder.getCallingUid());
}
/**
* @see #isSystemOrRoot()
*/
public static boolean isSystemOrRootOrShell(int uid) {
return uid == Process.SYSTEM_UID || uid == Process.ROOT_UID || uid == Process.SHELL_UID;
}
/**
* Check if the Binder caller is system UID or root's UID.
*/
public static boolean isSystemOrRoot() {
final int uid = Binder.getCallingUid();
return isSystemOrRoot(uid);
}
/**
* Check if a UID is system UID or root's UID.
*/
public static boolean isSystemOrRoot(int uid) {
return uid == Process.SYSTEM_UID || uid == Process.ROOT_UID;
}
/**
* Check if a UID is non-system UID adopted shell permission.
*/
public static boolean isAdoptedShell(int uid, Context context) {
return uid != Process.SYSTEM_UID && context.checkCallingOrSelfPermission(
Manifest.permission.USE_SYSTEM_DATA_LOADERS) == PackageManager.PERMISSION_GRANTED;
}
/**
* Check if a UID is system UID or shell's UID.
*/
public static boolean isRootOrShell(int uid) {
return uid == Process.ROOT_UID || uid == Process.SHELL_UID;
}
/**
* Enforces that only the system UID or root's UID can call a method exposed
* via Binder.
*
* @param message used as message if SecurityException is thrown
* @throws SecurityException if the caller is not system or root
*/
public static void enforceSystemOrRoot(String message) {
if (!isSystemOrRoot()) {
throw new SecurityException(message);
}
}
public static @Nullable File preparePackageParserCache(boolean forEngBuild,
boolean isUserDebugBuild, String incrementalVersion) {
if (!FORCE_PACKAGE_PARSED_CACHE_ENABLED) {
if (!DEFAULT_PACKAGE_PARSER_CACHE_ENABLED) {
return null;
}
// Disable package parsing on eng builds to allow for faster incremental development.
if (forEngBuild) {
return null;
}
if (SystemProperties.getBoolean("pm.boot.disable_package_cache", false)) {
Slog.i(TAG, "Disabling package parser cache due to system property.");
return null;
}
}
// The base directory for the package parser cache lives under /data/system/.
final File cacheBaseDir = Environment.getPackageCacheDirectory();
if (!FileUtils.createDir(cacheBaseDir)) {
return null;
}
// There are several items that need to be combined together to safely
// identify cached items. In particular, changing the value of certain
// feature flags should cause us to invalidate any caches.
final String cacheName = FORCE_PACKAGE_PARSED_CACHE_ENABLED ? "debug"
: PackagePartitions.FINGERPRINT;
// Reconcile cache directories, keeping only what we'd actually use.
for (File cacheDir : FileUtils.listFilesOrEmpty(cacheBaseDir)) {
if (Objects.equals(cacheName, cacheDir.getName())) {
Slog.d(TAG, "Keeping known cache " + cacheDir.getName());
} else {
Slog.d(TAG, "Destroying unknown cache " + cacheDir.getName());
FileUtils.deleteContentsAndDir(cacheDir);
}
}
// Return the versioned package cache directory.
File cacheDir = FileUtils.createDir(cacheBaseDir, cacheName);
if (cacheDir == null) {
// Something went wrong. Attempt to delete everything and return.
Slog.wtf(TAG, "Cache directory cannot be created - wiping base dir " + cacheBaseDir);
FileUtils.deleteContentsAndDir(cacheBaseDir);
return null;
}
// The following is a workaround to aid development on non-numbered userdebug
// builds or cases where "adb sync" is used on userdebug builds. If we detect that
// the system partition is newer.
//
// NOTE: When no BUILD_NUMBER is set by the build system, it defaults to a build
// that starts with "eng." to signify that this is an engineering build and not
// destined for release.
if (isUserDebugBuild && incrementalVersion.startsWith("eng.")) {
Slog.w(TAG, "Wiping cache directory because the system partition changed.");
// Heuristic: If the /system directory has been modified recently due to an "adb sync"
// or a regular make, then blow away the cache. Note that mtimes are *NOT* reliable
// in general and should not be used for production changes. In this specific case,
// we know that they will work.
File frameworkDir =
new File(Environment.getRootDirectory(), "framework");
if (cacheDir.lastModified() < frameworkDir.lastModified()) {
FileUtils.deleteContents(cacheBaseDir);
cacheDir = FileUtils.createDir(cacheBaseDir, cacheName);
}
}
return cacheDir;
}
/**
* Check and throw if the given before/after packages would be considered a
* downgrade.
*/
public static void checkDowngrade(AndroidPackage before, PackageInfoLite after)
throws PackageManagerException {
if (after.getLongVersionCode() < before.getLongVersionCode()) {
throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
"Update version code " + after.versionCode + " is older than current "
+ before.getLongVersionCode());
} else if (after.getLongVersionCode() == before.getLongVersionCode()) {
if (after.baseRevisionCode < before.getBaseRevisionCode()) {
throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
"Update base revision code " + after.baseRevisionCode
+ " is older than current " + before.getBaseRevisionCode());
}
if (!ArrayUtils.isEmpty(after.splitNames)) {
for (int i = 0; i < after.splitNames.length; i++) {
final String splitName = after.splitNames[i];
final int j = ArrayUtils.indexOf(before.getSplitNames(), splitName);
if (j != -1) {
if (after.splitRevisionCodes[i] < before.getSplitRevisionCodes()[j]) {
throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
"Update split " + splitName + " revision code "
+ after.splitRevisionCodes[i]
+ " is older than current "
+ before.getSplitRevisionCodes()[j]);
}
}
}
}
}
}
/**
* Check if package name is com.android.shell or is null.
*/
public static boolean isInstalledByAdb(String initiatingPackageName) {
return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName);
}
}