1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.content.om;
18 
19 import static com.android.internal.content.om.OverlayConfig.TAG;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.pm.PackagePartitions;
24 import android.content.pm.PackagePartitions.SystemPartition;
25 import android.os.Build;
26 import android.os.FileUtils;
27 import android.util.ArraySet;
28 import android.util.Log;
29 import android.util.Xml;
30 
31 import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo;
32 import com.android.internal.util.Preconditions;
33 import com.android.internal.util.XmlUtils;
34 
35 import libcore.io.IoUtils;
36 
37 import org.xmlpull.v1.XmlPullParser;
38 import org.xmlpull.v1.XmlPullParserException;
39 
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.io.FileReader;
43 import java.io.IOException;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Map;
47 
48 /**
49  * Responsible for parsing configurations of Runtime Resource Overlays that control mutability,
50  * default enable state, and priority. To configure an overlay, create or modify the file located
51  * at {@code partition}/overlay/config/config.xml where {@code partition} is the partition of the
52  * overlay to be configured. In order to be configured, an overlay must reside in the overlay
53  * directory of the partition in which the overlay is configured.
54  *
55  * @see #parseOverlay(File, XmlPullParser, OverlayScanner, ParsingContext)
56  * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext)
57  **/
58 final class OverlayConfigParser {
59 
60     /** Represents a part of a parsed overlay configuration XML file. */
61     public static class ParsedConfigFile {
62         @NonNull public final String path;
63         @NonNull public final int line;
64         @Nullable public final String xml;
65 
ParsedConfigFile(@onNull String path, int line, @Nullable String xml)66         ParsedConfigFile(@NonNull String path, int line, @Nullable String xml) {
67             this.path = path;
68             this.line = line;
69             this.xml = xml;
70         }
71 
72         @Override
toString()73         public String toString() {
74             StringBuilder sb = new StringBuilder(getClass().getSimpleName());
75             sb.append("{path=");
76             sb.append(path);
77             sb.append(", line=");
78             sb.append(line);
79             if (xml != null) {
80                 sb.append(", xml=");
81                 sb.append(xml);
82             }
83             sb.append("}");
84             return sb.toString();
85         }
86     }
87 
88     // Default values for overlay configurations.
89     static final boolean DEFAULT_ENABLED_STATE = false;
90     static final boolean DEFAULT_MUTABILITY = true;
91 
92     // Maximum recursive depth of processing merge tags.
93     private static final int MAXIMUM_MERGE_DEPTH = 5;
94 
95     // The subdirectory within a partition's overlay directory that contains the configuration files
96     // for the partition.
97     private static final String CONFIG_DIRECTORY = "config";
98 
99     /**
100      * The name of the configuration file to parse for overlay configurations. This class does not
101      * scan for overlay configuration files within the {@link #CONFIG_DIRECTORY}; rather, other
102      * files can be included at a particular position within this file using the <merge> tag.
103      *
104      * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext)
105      */
106     private static final String CONFIG_DEFAULT_FILENAME = CONFIG_DIRECTORY + "/config.xml";
107 
108     /** Represents the configurations of a particular overlay. */
109     public static class ParsedConfiguration {
110         @NonNull
111         public final String packageName;
112 
113         /** Whether or not the overlay is enabled by default. */
114         public final boolean enabled;
115 
116         /**
117          * Whether or not the overlay is mutable and can have its enabled state changed dynamically
118          * using the {@code OverlayManagerService}.
119          **/
120         public final boolean mutable;
121 
122         /** The policy granted to overlays on the partition in which the overlay is located. */
123         @NonNull
124         public final String policy;
125 
126         /**
127          * Information extracted from the manifest of the overlay.
128          * Null if the information was read from a config file instead of a manifest.
129          *
130          * @see parsedConfigFile
131          **/
132         @Nullable
133         public final ParsedOverlayInfo parsedInfo;
134 
135         /**
136          * The config file used to configure this overlay.
137          * Null if no config file was used, in which case the overlay's manifest was used instead.
138          *
139          * @see parsedInfo
140          **/
141         @Nullable
142         public final ParsedConfigFile parsedConfigFile;
143 
ParsedConfiguration(@onNull String packageName, boolean enabled, boolean mutable, @NonNull String policy, @Nullable ParsedOverlayInfo parsedInfo, @Nullable ParsedConfigFile parsedConfigFile)144         ParsedConfiguration(@NonNull String packageName, boolean enabled, boolean mutable,
145                 @NonNull String policy, @Nullable ParsedOverlayInfo parsedInfo,
146                 @Nullable ParsedConfigFile parsedConfigFile) {
147             this.packageName = packageName;
148             this.enabled = enabled;
149             this.mutable = mutable;
150             this.policy = policy;
151             this.parsedInfo = parsedInfo;
152             this.parsedConfigFile = parsedConfigFile;
153         }
154 
155         @Override
toString()156         public String toString() {
157             return getClass().getSimpleName() + String.format("{packageName=%s, enabled=%s"
158                             + ", mutable=%s, policy=%s, parsedInfo=%s, parsedConfigFile=%s}",
159                     packageName, enabled, mutable, policy, parsedInfo, parsedConfigFile);
160         }
161     }
162 
163     static class OverlayPartition extends SystemPartition {
164         // Policies passed to idmap2 during idmap creation.
165         // Keep partition policy constants in sync with f/b/cmds/idmap2/include/idmap2/Policies.h.
166         static final String POLICY_ODM = "odm";
167         static final String POLICY_OEM = "oem";
168         static final String POLICY_PRODUCT = "product";
169         static final String POLICY_PUBLIC = "public";
170         static final String POLICY_SYSTEM = "system";
171         static final String POLICY_VENDOR = "vendor";
172 
173         @NonNull
174         public final String policy;
175 
OverlayPartition(@onNull SystemPartition partition)176         OverlayPartition(@NonNull SystemPartition partition) {
177             super(partition);
178             this.policy = policyForPartition(partition);
179         }
180 
181         /**
182          * Creates a partition containing the same folders as the original partition but with a
183          * different root folder.
184          */
OverlayPartition(@onNull File folder, @NonNull SystemPartition original)185         OverlayPartition(@NonNull File folder, @NonNull SystemPartition original) {
186             super(folder, original);
187             this.policy = policyForPartition(original);
188         }
189 
policyForPartition(SystemPartition partition)190         private static String policyForPartition(SystemPartition partition) {
191             switch (partition.type) {
192                 case PackagePartitions.PARTITION_SYSTEM:
193                 case PackagePartitions.PARTITION_SYSTEM_EXT:
194                     return POLICY_SYSTEM;
195                 case PackagePartitions.PARTITION_VENDOR:
196                     return POLICY_VENDOR;
197                 case PackagePartitions.PARTITION_ODM:
198                     return POLICY_ODM;
199                 case PackagePartitions.PARTITION_OEM:
200                     return POLICY_OEM;
201                 case PackagePartitions.PARTITION_PRODUCT:
202                     return POLICY_PRODUCT;
203                 default:
204                     throw new IllegalStateException("Unable to determine policy for "
205                             + partition.getFolder());
206             }
207         }
208     }
209 
210     /** This class holds state related to parsing the configurations of a partition. */
211     private static class ParsingContext {
212         // The overlay directory of the partition
213         private final OverlayPartition mPartition;
214 
215         // The ordered list of configured overlays
216         private final ArrayList<ParsedConfiguration> mOrderedConfigurations = new ArrayList<>();
217 
218         // The packages configured in the partition
219         private final ArraySet<String> mConfiguredOverlays = new ArraySet<>();
220 
221         // Whether an mutable overlay has been configured in the partition
222         private boolean mFoundMutableOverlay;
223 
224         // The current recursive depth of merging configuration files
225         private int mMergeDepth;
226 
ParsingContext(OverlayPartition partition)227         private ParsingContext(OverlayPartition partition) {
228             mPartition = partition;
229         }
230     }
231 
232     /**
233      * Retrieves overlays configured within the partition in increasing priority order.
234      *
235      * If {@code scanner} is null, then the {@link ParsedConfiguration#parsedInfo} fields of the
236      * added configured overlays will be null and the parsing logic will not assert that the
237      * configured overlays exist within the partition.
238      *
239      * @return list of configured overlays if configuration file exists; otherwise, null
240      */
241     @Nullable
getConfigurations( @onNull OverlayPartition partition, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull List<String> activeApexes)242     static ArrayList<ParsedConfiguration> getConfigurations(
243             @NonNull OverlayPartition partition, @Nullable OverlayScanner scanner,
244             @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
245             @NonNull List<String> activeApexes) {
246         if (scanner != null) {
247             if (partition.getOverlayFolder() != null) {
248                 scanner.scanDir(partition.getOverlayFolder());
249             }
250             for (String apex : activeApexes) {
251                 scanner.scanDir(new File("/apex/" + apex + "/overlay/"));
252             }
253         }
254 
255         if (partition.getOverlayFolder() == null) {
256             return null;
257         }
258 
259         final File configFile = new File(partition.getOverlayFolder(), CONFIG_DEFAULT_FILENAME);
260         if (!configFile.exists()) {
261             return null;
262         }
263 
264         final ParsingContext parsingContext = new ParsingContext(partition);
265         readConfigFile(configFile, scanner, packageManagerOverlayInfos, parsingContext);
266         return parsingContext.mOrderedConfigurations;
267     }
268 
readConfigFile(@onNull File configFile, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)269     private static void readConfigFile(@NonNull File configFile, @Nullable OverlayScanner scanner,
270             @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
271             @NonNull ParsingContext parsingContext) {
272         FileReader configReader;
273         try {
274             configReader = new FileReader(configFile);
275         } catch (FileNotFoundException e) {
276             Log.w(TAG, "Couldn't find or open overlay configuration file " + configFile);
277             return;
278         }
279 
280         try {
281             final XmlPullParser parser = Xml.newPullParser();
282             parser.setInput(configReader);
283             XmlUtils.beginDocument(parser, "config");
284 
285             int depth = parser.getDepth();
286             while (XmlUtils.nextElementWithin(parser, depth)) {
287                 final String name = parser.getName();
288                 switch (name) {
289                     case "merge":
290                         parseMerge(configFile, parser, scanner, packageManagerOverlayInfos,
291                                 parsingContext);
292                         break;
293                     case "overlay":
294                         parseOverlay(configFile, parser, scanner, packageManagerOverlayInfos,
295                                 parsingContext);
296                         break;
297                     default:
298                         Log.w(TAG, String.format("Tag %s is unknown in %s at %s",
299                                 name, configFile, parser.getPositionDescription()));
300                         break;
301                 }
302             }
303         } catch (XmlPullParserException | IOException e) {
304             Log.w(TAG, "Got exception parsing overlay configuration.", e);
305         } finally {
306             IoUtils.closeQuietly(configReader);
307         }
308     }
309 
310     /**
311      * Parses a <merge> tag within an overlay configuration file.
312      *
313      * Merge tags allow for other configuration files to be "merged" at the current parsing
314      * position into the current configuration file being parsed. The {@code path} attribute of the
315      * tag represents the path of the file to merge relative to the directory containing overlay
316      * configuration files.
317      */
parseMerge(@onNull File configFile, @NonNull XmlPullParser parser, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)318     private static void parseMerge(@NonNull File configFile, @NonNull XmlPullParser parser,
319             @Nullable OverlayScanner scanner,
320             @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
321             @NonNull ParsingContext parsingContext) {
322         final String path = parser.getAttributeValue(null, "path");
323         if (path == null) {
324             throw new IllegalStateException(String.format("<merge> without path in %s at %s"
325                     + configFile, parser.getPositionDescription()));
326         }
327 
328         if (path.startsWith("/")) {
329             throw new IllegalStateException(String.format(
330                     "Path %s must be relative to the directory containing overlay configurations "
331                             + " files in %s at %s ", path, configFile,
332                     parser.getPositionDescription()));
333         }
334 
335         if (parsingContext.mMergeDepth++ == MAXIMUM_MERGE_DEPTH) {
336             throw new IllegalStateException(String.format(
337                     "Maximum <merge> depth exceeded in %s at %s", configFile,
338                     parser.getPositionDescription()));
339         }
340 
341         final File configDirectory;
342         final File includedConfigFile;
343         try {
344             configDirectory = new File(parsingContext.mPartition.getOverlayFolder(),
345                     CONFIG_DIRECTORY).getCanonicalFile();
346             includedConfigFile = new File(configDirectory, path).getCanonicalFile();
347         } catch (IOException e) {
348             throw new IllegalStateException(
349                     String.format("Couldn't find or open merged configuration file %s in %s at %s",
350                             path, configFile, parser.getPositionDescription()), e);
351         }
352 
353         if (!includedConfigFile.exists()) {
354             throw new IllegalStateException(
355                     String.format("Merged configuration file %s does not exist in %s at %s",
356                             path, configFile, parser.getPositionDescription()));
357         }
358 
359         if (!FileUtils.contains(configDirectory, includedConfigFile)) {
360             throw new IllegalStateException(
361                     String.format(
362                             "Merged file %s outside of configuration directory in %s at %s",
363                             includedConfigFile.getAbsolutePath(), includedConfigFile,
364                             parser.getPositionDescription()));
365         }
366 
367         readConfigFile(includedConfigFile, scanner, packageManagerOverlayInfos, parsingContext);
368         parsingContext.mMergeDepth--;
369     }
370 
371     /**
372      * Parses an <overlay> tag within an overlay configuration file.
373      *
374      * Requires a {@code package} attribute that indicates which package is being configured.
375      * The optional {@code enabled} attribute controls whether or not the overlay is enabled by
376      * default (default is false). The optional {@code mutable} attribute controls whether or
377      * not the overlay is mutable and can have its enabled state changed at runtime (default is
378      * true).
379      *
380      * The order in which overlays that override the same resources are configured matters. An
381      * overlay will have a greater priority than overlays with configurations preceding its own
382      * configuration.
383      *
384      * Configurations of immutable overlays must precede configurations of mutable overlays.
385      * An overlay cannot be configured in multiple locations. All configured overlay must exist
386      * within the partition of the configuration file. An overlay cannot be configured multiple
387      * times in a single partition.
388      *
389      * Overlays not listed within a configuration file will be mutable and disabled by default. The
390      * order of non-configured overlays when enabled by the OverlayManagerService is undefined.
391      */
parseOverlay(@onNull File configFile, @NonNull XmlPullParser parser, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)392     private static void parseOverlay(@NonNull File configFile, @NonNull XmlPullParser parser,
393             @Nullable OverlayScanner scanner,
394             @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos,
395             @NonNull ParsingContext parsingContext) {
396         Preconditions.checkArgument((scanner == null) != (packageManagerOverlayInfos == null),
397                 "scanner and packageManagerOverlayInfos cannot be both null or both non-null");
398 
399         final String packageName = parser.getAttributeValue(null, "package");
400         if (packageName == null) {
401             throw new IllegalStateException(String.format("\"<overlay> without package in %s at %s",
402                     configFile, parser.getPositionDescription()));
403         }
404 
405         // Ensure the overlay being configured is present in the partition during zygote
406         // initialization, unless the package is an excluded overlay package.
407         ParsedOverlayInfo info = null;
408         if (scanner != null) {
409             info = scanner.getParsedInfo(packageName);
410             if (info == null
411                     && scanner.isExcludedOverlayPackage(packageName, parsingContext.mPartition)) {
412                 Log.d(TAG, "overlay " + packageName + " in partition "
413                         + parsingContext.mPartition.getOverlayFolder() + " is ignored.");
414                 return;
415             } else if (info == null || !parsingContext.mPartition.containsOverlay(info.path)) {
416                 throw new IllegalStateException(
417                         String.format("overlay %s not present in partition %s in %s at %s",
418                                 packageName, parsingContext.mPartition.getOverlayFolder(),
419                                 configFile, parser.getPositionDescription()));
420             }
421         } else {
422             // Zygote shall have crashed itself, if there's an overlay apk not present in the
423             // partition. For the overlay package not found in the package manager, we can assume
424             // that it's an excluded overlay package.
425             if (packageManagerOverlayInfos.get(packageName) == null) {
426                 Log.d(TAG, "overlay " + packageName + " in partition "
427                         + parsingContext.mPartition.getOverlayFolder() + " is ignored.");
428                 return;
429             }
430         }
431 
432         if (parsingContext.mConfiguredOverlays.contains(packageName)) {
433             throw new IllegalStateException(
434                     String.format("overlay %s configured multiple times in a single partition"
435                                     + " in %s at %s", packageName, configFile,
436                             parser.getPositionDescription()));
437         }
438 
439         boolean isEnabled = DEFAULT_ENABLED_STATE;
440         final String enabled = parser.getAttributeValue(null, "enabled");
441         if (enabled != null) {
442             isEnabled = !"false".equals(enabled);
443         }
444 
445         boolean isMutable = DEFAULT_MUTABILITY;
446         final String mutable = parser.getAttributeValue(null, "mutable");
447         if (mutable != null) {
448             isMutable = !"false".equals(mutable);
449             if (!isMutable && parsingContext.mFoundMutableOverlay) {
450                 throw new IllegalStateException(String.format(
451                         "immutable overlays must precede mutable overlays:"
452                                 + " found in %s at %s",
453                         configFile, parser.getPositionDescription()));
454             }
455         }
456 
457         if (isMutable) {
458             parsingContext.mFoundMutableOverlay = true;
459         } else if (!isEnabled) {
460             // Default disabled, immutable overlays may be a misconfiguration of the system so warn
461             // developers.
462             Log.w(TAG, "found default-disabled immutable overlay " + packageName);
463         }
464 
465         final ParsedConfigFile parsedConfigFile = new ParsedConfigFile(
466                 configFile.getPath().intern(), parser.getLineNumber(),
467                 (Build.IS_ENG || Build.IS_USERDEBUG) ? currentParserContextToString(parser) : null);
468         final ParsedConfiguration config = new ParsedConfiguration(packageName, isEnabled,
469                 isMutable, parsingContext.mPartition.policy, info, parsedConfigFile);
470         parsingContext.mConfiguredOverlays.add(packageName);
471         parsingContext.mOrderedConfigurations.add(config);
472     }
473 
currentParserContextToString(@onNull XmlPullParser parser)474     private static String currentParserContextToString(@NonNull XmlPullParser parser) {
475         StringBuilder sb = new StringBuilder("<");
476         sb.append(parser.getName());
477         sb.append(" ");
478         for (int i = 0; i < parser.getAttributeCount(); i++) {
479             sb.append(parser.getAttributeName(i));
480             sb.append("=\"");
481             sb.append(parser.getAttributeValue(i));
482             sb.append("\" ");
483         }
484         sb.append("/>");
485         return sb.toString();
486     }
487 }
488