Allow UiAutomation to adopt the shell permission indentity
For testing we often need to run shell commands. This can be done
today via running a shell command from an instrumentation test
started from the shell. However, this requires adding shell commands
which are not in the API contract, involve boilerplate code, require
string parsing, etc.
This change allows an instrumentation started from the shell to
adopt the shell UID permission state. As a result one can call APIs
protected by permissions normal apps cannot get by are granted to
the shell. This enables adding dedicated test APIs protected by
signatures permissions granted to the shell.
Test: cts-tradefed run cts-dev -m CtsUiAutomationTestCases
-t android.app.uiautomation.cts.UiAutomationTest#testAdoptShellPermissions
bug:80415658
Change-Id: I4bfd4b475225125512abf80ea98cd8fcacb6a1be
diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java
index 1167e1d..b3f6bd1 100644
--- a/services/core/java/com/android/server/AppOpsService.java
+++ b/services/core/java/com/android/server/AppOpsService.java
@@ -22,6 +22,7 @@
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.AppOpsManagerInternal;
+import android.app.AppOpsManagerInternal.CheckOpsDelegate;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -207,6 +208,8 @@
SparseIntArray mProfileOwners;
+ private CheckOpsDelegate mCheckOpsDelegate;
+
/**
* All times are in milliseconds. These constants are kept synchronized with the system
* global Settings. Any access to this class or its fields should be done while
@@ -1411,15 +1414,39 @@
}
}
+ public CheckOpsDelegate getAppOpsServiceDelegate() {
+ synchronized (this) {
+ return mCheckOpsDelegate;
+ }
+ }
+
+ public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) {
+ synchronized (this) {
+ mCheckOpsDelegate = delegate;
+ }
+ }
+
@Override
public int checkOperation(int code, int uid, String packageName) {
- verifyIncomingUid(uid);
- verifyIncomingOp(code);
- String resolvedPackageName = resolvePackageName(uid, packageName);
- if (resolvedPackageName == null) {
- return AppOpsManager.MODE_IGNORED;
- }
+ final CheckOpsDelegate delegate;
synchronized (this) {
+ if (mCheckOpsDelegate == null) {
+ return checkOperationImpl(code, uid, packageName);
+ }
+ delegate = mCheckOpsDelegate;
+ }
+ return delegate.checkOperation(code, uid, packageName,
+ AppOpsService.this::checkOperationImpl);
+ }
+
+ private int checkOperationImpl(int code, int uid, String packageName) {
+ synchronized (this) {
+ verifyIncomingUid(uid);
+ verifyIncomingOp(code);
+ String resolvedPackageName = resolvePackageName(uid, packageName);
+ if (resolvedPackageName == null) {
+ return AppOpsManager.MODE_IGNORED;
+ }
if (isOpRestrictedLocked(uid, code, resolvedPackageName)) {
return AppOpsManager.MODE_IGNORED;
}
@@ -1439,20 +1466,33 @@
@Override
public int checkAudioOperation(int code, int usage, int uid, String packageName) {
- boolean suspended;
- try {
- suspended = isPackageSuspendedForUser(packageName, uid);
- } catch (IllegalArgumentException ex) {
- // Package not found.
- suspended = false;
- }
-
- if (suspended) {
- Slog.i(TAG, "Audio disabled for suspended package=" + packageName + " for uid=" + uid);
- return AppOpsManager.MODE_IGNORED;
- }
-
+ final CheckOpsDelegate delegate;
synchronized (this) {
+ if (mCheckOpsDelegate == null) {
+ return checkAudioOperationImpl(code, usage, uid, packageName);
+ }
+ delegate = mCheckOpsDelegate;
+ }
+ return delegate.checkAudioOperation(code, usage, uid, packageName,
+ AppOpsService.this::checkAudioOperationImpl);
+ }
+
+ private int checkAudioOperationImpl(int code, int usage, int uid, String packageName) {
+ synchronized (this) {
+ boolean suspended;
+ try {
+ suspended = isPackageSuspendedForUser(packageName, uid);
+ } catch (IllegalArgumentException ex) {
+ // Package not found.
+ suspended = false;
+ }
+
+ if (suspended) {
+ Slog.i(TAG, "Audio disabled for suspended package=" + packageName
+ + " for uid=" + uid);
+ return AppOpsManager.MODE_IGNORED;
+ }
+
final int mode = checkRestrictionLocked(code, usage, uid, packageName);
if (mode != AppOpsManager.MODE_ALLOWED) {
return mode;
@@ -1530,10 +1570,10 @@
}
@Override
- public int noteProxyOperation(int code, String proxyPackageName,
- int proxiedUid, String proxiedPackageName) {
+ public int noteProxyOperation(int code, int proxyUid,
+ String proxyPackageName, int proxiedUid, String proxiedPackageName) {
+ verifyIncomingUid(proxyUid);
verifyIncomingOp(code);
- final int proxyUid = Binder.getCallingUid();
String resolveProxyPackageName = resolvePackageName(proxyUid, proxyPackageName);
if (resolveProxyPackageName == null) {
return AppOpsManager.MODE_IGNORED;
@@ -1553,6 +1593,18 @@
@Override
public int noteOperation(int code, int uid, String packageName) {
+ final CheckOpsDelegate delegate;
+ synchronized (this) {
+ if (mCheckOpsDelegate == null) {
+ return noteOperationImpl(code, uid, packageName);
+ }
+ delegate = mCheckOpsDelegate;
+ }
+ return delegate.noteOperation(code, uid, packageName,
+ AppOpsService.this::noteOperationImpl);
+ }
+
+ private int noteOperationImpl(int code, int uid, String packageName) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
String resolvedPackageName = resolvePackageName(uid, packageName);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 89810f9..8555dd4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -170,6 +170,7 @@
import android.app.AlertDialog;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.AppOpsManagerInternal.CheckOpsDelegate;
import android.app.ApplicationErrorReport;
import android.app.ApplicationThreadConstants;
import android.app.BroadcastOptions;
@@ -225,6 +226,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageManagerInternal.CheckPermissionDelegate;
import android.content.pm.ParceledListSlice;
import android.content.pm.PathPermission;
import android.content.pm.PermissionInfo;
@@ -345,6 +347,8 @@
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriFunction;
import com.android.server.AlarmManagerInternal;
import com.android.server.AppOpsService;
import com.android.server.AttributeCache;
@@ -425,6 +429,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiFunction;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
@@ -18903,6 +18908,8 @@
// Can't call out of the system process with a lock held, so post a message.
if (app.instr.mUiAutomationConnection != null) {
+ mAppOpsService.setAppOpsServiceDelegate(null);
+ getPackageManagerInternalLocked().setCheckPermissionDelegate(null);
mHandler.obtainMessage(SHUTDOWN_UI_AUTOMATION_CONNECTION_MSG,
app.instr.mUiAutomationConnection).sendToTarget();
}
@@ -23057,4 +23064,143 @@
return mNmi != null;
}
}
+
+ @Override
+ public void startDelegateShellPermissionIdentity(int delegateUid) {
+ if (UserHandle.getCallingAppId() != Process.SHELL_UID
+ && UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only the shell can delegate its permissions");
+ }
+
+ // We allow delegation only to one instrumentation started from the shell
+ synchronized (ActivityManagerService.this) {
+ // If there is a delegate it should be the same instance for app ops and permissions.
+ if (mAppOpsService.getAppOpsServiceDelegate()
+ != getPackageManagerInternalLocked().getCheckPermissionDelegate()) {
+ throw new IllegalStateException("Bad shell delegate state");
+ }
+
+ // If the delegate is already set up for the target UID, nothing to do.
+ if (mAppOpsService.getAppOpsServiceDelegate() != null) {
+ if (!(mAppOpsService.getAppOpsServiceDelegate() instanceof ShellDelegate)) {
+ throw new IllegalStateException("Bad shell delegate state");
+ }
+ if (((ShellDelegate) mAppOpsService.getAppOpsServiceDelegate())
+ .getDelegateUid() != delegateUid) {
+ throw new SecurityException("Shell can delegate permissions only "
+ + "to one instrumentation at a time");
+ }
+ return;
+ }
+
+ final int instrCount = mActiveInstrumentation.size();
+ for (int i = 0; i < instrCount; i++) {
+ final ActiveInstrumentation instr = mActiveInstrumentation.get(i);
+ if (instr.mTargetInfo.uid != delegateUid) {
+ continue;
+ }
+ // If instrumentation started from the shell the connection is not null
+ if (instr.mUiAutomationConnection == null) {
+ throw new SecurityException("Shell can delegate its permissions" +
+ " only to an instrumentation started from the shell");
+ }
+
+ // Hook them up...
+ final ShellDelegate shellDelegate = new ShellDelegate(
+ instr.mTargetInfo.packageName, delegateUid);
+ mAppOpsService.setAppOpsServiceDelegate(shellDelegate);
+ getPackageManagerInternalLocked().setCheckPermissionDelegate(shellDelegate);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void stopDelegateShellPermissionIdentity() {
+ if (UserHandle.getCallingAppId() != Process.SHELL_UID
+ && UserHandle.getCallingAppId() != Process.ROOT_UID) {
+ throw new SecurityException("Only the shell can delegate its permissions");
+ }
+ synchronized (ActivityManagerService.this) {
+ mAppOpsService.setAppOpsServiceDelegate(null);
+ getPackageManagerInternalLocked().setCheckPermissionDelegate(null);
+ }
+ }
+
+ private class ShellDelegate implements CheckOpsDelegate, CheckPermissionDelegate {
+ private final String mTargetPackageName;
+ private final int mTargetUid;
+
+ ShellDelegate(String targetPacakgeName, int targetUid) {
+ mTargetPackageName = targetPacakgeName;
+ mTargetUid = targetUid;
+ }
+
+ int getDelegateUid() {
+ return mTargetUid;
+ }
+
+ @Override
+ public int checkOperation(int code, int uid, String packageName,
+ TriFunction<Integer, Integer, String, Integer> superImpl) {
+ if (uid == mTargetUid) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code, Process.SHELL_UID,
+ "com.android.shell");
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, uid, packageName);
+ }
+
+ @Override
+ public int checkAudioOperation(int code, int usage, int uid, String packageName,
+ QuadFunction<Integer, Integer, Integer, String, Integer> superImpl) {
+ if (uid == mTargetUid) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return superImpl.apply(code, usage, Process.SHELL_UID,
+ "com.android.shell");
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, usage, uid, packageName);
+ }
+
+ @Override
+ public int noteOperation(int code, int uid, String packageName,
+ TriFunction<Integer, Integer, String, Integer> superImpl) {
+ if (uid == mTargetUid) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return mAppOpsService.noteProxyOperation(code, Process.SHELL_UID,
+ "com.android.shell", uid, packageName);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ return superImpl.apply(code, uid, packageName);
+ }
+
+ @Override
+ public int checkPermission(String permName, String pkgName, int userId,
+ TriFunction<String, String, Integer, Integer> superImpl) {
+ if (mTargetPackageName.equals(pkgName)) {
+ return superImpl.apply(permName, "com.android.shell", userId);
+ }
+ return superImpl.apply(permName, pkgName, userId);
+ }
+
+ @Override
+ public int checkUidPermission(String permName, int uid,
+ BiFunction<String, Integer, Integer> superImpl) {
+ if (uid == mTargetUid) {
+ return superImpl.apply(permName, Process.SHELL_UID);
+ }
+ return superImpl.apply(permName, uid);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c536e4d..734a872 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -169,6 +169,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.LegacyPackageDeleteObserver;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageManagerInternal.CheckPermissionDelegate;
import android.content.pm.PackageManagerInternal.PackageListObserver;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.ActivityIntentInfo;
@@ -289,6 +290,8 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriFunction;
import com.android.server.AttributeCache;
import com.android.server.DeviceIdleController;
import com.android.server.EventLogTags;
@@ -363,6 +366,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
import java.util.function.Predicate;
import dalvik.system.CloseGuard;
@@ -1031,6 +1035,9 @@
void receiveVerificationResponse(int verificationId);
}
+ @GuardedBy("mPackages")
+ private CheckPermissionDelegate mCheckPermissionDelegate;
+
private class IntentVerifierProxy implements IntentFilterVerifier<ActivityIntentInfo> {
private Context mContext;
private ComponentName mIntentFilterVerifierComponent;
@@ -5260,11 +5267,35 @@
@Override
public int checkPermission(String permName, String pkgName, int userId) {
+ final CheckPermissionDelegate checkPermissionDelegate;
+ synchronized (mPackages) {
+ if (mCheckPermissionDelegate == null) {
+ return checkPermissionImpl(permName, pkgName, userId);
+ }
+ checkPermissionDelegate = mCheckPermissionDelegate;
+ }
+ return checkPermissionDelegate.checkPermission(permName, pkgName, userId,
+ PackageManagerService.this::checkPermissionImpl);
+ }
+
+ private int checkPermissionImpl(String permName, String pkgName, int userId) {
return mPermissionManager.checkPermission(permName, pkgName, getCallingUid(), userId);
}
@Override
public int checkUidPermission(String permName, int uid) {
+ final CheckPermissionDelegate checkPermissionDelegate;
+ synchronized (mPackages) {
+ if (mCheckPermissionDelegate == null) {
+ return checkUidPermissionImpl(permName, uid);
+ }
+ checkPermissionDelegate = mCheckPermissionDelegate;
+ }
+ return checkPermissionDelegate.checkUidPermission(permName, uid,
+ PackageManagerService.this::checkUidPermissionImpl);
+ }
+
+ private int checkUidPermissionImpl(String permName, int uid) {
synchronized (mPackages) {
final String[] packageNames = getPackagesForUid(uid);
final PackageParser.Package pkg = (packageNames != null && packageNames.length > 0)
@@ -9150,6 +9181,16 @@
}
@GuardedBy("mPackages")
+ public CheckPermissionDelegate getCheckPermissionDelegateLocked() {
+ return mCheckPermissionDelegate;
+ }
+
+ @GuardedBy("mPackages")
+ public void setCheckPermissionDelegateLocked(CheckPermissionDelegate delegate) {
+ mCheckPermissionDelegate = delegate;
+ }
+
+ @GuardedBy("mPackages")
private void notifyPackageUseLocked(String packageName, int reason) {
final PackageParser.Package p = mPackages.get(packageName);
if (p == null) {
@@ -24150,6 +24191,20 @@
PackageManagerService.this.notifyPackageUseLocked(packageName, reason);
}
}
+
+ @Override
+ public CheckPermissionDelegate getCheckPermissionDelegate() {
+ synchronized (mPackages) {
+ return PackageManagerService.this.getCheckPermissionDelegateLocked();
+ }
+ }
+
+ @Override
+ public void setCheckPermissionDelegate(CheckPermissionDelegate delegate) {
+ synchronized (mPackages) {
+ PackageManagerService.this.setCheckPermissionDelegateLocked(delegate);
+ }
+ }
}
@Override
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerInternal.java
index a042fed..ec15c16 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerInternal.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerInternal.java
@@ -21,19 +21,11 @@
import android.content.pm.PackageParser;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.PermissionInfoFlags;
-import android.content.pm.PackageParser.Permission;
-
-import com.android.server.pm.SharedUserSetting;
-import com.android.server.pm.permission.PermissionManagerInternal.PermissionCallback;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
/**
* Internal interfaces to be used by other components within the system server.