diff options
19 files changed, 2108 insertions, 52 deletions
diff --git a/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp index 41a17064c3ba..66f5c3908e4b 100644 --- a/cmds/idmap2/Android.bp +++ b/cmds/idmap2/Android.bp @@ -146,6 +146,7 @@ cc_binary { host_supported: true, srcs: [ "idmap2/Create.cpp", + "idmap2/CreateMultiple.cpp", "idmap2/Dump.cpp", "idmap2/Lookup.cpp", "idmap2/Main.cpp", diff --git a/cmds/idmap2/idmap2/Commands.h b/cmds/idmap2/idmap2/Commands.h index 718e361b38ab..e626738a2895 100644 --- a/cmds/idmap2/idmap2/Commands.h +++ b/cmds/idmap2/idmap2/Commands.h @@ -23,6 +23,7 @@ #include "idmap2/Result.h" android::idmap2::Result<android::idmap2::Unit> Create(const std::vector<std::string>& args); +android::idmap2::Result<android::idmap2::Unit> CreateMultiple(const std::vector<std::string>& args); android::idmap2::Result<android::idmap2::Unit> Dump(const std::vector<std::string>& args); android::idmap2::Result<android::idmap2::Unit> Lookup(const std::vector<std::string>& args); android::idmap2::Result<android::idmap2::Unit> Scan(const std::vector<std::string>& args); diff --git a/cmds/idmap2/idmap2/CreateMultiple.cpp b/cmds/idmap2/idmap2/CreateMultiple.cpp new file mode 100644 index 000000000000..0b0541fb6221 --- /dev/null +++ b/cmds/idmap2/idmap2/CreateMultiple.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 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. + */ + +#include <sys/stat.h> // umask +#include <sys/types.h> // umask + +#include <fstream> +#include <memory> +#include <ostream> +#include <sstream> +#include <string> +#include <vector> + +#include "android-base/stringprintf.h" +#include "idmap2/BinaryStreamVisitor.h" +#include "idmap2/CommandLineOptions.h" +#include "idmap2/FileUtils.h" +#include "idmap2/Idmap.h" +#include "idmap2/Policies.h" +#include "idmap2/SysTrace.h" + +using android::ApkAssets; +using android::base::StringPrintf; +using android::idmap2::BinaryStreamVisitor; +using android::idmap2::CommandLineOptions; +using android::idmap2::Error; +using android::idmap2::Idmap; +using android::idmap2::PoliciesToBitmask; +using android::idmap2::PolicyBitmask; +using android::idmap2::PolicyFlags; +using android::idmap2::Result; +using android::idmap2::Unit; +using android::idmap2::utils::kIdmapCacheDir; +using android::idmap2::utils::kIdmapFilePermissionMask; +using android::idmap2::utils::UidHasWriteAccessToPath; + +Result<Unit> CreateMultiple(const std::vector<std::string>& args) { + SYSTRACE << "CreateMultiple " << args; + std::string target_apk_path; + std::string idmap_dir = kIdmapCacheDir; + std::vector<std::string> overlay_apk_paths; + std::vector<std::string> policies; + bool ignore_overlayable = false; + + const CommandLineOptions opts = + CommandLineOptions("idmap2 create-multiple") + .MandatoryOption("--target-apk-path", + "input: path to apk which will have its resources overlaid", + &target_apk_path) + .MandatoryOption("--overlay-apk-path", + "input: path to apk which contains the new resource values", + &overlay_apk_paths) + .OptionalOption("--idmap-dir", + StringPrintf("output: path to the directory in which to write idmap file" + " (defaults to %s)", + kIdmapCacheDir), + &idmap_dir) + .OptionalOption("--policy", + "input: an overlayable policy this overlay fulfills" + " (if none or supplied, the overlay policy will default to \"public\")", + &policies) + .OptionalFlag("--ignore-overlayable", "disables overlayable and policy checks", + &ignore_overlayable); + const auto opts_ok = opts.Parse(args); + if (!opts_ok) { + return opts_ok.GetError(); + } + + PolicyBitmask fulfilled_policies = 0; + auto conv_result = PoliciesToBitmask(policies); + if (conv_result) { + fulfilled_policies |= *conv_result; + } else { + return conv_result.GetError(); + } + + if (fulfilled_policies == 0) { + fulfilled_policies |= PolicyFlags::POLICY_PUBLIC; + } + + const std::unique_ptr<const ApkAssets> target_apk = ApkAssets::Load(target_apk_path); + if (!target_apk) { + return Error("failed to load apk %s", target_apk_path.c_str()); + } + + std::vector<std::string> idmap_paths; + for (const std::string& overlay_apk_path : overlay_apk_paths) { + const std::string idmap_path = Idmap::CanonicalIdmapPathFor(idmap_dir, overlay_apk_path); + const uid_t uid = getuid(); + if (!UidHasWriteAccessToPath(uid, idmap_path)) { + LOG(WARNING) << "uid " << uid << "does not have write access to " << idmap_path.c_str(); + continue; + } + + const std::unique_ptr<const ApkAssets> overlay_apk = ApkAssets::Load(overlay_apk_path); + if (!overlay_apk) { + LOG(WARNING) << "failed to load apk " << overlay_apk_path.c_str(); + continue; + } + + const auto idmap = + Idmap::FromApkAssets(*target_apk, *overlay_apk, fulfilled_policies, !ignore_overlayable); + if (!idmap) { + LOG(WARNING) << "failed to create idmap"; + continue; + } + + umask(kIdmapFilePermissionMask); + std::ofstream fout(idmap_path); + if (fout.fail()) { + LOG(WARNING) << "failed to open idmap path " << idmap_path.c_str(); + continue; + } + + BinaryStreamVisitor visitor(fout); + (*idmap)->accept(&visitor); + fout.close(); + if (fout.fail()) { + LOG(WARNING) << "failed to write to idmap path %s" << idmap_path.c_str(); + continue; + } + + idmap_paths.emplace_back(idmap_path); + } + + for (const std::string& idmap_path : idmap_paths) { + std::cout << idmap_path << std::endl; + } + + return Unit{}; +} diff --git a/cmds/idmap2/idmap2/Main.cpp b/cmds/idmap2/idmap2/Main.cpp index 87949085cf1d..a07e793d9f47 100644 --- a/cmds/idmap2/idmap2/Main.cpp +++ b/cmds/idmap2/idmap2/Main.cpp @@ -53,7 +53,9 @@ void PrintUsage(const NameToFunctionMap& commands, std::ostream& out) { int main(int argc, char** argv) { SYSTRACE << "main"; const NameToFunctionMap commands = { - {"create", Create}, {"dump", Dump}, {"lookup", Lookup}, {"scan", Scan}, {"verify", Verify}, + {"create", Create}, {"create-multiple", CreateMultiple}, + {"dump", Dump}, {"lookup", Lookup}, + {"scan", Scan}, {"verify", Verify}, }; if (argc <= 1) { PrintUsage(commands, std::cerr); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index b6b27b6e514a..5be5afbd4918 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -484,6 +484,9 @@ public class PackageParser { public final boolean isolatedSplits; public final boolean isSplitRequired; public final boolean useEmbeddedDex; + public final String targetPackageName; + public final boolean overlayIsStatic; + public final int overlayPriority; public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit, @@ -493,6 +496,7 @@ public class PackageParser { SigningDetails signingDetails, boolean coreApp, boolean debuggable, boolean multiArch, boolean use32bitAbi, boolean useEmbeddedDex, boolean extractNativeLibs, boolean isolatedSplits, + String targetPackageName, boolean overlayIsStatic, int overlayPriority, int minSdkVersion, int targetSdkVersion) { this.codePath = codePath; this.packageName = packageName; @@ -514,6 +518,9 @@ public class PackageParser { this.extractNativeLibs = extractNativeLibs; this.isolatedSplits = isolatedSplits; this.isSplitRequired = isSplitRequired; + this.targetPackageName = targetPackageName; + this.overlayIsStatic = overlayIsStatic; + this.overlayPriority = overlayPriority; this.minSdkVersion = minSdkVersion; this.targetSdkVersion = targetSdkVersion; } @@ -1797,6 +1804,12 @@ public class PackageParser { boolean useEmbeddedDex = false; String configForSplit = null; String usesSplitName = null; + String targetPackage = null; + boolean overlayIsStatic = false; + int overlayPriority = 0; + + String requiredSystemPropertyName = null; + String requiredSystemPropertyValue = null; for (int i = 0; i < attrs.getAttributeCount(); i++) { final String attr = attrs.getAttributeName(i); @@ -1861,6 +1874,21 @@ public class PackageParser { useEmbeddedDex = attrs.getAttributeBooleanValue(i, false); } } + } else if (PackageParser.TAG_OVERLAY.equals(parser.getName())) { + for (int i = 0; i < attrs.getAttributeCount(); ++i) { + final String attr = attrs.getAttributeName(i); + if ("requiredSystemPropertyName".equals(attr)) { + requiredSystemPropertyName = attrs.getAttributeValue(i); + } else if ("requiredSystemPropertyValue".equals(attr)) { + requiredSystemPropertyValue = attrs.getAttributeValue(i); + } else if ("targetPackage".equals(attr)) { + targetPackage = attrs.getAttributeValue(i);; + } else if ("isStatic".equals(attr)) { + overlayIsStatic = attrs.getAttributeBooleanValue(i, false); + } else if ("priority".equals(attr)) { + overlayPriority = attrs.getAttributeIntValue(i, 0); + } + } } else if (TAG_USES_SPLIT.equals(parser.getName())) { if (usesSplitName != null) { Slog.w(TAG, "Only one <uses-split> permitted. Ignoring others."); @@ -1887,11 +1915,22 @@ public class PackageParser { } } + // Check to see if overlay should be excluded based on system property condition + if (!checkRequiredSystemProperty(requiredSystemPropertyName, + requiredSystemPropertyValue)) { + Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + + codePath + ": overlay ignored due to required system property: " + + requiredSystemPropertyName + " with value: " + requiredSystemPropertyValue); + targetPackage = null; + overlayIsStatic = false; + overlayPriority = 0; + } + return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, configForSplit, usesSplitName, isSplitRequired, versionCode, versionCodeMajor, revisionCode, installLocation, verifiers, signingDetails, coreApp, debuggable, multiArch, use32bitAbi, useEmbeddedDex, extractNativeLibs, isolatedSplits, - minSdkVersion, targetSdkVersion); + targetPackage, overlayIsStatic, overlayPriority, minSdkVersion, targetSdkVersion); } /** @@ -2175,7 +2214,7 @@ public class PackageParser { } // check to see if overlay should be excluded based on system property condition - if (!checkOverlayRequiredSystemProperty(propName, propValue)) { + if (!checkRequiredSystemProperty(propName, propValue)) { Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and " + pkg.baseCodePath+ ": overlay ignored due to required system property: " + propName + " with value: " + propValue); @@ -2603,8 +2642,11 @@ public class PackageParser { return pkg; } - private boolean checkOverlayRequiredSystemProperty(String propName, String propValue) { - + /** + * Returns {@code true} if both the property name and value are empty or if the given system + * property is set to the specified value. In all other cases, returns {@code false} + */ + public static boolean checkRequiredSystemProperty(String propName, String propValue) { if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { // malformed condition - incomplete diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 5be9c910744d..9087f422cef2 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -289,6 +289,12 @@ public class ApkLiteParseUtils { boolean useEmbeddedDex = false; String configForSplit = null; String usesSplitName = null; + String targetPackage = null; + boolean overlayIsStatic = false; + int overlayPriority = 0; + + String requiredSystemPropertyName = null; + String requiredSystemPropertyValue = null; for (int i = 0; i < attrs.getAttributeCount(); i++) { final String attr = attrs.getAttributeName(i); @@ -365,6 +371,21 @@ public class ApkLiteParseUtils { break; } } + } else if (PackageParser.TAG_OVERLAY.equals(parser.getName())) { + for (int i = 0; i < attrs.getAttributeCount(); ++i) { + final String attr = attrs.getAttributeName(i); + if ("requiredSystemPropertyName".equals(attr)) { + requiredSystemPropertyName = attrs.getAttributeValue(i); + } else if ("requiredSystemPropertyValue".equals(attr)) { + requiredSystemPropertyValue = attrs.getAttributeValue(i); + } else if ("targetPackage".equals(attr)) { + targetPackage = attrs.getAttributeValue(i);; + } else if ("isStatic".equals(attr)) { + overlayIsStatic = attrs.getAttributeBooleanValue(i, false); + } else if ("priority".equals(attr)) { + overlayPriority = attrs.getAttributeIntValue(i, 0); + } + } } else if (PackageParser.TAG_USES_SPLIT.equals(parser.getName())) { if (usesSplitName != null) { Slog.w(TAG, "Only one <uses-split> permitted. Ignoring others."); @@ -391,11 +412,23 @@ public class ApkLiteParseUtils { } } + // Check to see if overlay should be excluded based on system property condition + if (!PackageParser.checkRequiredSystemProperty(requiredSystemPropertyName, + requiredSystemPropertyValue)) { + Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + + codePath + ": overlay ignored due to required system property: " + + requiredSystemPropertyName + " with value: " + requiredSystemPropertyValue); + targetPackage = null; + overlayIsStatic = false; + overlayPriority = 0; + } + return new PackageParser.ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, configForSplit, usesSplitName, isSplitRequired, versionCode, versionCodeMajor, revisionCode, installLocation, verifiers, signingDetails, coreApp, debuggable, multiArch, use32bitAbi, useEmbeddedDex, extractNativeLibs, - isolatedSplits, minSdkVersion, targetSdkVersion); + isolatedSplits, targetPackage, overlayIsStatic, overlayPriority, minSdkVersion, + targetSdkVersion); } public static VerifierInfo parseVerifier(AttributeSet attrs) { diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 1b0175812949..f295f8c531e9 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -38,6 +38,7 @@ import android.util.TypedValue; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.om.OverlayConfig; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -242,14 +243,11 @@ public final class AssetManager implements AutoCloseable { try { final ArrayList<ApkAssets> apkAssets = new ArrayList<>(); apkAssets.add(ApkAssets.loadFromPath(frameworkPath, true /*system*/)); - final String[] systemIdmapPaths = nativeCreateIdmapsForStaticOverlaysTargetingAndroid(); - if (systemIdmapPaths != null) { - for (String idmapPath : systemIdmapPaths) { - apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, true /*system*/)); - } - } else { - Log.w(TAG, "'idmap2 --scan' failed: no static=\"true\" overlays targeting " - + "\"android\" will be loaded"); + + final String[] systemIdmapPaths = + OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); + for (String idmapPath : systemIdmapPaths) { + apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, true /*system*/)); } sSystemApkAssetsSet = new ArraySet<>(apkAssets); diff --git a/core/java/com/android/internal/content/om/OverlayConfig.java b/core/java/com/android/internal/content/om/OverlayConfig.java new file mode 100644 index 000000000000..1a862fa016f5 --- /dev/null +++ b/core/java/com/android/internal/content/om/OverlayConfig.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2020 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.internal.content.om; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PackagePartitions; +import android.content.pm.parsing.AndroidPackage; +import android.os.Build; +import android.os.Process; +import android.os.Trace; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.om.OverlayConfigParser.OverlayPartition; +import com.android.internal.content.om.OverlayConfigParser.ParsedConfiguration; +import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo; +import com.android.internal.util.Preconditions; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Responsible for reading overlay configuration files and handling queries of overlay mutability, + * default-enabled state, and priority. + * + * @see OverlayConfigParser + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +final public class OverlayConfig { + static final String TAG = "OverlayConfig"; + + // The default priority of an overlay that has not been configured. Overlays with default + // priority have a higher precedence than configured overlays. + private static final int DEFAULT_PRIORITY = Integer.MAX_VALUE; + + @VisibleForTesting + public static final class Configuration { + @Nullable + public final ParsedConfiguration parsedConfig; + + public final int configIndex; + + public Configuration(@Nullable ParsedConfiguration parsedConfig, int configIndex) { + this.parsedConfig = parsedConfig; + this.configIndex = configIndex; + } + } + + /** + * Interface for providing information on scanned packages. + * TODO(147840005): Remove this when android:isStatic and android:priority are fully deprecated + */ + public interface AndroidPackageProvider { + + /** Performs the given action for each package. */ + void forEachPackage(Consumer<AndroidPackage> p); + } + + private static final Comparator<ParsedConfiguration> sStaticOverlayComparator = (c1, c2) -> { + final ParsedOverlayInfo o1 = c1.parsedInfo; + final ParsedOverlayInfo o2 = c2.parsedInfo; + Preconditions.checkArgument(o1.isStatic && o2.isStatic, + "attempted to sort non-static overlay"); + + if (!o1.targetPackageName.equals(o2.targetPackageName)) { + return o1.targetPackageName.compareTo(o2.targetPackageName); + } + + final int comparedPriority = o1.priority - o2.priority; + return comparedPriority == 0 ? o1.path.compareTo(o2.path) : comparedPriority; + }; + + // Map of overlay package name to configured overlay settings + private final ArrayMap<String, Configuration> mConfigurations = new ArrayMap<>(); + + // Singleton instance only assigned in system server + private static OverlayConfig sInstance; + + @VisibleForTesting + public OverlayConfig(@Nullable File rootDirectory, + @Nullable Supplier<OverlayScanner> scannerFactory, + @Nullable AndroidPackageProvider packageProvider) { + Preconditions.checkArgument((scannerFactory == null) != (packageProvider == null), + "scannerFactory and packageProvider cannot be both null or both non-null"); + + final ArrayList<OverlayPartition> partitions; + if (rootDirectory == null) { + partitions = new ArrayList<>( + PackagePartitions.getOrderedPartitions(OverlayPartition::new)); + } else { + // Rebase the system partitions and settings file on the specified root directory. + partitions = new ArrayList<>(PackagePartitions.getOrderedPartitions( + p -> new OverlayPartition(new File(rootDirectory, p.folder.getPath()), p))); + } + + boolean foundConfigFile = false; + ArrayList<ParsedOverlayInfo> packageManagerOverlayInfos = null; + + final ArrayList<ParsedConfiguration> overlays = new ArrayList<>(); + for (int i = 0, n = partitions.size(); i < n; i++) { + final OverlayPartition partition = partitions.get(i); + final OverlayScanner scanner = (scannerFactory == null) ? null : scannerFactory.get(); + final ArrayList<ParsedConfiguration> partitionOverlays = + OverlayConfigParser.getConfigurations(partition, scanner); + if (partitionOverlays != null) { + foundConfigFile = true; + overlays.addAll(partitionOverlays); + continue; + } + + // If the configuration file is not present, then use android:isStatic and + // android:priority to configure the overlays in the partition. + // TODO(147840005): Remove converting static overlays to immutable, default-enabled + // overlays when android:siStatic and android:priority are fully deprecated. + final ArrayList<ParsedOverlayInfo> partitionOverlayInfos; + if (scannerFactory != null) { + partitionOverlayInfos = new ArrayList<>(scanner.getAllParsedInfos()); + } else { + if (packageManagerOverlayInfos == null) { + packageManagerOverlayInfos = getOverlayPackageInfos(packageProvider); + } + + // Filter out overlays not present in the partition. + partitionOverlayInfos = new ArrayList<>(packageManagerOverlayInfos); + for (int j = partitionOverlayInfos.size() - 1; j >= 0; j--) { + if (!partition.containsPath(partitionOverlayInfos.get(j).path.getPath())) { + partitionOverlayInfos.remove(j); + } + } + } + + // Static overlays are configured as immutable, default-enabled overlays. + final ArrayList<ParsedConfiguration> partitionConfigs = new ArrayList<>(); + for (int j = 0, m = partitionOverlayInfos.size(); j < m; j++) { + final ParsedOverlayInfo p = partitionOverlayInfos.get(j); + if (p.isStatic) { + partitionConfigs.add(new ParsedConfiguration(p.packageName, + true /* enabled */, false /* mutable */, partition.policy, p)); + } + } + + partitionConfigs.sort(sStaticOverlayComparator); + overlays.addAll(partitionConfigs); + } + + if (!foundConfigFile) { + // If no overlay configuration files exist, disregard partition precedence and allow + // android:priority to reorder overlays across partition boundaries. + overlays.sort(sStaticOverlayComparator); + } + + for (int i = 0, n = overlays.size(); i < n; i++) { + // Add the configurations to a map so definitions of an overlay in an earlier + // partition can be replaced by an overlay with the same package name in a later + // partition. + final ParsedConfiguration config = overlays.get(i); + mConfigurations.put(config.packageName, new Configuration(config, i)); + } + } + + /** + * Creates an instance of OverlayConfig for use in the zygote process. + * This instance will not include information of static overlays existing outside of a partition + * overlay directory. + */ + @NonNull + public static OverlayConfig getZygoteInstance() { + if (Process.myUid() != Process.ROOT_UID) { + // Scan the overlays in the zygote process to generate configuration settings for + // overlays on the system image. Do not cache this instance so OverlayConfig will not + // be present in applications by default. + throw new IllegalStateException("Can only be invoked in the root process"); + } + + Trace.traceBegin(Trace.TRACE_TAG_RRO, "OverlayConfig#getZygoteInstance"); + try { + return new OverlayConfig(null /* rootDirectory */, OverlayScanner::new, + null /* packageProvider */); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_RRO); + } + } + + /** + * Initializes a singleton instance for use in the system process. + * Can only be called once. This instance is cached so future invocations of + * {@link #getSystemInstance()} will return the initialized instance. + */ + @NonNull + public static OverlayConfig initializeSystemInstance(AndroidPackageProvider packageProvider) { + if (Process.myUid() != Process.SYSTEM_UID) { + throw new IllegalStateException("Can only be invoked in the system process"); + } + + Trace.traceBegin(Trace.TRACE_TAG_RRO, "OverlayConfig#initializeSystemInstance"); + sInstance = new OverlayConfig(null, null, packageProvider); + Trace.traceEnd(Trace.TRACE_TAG_RRO); + return sInstance; + } + + /** + * Retrieves the singleton instance initialized by + * {@link #initializeSystemInstance(AndroidPackageProvider)}. + */ + @NonNull + public static OverlayConfig getSystemInstance() { + if (sInstance == null) { + throw new IllegalStateException("System instance not initialized"); + } + + return sInstance; + } + + @VisibleForTesting + @Nullable + public Configuration getConfiguration(@NonNull String packageName) { + return mConfigurations.get(packageName); + } + + /** + * Returns whether the overlay is enabled by default. + * Overlays that are not configured are disabled by default mutable. + */ + public boolean isEnabled(String packageName) { + final Configuration config = mConfigurations.get(packageName); + + // STOPSHIP(149499802): Enabling a mutable overlay currently has no effect. Either implement + // some behavior for default-enabled, mutable overlays or prevent parsing of the enabled + // attribute on overlays that are mutable. + if (config != null && config.parsedConfig.mutable) { + Log.w(TAG, "Default-enabled configuration for mutable overlay " + + config.parsedConfig.packageName + " has no effect"); + return OverlayConfigParser.DEFAULT_ENABLED_STATE; + } + + return config == null? OverlayConfigParser.DEFAULT_ENABLED_STATE + : config.parsedConfig.enabled; + } + + /** + * Returns whether the overlay is mutable and can have its enabled state changed dynamically. + * Overlays that are not configured are mutable. + */ + public boolean isMutable(String packageName) { + final Configuration config = mConfigurations.get(packageName); + return config == null ? OverlayConfigParser.DEFAULT_MUTABILITY + : config.parsedConfig.mutable; + } + + /** + * Returns an integer corresponding to the priority of the overlay. + * When multiple overlays override the same resource, the overlay with the highest priority will + * will have its value chosen. Overlays that are not configured have a priority of + * {@link Integer#MAX_VALUE}. + */ + public int getPriority(String packageName) { + final Configuration config = mConfigurations.get(packageName); + return config == null ? DEFAULT_PRIORITY : config.configIndex; + } + + @NonNull + private ArrayList<Configuration> getSortedOverlays() { + final ArrayList<Configuration> sortedOverlays = new ArrayList<>(); + for (int i = 0, n = mConfigurations.size(); i < n; i++) { + sortedOverlays.add(mConfigurations.valueAt(i)); + } + sortedOverlays.sort(Comparator.comparingInt(o -> o.configIndex)); + return sortedOverlays; + } + + @NonNull + private static ArrayList<ParsedOverlayInfo> getOverlayPackageInfos( + @NonNull AndroidPackageProvider packageManager) { + final ArrayList<ParsedOverlayInfo> overlays = new ArrayList<>(); + packageManager.forEachPackage((AndroidPackage p) -> { + if (p.getOverlayTarget() != null && p.isSystem()) { + overlays.add(new ParsedOverlayInfo(p.getPackageName(), p.getOverlayTarget(), + p.getTargetSdkVersion(), p.isOverlayIsStatic(), p.getOverlayPriority(), + new File(p.getBaseCodePath()))); + } + }); + return overlays; + } + + /** Represents a single call to idmap create-multiple. */ + @VisibleForTesting + public static class IdmapInvocation { + public final boolean enforceOverlayable; + public final String policy; + public final ArrayList<String> overlayPaths = new ArrayList<>(); + + IdmapInvocation(boolean enforceOverlayable, @NonNull String policy) { + this.enforceOverlayable = enforceOverlayable; + this.policy = policy; + } + + @Override + public String toString() { + return getClass().getSimpleName() + String.format("{enforceOverlayable=%s, policy=%s" + + ", overlayPaths=[%s]}", enforceOverlayable, policy, + String.join(", ", overlayPaths)); + } + } + + /** + * Retrieves a list of immutable framework overlays in order of least precedence to greatest + * precedence. + */ + @VisibleForTesting + public ArrayList<IdmapInvocation> getImmutableFrameworkOverlayIdmapInvocations() { + final ArrayList<IdmapInvocation> idmapInvocations = new ArrayList<>(); + final ArrayList<Configuration> sortedConfigs = getSortedOverlays(); + for (int i = 0, n = sortedConfigs.size(); i < n; i++) { + final Configuration overlay = sortedConfigs.get(i); + if (overlay.parsedConfig.mutable || !overlay.parsedConfig.enabled + || !"android".equals(overlay.parsedConfig.parsedInfo.targetPackageName)) { + continue; + } + + // Only enforce that overlays targeting packages with overlayable declarations abide by + // those declarations if the target sdk of the overlay is at least Q (when overlayable + // was introduced). + final boolean enforceOverlayable = overlay.parsedConfig.parsedInfo.targetSdkVersion + >= Build.VERSION_CODES.Q; + + // Determine if the idmap for the current overlay can be generated in the last idmap + // create-multiple invocation. + IdmapInvocation invocation = null; + if (!idmapInvocations.isEmpty()) { + final IdmapInvocation last = idmapInvocations.get(idmapInvocations.size() - 1); + if (last.enforceOverlayable == enforceOverlayable + && last.policy.equals(overlay.parsedConfig.policy)) { + invocation = last; + } + } + + if (invocation == null) { + invocation = new IdmapInvocation(enforceOverlayable, overlay.parsedConfig.policy); + idmapInvocations.add(invocation); + } + + invocation.overlayPaths.add(overlay.parsedConfig.parsedInfo.path.getAbsolutePath()); + } + return idmapInvocations; + } + + /** + * Creates idmap files for immutable overlays targeting the framework packages. Currently the + * android package is the only preloaded system package. Only the zygote can invoke this method. + * + * @return the paths of the created idmap files + */ + @NonNull + public String[] createImmutableFrameworkIdmapsInZygote() { + if (Process.myUid() != Process.ROOT_UID) { + throw new IllegalStateException("This method can only be called from the root process"); + } + + final String targetPath = "/system/framework/framework-res.apk"; + final ArrayList<String> idmapPaths = new ArrayList<>(); + final ArrayList<IdmapInvocation> idmapInvocations = + getImmutableFrameworkOverlayIdmapInvocations(); + + for (int i = 0, n = idmapInvocations.size(); i < n; i++) { + final IdmapInvocation invocation = idmapInvocations.get(i); + final String[] idmaps = createIdmap(targetPath, + invocation.overlayPaths.toArray(new String[0]), + new String[]{OverlayConfigParser.OverlayPartition.POLICY_PUBLIC, + invocation.policy}, + invocation.enforceOverlayable); + + if (idmaps == null) { + Log.w(TAG, "'idmap2 create-multiple' failed: no mutable=\"false\" overlays" + + " targeting \"android\" will be loaded"); + return new String[0]; + } + + idmapPaths.addAll(Arrays.asList(idmaps)); + } + + return idmapPaths.toArray(new String[0]); + } + + /** + * For each overlay APK, this creates the idmap file that allows the overlay to override the + * target package. + * + * @return the paths of the created idmap + */ + private static native String[] createIdmap(@NonNull String targetPath, + @NonNull String[] overlayPath, @NonNull String[] policies, boolean enforceOverlayable); +} diff --git a/core/java/com/android/internal/content/om/OverlayConfigParser.java b/core/java/com/android/internal/content/om/OverlayConfigParser.java new file mode 100644 index 000000000000..139607ff7196 --- /dev/null +++ b/core/java/com/android/internal/content/om/OverlayConfigParser.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2020 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.internal.content.om; + +import static com.android.internal.content.om.OverlayConfig.TAG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PackagePartitions; +import android.content.pm.PackagePartitions.SystemPartition; +import android.os.FileUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.Xml; + +import com.android.internal.util.XmlUtils; +import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Responsible for parsing configurations of Runtime Resource Overlays that control mutability, + * default enable state, and priority. To configure an overlay, create or modify the file located + * at {@code partition}/overlay/config/config.xml where {@code partition} is the partition of the + * overlay to be configured. In order to be configured, an overlay must reside in the overlay + * directory of the partition in which the overlay is configured. + * + * @see #parseOverlay(File, XmlPullParser, OverlayScanner, ParsingContext) + * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext) + **/ +final class OverlayConfigParser { + + // Default values for overlay configurations. + static final boolean DEFAULT_ENABLED_STATE = false; + static final boolean DEFAULT_MUTABILITY = true; + + // Maximum recursive depth of processing merge tags. + private static final int MAXIMUM_MERGE_DEPTH = 5; + + // The subdirectory within a partition's overlay directory that contains the configuration files + // for the partition. + private static final String CONFIG_DIRECTORY = "config"; + + /** + * The name of the configuration file to parse for overlay configurations. This class does not + * scan for overlay configuration files within the {@link #CONFIG_DIRECTORY}; rather, other + * files can be included at a particular position within this file using the <merge> tag. + * + * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext) + */ + private static final String CONFIG_DEFAULT_FILENAME = CONFIG_DIRECTORY + "/config.xml"; + + /** Represents the configurations of a particular overlay. */ + public static class ParsedConfiguration { + @NonNull + public final String packageName; + + /** Whether or not the overlay is enabled by default. */ + public final boolean enabled; + + /** + * Whether or not the overlay is mutable and can have its enabled state changed dynamically + * using the {@code OverlayManagerService}. + **/ + public final boolean mutable; + + /** The policy granted to overlays on the partition in which the overlay is located. */ + @NonNull + public final String policy; + + /** Information extracted from the manifest of the overlay. */ + @NonNull + public final ParsedOverlayInfo parsedInfo; + + ParsedConfiguration(@NonNull String packageName, boolean enabled, boolean mutable, + @NonNull String policy, @NonNull ParsedOverlayInfo parsedInfo) { + this.packageName = packageName; + this.enabled = enabled; + this.mutable = mutable; + this.policy = policy; + this.parsedInfo = parsedInfo; + } + + @Override + public String toString() { + return getClass().getSimpleName() + String.format("{packageName=%s, enabled=%s" + + ", mutable=%s, policy=%s, parsedInfo=%s}", packageName, enabled, + mutable, policy, parsedInfo); + } + } + + static class OverlayPartition extends SystemPartition { + // Policies passed to idmap2 during idmap creation. + // Keep partition policy constants in sync with f/b/cmds/idmap2/include/idmap2/Policies.h. + static final String POLICY_ODM = "odm"; + static final String POLICY_OEM = "oem"; + static final String POLICY_PRODUCT = "product"; + static final String POLICY_PUBLIC = "public"; + static final String POLICY_SYSTEM = "system"; + static final String POLICY_VENDOR = "vendor"; + + @NonNull + public final String policy; + + OverlayPartition(@NonNull SystemPartition partition) { + super(partition); + this.policy = policyForPartition(partition); + } + + /** + * Creates a partition containing the same folders as the original partition but with a + * different root folder. + */ + OverlayPartition(@NonNull File folder, @NonNull SystemPartition original) { + super(folder, original); + this.policy = policyForPartition(original); + } + + private static String policyForPartition(SystemPartition partition) { + switch (partition.type) { + case PackagePartitions.PARTITION_SYSTEM: + case PackagePartitions.PARTITION_SYSTEM_EXT: + return POLICY_SYSTEM; + case PackagePartitions.PARTITION_VENDOR: + return POLICY_VENDOR; + case PackagePartitions.PARTITION_ODM: + return POLICY_ODM; + case PackagePartitions.PARTITION_OEM: + return POLICY_OEM; + case PackagePartitions.PARTITION_PRODUCT: + return POLICY_PRODUCT; + default: + throw new IllegalStateException("Unable to determine policy for " + + partition.folder); + } + } + } + + /** This class holds state related to parsing the configurations of a partition. */ + private static class ParsingContext { + // The overlay directory of the partition + private final OverlayPartition mPartition; + + // The ordered list of configured overlays + private final ArrayList<ParsedConfiguration> mOrderedConfigurations = new ArrayList<>(); + + // The packages configured in the partition + private final ArraySet<String> mConfiguredOverlays = new ArraySet<>(); + + // Whether an mutable overlay has been configured in the partition + private boolean mFoundMutableOverlay; + + // The current recursive depth of merging configuration files + private int mMergeDepth; + + private ParsingContext(OverlayPartition partition) { + mPartition = partition; + } + } + + /** + * Retrieves overlays configured within the partition in increasing priority order. + * + * If {@code scanner} is null, then the {@link ParsedConfiguration#parsedInfo} fields of the + * added configured overlays will be null and the parsing logic will not assert that the + * configured overlays exist within the partition. + * + * @return list of configured overlays if configuration file exists; otherwise, null + */ + @Nullable + static ArrayList<ParsedConfiguration> getConfigurations( + @NonNull OverlayPartition partition, @Nullable OverlayScanner scanner) { + if (partition.getOverlayFolder() == null) { + return null; + } + + if (scanner != null) { + scanner.scanDir(partition.getOverlayFolder()); + } + + final File configFile = new File(partition.getOverlayFolder(), CONFIG_DEFAULT_FILENAME); + if (!configFile.exists()) { + return null; + } + + final ParsingContext parsingContext = new ParsingContext(partition); + readConfigFile(configFile, scanner, parsingContext); + return parsingContext.mOrderedConfigurations; + } + + private static void readConfigFile(@NonNull File configFile, @Nullable OverlayScanner scanner, + @NonNull ParsingContext parsingContext) { + FileReader configReader; + try { + configReader = new FileReader(configFile); + } catch (FileNotFoundException e) { + Log.w(TAG, "Couldn't find or open overlay configuration file " + configFile); + return; + } + + try { + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(configReader); + XmlUtils.beginDocument(parser, "config"); + + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + final String name = parser.getName(); + switch (name) { + case "merge": + parseMerge(configFile, parser, scanner, parsingContext); + break; + case "overlay": + parseOverlay(configFile, parser, scanner, parsingContext); + break; + default: + Log.w(TAG, String.format("Tag %s is unknown in %s at %s", + name, configFile, parser.getPositionDescription())); + break; + } + } + } catch (XmlPullParserException | IOException e) { + Log.w(TAG, "Got exception parsing overlay configuration.", e); + } finally { + IoUtils.closeQuietly(configReader); + } + } + + /** + * Parses a <merge> tag within an overlay configuration file. + * + * Merge tags allow for other configuration files to be "merged" at the current parsing + * position into the current configuration file being parsed. The {@code path} attribute of the + * tag represents the path of the file to merge relative to the directory containing overlay + * configuration files. + */ + private static void parseMerge(@NonNull File configFile, @NonNull XmlPullParser parser, + @Nullable OverlayScanner scanner, @NonNull ParsingContext parsingContext) { + final String path = parser.getAttributeValue(null, "path"); + if (path == null) { + throw new IllegalStateException(String.format("<merge> without path in %s at %s" + + configFile, parser.getPositionDescription())); + } + + if (path.startsWith("/")) { + throw new IllegalStateException(String.format( + "Path %s must be relative to the directory containing overlay configurations " + + " files in %s at %s ", path, configFile, + parser.getPositionDescription())); + } + + if (parsingContext.mMergeDepth++ == MAXIMUM_MERGE_DEPTH) { + throw new IllegalStateException(String.format( + "Maximum <merge> depth exceeded in %s at %s", configFile, + parser.getPositionDescription())); + } + + final File configDirectory; + final File includedConfigFile; + try { + configDirectory = new File(parsingContext.mPartition.getOverlayFolder(), + CONFIG_DIRECTORY).getCanonicalFile(); + includedConfigFile = new File(configDirectory, path).getCanonicalFile(); + } catch (IOException e) { + throw new IllegalStateException( + String.format("Couldn't find or open merged configuration file %s in %s at %s", + path, configFile, parser.getPositionDescription()), e); + } + + if (!includedConfigFile.exists()) { + throw new IllegalStateException( + String.format("Merged configuration file %s does not exist in %s at %s", + path, configFile, parser.getPositionDescription())); + } + + if (!FileUtils.contains(configDirectory, includedConfigFile)) { + throw new IllegalStateException( + String.format( + "Merged file %s outside of configuration directory in %s at %s", + includedConfigFile.getAbsolutePath(), includedConfigFile, + parser.getPositionDescription())); + } + + readConfigFile(includedConfigFile, scanner, parsingContext); + parsingContext.mMergeDepth--; + } + + /** + * Parses an <overlay> tag within an overlay configuration file. + * + * Requires a {@code package} attribute that indicates which package is being configured. + * The optional {@code enabled} attribute controls whether or not the overlay is enabled by + * default (default is false). The optional {@code mutable} attribute controls whether or + * not the overlay is mutable and can have its enabled state changed at runtime (default is + * true). + * + * The order in which overlays that override the same resources are configured matters. An + * overlay will have a greater priority than overlays with configurations preceding its own + * configuration. + * + * Configurations of immutable overlays must precede configurations of mutable overlays. + * An overlay cannot be configured in multiple locations. All configured overlay must exist + * within the partition of the configuration file. An overlay cannot be configured multiple + * times in a single partition. + * + * Overlays not listed within a configuration file will be mutable and disabled by default. The + * order of non-configured overlays when enabled by the OverlayManagerService is undefined. + */ + private static void parseOverlay(@NonNull File configFile, @NonNull XmlPullParser parser, + @Nullable OverlayScanner scanner, @NonNull ParsingContext parsingContext) { + final String packageName = parser.getAttributeValue(null, "package"); + if (packageName == null) { + throw new IllegalStateException(String.format("\"<overlay> without package in %s at %s", + configFile, parser.getPositionDescription())); + } + + // Ensure the overlay being configured is present in the partition during zygote + // initialization. + ParsedOverlayInfo info = null; + if (scanner != null) { + info = scanner.getParsedInfo(packageName); + if (info == null|| !parsingContext.mPartition.containsOverlay(info.path)) { + throw new IllegalStateException( + String.format("overlay %s not present in partition %s in %s at %s", + packageName, parsingContext.mPartition.getOverlayFolder(), + configFile, parser.getPositionDescription())); + } + } + + if (parsingContext.mConfiguredOverlays.contains(packageName)) { + throw new IllegalStateException( + String.format("overlay %s configured multiple times in a single partition" + + " in %s at %s", packageName, configFile, + parser.getPositionDescription())); + } + + boolean isEnabled = DEFAULT_ENABLED_STATE; + final String enabled = parser.getAttributeValue(null, "enabled"); + if (enabled != null) { + isEnabled = !"false".equals(enabled); + } + + boolean isMutable = DEFAULT_MUTABILITY; + final String mutable = parser.getAttributeValue(null, "mutable"); + if (mutable != null) { + isMutable = !"false".equals(mutable); + if (!isMutable && parsingContext.mFoundMutableOverlay) { + throw new IllegalStateException(String.format( + "immutable overlays must precede mutable overlays:" + + " found in %s at %s", + configFile, parser.getPositionDescription())); + } + } + + if (isMutable) { + parsingContext.mFoundMutableOverlay = true; + } else if (!isEnabled) { + // Default disabled, immutable overlays may be a misconfiguration of the system so warn + // developers. + Log.w(TAG, "found default-disabled immutable overlay " + packageName); + } + + final ParsedConfiguration Config = new ParsedConfiguration(packageName, isEnabled, + isMutable, parsingContext.mPartition.policy, info); + parsingContext.mConfiguredOverlays.add(packageName); + parsingContext.mOrderedConfigurations.add(Config); + } +} diff --git a/core/java/com/android/internal/content/om/OverlayScanner.java b/core/java/com/android/internal/content/om/OverlayScanner.java new file mode 100644 index 000000000000..a85cf56068cd --- /dev/null +++ b/core/java/com/android/internal/content/om/OverlayScanner.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 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.internal.content.om; + +import static com.android.internal.content.om.OverlayConfig.TAG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PackageParser; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.util.Collection; + +/** + * This class scans a directory containing overlay APKs and extracts information from the overlay + * manifests by parsing the overlay manifests. + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class OverlayScanner { + + /** Represents information parsed from the manifest of an overlay. */ + public static class ParsedOverlayInfo { + public final String packageName; + public final String targetPackageName; + public final int targetSdkVersion; + public final boolean isStatic; + public final int priority; + public final File path; + + public ParsedOverlayInfo(String packageName, String targetPackageName, + int targetSdkVersion, boolean isStatic, int priority, File path) { + this.packageName = packageName; + this.targetPackageName = targetPackageName; + this.targetSdkVersion = targetSdkVersion; + this.isStatic = isStatic; + this.priority = priority; + this.path = path; + } + + @Override + public String toString() { + return getClass().getSimpleName() + String.format("{packageName=%s" + + ", targetPackageName=%s, targetSdkVersion=%s, isStatic=%s" + + ", priority=%s, path=%s}", + packageName, targetPackageName, targetSdkVersion, isStatic, priority, path); + } + } + + /** + * A map of overlay package name to the parsed manifest information of the latest version of + * the overlay. + */ + private final ArrayMap<String, ParsedOverlayInfo> mParsedOverlayInfos = new ArrayMap<>(); + + /** Retrieves information parsed from the overlay with the package name. */ + @Nullable + public final ParsedOverlayInfo getParsedInfo(String packageName) { + return mParsedOverlayInfos.get(packageName); + } + + /** Retrieves all of the scanned overlays. */ + @NonNull + final Collection<ParsedOverlayInfo> getAllParsedInfos() { + return mParsedOverlayInfos.values(); + } + + /** + * Recursively searches the directory for overlay APKs. If an overlay is found with the same + * package name as a previously scanned overlay, the info of the new overlay will replace the + * info of the previously scanned overlay. + */ + public void scanDir(File partitionOverlayDir) { + if (!partitionOverlayDir.exists() || !partitionOverlayDir.isDirectory()) { + return; + } + + if (!partitionOverlayDir.canRead()) { + Log.w(TAG, "Directory " + partitionOverlayDir + " cannot be read"); + return; + } + + final File[] files = partitionOverlayDir.listFiles(); + if (files == null) { + return; + } + + for (int i = 0; i < files.length; i++) { + final File f = files[i]; + if (f.isDirectory()) { + scanDir(f); + } + + if (!f.isFile() || !f.getPath().endsWith(".apk")) { + continue; + } + + final ParsedOverlayInfo info = parseOverlayManifest(f); + if (info == null) { + continue; + } + + mParsedOverlayInfos.put(info.packageName, info); + } + } + + /** Extracts information about the overlay from its manifest. */ + @VisibleForTesting + public ParsedOverlayInfo parseOverlayManifest(File overlayApk) { + try { + final PackageParser.ApkLite apkLite = PackageParser.parseApkLite(overlayApk, 0); + return apkLite.targetPackageName == null ? null : + new ParsedOverlayInfo(apkLite.packageName, apkLite.targetPackageName, + apkLite.targetSdkVersion, apkLite.overlayIsStatic, + apkLite.overlayPriority, new File(apkLite.codePath)); + } catch (PackageParser.PackageParserException e) { + Log.w(TAG, "Got exception loading overlay.", e); + return null; + } + } +} diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 8959d6fb845e..8704805f8925 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -197,6 +197,7 @@ cc_library_shared { "android_content_res_ObbScanner.cpp", "android_content_res_Configuration.cpp", "android_security_Scrypt.cpp", + "com_android_internal_content_om_OverlayConfig.cpp", "com_android_internal_os_ClassLoaderFactory.cpp", "com_android_internal_os_FuseAppLoop.cpp", "com_android_internal_os_Zygote.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b47b7e39d1e8..019b92f2f9c7 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -187,6 +187,7 @@ extern int register_android_content_res_Configuration(JNIEnv* env); extern int register_android_animation_PropertyValuesHolder(JNIEnv *env); extern int register_android_security_Scrypt(JNIEnv *env); extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env); +extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env); extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env); extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env); extern int register_com_android_internal_os_Zygote(JNIEnv *env); @@ -1511,6 +1512,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_os_MemoryFile), REG_JNI(register_android_os_SharedMemory), REG_JNI(register_android_os_incremental_IncrementalManager), + REG_JNI(register_com_android_internal_content_om_OverlayConfig), REG_JNI(register_com_android_internal_os_ClassLoaderFactory), REG_JNI(register_com_android_internal_os_Zygote), REG_JNI(register_com_android_internal_os_ZygoteInit), diff --git a/core/jni/com_android_internal_content_om_OverlayConfig.cpp b/core/jni/com_android_internal_content_om_OverlayConfig.cpp new file mode 100644 index 000000000000..6aa7c10509bc --- /dev/null +++ b/core/jni/com_android_internal_content_om_OverlayConfig.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 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. + */ + +#include <sstream> +#include <string> +#include <vector> + +#include <nativehelper/JNIHelp.h> +#include "jni.h" +#include "core_jni_helpers.h" + +#include "android-base/logging.h" +#include "androidfw/PosixUtils.h" + +using ::android::util::ExecuteBinary; + +static jclass g_stringClass = nullptr; + +static jobjectArray createIdmap(JNIEnv* env, jclass /*clazz*/, jstring targetPath, + jobjectArray overlayPath, jobjectArray policies, + jboolean enforceOverlayable) { + if (access("/system/bin/idmap2", X_OK) == -1) { + PLOG(WARNING) << "unable to execute idmap2"; + return nullptr; + } + + const char* targetApkPath = env->GetStringUTFChars(targetPath, NULL /* isCopy */); + std::vector<std::string> argv{"/system/bin/idmap2", + "create-multiple", + "--target-apk-path", targetApkPath, + }; + env->ReleaseStringUTFChars(targetPath, targetApkPath); + + // Add the overlays for which to generate idmap files to the idmap arguments. + for (size_t i = 0, count = env->GetArrayLength(overlayPath); i < count; ++i) { + jstring element = (jstring) env->GetObjectArrayElement(overlayPath, i); + const char* overlayApkPath = env->GetStringUTFChars(element, NULL /* isCopy */); + argv.emplace_back("--overlay-apk-path"); + argv.emplace_back(overlayApkPath); + env->ReleaseStringUTFChars(element, overlayApkPath); + } + + // Add the policies the overlays fulfill to the idmap arguments. + for (size_t i = 0, count = env->GetArrayLength(policies); i < count; ++i) { + jstring element = (jstring)env->GetObjectArrayElement(policies, i); + const char* policy = env->GetStringUTFChars(element, NULL /* isCopy */); + argv.emplace_back("--policy"); + argv.emplace_back(policy); + env->ReleaseStringUTFChars(element, policy); + } + + if (!enforceOverlayable) { + argv.emplace_back("--ignore-overlayable"); + } + + const auto result = ExecuteBinary(argv); + if (!result) { + LOG(ERROR) << "failed to execute idmap2"; + return nullptr; + } + + if (result->status != 0) { + LOG(ERROR) << "idmap2: " << result->stderr; + return nullptr; + } + + // Return the paths of the idmaps created or updated during the idmap invocation. + std::vector<std::string> idmap_paths; + std::istringstream input(result->stdout); + std::string path; + while (std::getline(input, path)) { + idmap_paths.push_back(path); + } + + jobjectArray array = env->NewObjectArray(idmap_paths.size(), g_stringClass, nullptr); + if (array == nullptr) { + return nullptr; + } + for (size_t i = 0; i < idmap_paths.size(); i++) { + const std::string path = idmap_paths[i]; + jstring java_string = env->NewStringUTF(path.c_str()); + if (env->ExceptionCheck()) { + return nullptr; + } + env->SetObjectArrayElement(array, i, java_string); + env->DeleteLocalRef(java_string); + } + + return array; +} + +static const JNINativeMethod g_methods[] = { + { "createIdmap", + "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Z)[Ljava/lang/String;", + (void *)createIdmap }, +}; + +static const char* const kOverlayConfigPathName = "com/android/internal/content/om/OverlayConfig"; + +namespace android { + +int register_com_android_internal_content_om_OverlayConfig(JNIEnv* env) { + jclass stringClass = FindClassOrDie(env, "java/lang/String"); + g_stringClass = MakeGlobalRefOrDie(env, stringClass); + + return RegisterMethodsOrDie(env, kOverlayConfigPathName, g_methods, NELEM(g_methods)); +} + +} // namespace android diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index e04d3de622d8..72e19c06bb90 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -113,6 +113,7 @@ java_genrule { ":FrameworksCoreTests_keyset_splata_api", ":FrameworksCoreTests_keyset_splat_api", ":FrameworksCoreTests_locales", + ":FrameworksCoreTests_overlay_config", ":FrameworksCoreTests_version_1", ":FrameworksCoreTests_version_1_diff", ":FrameworksCoreTests_version_1_nosys", diff --git a/core/tests/coretests/apks/overlay_config/Android.bp b/core/tests/coretests/apks/overlay_config/Android.bp new file mode 100644 index 000000000000..957355726fe8 --- /dev/null +++ b/core/tests/coretests/apks/overlay_config/Android.bp @@ -0,0 +1,4 @@ +android_test_helper_app { + name: "FrameworksCoreTests_overlay_config", + defaults: ["FrameworksCoreTests_apks_defaults"], +} diff --git a/core/tests/coretests/apks/overlay_config/AndroidManifest.xml b/core/tests/coretests/apks/overlay_config/AndroidManifest.xml new file mode 100644 index 000000000000..b15338eb545b --- /dev/null +++ b/core/tests/coretests/apks/overlay_config/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2019 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.coretests.overlay_config"> + + <application android:hasCode="false" /> + + <uses-sdk android:targetSdkVersion="21"/> + + <overlay android:targetPackage="android" + android:targetName="TestResources" /> +</manifest> diff --git a/core/tests/coretests/src/com/android/internal/content/OverlayConfigIterationRule.java b/core/tests/coretests/src/com/android/internal/content/OverlayConfigIterationRule.java new file mode 100644 index 000000000000..23655a08397d --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/content/OverlayConfigIterationRule.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 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.internal.content; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import android.content.pm.parsing.AndroidPackage; +import android.os.Build; +import android.util.ArrayMap; + +import com.android.internal.content.om.OverlayConfig.AndroidPackageProvider; +import com.android.internal.content.om.OverlayScanner; +import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo; + +import org.junit.Assert; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * A {@link TestRule} that runs a test case twice. First, the test case runs with a non-null + * {@link OverlayScanner} as if the zygote process is scanning the overlay packages + * and parsing configuration files. The test case then runs with a non-null + * {@link AndroidPackageProvider} as if the system server is parsing configuration files. + * + * This simulates what will happen on device. If an exception would be thrown in the zygote, then + * the exception should be thrown in the first run of the test case. + */ +public class OverlayConfigIterationRule implements TestRule { + + enum Iteration { + ZYGOTE, + SYSTEM_SERVER, + } + + private final ArrayMap<File, ParsedOverlayInfo> mOverlayStubResults = new ArrayMap<>(); + private Supplier<OverlayScanner> mOverlayScanner; + private AndroidPackageProvider mAndroidPackageProvider; + private Iteration mIteration; + + /** + * Mocks the parsing of the file to make it appear to the scanner that the file is a valid + * overlay APK. + **/ + void addOverlay(File path, String packageName, String targetPackage, int targetSdkVersion, + boolean isStatic, int priority) { + try { + final File canonicalPath = new File(path.getCanonicalPath()); + mOverlayStubResults.put(canonicalPath, new ParsedOverlayInfo( + packageName, targetPackage, targetSdkVersion, isStatic, priority, + canonicalPath)); + } catch (IOException e) { + Assert.fail("Failed to add overlay " + e); + } + } + + void addOverlay(File path, String packageName) { + addOverlay(path, packageName, "target"); + } + + void addOverlay(File path, String packageName, String targetPackage) { + addOverlay(path, packageName, targetPackage, Build.VERSION_CODES.CUR_DEVELOPMENT); + } + + void addOverlay(File path, String packageName, String targetPackage, int targetSdkVersion) { + addOverlay(path, packageName, targetPackage, targetSdkVersion, false, 0); + } + + /** Retrieves the {@link OverlayScanner} for the current run of the test. */ + Supplier<OverlayScanner> getScannerFactory() { + return mOverlayScanner; + } + + /** Retrieves the {@link AndroidPackageProvider} for the current run of the test. */ + AndroidPackageProvider getPackageProvider() { + return mAndroidPackageProvider; + } + + /** Retrieves the current iteration of the test. */ + Iteration getIteration() { + return mIteration; + } + + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + // Run the test once as if the zygote process is scanning the overlay packages + // and parsing configuration files. + mOverlayScanner = () -> { + OverlayScanner scanner = Mockito.spy(new OverlayScanner()); + for (Map.Entry<File, ParsedOverlayInfo> overlay : + mOverlayStubResults.entrySet()) { + doReturn(overlay.getValue()).when(scanner) + .parseOverlayManifest(overlay.getKey()); + } + return scanner; + }; + mAndroidPackageProvider = null; + mIteration = Iteration.ZYGOTE; + base.evaluate(); + + // Run the test once more (if the first test did not throw an exception) as if + // the system server is parsing the configuration files and using PackageManager to + // retrieving information of overlays. + mOverlayScanner = null; + mAndroidPackageProvider = Mockito.mock(AndroidPackageProvider.class); + mIteration = Iteration.SYSTEM_SERVER; + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final Consumer<AndroidPackage> f = (Consumer<AndroidPackage>) args[0]; + for (Map.Entry<File, ParsedOverlayInfo> overlay : + mOverlayStubResults.entrySet()) { + final AndroidPackage a = Mockito.mock(AndroidPackage.class); + final ParsedOverlayInfo info = overlay.getValue(); + when(a.getPackageName()).thenReturn(info.packageName); + when(a.getOverlayTarget()).thenReturn(info.targetPackageName); + when(a.getTargetSdkVersion()).thenReturn(info.targetSdkVersion); + when(a.isOverlayIsStatic()).thenReturn(info.isStatic); + when(a.getOverlayPriority()).thenReturn(info.priority); + when(a.getBaseCodePath()).thenReturn(info.path.getPath()); + when(a.isSystem()).thenReturn( + !info.path.getPath().contains("data/overlay")); + f.accept(a); + } + return null; + }).when(mAndroidPackageProvider).forEachPackage(any()); + + base.evaluate(); + } + }; + } +} + + diff --git a/core/tests/coretests/src/com/android/internal/content/OverlayConfigTest.java b/core/tests/coretests/src/com/android/internal/content/OverlayConfigTest.java new file mode 100644 index 000000000000..dee118fbe0fe --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/content/OverlayConfigTest.java @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2020 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.internal.content; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.os.FileUtils; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.coretests.R; +import com.android.internal.content.om.OverlayConfig; +import com.android.internal.content.om.OverlayConfig.IdmapInvocation; +import com.android.internal.content.om.OverlayScanner; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public class OverlayConfigTest { + private static final String TEST_APK_PACKAGE_NAME = + "com.android.frameworks.coretests.overlay_config"; + + private ExpectedException mExpectedException = ExpectedException.none(); + private OverlayConfigIterationRule mScannerRule = new OverlayConfigIterationRule(); + private TemporaryFolder mTestFolder = new TemporaryFolder(); + + @Rule + public RuleChain chain = RuleChain.outerRule(mExpectedException) + .around(mTestFolder).around(mScannerRule); + + private OverlayConfig createConfigImpl() throws IOException { + return new OverlayConfig(mTestFolder.getRoot().getCanonicalFile(), + mScannerRule.getScannerFactory(), mScannerRule.getPackageProvider()); + } + + private File createFile(String fileName) throws IOException { + return createFile(fileName, ""); + } + + private File createFile(String fileName, String content) throws IOException { + final File f = new File(String.format("%s/%s", mTestFolder.getRoot(), fileName)); + if (!f.getParentFile().equals(mTestFolder.getRoot())) { + f.getParentFile().mkdirs(); + } + FileUtils.stringToFile(f.getPath(), content); + return f; + } + + private static void assertConfig(OverlayConfig overlayConfig, String packageName, + boolean mutable, boolean enabled, int configIndex) { + final OverlayConfig.Configuration config = overlayConfig.getConfiguration(packageName); + assertNotNull(config); + assertEquals(mutable, config.parsedConfig.mutable); + assertEquals(enabled, config.parsedConfig.enabled); + assertEquals(configIndex, config.configIndex); + } + + @Test + public void testImmutableAfterNonImmutableFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("immutable overlays must precede mutable overlays"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + " <overlay package=\"two\" mutable=\"false\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two"); + createConfigImpl(); + } + + @Test + public void testConfigureAbsentPackageFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("not present in partition"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + "</config>"); + + createConfigImpl(); + } + + @Test + public void testConfigurePackageTwiceFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("configured multiple times in a single partition"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + " <overlay package=\"one\" mutable=\"false\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + createConfigImpl(); + } + + @Test + public void testConfigureOverlayAcrossPartitionsFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("not present in partition"); + + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + createConfigImpl(); + } + + @Test + public void testConfigureOverlayOutsideOverlayDirFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("not present in partition"); + + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/app/one.apk"), "one"); + createConfigImpl(); + } + + @Test + public void testMergeOAbsolutePathFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("must be relative to the directory"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <merge path=\"/product/overlay/config/auto-generated-config.xml\" />" + + "</config>"); + + createConfigImpl(); + } + + @Test + public void testMergeOutsideDirFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("outside of configuration directory"); + + createFile("/product/overlay/auto-generated-config.xml"); + createFile("/product/overlay/config/config.xml", + "<config>" + + " <merge path=\"../auto-generated-config.xml\" />" + + "</config>"); + + createConfigImpl(); + } + + @Test + public void testMergeOutsidePartitionFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("outside of configuration directory"); + + createFile("/vendor/overlay/config/config2.xml"); + createFile("/product/overlay/config/config.xml", + "<config>" + + " <merge path=\"../../../vendor/overlay/config/config2.xml\" />" + + "</config>"); + + createConfigImpl(); + } + + @Test + public void testMergeCircularFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("Maximum <merge> depth exceeded"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <merge path=\"config2.xml\" />" + + "</config>"); + createFile("/product/overlay/config/config2.xml", + "<config>" + + " <merge path=\"config.xml\" />" + + "</config>"); + + createConfigImpl(); + } + + @Test + public void testMergeMissingFileFails() throws IOException { + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("does not exist"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <merge path=\"config2.xml\" />" + + "</config>"); + createConfigImpl(); + } + + @Test + public void testProductOverridesVendor() throws IOException { + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"false\" />" + + "</config>"); + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", true, true, 1); + } + + @Test + public void testPartitionPrecedence() throws IOException { + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" enabled=\"true\" />" + + "</config>"); + createFile("/odm/overlay/config/config.xml", + "<config>" + + " <overlay package=\"two\" enabled=\"true\" />" + + "</config>"); + createFile("/oem/overlay/config/config.xml", + "<config>" + + " <overlay package=\"three\" enabled=\"true\" />" + + "</config>"); + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"four\" enabled=\"true\" />" + + "</config>"); + createFile("/system_ext/overlay/config/config.xml", + "<config>" + + " <overlay package=\"five\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/odm/overlay/two.apk"), "two"); + mScannerRule.addOverlay(createFile("/oem/overlay/three.apk"), "three"); + mScannerRule.addOverlay(createFile("/product/overlay/four.apk"), "four"); + mScannerRule.addOverlay(createFile("/system_ext/overlay/five.apk"), "five"); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", true, true, 0); + assertConfig(overlayConfig, "two", true, true, 1); + assertConfig(overlayConfig, "three", true, true, 2); + assertConfig(overlayConfig, "four", true, true, 3); + assertConfig(overlayConfig, "five", true, true, 4); + } + + @Test + public void testImmutable() throws IOException { + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" mutable=\"false\" />" + + " <overlay package=\"two\" />" + + " <overlay package=\"three\" mutable=\"true\" />" + + "</config>"); + + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two"); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three"); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, false, 0); + assertConfig(overlayConfig, "two", true, false, 1); + assertConfig(overlayConfig, "three", true, false, 2); + } + + @Test + public void testEnabled() throws IOException { + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" />" + + " <overlay package=\"two\" enabled=\"true\" />" + + " <overlay package=\"three\" enabled=\"false\" />" + + "</config>"); + + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two"); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three"); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", true, false, 0); + assertConfig(overlayConfig, "two", true, true, 1); + assertConfig(overlayConfig, "three", true, false, 2); + } + + @Test + public void testMerge() throws IOException { + createFile("/product/overlay/config/auto-generated-config.xml", + "<config>" + + " <overlay package=\"two\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"three\" mutable=\"false\" enabled=\"true\" />" + + "</config>"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" mutable=\"false\" enabled=\"true\" />" + + " <merge path=\"auto-generated-config.xml\" />" + + " <overlay package=\"four\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one"); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two"); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three"); + mScannerRule.addOverlay(createFile("/product/overlay/four.apk"), "four"); + + final OverlayConfig overlayConfig = createConfigImpl(); + OverlayConfig.Configuration o1 = overlayConfig.getConfiguration("one"); + assertNotNull(o1); + assertFalse(o1.parsedConfig.mutable); + assertTrue(o1.parsedConfig.enabled); + assertEquals(0, o1.configIndex); + + OverlayConfig.Configuration o2 = overlayConfig.getConfiguration("two"); + assertNotNull(o2); + assertFalse(o2.parsedConfig.mutable); + assertTrue(o2.parsedConfig.enabled); + assertEquals(1, o2.configIndex); + + OverlayConfig.Configuration o3 = overlayConfig.getConfiguration("three"); + assertNotNull(o3); + assertFalse(o3.parsedConfig.mutable); + assertTrue(o3.parsedConfig.enabled); + assertEquals(2, o3.configIndex); + + OverlayConfig.Configuration o4 = overlayConfig.getConfiguration("four"); + assertNotNull(o4); + assertTrue(o4.parsedConfig.mutable); + assertTrue(o4.parsedConfig.enabled); + assertEquals(3, o4.configIndex); + } + + @Test + public void testIdmapInvocationsFrameworkImmutable() throws IOException { + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"two\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"three\" enabled=\"true\" />" + + "</config>"); + + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"four\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"five\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"six\" mutable=\"false\" enabled=\"false\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android"); + mScannerRule.addOverlay(createFile("/vendor/overlay/two.apk"), "two", "android"); + mScannerRule.addOverlay(createFile("/vendor/overlay/three.apk"), "three", "android"); + mScannerRule.addOverlay(createFile("/product/overlay/four.apk"), "four", "android"); + mScannerRule.addOverlay(createFile("/product/overlay/five.apk"), "five"); + mScannerRule.addOverlay(createFile("/product/overlay/six.apk"), "six", "android"); + + final OverlayConfig overlayConfig = createConfigImpl(); + if (mScannerRule.getIteration() == OverlayConfigIterationRule.Iteration.ZYGOTE) { + final ArrayList<IdmapInvocation> idmapInvocations = + overlayConfig.getImmutableFrameworkOverlayIdmapInvocations(); + assertEquals(2, idmapInvocations.size()); + + final IdmapInvocation i0 = idmapInvocations.get(0); + assertTrue(i0.enforceOverlayable); + assertEquals("vendor", i0.policy); + assertEquals(2, i0.overlayPaths.size()); + assertTrue(i0.overlayPaths.get(0).endsWith("/vendor/overlay/one.apk")); + assertTrue(i0.overlayPaths.get(1).endsWith("/vendor/overlay/two.apk")); + + final IdmapInvocation i1 = idmapInvocations.get(1); + assertTrue(i1.enforceOverlayable); + assertEquals("product", i1.policy); + assertEquals(1, i1.overlayPaths.size()); + assertTrue(i1.overlayPaths.get(0).endsWith("/product/overlay/four.apk")); + } + } + + @Test + public void testIdmapInvocationsDifferentTargetSdk() throws IOException { + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"two\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"three\" mutable=\"false\" enabled=\"true\" />" + + " <overlay package=\"four\" mutable=\"false\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one", "android"); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android"); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three", "android", 28); + mScannerRule.addOverlay(createFile("/product/overlay/four.apk"), "four", "android"); + + final OverlayConfig overlayConfig = createConfigImpl(); + + if (mScannerRule.getIteration() == OverlayConfigIterationRule.Iteration.ZYGOTE) { + final ArrayList<IdmapInvocation> idmapInvocations = + overlayConfig.getImmutableFrameworkOverlayIdmapInvocations(); + assertEquals(3, idmapInvocations.size()); + + final IdmapInvocation i0 = idmapInvocations.get(0); + assertTrue(i0.enforceOverlayable); + assertEquals(2, i0.overlayPaths.size()); + assertTrue(i0.overlayPaths.get(0).endsWith("/product/overlay/one.apk")); + assertTrue(i0.overlayPaths.get(1).endsWith("/product/overlay/two.apk")); + + final IdmapInvocation i1 = idmapInvocations.get(1); + assertFalse(i1.enforceOverlayable); + assertEquals(1, i1.overlayPaths.size()); + assertTrue(i1.overlayPaths.get(0).endsWith("/product/overlay/three.apk")); + + final IdmapInvocation i2 = idmapInvocations.get(2); + assertTrue(i2.enforceOverlayable); + assertEquals(1, i2.overlayPaths.size()); + assertTrue(i2.overlayPaths.get(0).endsWith("/product/overlay/four.apk")); + } + } + + @Test + public void testNoConfigIsStatic() throws IOException { + mScannerRule.addOverlay(createFile("/product/overlay/one.apk"), "one", "android", 28, true, + 1); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 28, false, + 0); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three", "android", 28, + true, 0); + mScannerRule.addOverlay(createFile("/product/overlay/four.apk"), "four", "android", 28, + false, 2); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 1); + assertConfig(overlayConfig, "three", false, true, 0); + + } + + @Test + public void testVendorStaticPrecedesProductImmutable() throws IOException { + createFile("/product/overlay/config/config.xml", + "<config>" + + " <overlay package=\"two\" mutable=\"false\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android", 0, true, + 1); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 0); + assertConfig(overlayConfig, "two", false, true, 1); + } + + @Test + public void testVendorImmutablePrecededProductStatic() throws IOException { + createFile("/vendor/overlay/config/config.xml", + "<config>" + + " <overlay package=\"one\" mutable=\"false\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android", 0, true, + 1); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 0); + assertConfig(overlayConfig, "two", false, true, 1); + } + + @Test + public void testNoConfigsAllowPartitionReordering() throws IOException { + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android", 0, true, + 1); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 1); + assertConfig(overlayConfig, "two", false, true, 0); + } + + @Test + public void testConfigDisablesPartitionReordering() throws IOException { + createFile("/odm/overlay/config/config.xml", + "<config>" + + " <overlay package=\"two\" enabled=\"true\" />" + + "</config>"); + + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android", 0, true, + 1); + mScannerRule.addOverlay(createFile("/odm/overlay/two.apk"), "two"); + mScannerRule.addOverlay(createFile("/product/overlay/three.apk"), "three", "android", 0, + true, 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 0); + assertConfig(overlayConfig, "two", true, true, 1); + assertConfig(overlayConfig, "three", false, true, 2); + } + + @Test + public void testStaticOverlayOutsideOverlayDir() throws IOException { + mScannerRule.addOverlay(createFile("/product/app/one.apk"), "one", "android", 0, true, 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + if (mScannerRule.getIteration() == OverlayConfigIterationRule.Iteration.SYSTEM_SERVER) { + assertConfig(overlayConfig, "one", false, true, 0); + } + } + + @Test + public void testSortStaticOverlaysDifferentTargets() throws IOException { + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "other", 0, true, 0); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 1); + assertConfig(overlayConfig, "two", false, true, 0); + } + + @Test + public void testSortStaticOverlaysSamePriority() throws IOException { + mScannerRule.addOverlay(createFile("/vendor/overlay/one.apk"), "one", "android", 0, true, + 0); + mScannerRule.addOverlay(createFile("/product/overlay/two.apk"), "two", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertConfig(overlayConfig, "one", false, true, 1); + assertConfig(overlayConfig, "two", false, true, 0); + } + + @Test + public void testNonSystemOverlayCannotBeStatic() throws IOException { + mScannerRule.addOverlay(createFile("/data/overlay/one.apk"), "one", "android", 0, true, + 0); + + final OverlayConfig overlayConfig = createConfigImpl(); + assertTrue(overlayConfig.isMutable("one")); + assertFalse(overlayConfig.isEnabled("one")); + assertEquals(Integer.MAX_VALUE, overlayConfig.getPriority("one")); + } + + @Test + public void testGetOverlayInfo() throws IOException { + if (mScannerRule.getIteration() != OverlayConfigIterationRule.Iteration.ZYGOTE) { + // Run only one iteration of the test. + return; + } + + final InputStream is = InstrumentationRegistry.getContext().getResources() + .openRawResource(R.raw.overlay_config); + final File partitionDir = mTestFolder.newFolder("product", "overlay"); + final File testApk = new File(partitionDir, "test.apk"); + FileUtils.copy(is, new FileOutputStream(testApk)); + + final OverlayScanner scanner = new OverlayScanner(); + scanner.scanDir(partitionDir); + + final OverlayScanner.ParsedOverlayInfo info = scanner.getParsedInfo(TEST_APK_PACKAGE_NAME); + assertNotNull(info); + assertEquals(TEST_APK_PACKAGE_NAME, info.packageName); + assertEquals("android", info.targetPackageName); + assertEquals(testApk.getPath(), info.path.getPath()); + assertEquals(21, info.targetSdkVersion); + } +} diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 85c7f4b41b61..6a922c4abc3d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -307,6 +307,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ResolverActivity; import com.android.internal.content.NativeLibraryHelper; import com.android.internal.content.PackageHelper; +import com.android.internal.content.om.OverlayConfig; import com.android.internal.logging.MetricsLogger; import com.android.internal.os.SomeArgs; import com.android.internal.os.Zygote; @@ -799,6 +800,8 @@ public class PackageManagerService extends IPackageManager.Stub private final List<ScanPartition> mDirsToScanAsSystem; + private final OverlayConfig mOverlayConfig; + /** * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors. * @@ -2896,6 +2899,9 @@ public class PackageManagerService extends IPackageManager.Stub packageParser, executorService); } + // Parse overlay configuration files to set default enable state, mutability, and + // priority of system overlays. + mOverlayConfig = OverlayConfig.initializeSystemInstance(mPmInternal::forEachPackage); // Prune any system packages that no longer exist. final List<String> possiblyDeletedUpdatedSystemApps = new ArrayList<>(); @@ -11531,50 +11537,17 @@ public class PackageManagerService extends IPackageManager.Stub // We are scanning a system overlay. This can be the first scan of the // system/vendor/oem partition, or an update to the system overlay. if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) { - // This must be an update to a system overlay. - final PackageSetting previousPkg = assertNotNull( - mSettings.getPackageLPr(pkg.getPackageName()), - "previous package state not present"); - - // previousPkg.pkg may be null: the package will be not be scanned if the - // package manager knows there is a newer version on /data. - // TODO[b/79435695]: Find a better way to keep track of the "static" - // property for RROs instead of having to parse packages on /system - AndroidPackage ppkg = previousPkg.pkg; - if (ppkg == null) { - try { - final PackageParser pp = new PackageParser(); - // TODO(b/135203078): Do we really need to parse here? Maybe use - // a shortened path? - ppkg = pp.parseParsedPackage(previousPkg.codePath, - parseFlags | PackageParser.PARSE_IS_SYSTEM_DIR, - false) - .hideAsFinal(); - } catch (PackageParserException e) { - Slog.w(TAG, "failed to parse " + previousPkg.codePath, e); - } - } - - // Static overlays cannot be updated. - if (ppkg != null && ppkg.isOverlayIsStatic()) { + // This must be an update to a system overlay. Immutable overlays cannot be + // upgraded. + Objects.requireNonNull(mOverlayConfig, + "Parsing non-system dir before overlay configs are initialized"); + if (!mOverlayConfig.isMutable(pkg.getPackageName())) { throw new PackageManagerException("Overlay " + pkg.getPackageName() + " is static and cannot be upgraded."); - // Non-static overlays cannot be converted to static overlays. - } else if (pkg.isOverlayIsStatic()) { - throw new PackageManagerException("Overlay " - + pkg.getPackageName() - + " cannot be upgraded into a static overlay."); } } } else { - // The overlay is a non-system overlay. Non-system overlays cannot be static. - if (pkg.isOverlayIsStatic()) { - throw new PackageManagerException("Overlay " - + pkg.getPackageName() - + " is static but not pre-installed."); - } - // A non-preloaded overlay packages must have targetSdkVersion >= Q, or be // signed with the platform certificate. Check this in increasing order of // computational cost. |