Merge "Allow any actor when target package is debuggable"

This commit is contained in:
TreeHugger Robot
2020-09-10 02:48:46 +00:00
committed by Android (Google) Code Review
3 changed files with 392 additions and 161 deletions

View File

@@ -26,6 +26,7 @@ import android.os.Process;
import android.text.TextUtils;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
@@ -66,7 +67,7 @@ public class OverlayActorEnforcer {
String actorNamespace = actorUri.getAuthority();
Map<String, String> namespace = namedActors.get(actorNamespace);
if (namespace == null) {
if (ArrayUtils.isEmpty(namespace)) {
return Pair.create(null, ActorState.MISSING_NAMESPACE);
}
@@ -102,21 +103,32 @@ public class OverlayActorEnforcer {
* See {@link OverlayActorEnforcer} class comment for actor requirements.
* @return true if the actor is allowed to act on the target overlayInfo
*/
private ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
int callingUid, int userId) {
// Checked first to avoid package not found errors, which are ignored for calls from shell
switch (callingUid) {
case Process.ROOT_UID:
case Process.SYSTEM_UID:
return ActorState.ALLOWED;
}
final String targetPackageName = overlayInfo.targetPackageName;
final PackageInfo targetPkgInfo = mPackageManager.getPackageInfo(targetPackageName, userId);
if (targetPkgInfo == null) {
return ActorState.TARGET_NOT_FOUND;
}
if ((targetPkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
return ActorState.ALLOWED;
}
String[] callingPackageNames = mPackageManager.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;
}
@@ -149,7 +161,7 @@ public class OverlayActorEnforcer {
targetOverlayable = mPackageManager.getOverlayableForTarget(targetPackageName,
targetOverlayableName, userId);
} catch (IOException e) {
return ActorState.UNABLE_TO_GET_TARGET;
return ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE;
}
if (targetOverlayable == null) {
@@ -189,7 +201,7 @@ public class OverlayActorEnforcer {
}
// Currently only pre-installed apps can be actors
if (!appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
if (!appInfo.isSystemApp()) {
return ActorState.ACTOR_NOT_PREINSTALLED;
}
@@ -203,22 +215,25 @@ public class OverlayActorEnforcer {
/**
* For easier logging/debugging, a set of all possible failure/success states when running
* enforcement.
*
* The ordering of this enum should be maintained in the order that cases are checked in code,
* as this ordering is used inside OverlayActorEnforcerTests.
*/
public enum ActorState {
ALLOWED,
INVALID_ACTOR,
MISSING_NAMESPACE,
MISSING_PACKAGE,
MISSING_APP_INFO,
ACTOR_NOT_PREINSTALLED,
TARGET_NOT_FOUND,
NO_PACKAGES_FOR_UID,
MISSING_ACTOR_NAME,
ERROR_READING_OVERLAYABLE,
MISSING_TARGET_OVERLAYABLE_NAME,
MISSING_LEGACY_PERMISSION,
ERROR_READING_OVERLAYABLE,
UNABLE_TO_GET_TARGET_OVERLAYABLE,
MISSING_OVERLAYABLE,
INVALID_OVERLAYABLE_ACTOR_NAME,
NO_NAMED_ACTORS,
UNABLE_TO_GET_TARGET,
MISSING_LEGACY_PERMISSION
MISSING_NAMESPACE,
MISSING_ACTOR_NAME,
MISSING_APP_INFO,
ACTOR_NOT_PREINSTALLED,
INVALID_ACTOR,
ALLOWED
}
}

View File

@@ -21,172 +21,387 @@ import android.content.om.OverlayableInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Process
import org.junit.Rule
import com.android.server.om.OverlayActorEnforcer.ActorState
import com.android.server.testutils.mockThrowOnUnmocked
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.BeforeClass
import org.junit.Test
import org.junit.rules.ExpectedException
import java.lang.UnsupportedOperationException
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mockito.spy
import java.io.IOException
@RunWith(Parameterized::class)
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 TARGET_PKG = "com.test.target"
private const val OVERLAY_PKG = "com.test.overlay"
private const val VALID_NAMESPACE = "testNamespaceValid"
private const val INVALID_NAMESPACE = "testNamespaceInvalid"
private const val VALID_ACTOR_NAME = "testActorOne"
private const val INVALID_ACTOR_NAME = "testActorTwo"
private const val VALID_ACTOR_PKG = "com.test.actor.valid"
private const val INVALID_ACTOR_PKG = "com.test.actor.invalid"
private const val OVERLAYABLE_NAME = "TestOverlayable"
private const val UID = 3536
private const val NULL_UID = 3536
private const val EMPTY_UID = NULL_UID + 1
private const val INVALID_ACTOR_UID = NULL_UID + 2
private const val VALID_ACTOR_UID = NULL_UID + 3
private const val TARGET_UID = NULL_UID + 4
private const val USER_ID = 55
}
@get:Rule
val expectedException = ExpectedException.none()!!
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun parameters() = CASES.mapIndexed { caseIndex, testCase ->
fun param(pair: Pair<String, TestState.() -> Unit>, type: Params.Type): Params {
val expectedState = testCase.state.takeUnless { type == Params.Type.ALLOWED }
?: ActorState.ALLOWED
val (caseName, case) = pair
val testName = makeTestName(testCase, caseName, type)
return Params(caseIndex, expectedState, testName, type, case)
}
@Test
fun isRoot() {
verify(callingUid = Process.ROOT_UID)
}
testCase.failures.map { param(it, Params.Type.FAILURE) } +
testCase.allowed.map { param(it, Params.Type.ALLOWED) }
}.flatten()
@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")
) : PackageManagerHelper {
override fun getNamedActors() = if (isActor) {
mapOf(NAMESPACE to mapOf(ACTOR_NAME to ACTOR_PKG_NAME))
} else {
emptyMap()
@BeforeClass
@JvmStatic
fun checkAllCasesHandled() {
// Assert that all states have been tested at least once.
assertThat(CASES.map { it.state }.distinct()).containsAllIn(ActorState.values())
}
@BeforeClass
@JvmStatic
fun checkAllCasesUniquelyNamed() {
val duplicateCaseNames = CASES.mapIndexed { caseIndex, testCase ->
testCase.failures.map {
makeTestName(testCase, it.first, Params.Type.FAILURE)
} + testCase.allowed.map {
makeTestName(testCase, it.first, Params.Type.ALLOWED)
}
}
.flatten()
.groupingBy { it }
.eachCount()
.filterValues { it > 1 }
.keys
assertThat(duplicateCaseNames).isEmpty()
}
/*
The pattern in this block is a result of the incredible number of branches in
enforcement logic. It serves to verify failures with the assumption that all errors
are checked in order. The idea is to emulate the if-else branches from code, but using
actual test data instead of if statements.
Each state is verified by providing a failure or exclusive set of failures which cause
a failure state to be returned. Each state also provides a success case which will
"skip" the state. This allows subsequent failure cases to cascade from the first case
by calling all the skip branches for preceding states and then choosing only 1 of
the failures to test.
Given the failure states A, B, and C: testA calls A.failure + assert, testB calls
A.skip + B.failure + assert, testC calls A.skip + B.skip + C.failure + assert, etc.
Calling `allowed` is a special case for when there is a combination of parameters that
skips the remaining checks and immediately allows the actor through. For these cases,
the first failure branch will be run, assert that it's not allowed, and the
allowed branch will run, asserting that it now results in ALLOWED, skipping all
remaining functions.
This is an ordered list of TestCase objects, with the possibility to repeat failure
states if any can occur multiple times in the logic tree.
Each failure must be handled at least once.
*/
private val CASES = listOf(
ActorState.TARGET_NOT_FOUND withCases {
failure("nullPkgInfo") { targetPkgInfo = null }
allowed("debuggable") {
targetPkgInfo = pkgInfo(TARGET_PKG).apply {
applicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE
}
}
skip { targetPkgInfo = pkgInfo(TARGET_PKG) }
},
ActorState.NO_PACKAGES_FOR_UID withCases {
failure("empty") { callingUid = EMPTY_UID }
failure("null") { callingUid = NULL_UID }
failure("shell") { callingUid = Process.SHELL_UID }
allowed("targetUid") { callingUid = TARGET_UID }
allowed("rootUid") { callingUid = Process.ROOT_UID }
allowed("systemUid") { callingUid = Process.SYSTEM_UID }
skip { callingUid = INVALID_ACTOR_UID }
},
ActorState.MISSING_TARGET_OVERLAYABLE_NAME withCases {
failure("nullTargetOverlayableName") {
overlayInfoParams.targetOverlayableName = null
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
}
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
},
ActorState.MISSING_LEGACY_PERMISSION withCases {
failure("noPermission") {
overlayInfoParams.targetOverlayableName = null
targetOverlayableInfo = null
hasPermission = false
}
allowed("hasPermission") { hasPermission = true }
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
},
ActorState.ERROR_READING_OVERLAYABLE withCases {
failure("doesTargetDefineOverlayableIOException") {
overlayInfoParams.targetOverlayableName = null
whenever(doesTargetDefineOverlayable(TARGET_PKG, USER_ID))
.thenThrow(IOException::class.java)
}
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
},
ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE withCases {
failure("getOverlayableForTargetIOException") {
whenever(getOverlayableForTarget(TARGET_PKG, OVERLAYABLE_NAME,
USER_ID)).thenThrow(IOException::class.java)
}
},
ActorState.MISSING_OVERLAYABLE withCases {
failure("nullTargetOverlayableInfo") { targetOverlayableInfo = null }
skip {
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
}
},
ActorState.MISSING_LEGACY_PERMISSION withCases {
failure("noPermissionNullActor") {
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
hasPermission = false
}
failure("noPermissionEmptyActor") {
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "")
hasPermission = false
}
allowed("hasPermissionNullActor") {
hasPermission = true
}
skip {
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
}
},
ActorState.INVALID_OVERLAYABLE_ACTOR_NAME withCases {
fun TestState.mockActor(actorUri: String) {
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, actorUri)
}
failure("wrongScheme") {
mockActor("notoverlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
}
failure("extraPath") {
mockActor("overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME/extraPath")
}
failure("missingPath") { mockActor("overlay://$VALID_NAMESPACE") }
failure("missingAuthority") { mockActor("overlay://") }
skip { mockActor("overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME") }
},
ActorState.NO_NAMED_ACTORS withCases {
failure("empty") { namedActorsMap = emptyMap() }
skip {
namedActorsMap = mapOf(INVALID_NAMESPACE to
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
}
},
ActorState.MISSING_NAMESPACE withCases {
failure("invalidNamespace") {
namedActorsMap = mapOf(INVALID_NAMESPACE to
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
}
skip {
namedActorsMap = mapOf(VALID_NAMESPACE to
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
}
},
ActorState.MISSING_ACTOR_NAME withCases {
failure("invalidActorName") {
namedActorsMap = mapOf(VALID_NAMESPACE to
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
}
skip {
namedActorsMap = mapOf(VALID_NAMESPACE to
mapOf(VALID_ACTOR_NAME to VALID_ACTOR_PKG))
}
},
ActorState.MISSING_APP_INFO withCases {
failure("nullActorPkgInfo") { actorPkgInfo = null }
failure("nullActorAppInfo") {
actorPkgInfo = PackageInfo().apply { applicationInfo = null }
}
skip { actorPkgInfo = pkgInfo(VALID_ACTOR_PKG) }
},
ActorState.ACTOR_NOT_PREINSTALLED withCases {
failure("notSystem") {
actorPkgInfo = pkgInfo(VALID_ACTOR_PKG).apply {
applicationInfo.flags = 0
}
}
skip {
actorPkgInfo = pkgInfo(VALID_ACTOR_PKG).apply {
applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM
}
}
},
ActorState.INVALID_ACTOR withCases {
failure("invalidUid") { callingUid = INVALID_ACTOR_UID }
skip { callingUid = VALID_ACTOR_UID }
},
ActorState.ALLOWED withCases {
// No point making an exception for this case in all of the test code, so
// just pretend this is a failure that results in a success result code.
failure("allowed") { /* Do nothing */ }
}
)
data class OverlayInfoParams(
var targetPackageName: String = TARGET_PKG,
var targetOverlayableName: String? = null
) {
fun toOverlayInfo() = OverlayInfo(
OVERLAY_PKG,
targetPackageName,
targetOverlayableName,
null,
"/path",
OverlayInfo.STATE_UNKNOWN, 0,
0, false)
}
private infix fun ActorState.withCases(block: TestCase.() -> Unit) =
TestCase(this).apply(block)
private fun pkgInfo(pkgName: String): PackageInfo = mockThrowOnUnmocked {
this.packageName = pkgName
this.applicationInfo = ApplicationInfo().apply {
this.packageName = pkgName
}
}
private fun makeTestName(testCase: TestCase, caseName: String, type: Params.Type): String {
val resultSuffix = if (type == Params.Type.ALLOWED) "allowed" else "failed"
return "${testCase.state}_${resultSuffix}_$caseName"
}
}
@Parameterized.Parameter(0)
lateinit var params: Params
@Test
fun verify() {
// Apply all the skip states before the failure to be verified
val testState = CASES.take(params.index)
.fold(TestState.create()) { testState, case ->
testState.apply(case.skip)
}
// If testing an allowed branch, first apply a failure to ensure it fails
if (params.type == Params.Type.ALLOWED) {
CASES[params.index].failures.firstOrNull()?.second?.run(testState::apply)
assertThat(testState.toResult()).isNotEqualTo(ActorState.ALLOWED)
}
// Apply the test case in the params to the collected state
testState.apply(params.function)
// Assert the result matches the expected state
assertThat(testState.toResult()).isEqualTo(params.expectedState)
}
private fun TestState.toResult() = OverlayActorEnforcer(this)
.isAllowedActor("test", overlayInfoParams.toOverlayInfo(), callingUid, USER_ID)
data class Params(
var index: Int,
var expectedState: ActorState,
val testName: String,
val type: Type,
val function: TestState.() -> Unit
) {
override fun toString() = testName
enum class Type {
FAILURE,
ALLOWED
}
}
data class TestCase(
val state: ActorState,
val failures: MutableList<Pair<String, TestState.() -> Unit>> = mutableListOf(),
var allowed: MutableList<Pair<String, TestState.() -> Unit>> = mutableListOf(),
var skip: (TestState.() -> Unit) = {}
) {
fun failure(caseName: String, block: TestState.() -> Unit) {
failures.add(caseName to block)
}
fun allowed(caseName: String, block: TestState.() -> Unit) {
allowed.add(caseName to block)
}
fun skip(block: TestState.() -> Unit) {
this.skip = block
}
}
open class TestState private constructor(
var callingUid: Int = NULL_UID,
val overlayInfoParams: OverlayInfoParams = OverlayInfoParams(),
var namedActorsMap: Map<String, Map<String, String>> = emptyMap(),
var hasPermission: Boolean = false,
var targetOverlayableInfo: OverlayableInfo? = null,
var targetPkgInfo: PackageInfo? = null,
var actorPkgInfo: PackageInfo? = null,
vararg val packageNames: String = arrayOf("com.test.actor.one")
) : PackageManagerHelper {
companion object {
// Enforce that new instances are spied
fun create() = spy(TestState())!!
}
override fun getNamedActors() = namedActorsMap
@Throws(IOException::class)
override fun getOverlayableForTarget(
packageName: String,
targetOverlayableName: String,
userId: Int
) = overlayableInfo
) = targetOverlayableInfo?.takeIf {
// Protect against this method being called with the wrong package name
targetPkgInfo == null || targetPkgInfo?.packageName == packageName
}
override fun getPackagesForUid(uid: Int) = when (uid) {
UID -> packageNames
EMPTY_UID -> emptyArray()
INVALID_ACTOR_UID -> arrayOf(INVALID_ACTOR_PKG)
VALID_ACTOR_UID -> arrayOf(VALID_ACTOR_PKG)
TARGET_UID -> arrayOf(TARGET_PKG)
NULL_UID -> null
else -> null
}
override fun getPackageInfo(packageName: String, userId: Int) = PackageInfo().apply {
applicationInfo = ApplicationInfo().apply {
flags = if (isPreInstalled) ApplicationInfo.FLAG_SYSTEM else 0
}
}
override fun getPackageInfo(packageName: String, userId: Int) =
listOfNotNull(targetPkgInfo, actorPkgInfo).find { it.packageName == packageName }
@Throws(IOException::class) // Mockito requires this checked exception to be declared
override fun doesTargetDefineOverlayable(targetPackageName: String?, userId: Int): Boolean {
return overlayableInfo != null
return targetOverlayableInfo?.takeIf {
// Protect against this method being called with the wrong package name
targetPkgInfo == null || targetPkgInfo?.packageName == targetPackageName
} != null
}
override fun enforcePermission(permission: String?, message: String?) {

View File

@@ -62,7 +62,7 @@ fun <Type : Any?> whenever(mock: Type, block: InvocationOnMock.() -> Any?) =
fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { }
inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T {
inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit = {}): T {
val swappingAnswer = object : Answer<Any?> {
var delegate: Answer<*> = Answers.RETURNS_DEFAULTS
@@ -79,4 +79,5 @@ inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T {
}
}
inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit) = spyThrowOnUnmocked<T>(null, block)
inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit = {}) =
spyThrowOnUnmocked<T>(null, block)