diff options
author | 2020-02-04 10:18:53 -0800 | |
---|---|---|
committer | 2020-02-18 16:13:22 -0800 | |
commit | 9b93942a801cd042d0486d13f4a25fdf644990e0 (patch) | |
tree | fe57d05bedc029fd9d4291ebd8c2f1c2047dc87a | |
parent | 76e669069c68e73d6f5404f415cc5fb0cc0e02af (diff) |
Add xml configuration of RROs
This change adds the ability to configure the priority, default enable
state, and mutability (previously know as staticness) of an overlay.
Rather than overlays configuring themselves, the system can configure
overlays relative to each other.
An example configuration file looks like:
<config>
<merge path="auto-generated.xml" />
<overlay package="com.example.one" mutable="false"
enabled="true"/>
<overlay package="com.example.two" mutable="false"
enabled="true"/>
<overlay package="com.example.three" enabled="true"/>
</config>
The <overlay> tag configures the overlay while the <merge> tag allows
additional configuration files to be included at a position within
the configuration file.
If the configuration file is not present for a partition, the legacy
android:isStatic and android:priority will continue to configure the
overlays in the partition. Once at least one configuration file has
been defined in any partition, strict partition precedence will be
enforced and overlays on separate partitions will no longer be able
to use android:priority to reorder themselves conversely from the
overlay partition precedence.
The order of the system partitions from least to greatest precedence
is system, vendor, odm, oem, product, system_ext.
Bug: 135048762
Test: atest OverlayConfigTest
Change-Id: If57e8caa9b881f9d424ef48bba80b18cc8b7b943
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. |