From d9d17367670eb930d74d2e2ffeeb3c0e9bea0a23 Mon Sep 17 00:00:00 2001 From: Winson Date: Wed, 2 Oct 2019 12:41:29 -0700 Subject: [PATCH] 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. 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 --- .../android/content/om/OverlayableInfo.java | 120 ++++++++ core/java/android/content/res/ApkAssets.java | 15 + .../java/com/android/server/SystemConfig.java | 52 +++- core/jni/android_content_res_ApkAssets.cpp | 56 ++++ libs/androidfw/include/androidfw/LoadedArsc.h | 2 + .../server/om/OverlayActorEnforcer.java | 261 ++++++++++++++++++ .../server/om/OverlayManagerService.java | 151 ++++++++-- services/tests/servicestests/Android.bp | 1 + .../server/om/OverlayActorEnforcerTests.kt | 197 +++++++++++++ .../SystemConfigNamedActorTest.kt | 232 ++++++++++++++++ .../{ => systemconfig}/SystemConfigTest.java | 4 +- 11 files changed, 1065 insertions(+), 26 deletions(-) create mode 100644 core/java/android/content/om/OverlayableInfo.java create mode 100644 services/core/java/com/android/server/om/OverlayActorEnforcer.java create mode 100644 services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt create mode 100644 services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt rename services/tests/servicestests/src/com/android/server/{ => systemconfig}/SystemConfigTest.java (98%) diff --git a/core/java/android/content/om/OverlayableInfo.java b/core/java/android/content/om/OverlayableInfo.java new file mode 100644 index 0000000000000..5923907b11e76 --- /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 de1d514d0a5b5..ad375552837d5 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 ed7f5de83fd14..49a73ee7790fd 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> mPackageToUserTypeWhitelist = new ArrayMap<>(); private ArrayMap> mPackageToUserTypeBlacklist = new ArrayMap<>(); + /** + * Map of system pre-defined, uniquely named actors; keys are namespace, + * value maps actor name to package name. + */ + private ArrayMap> 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> 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 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 637025329e376..f3a626e1e193a 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(xml_tree.release()); } +static jobject NativeGetOverlayableInfo(JNIEnv* env, jclass /*clazz*/, jlong ptr, + jstring overlayable_name) { + const ApkAssets* apk_assets = reinterpret_cast(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, "", + "(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(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 6cbda07b6950c..b5d3a1fc6c1fc 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -273,6 +273,8 @@ class LoadedPackage { ByteBucketArray resource_ids_; std::vector dynamic_package_map_; std::vector>> overlayable_infos_; + + // A map of overlayable name to actor std::unordered_map 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 0000000000000..e05511681ba84 --- /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> namedActors = mVerifyCallback.getNamedActors(); + if (namedActors.isEmpty()) { + return ActorState.NO_NAMED_ACTORS; + } + + Uri actorUri = Uri.parse(actor); + + String actorScheme = actorUri.getScheme(); + List actorPathSegments = actorUri.getPathSegments(); + if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) { + return ActorState.INVALID_OVERLAYABLE_ACTOR_NAME; + } + + String actorNamespace = actorUri.getAuthority(); + Map 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> 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 5f3e50320752f..63de61c9782f7 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 { @@ -860,18 +866,6 @@ public final class OverlayManagerService extends SystemService { Binder.getCallingUid(), userId, false, true, message, null); } - /** - * 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). * @@ -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> 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> 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 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 30ccb717e8a2b..52fb69eb99be4 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 0000000000000..233e16c297a30 --- /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 0000000000000..b7199d4a24435 --- /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() { + """ + + + + + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_ONE to PACKAGE_ONE, + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun twoSamePackage() { + """ + + + + + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_ONE to PACKAGE_ONE, + ACTOR_TWO to PACKAGE_ONE + ) + ) + ) + } + + @Test + fun missingNamespace() { + """ + + + + + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun missingName() { + """ + + + + + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun missingPackage() { + """ + + + + + """.write() + + assertPermissions().containsExactlyEntriesIn( + mapOf( + NAMESPACE_TEST to mapOf( + ACTOR_TWO to PACKAGE_TWO + ) + ) + ) + } + + @Test + fun androidNamespaceThrows() { + """ + + + + + """.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() { + """ + + + + + """.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 similarity index 98% rename from services/tests/servicestests/src/com/android/server/SystemConfigTest.java rename to services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java index ff03391ea0310..fde0ddffa3659 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;