diff options
author | Winson <chiuwinson@google.com> | 2019-10-02 12:41:29 -0700 |
---|---|---|
committer | Winson <chiuwinson@google.com> | 2019-11-21 12:01:41 -0800 |
commit | d9d17367670eb930d74d2e2ffeeb3c0e9bea0a23 (patch) | |
tree | 38a6bcc86d8166a2bdb1122e0dbd5f4bad20e5ce | |
parent | 64ed3ec44c68664ade3c7fa04f692a7144479ca6 (diff) |
Overlayable actor enforcement
Validates that the caller of an OverlayManager API that mutates state
is actually allowed to act on the target as defined in the target's
overlayable tag.
<overlayable name="MyResources" actor="namespace/name">
An actor is valid if any of the following is true:
- is root/system
- is the target overlay package
- has the CHANGE_OVERLAY_PACKAGES permission and an actor is not defined
- is the same package name as the sole resolved Activity for the actor specified
in the overlayable definition, with only pre-installed, namespaced actors
currently supported
Bug: 119442583
Bug: 135052950
Test: atest SystemConfigNamedActorTest
Test: atest com.android.server.om
Change-Id: If56b9e8366852eaef84f6bb25c3e6871eaa3f219
-rw-r--r-- | core/java/android/content/om/OverlayableInfo.java | 120 | ||||
-rw-r--r-- | core/java/android/content/res/ApkAssets.java | 15 | ||||
-rw-r--r-- | core/java/com/android/server/SystemConfig.java | 52 | ||||
-rw-r--r-- | core/jni/android_content_res_ApkAssets.cpp | 56 | ||||
-rw-r--r-- | libs/androidfw/include/androidfw/LoadedArsc.h | 2 | ||||
-rw-r--r-- | services/core/java/com/android/server/om/OverlayActorEnforcer.java | 261 | ||||
-rw-r--r-- | services/core/java/com/android/server/om/OverlayManagerService.java | 151 | ||||
-rw-r--r-- | services/tests/servicestests/Android.bp | 1 | ||||
-rw-r--r-- | services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt | 197 | ||||
-rw-r--r-- | services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt | 232 | ||||
-rw-r--r-- | services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java (renamed from services/tests/servicestests/src/com/android/server/SystemConfigTest.java) | 4 |
11 files changed, 1065 insertions, 26 deletions
diff --git a/core/java/android/content/om/OverlayableInfo.java b/core/java/android/content/om/OverlayableInfo.java new file mode 100644 index 000000000000..5923907b11e7 --- /dev/null +++ b/core/java/android/content/om/OverlayableInfo.java @@ -0,0 +1,120 @@ +/* + * 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. + */ + +package android.content.om; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.DataClass; + +import java.util.Objects; + +/** + * Immutable info on an overlayable defined inside a target package. + * + * @hide + */ +@DataClass(genSetters = false, genEqualsHashCode = true, genHiddenConstructor = true) +public final class OverlayableInfo { + + /** + * The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid. + */ + @NonNull + public final String name; + + /** + * The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to + * modify overlay state for this overlayable. + */ + @Nullable + public final String actor; + + // CHECKSTYLE:OFF Generated code + // + + + + // Code below generated by codegen v1.0.3. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/om/OverlayableInfo.java + + + /** + * Creates a new OverlayableInfo. + * + * @param name + * The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid. + * @param actor + * The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to + * modify overlay state for this overlayable. + * @hide + */ + @DataClass.Generated.Member + public OverlayableInfo( + @NonNull String name, + @Nullable String actor) { + this.name = name; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, name); + this.actor = actor; + + // onConstructed(); // You can define this method to get a callback + } + + @Override + @DataClass.Generated.Member + public boolean equals(Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(OverlayableInfo other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + OverlayableInfo that = (OverlayableInfo) o; + //noinspection PointlessBooleanExpression + return true + && Objects.equals(name, that.name) + && Objects.equals(actor, that.actor); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + Objects.hashCode(name); + _hash = 31 * _hash + Objects.hashCode(actor); + return _hash; + } + + @DataClass.Generated( + time = 1570059850579L, + codegenVersion = "1.0.3", + sourceFile = "frameworks/base/core/java/android/content/om/OverlayableInfo.java", + inputSignatures = "public final @android.annotation.NonNull java.lang.String name\npublic final @android.annotation.Nullable java.lang.String actor\nclass OverlayableInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=false, genEqualsHashCode=true, genHiddenConstructor=true)") + @Deprecated + private void __metadata() {} + +} diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index de1d514d0a5b..ad375552837d 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -18,6 +18,7 @@ package android.content.res; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; +import android.content.om.OverlayableInfo; import android.content.res.loader.ResourcesProvider; import android.text.TextUtils; @@ -254,6 +255,17 @@ public final class ApkAssets { } } + /** @hide */ + @Nullable + public OverlayableInfo getOverlayableInfo(String overlayableName) throws IOException { + return nativeGetOverlayableInfo(mNativePtr, overlayableName); + } + + /** @hide */ + public boolean definesOverlayable() throws IOException { + return nativeDefinesOverlayable(mNativePtr); + } + /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ @@ -305,4 +317,7 @@ public final class ApkAssets { private static native long nativeGetStringBlock(long ptr); private static native boolean nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; + private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, + String overlayableName) throws IOException; + private static native boolean nativeDefinesOverlayable(long ptr) throws IOException; } diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java index ed7f5de83fd1..49a73ee7790f 100644 --- a/core/java/com/android/server/SystemConfig.java +++ b/core/java/com/android/server/SystemConfig.java @@ -18,6 +18,7 @@ package com.android.server; import static com.android.internal.util.ArrayUtils.appendInt; +import android.annotation.NonNull; import android.app.ActivityManager; import android.content.ComponentName; import android.content.pm.FeatureInfo; @@ -221,6 +222,12 @@ public class SystemConfig { private ArrayMap<String, Set<String>> mPackageToUserTypeWhitelist = new ArrayMap<>(); private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>(); + /** + * Map of system pre-defined, uniquely named actors; keys are namespace, + * value maps actor name to package name. + */ + private ArrayMap<String, ArrayMap<String, String>> mNamedActors = null; + public static SystemConfig getInstance() { if (!isSystemProcess()) { Slog.wtf(TAG, "SystemConfig is being accessed by a process other than " @@ -398,12 +405,17 @@ public class SystemConfig { return r; } + @NonNull + public Map<String, ? extends Map<String, String>> getNamedActors() { + return mNamedActors != null ? mNamedActors : Collections.emptyMap(); + } + /** * Only use for testing. Do NOT use in production code. * @param readPermissions false to create an empty SystemConfig; true to read the permissions. */ @VisibleForTesting - protected SystemConfig(boolean readPermissions) { + public SystemConfig(boolean readPermissions) { if (readPermissions) { Slog.w(TAG, "Constructing a test SystemConfig"); readAllPermissions(); @@ -1028,6 +1040,44 @@ public class SystemConfig { readInstallInUserType(parser, mPackageToUserTypeWhitelist, mPackageToUserTypeBlacklist); } break; + case "named-actor": { + String namespace = TextUtils.safeIntern( + parser.getAttributeValue(null, "namespace")); + String actorName = parser.getAttributeValue(null, "name"); + String pkgName = TextUtils.safeIntern( + parser.getAttributeValue(null, "package")); + if (TextUtils.isEmpty(namespace)) { + Slog.wtf(TAG, "<" + name + "> without namespace in " + permFile + + " at " + parser.getPositionDescription()); + } else if (TextUtils.isEmpty(actorName)) { + Slog.wtf(TAG, "<" + name + "> without actor name in " + permFile + + " at " + parser.getPositionDescription()); + } else if (TextUtils.isEmpty(pkgName)) { + Slog.wtf(TAG, "<" + name + "> without package name in " + permFile + + " at " + parser.getPositionDescription()); + } else if ("android".equalsIgnoreCase(namespace)) { + throw new IllegalStateException("Defining " + actorName + " as " + + pkgName + " for the android namespace is not allowed"); + } else { + if (mNamedActors == null) { + mNamedActors = new ArrayMap<>(); + } + + ArrayMap<String, String> nameToPkgMap = mNamedActors.get(namespace); + if (nameToPkgMap == null) { + nameToPkgMap = new ArrayMap<>(); + mNamedActors.put(namespace, nameToPkgMap); + } else if (nameToPkgMap.containsKey(actorName)) { + String existing = nameToPkgMap.get(actorName); + throw new IllegalStateException("Duplicate actor definition for " + + namespace + "/" + actorName + + "; defined as both " + existing + " and " + pkgName); + } + + nameToPkgMap.put(actorName, pkgName); + } + XmlUtils.skipCurrentTag(parser); + } break; default: { Slog.w(TAG, "Tag " + name + " is unknown in " + permFile + " at " + parser.getPositionDescription()); diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index 637025329e37..f3a626e1e193 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -194,6 +194,59 @@ static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring fil return reinterpret_cast<jlong>(xml_tree.release()); } +static jobject NativeGetOverlayableInfo(JNIEnv* env, jclass /*clazz*/, jlong ptr, + jstring overlayable_name) { + const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr); + + const auto& packages = apk_assets->GetLoadedArsc()->GetPackages(); + if (packages.empty()) { + jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK"); + return 0; + } + + // TODO(b/119899133): Convert this to a search for the info rather than assuming it's at index 0 + const auto& overlayable_map = packages[0]->GetOverlayableMap(); + if (overlayable_map.empty()) { + return nullptr; + } + + auto overlayable_name_native = std::string(env->GetStringUTFChars(overlayable_name, NULL)); + auto actor = overlayable_map.find(overlayable_name_native); + if (actor == overlayable_map.end()) { + return nullptr; + } + + jstring actor_string = env->NewStringUTF(actor->first.c_str()); + if (env->ExceptionCheck() || actor_string == nullptr) { + jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK"); + return 0; + } + + jclass overlayable_class = env->FindClass("android/content/om/OverlayableInfo"); + jmethodID overlayable_constructor = env->GetMethodID(overlayable_class, "<init>", + "(Ljava/lang/String;Ljava/lang/String;I)V"); + return env->NewObject( + overlayable_class, + overlayable_constructor, + overlayable_name, + actor_string + ); +} + +static jboolean NativeDefinesOverlayable(JNIEnv* env, jclass /*clazz*/, jlong ptr) { + const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr); + + const auto& packages = apk_assets->GetLoadedArsc()->GetPackages(); + if (packages.empty()) { + // Must throw to prevent bypass by returning false + jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK"); + return 0; + } + + const auto& overlayable_infos = packages[0]->GetOverlayableMap(); + return overlayable_infos.empty() ? JNI_FALSE : JNI_TRUE; +} + // JNI registration. static const JNINativeMethod gApkAssetsMethods[] = { {"nativeLoad", "(Ljava/lang/String;ZZZZ)J", (void*)NativeLoad}, @@ -208,6 +261,9 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, + {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", + (void*)NativeGetOverlayableInfo}, + {"nativeDefinesOverlayable", "(J)Z", (void*)NativeDefinesOverlayable}, }; int register_android_content_res_ApkAssets(JNIEnv* env) { diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 6cbda07b6950..b5d3a1fc6c1f 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -273,6 +273,8 @@ class LoadedPackage { ByteBucketArray<uint32_t> resource_ids_; std::vector<DynamicPackageEntry> dynamic_package_map_; std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_; + + // A map of overlayable name to actor std::unordered_map<std::string, std::string> overlayable_map_; }; diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java new file mode 100644 index 000000000000..e05511681ba8 --- /dev/null +++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java @@ -0,0 +1,261 @@ +/* + * 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. + */ + +package com.android.server.om; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.om.OverlayInfo; +import android.content.om.OverlayableInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.os.Process; +import android.os.RemoteException; +import android.text.TextUtils; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; +import com.android.server.SystemConfig; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Performs verification that a calling UID can act on a target package's overlayable. + * + * @hide + */ +public class OverlayActorEnforcer { + + private final VerifyCallback mVerifyCallback; + + public OverlayActorEnforcer(@NonNull VerifyCallback verifyCallback) { + mVerifyCallback = verifyCallback; + } + + void enforceActor(@NonNull OverlayInfo overlayInfo, @NonNull String methodName, + int callingUid, int userId) throws SecurityException { + ActorState actorState = isAllowedActor(methodName, overlayInfo, callingUid, userId); + if (actorState == ActorState.ALLOWED) { + return; + } + + String targetOverlayableName = overlayInfo.targetOverlayableName; + throw new SecurityException("UID" + callingUid + " is not allowed to call " + + methodName + " for " + + (TextUtils.isEmpty(targetOverlayableName) ? "" : (targetOverlayableName + " in ")) + + overlayInfo.targetPackageName + " because " + actorState + ); + } + + /** + * An actor is valid if any of the following is true: + * - is {@link Process#ROOT_UID}, {@link Process#SYSTEM_UID} + * - is the target overlay package + * - has the CHANGE_OVERLAY_PACKAGES permission and an actor is not defined + * - is the same the as the package defined in {@link SystemConfig#getNamedActors()} for a given + * namespace and actor name + * + * @return true if the actor is allowed to act on the target overlayInfo + */ + private ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo, + int callingUid, int userId) { + switch (callingUid) { + case Process.ROOT_UID: + case Process.SYSTEM_UID: + return ActorState.ALLOWED; + } + + String[] callingPackageNames = mVerifyCallback.getPackagesForUid(callingUid); + if (ArrayUtils.isEmpty(callingPackageNames)) { + return ActorState.NO_PACKAGES_FOR_UID; + } + + // A target is always an allowed actor for itself + String targetPackageName = overlayInfo.targetPackageName; + if (ArrayUtils.contains(callingPackageNames, targetPackageName)) { + return ActorState.ALLOWED; + } + + String targetOverlayableName = overlayInfo.targetOverlayableName; + + if (TextUtils.isEmpty(targetOverlayableName)) { + try { + if (mVerifyCallback.doesTargetDefineOverlayable(targetPackageName, userId)) { + return ActorState.MISSING_TARGET_OVERLAYABLE_NAME; + } else { + // If there's no overlayable defined, fallback to the legacy permission check + try { + mVerifyCallback.enforcePermission( + android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName); + + // If the previous method didn't throw, check passed + return ActorState.ALLOWED; + } catch (SecurityException e) { + return ActorState.MISSING_LEGACY_PERMISSION; + } + } + } catch (RemoteException | IOException e) { + return ActorState.ERROR_READING_OVERLAYABLE; + } + } + + OverlayableInfo targetOverlayable; + try { + targetOverlayable = mVerifyCallback.getOverlayableForTarget(targetPackageName, + targetOverlayableName, userId); + } catch (IOException e) { + return ActorState.UNABLE_TO_GET_TARGET; + } + + if (targetOverlayable == null) { + return ActorState.MISSING_OVERLAYABLE; + } + + String actor = targetOverlayable.actor; + if (TextUtils.isEmpty(actor)) { + // If there's no actor defined, fallback to the legacy permission check + try { + mVerifyCallback.enforcePermission( + android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName); + + // If the previous method didn't throw, check passed + return ActorState.ALLOWED; + } catch (SecurityException e) { + return ActorState.MISSING_LEGACY_PERMISSION; + } + } + + Map<String, ? extends Map<String, String>> namedActors = mVerifyCallback.getNamedActors(); + if (namedActors.isEmpty()) { + return ActorState.NO_NAMED_ACTORS; + } + + Uri actorUri = Uri.parse(actor); + + String actorScheme = actorUri.getScheme(); + List<String> actorPathSegments = actorUri.getPathSegments(); + if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) { + return ActorState.INVALID_OVERLAYABLE_ACTOR_NAME; + } + + String actorNamespace = actorUri.getAuthority(); + Map<String, String> namespace = namedActors.get(actorNamespace); + if (namespace == null) { + return ActorState.MISSING_NAMESPACE; + } + + String actorName = actorPathSegments.get(0); + String packageName = namespace.get(actorName); + if (TextUtils.isEmpty(packageName)) { + return ActorState.MISSING_ACTOR_NAME; + } + + PackageInfo packageInfo = mVerifyCallback.getPackageInfo(packageName, userId); + if (packageInfo == null) { + return ActorState.MISSING_APP_INFO; + } + + ApplicationInfo appInfo = packageInfo.applicationInfo; + if (appInfo == null) { + return ActorState.MISSING_APP_INFO; + } + + // Currently only pre-installed apps can be actors + if (!appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { + return ActorState.ACTOR_NOT_PREINSTALLED; + } + + if (ArrayUtils.contains(callingPackageNames, packageName)) { + return ActorState.ALLOWED; + } + + return ActorState.INVALID_ACTOR; + } + + /** + * For easier logging/debugging, a set of all possible failure/success states when running + * enforcement. + */ + private enum ActorState { + ALLOWED, + INVALID_ACTOR, + MISSING_NAMESPACE, + MISSING_PACKAGE, + MISSING_APP_INFO, + ACTOR_NOT_PREINSTALLED, + NO_PACKAGES_FOR_UID, + MISSING_ACTOR_NAME, + ERROR_READING_OVERLAYABLE, + MISSING_TARGET_OVERLAYABLE_NAME, + MISSING_OVERLAYABLE, + INVALID_OVERLAYABLE_ACTOR_NAME, + NO_NAMED_ACTORS, + UNABLE_TO_GET_TARGET, + MISSING_LEGACY_PERMISSION + } + + /** + * Delegate to the system for querying information about packages. + */ + public interface VerifyCallback { + + /** + * Read from the APK and AndroidManifest of a package to return the overlayable defined for + * a given name. + * + * @throws IOException if the target can't be read + */ + @Nullable + OverlayableInfo getOverlayableForTarget(@NonNull String packageName, + @Nullable String targetOverlayableName, int userId) + throws IOException; + + /** + * @see android.content.pm.PackageManager#getPackagesForUid(int) + */ + @Nullable + String[] getPackagesForUid(int uid); + + /** + * @param userId user to filter package visibility by + * @see android.content.pm.PackageManager#getPackageInfo(String, int) + */ + @Nullable + PackageInfo getPackageInfo(@NonNull String packageName, int userId); + + /** + * @return map of system pre-defined, uniquely named actors; keys are namespace, + * value maps actor name to package name + */ + @NonNull + Map<String, ? extends Map<String, String>> getNamedActors(); + + /** + * @return true if the target package has declared an overlayable + */ + boolean doesTargetDefineOverlayable(String targetPackageName, int userId) + throws RemoteException, IOException; + + /** + * @throws SecurityException containing message if the caller doesn't have the given + * permission + */ + void enforcePermission(String permission, String message) throws SecurityException; + } +} diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index 5f3e50320752..63de61c9782f 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -39,10 +39,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; +import android.content.om.OverlayableInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; +import android.content.res.ApkAssets; import android.net.Uri; import android.os.Binder; import android.os.Environment; @@ -63,6 +65,7 @@ import android.util.SparseArray; import com.android.server.FgThread; import com.android.server.IoThread; import com.android.server.LocalServices; +import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.pm.UserManagerService; @@ -229,6 +232,8 @@ public final class OverlayManagerService extends SystemService { private final OverlayManagerServiceImpl mImpl; + private final OverlayActorEnforcer mActorEnforcer; + private final AtomicBoolean mPersistSettingsScheduled = new AtomicBoolean(false); public OverlayManagerService(@NonNull final Context context) { @@ -237,12 +242,13 @@ public final class OverlayManagerService extends SystemService { traceBegin(TRACE_TAG_RRO, "OMS#OverlayManagerService"); mSettingsFile = new AtomicFile( new File(Environment.getDataSystemDirectory(), "overlays.xml"), "overlays"); - mPackageManager = new PackageManagerHelper(); + mPackageManager = new PackageManagerHelper(context); mUserManager = UserManagerService.getInstance(); IdmapManager im = new IdmapManager(mPackageManager); mSettings = new OverlayManagerSettings(); mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings, getDefaultOverlayPackages(), new OverlayChangeListener()); + mActorEnforcer = new OverlayActorEnforcer(mPackageManager); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(ACTION_PACKAGE_ADDED); @@ -581,7 +587,7 @@ public final class OverlayManagerService extends SystemService { int userId) throws RemoteException { try { traceBegin(TRACE_TAG_RRO, "OMS#setEnabled " + packageName + " " + enable); - enforceChangeOverlayPackagesPermission("setEnabled"); + enforceActor(packageName, "setEnabled", userId); userId = handleIncomingUser(userId, "setEnabled"); if (packageName == null) { return false; @@ -605,7 +611,7 @@ public final class OverlayManagerService extends SystemService { int userId) throws RemoteException { try { traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusive " + packageName + " " + enable); - enforceChangeOverlayPackagesPermission("setEnabledExclusive"); + enforceActor(packageName, "setEnabledExclusive", userId); userId = handleIncomingUser(userId, "setEnabledExclusive"); if (packageName == null || !enable) { return false; @@ -630,7 +636,7 @@ public final class OverlayManagerService extends SystemService { throws RemoteException { try { traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusiveInCategory " + packageName); - enforceChangeOverlayPackagesPermission("setEnabledExclusiveInCategory"); + enforceActor(packageName, "setEnabledExclusiveInCategory", userId); userId = handleIncomingUser(userId, "setEnabledExclusiveInCategory"); if (packageName == null) { return false; @@ -656,7 +662,7 @@ public final class OverlayManagerService extends SystemService { try { traceBegin(TRACE_TAG_RRO, "OMS#setPriority " + packageName + " " + parentPackageName); - enforceChangeOverlayPackagesPermission("setPriority"); + enforceActor(packageName, "setPriority", userId); userId = handleIncomingUser(userId, "setPriority"); if (packageName == null || parentPackageName == null) { return false; @@ -680,7 +686,7 @@ public final class OverlayManagerService extends SystemService { throws RemoteException { try { traceBegin(TRACE_TAG_RRO, "OMS#setHighestPriority " + packageName); - enforceChangeOverlayPackagesPermission("setHighestPriority"); + enforceActor(packageName, "setHighestPriority", userId); userId = handleIncomingUser(userId, "setHighestPriority"); if (packageName == null) { return false; @@ -704,7 +710,7 @@ public final class OverlayManagerService extends SystemService { throws RemoteException { try { traceBegin(TRACE_TAG_RRO, "OMS#setLowestPriority " + packageName); - enforceChangeOverlayPackagesPermission("setLowestPriority"); + enforceActor(packageName, "setLowestPriority", userId); userId = handleIncomingUser(userId, "setLowestPriority"); if (packageName == null) { return false; @@ -750,7 +756,7 @@ public final class OverlayManagerService extends SystemService { return; } - enforceChangeOverlayPackagesPermission("invalidateCachesForOverlay"); + enforceActor(packageName, "invalidateCachesForOverlay", userId); userId = handleIncomingUser(userId, "invalidateCachesForOverlay"); final long ident = Binder.clearCallingIdentity(); try { @@ -861,18 +867,6 @@ public final class OverlayManagerService extends SystemService { } /** - * Enforce that the caller holds the CHANGE_OVERLAY_PACKAGES permission (or is - * system or root). - * - * @param message used as message if SecurityException is thrown - * @throws SecurityException if the permission check fails - */ - private void enforceChangeOverlayPackagesPermission(@NonNull final String message) { - getContext().enforceCallingOrSelfPermission( - android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, message); - } - - /** * Enforce that the caller holds the DUMP permission (or is system or root). * * @param message used as message if SecurityException is thrown @@ -881,6 +875,13 @@ public final class OverlayManagerService extends SystemService { private void enforceDumpPermission(@NonNull final String message) { getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, message); } + + private void enforceActor(String packageName, String methodName, int userId) + throws SecurityException { + OverlayInfo overlayInfo = mImpl.getOverlayInfo(packageName, userId); + int callingUid = Binder.getCallingUid(); + mActorEnforcer.enforceActor(overlayInfo, methodName, callingUid, userId); + } }; private final class OverlayChangeListener @@ -1035,9 +1036,16 @@ public final class OverlayManagerService extends SystemService { } } - private static final class PackageManagerHelper implements - OverlayManagerServiceImpl.PackageManagerHelper { + /** + * Delegate for {@link android.content.pm.PackageManager} and {@link PackageManagerInternal} + * functionality, separated for easy testing. + * + * @hide + */ + public static final class PackageManagerHelper implements + OverlayManagerServiceImpl.PackageManagerHelper, OverlayActorEnforcer.VerifyCallback { + private final Context mContext; private final IPackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; @@ -1048,11 +1056,14 @@ public final class OverlayManagerService extends SystemService { // behind until all pending intents have been processed. private final SparseArray<HashMap<String, PackageInfo>> mCache = new SparseArray<>(); - PackageManagerHelper() { + PackageManagerHelper(Context context) { + mContext = context; mPackageManager = getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } + // TODO(b/143096091): Remove PackageInfo cache so that PackageManager is always queried + // to enforce visibility/other permission checks public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId, final boolean useCache) { if (useCache) { @@ -1075,7 +1086,19 @@ public final class OverlayManagerService extends SystemService { @Override public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId) { - return getPackageInfo(packageName, userId, true); + // TODO(b/143096091): Remove clearing calling ID + long callingIdentity = Binder.clearCallingIdentity(); + try { + return getPackageInfo(packageName, userId, true); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @NonNull + @Override + public Map<String, ? extends Map<String, String>> getNamedActors() { + return SystemConfig.getInstance().getNamedActors(); } @Override @@ -1097,6 +1120,70 @@ public final class OverlayManagerService extends SystemService { return mPackageManagerInternal.getOverlayPackages(userId); } + @Nullable + @Override + public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, + @Nullable String targetOverlayableName, int userId) + throws IOException { + // TODO(b/143096091): Remove clearing calling ID + long callingIdentity = Binder.clearCallingIdentity(); + try { + PackageInfo packageInfo = getPackageInfo(packageName, userId); + if (packageInfo == null) { + throw new IOException("Unable to get target package"); + } + + String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); + + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(baseCodePath); + return apkAssets.getOverlayableInfo(targetOverlayableName); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } + } + } + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @Override + public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) + throws RemoteException, IOException { + // TODO(b/143096091): Remove clearing calling ID + long callingIdentity = Binder.clearCallingIdentity(); + try { + PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, 0, + userId); + String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); + + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(baseCodePath); + return apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } + } + } + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @Override + public void enforcePermission(String permission, String message) throws SecurityException { + mContext.enforceCallingOrSelfPermission(permission, message); + } + public PackageInfo getCachedPackageInfo(@NonNull final String packageName, final int userId) { final HashMap<String, PackageInfo> map = mCache.get(userId); @@ -1128,6 +1215,22 @@ public final class OverlayManagerService extends SystemService { mCache.delete(userId); } + @Nullable + @Override + public String[] getPackagesForUid(int uid) { + // TODO(b/143096091): Remove clearing calling ID + long callingIdentity = Binder.clearCallingIdentity(); + try { + try { + return mPackageManager.getPackagesForUid(uid); + } catch (RemoteException ignored) { + return null; + } + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + private static final String TAB1 = " "; private static final String TAB2 = TAB1 + TAB1; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 30ccb717e8a2..52fb69eb99be 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -8,6 +8,7 @@ android_test { // Include all test java files. srcs: [ "src/**/*.java", + "src/**/*.kt", "aidl/com/android/servicestests/aidl/INetworkStateObserver.aidl", "aidl/com/android/servicestests/aidl/ICmdReceiverService.aidl", diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt new file mode 100644 index 000000000000..233e16c297a3 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt @@ -0,0 +1,197 @@ +/* + * 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. + */ + +package com.android.server.om + +import android.content.om.OverlayInfo +import android.content.om.OverlayableInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.os.Process +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + +class OverlayActorEnforcerTests { + companion object { + private const val NAMESPACE = "testnamespace" + private const val ACTOR_NAME = "testactor" + private const val ACTOR_PKG_NAME = "com.test.actor.one" + private const val OVERLAYABLE_NAME = "TestOverlayable" + private const val UID = 3536 + private const val USER_ID = 55 + } + + @get:Rule + val expectedException = ExpectedException.none()!! + + @Test + fun isRoot() { + verify(callingUid = Process.ROOT_UID) + } + + @Test(expected = SecurityException::class) + fun isShell() { + verify(callingUid = Process.SHELL_UID) + } + + @Test + fun isSystem() { + verify(callingUid = Process.SYSTEM_UID) + } + + @Test(expected = SecurityException::class) + fun noOverlayable_noTarget() { + verify(targetOverlayableName = null) + } + + @Test + fun noOverlayable_noTarget_withPermission() { + verify(targetOverlayableName = null, hasPermission = true) + } + + @Test(expected = SecurityException::class) + fun noOverlayable_withTarget() { + verify(targetOverlayableName = OVERLAYABLE_NAME) + } + + @Test(expected = SecurityException::class) + fun withOverlayable_noTarget() { + verify( + targetOverlayableName = null, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null) + ) + } + + @Test(expected = SecurityException::class) + fun withOverlayable_noActor() { + verify( + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null) + ) + } + + @Test + fun withOverlayable_noActor_withPermission() { + verify( + hasPermission = true, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null) + ) + } + + @Test(expected = SecurityException::class) + fun withOverlayable_withActor_notActor() { + verify( + isActor = false, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, + "overlay://$NAMESPACE/$ACTOR_NAME") + ) + } + + @Test(expected = SecurityException::class) + fun withOverlayable_withActor_isActor_notPreInstalled() { + verify( + isActor = true, + isPreInstalled = false, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, + "overlay://$NAMESPACE/$ACTOR_NAME") + ) + } + + @Test + fun withOverlayable_withActor_isActor_isPreInstalled() { + verify( + isActor = true, + isPreInstalled = true, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, + "overlay://$NAMESPACE/$ACTOR_NAME") + ) + } + + @Test(expected = SecurityException::class) + fun withOverlayable_invalidActor() { + verify( + isActor = true, + isPreInstalled = true, + overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "notValidActor") + ) + } + + private fun verify( + isActor: Boolean = false, + isPreInstalled: Boolean = false, + hasPermission: Boolean = false, + overlayableInfo: OverlayableInfo? = null, + callingUid: Int = UID, + targetOverlayableName: String? = OVERLAYABLE_NAME + ) { + val callback = MockCallback( + isActor = isActor, + isPreInstalled = isPreInstalled, + hasPermission = hasPermission, + overlayableInfo = overlayableInfo + ) + + val overlayInfo = overlayInfo(targetOverlayableName) + OverlayActorEnforcer(callback) + .enforceActor(overlayInfo, "test", callingUid, USER_ID) + } + + private fun overlayInfo(targetOverlayableName: String?) = OverlayInfo("com.test.overlay", + "com.test.target", targetOverlayableName, null, "/path", OverlayInfo.STATE_UNKNOWN, 0, + 0, false) + + private class MockCallback( + private val isActor: Boolean = false, + private val isPreInstalled: Boolean = false, + private val hasPermission: Boolean = false, + private val overlayableInfo: OverlayableInfo? = null, + private vararg val packageNames: String = arrayOf("com.test.actor.one") + ) : OverlayActorEnforcer.VerifyCallback { + + override fun getNamedActors() = if (isActor) { + mapOf(NAMESPACE to mapOf(ACTOR_NAME to ACTOR_PKG_NAME)) + } else { + emptyMap() + } + + override fun getOverlayableForTarget( + packageName: String, + targetOverlayableName: String?, + userId: Int + ) = overlayableInfo + + override fun getPackagesForUid(uid: Int) = when (uid) { + UID -> packageNames + else -> null + } + + override fun getPackageInfo(packageName: String, userId: Int) = PackageInfo().apply { + applicationInfo = ApplicationInfo().apply { + flags = if (isPreInstalled) ApplicationInfo.FLAG_SYSTEM else 0 + } + } + + override fun doesTargetDefineOverlayable(targetPackageName: String?, userId: Int): Boolean { + return overlayableInfo != null + } + + override fun enforcePermission(permission: String?, message: String?) { + if (!hasPermission) { + throw SecurityException() + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt new file mode 100644 index 000000000000..b7199d4a2443 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt @@ -0,0 +1,232 @@ +/* + * 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. + */ + +package com.android.server.systemconfig + +import android.content.Context +import androidx.test.InstrumentationRegistry +import com.android.server.SystemConfig +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder + +class SystemConfigNamedActorTest { + + companion object { + private const val NAMESPACE_TEST = "someTestNamespace" + private const val NAMESPACE_ANDROID = "android" + private const val ACTOR_ONE = "iconShaper" + private const val ACTOR_TWO = "colorChanger" + private const val PACKAGE_ONE = "com.test.actor.one" + private const val PACKAGE_TWO = "com.test.actor.two" + } + + private val context: Context = InstrumentationRegistry.getContext() + + @get:Rule + val tempFolder = TemporaryFolder(context.filesDir) + + @get:Rule + val expected = ExpectedException.none() + + private var uniqueCounter = 0 + + @Test + fun twoUnique() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_TWO" + package="$PACKAGE_TWO" + /> + </config> + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_ONE to PACKAGE_ONE, + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun twoSamePackage() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_TWO" + package="$PACKAGE_ONE" + /> + </config> + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_ONE to PACKAGE_ONE, + ACTOR_TWO to PACKAGE_ONE + ) + ) + ) + } + + @Test + fun missingNamespace() { + """ + <config> + <named-actor + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_TWO" + package="$PACKAGE_TWO" + /> + </config> + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun missingName() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_TWO" + package="$PACKAGE_TWO" + /> + </config> + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun missingPackage() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_TWO" + package="$PACKAGE_TWO" + /> + </config> + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun androidNamespaceThrows() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_ANDROID" + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + </config> + """.write() + + expected.expect(IllegalStateException::class.java) + expected.expectMessage("Defining $ACTOR_ONE as $PACKAGE_ONE " + + "for the android namespace is not allowed") + + assertPermissions() + } + + @Test + fun duplicateActorNameThrows() { + """ + <config> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + package="$PACKAGE_ONE" + /> + <named-actor + namespace="$NAMESPACE_TEST" + name="$ACTOR_ONE" + package="$PACKAGE_TWO" + /> + </config> + """.write() + + expected.expect(IllegalStateException::class.java) + expected.expectMessage("Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" + + " defined as both $PACKAGE_ONE and $PACKAGE_TWO") + + assertPermissions() + } + + private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml") + .writeText(this.trimIndent()) + + private fun assertPermissions() = SystemConfig(false).apply { + readPermissions(tempFolder.root, 0) + }. let { assertThat(it.namedActors) } +} diff --git a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java index ff03391ea031..fde0ddffa365 100644 --- a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.systemconfig; import static org.junit.Assert.assertEquals; @@ -25,6 +25,8 @@ import android.util.Log; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.SystemConfig; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; |