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
This commit is contained in:
Winson
2019-10-02 12:41:29 -07:00
parent 64ed3ec44c
commit d9d1736767
11 changed files with 1065 additions and 26 deletions

View File

@@ -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() {}
}

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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_;
};

View File

@@ -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;
}
}

View File

@@ -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<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;

View File

@@ -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",

View File

@@ -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()
}
}
}
}

View File

@@ -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) }
}

View File

@@ -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;