summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-14 16:38:14 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-14 16:38:14 +0000
commitccfe25a703ffeebb362d0c658cc0181f573c86dd (patch)
tree6e7ed8a11417623174cebe8164bdfab92e738b93
parentc25139786bc3b9d76d896bf6f23fc90168d33af9 (diff)
parent4d7d96a648941d07ef912c6becf1bbab822c41dd (diff)
downloadplatform_testing-aml_tz4_332714010.tar.gz
Snap for 11219529 from 4d7d96a648941d07ef912c6becf1bbab822c41dd to mainline-tzdata4-releaseaml_tz4_332714050aml_tz4_332714010aml_tz4_332714010
Change-Id: I7b752d5d13adedfe82ef7290ae178511528bd1d5
-rw-r--r--libraries/audio-test-harness/client-lib/Android.bp3
-rw-r--r--libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java86
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java184
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java93
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java80
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java277
-rw-r--r--libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java106
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java86
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java90
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java99
-rw-r--r--libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java32
-rw-r--r--libraries/sts-common-util/sts-sdk/package/README.md2
-rw-r--r--libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java123
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml8
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java98
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java13
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java38
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml23
-rw-r--r--libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml21
-rw-r--r--libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java92
-rw-r--r--tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java41
-rw-r--r--tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java76
-rw-r--r--tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java41
-rw-r--r--utils/shell-as/Android.bp107
-rw-r--r--utils/shell-as/AndroidManifest.xml.template33
-rw-r--r--utils/shell-as/OWNERS4
-rw-r--r--utils/shell-as/README.md33
-rw-r--r--utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java28
-rw-r--r--utils/shell-as/command-line.cpp210
-rw-r--r--utils/shell-as/command-line.h36
-rw-r--r--utils/shell-as/context.cpp138
-rw-r--r--utils/shell-as/context.h71
-rw-r--r--utils/shell-as/elf-utils.cpp89
-rw-r--r--utils/shell-as/elf-utils.h35
-rw-r--r--utils/shell-as/execute.cpp382
-rw-r--r--utils/shell-as/execute.h35
-rwxr-xr-xutils/shell-as/gen-manifest.sh43
-rw-r--r--utils/shell-as/registers.h44
-rw-r--r--utils/shell-as/shell-as-main.cpp93
-rw-r--r--utils/shell-as/shell-as-test-app-key.pk8bin0 -> 1218 bytes
-rw-r--r--utils/shell-as/shell-as-test-app-key.x509.pem24
-rw-r--r--utils/shell-as/shell-code.cpp82
-rw-r--r--utils/shell-as/shell-code.h40
-rw-r--r--utils/shell-as/shell-code/constants-arm.S24
-rw-r--r--utils/shell-as/shell-code/constants-arm64.S24
-rw-r--r--utils/shell-as/shell-code/constants-x86.S24
-rw-r--r--utils/shell-as/shell-code/constants-x86_64.S24
-rw-r--r--utils/shell-as/shell-code/constants.S30
-rw-r--r--utils/shell-as/shell-code/selinux-arm.S91
-rw-r--r--utils/shell-as/shell-code/selinux-arm64.S88
-rw-r--r--utils/shell-as/shell-code/selinux-x86.S91
-rw-r--r--utils/shell-as/shell-code/selinux-x86_64.S83
-rw-r--r--utils/shell-as/shell-code/trap-arm.S30
-rw-r--r--utils/shell-as/shell-code/trap-arm64.S28
-rw-r--r--utils/shell-as/shell-code/trap-x86.S28
-rw-r--r--utils/shell-as/shell-code/trap-x86_64.S28
-rw-r--r--utils/shell-as/string-utils.cpp72
-rw-r--r--utils/shell-as/string-utils.h50
-rw-r--r--utils/shell-as/test-app.cpp136
-rw-r--r--utils/shell-as/test-app.h31
60 files changed, 3917 insertions, 204 deletions
diff --git a/libraries/audio-test-harness/client-lib/Android.bp b/libraries/audio-test-harness/client-lib/Android.bp
index 5b14d101b..554e901e3 100644
--- a/libraries/audio-test-harness/client-lib/Android.bp
+++ b/libraries/audio-test-harness/client-lib/Android.bp
@@ -65,7 +65,6 @@ java_library {
java_test {
name: "audiotestharness-client-grpclib-tests",
- test_suites: ["general-tests"],
host_supported: true,
srcs: [
"src/test/java/com/android/media/audiotestharness/client/grpc/*.java",
@@ -81,6 +80,6 @@ java_test {
],
sdk_version: "current",
test_options: {
- unit_test: true,
+ unit_test: false,
},
}
diff --git a/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java b/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java
index b8e458fbf..65e5c8fcb 100644
--- a/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java
+++ b/libraries/sts-common-util/device-side/src/com/android/sts/common/SystemUtil.java
@@ -18,36 +18,49 @@ package com.android.sts.common;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assume.assumeThat;
+import static org.junit.Assume.assumeTrue;
import android.app.Instrumentation;
+import android.os.Handler;
+import android.os.HandlerThread;
import com.android.compatibility.common.util.SettingsUtils;
-import java.io.IOException;
import java.util.Optional;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
public class SystemUtil {
+ private static final String TAG = "SystemUtil";
+ public static final long DEFAULT_MAX_POLL_TIME_MS = 30_000L;
+ public static final long DEFAULT_POLL_TIME_MS = 100L;
/**
* Set the value of a device setting and set it back to old value upon closing.
*
* @param instrumentation {@link Instrumentation} instance, obtained from a test running in
- * instrumentation framework
+ * instrumentation framework
* @param namespace "system", "secure", or "global"
* @param key setting key to set
* @param value setting value to set to
* @return AutoCloseable that resets the setting back to existing value upon closing.
*/
- public static AutoCloseable withSetting(Instrumentation instrumentation, final String namespace,
- final String key, String value) {
+ public static AutoCloseable withSetting(
+ Instrumentation instrumentation,
+ final String namespace,
+ final String key,
+ String value) {
String getSettingRes = SettingsUtils.get(namespace, key);
final Optional<String> oldSetting = Optional.ofNullable(getSettingRes);
SettingsUtils.set(namespace, key, value);
String getSettingCurrent = SettingsUtils.get(namespace, key);
Optional<String> currSetting = Optional.ofNullable(getSettingCurrent);
- assumeThat(String.format("Could not set %s:%s to %s", namespace, key, value),
- currSetting.isPresent() ? currSetting.get().trim() : null, equalTo(value));
+ assumeThat(
+ String.format("Could not set %s:%s to %s", namespace, key, value),
+ currSetting.isPresent() ? currSetting.get().trim() : null,
+ equalTo(value));
return new AutoCloseable() {
@Override
@@ -61,10 +74,69 @@ public class SystemUtil {
String.format("could not reset '%s' back to '%s'", key, oldValue);
String getSettingCurrent = SettingsUtils.get(namespace, key);
Optional<String> currSetting = Optional.ofNullable(getSettingCurrent);
- assumeThat(failMsg, currSetting.isPresent() ? currSetting.get().trim() : null,
+ assumeThat(
+ failMsg,
+ currSetting.isPresent() ? currSetting.get().trim() : null,
equalTo(oldValue));
}
}
};
}
+
+ /**
+ * Poll on a condition supplied by the user.
+ *
+ * @param waitCondition returns true when the polling condition is met, false otherwise.
+ * @return boolean value of {@code waitCondition}.
+ * @throws IllegalArgumentException when {@code pollingTime} is not a positive ineteger and is
+ * not less than {@code maxPollingTime}.
+ * @throws InterruptedException if the current thread is interrupted.
+ */
+ public static boolean poll(BooleanSupplier waitCondition)
+ throws IllegalArgumentException, InterruptedException {
+ return poll(waitCondition, DEFAULT_POLL_TIME_MS, DEFAULT_MAX_POLL_TIME_MS);
+ }
+
+ /**
+ * Poll on a condition supplied by the user.
+ *
+ * @param waitCondition returns true when the polling condition is met, false otherwise.
+ * @param pollingTime wait between successive calls to fetch value of {@code waitCondition} in
+ * milliseconds
+ * @param maxPollingTime maximum waiting time before return.
+ * @return boolean value of {@code waitCondition}.
+ * @throws IllegalArgumentException when {@code pollingTime} is not a positive ineteger and is
+ * not less than {@code maxPollingTime}.
+ * @throws InterruptedException if the current thread is interrupted.
+ */
+ public static boolean poll(BooleanSupplier waitCondition, long pollingTime, long maxPollingTime)
+ throws IllegalArgumentException, InterruptedException {
+ // The value of pollingTime should be a positive integer
+ if (pollingTime <= 0) {
+ throw new IllegalArgumentException("pollingTime should be a positive integer");
+ }
+
+ // The value of pollingTime should be less than maxPollingTime
+ if (pollingTime >= maxPollingTime) {
+ throw new IllegalArgumentException("pollingTime should be less than maxPollingTime");
+ }
+
+ // Use handlerThread to run task in a separate thread.
+ final HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
+ final Semaphore semaphore = new Semaphore(0);
+ final long startTime = System.currentTimeMillis();
+ do {
+ // Check for the status.
+ if (waitCondition.getAsBoolean()) {
+ return true;
+ }
+
+ // Wait before checking status again.
+ handler.postDelayed(() -> semaphore.release(), pollingTime);
+ assumeTrue(semaphore.tryAcquire(maxPollingTime, TimeUnit.MILLISECONDS));
+ } while (System.currentTimeMillis() - startTime <= maxPollingTime);
+ return waitCondition.getAsBoolean();
+ }
}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java
new file mode 100644
index 000000000..e4997d913
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/DumpsysUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 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.sts.common;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** Util to parse dumpsys */
+public class DumpsysUtils {
+
+ /**
+ * Fetch dumpsys for the service
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param args the arguments {@link String} to filter output
+ * @return the raw output without newline character
+ * @throws Exception
+ */
+ public static String getRawDumpsys(ITestDevice device, String args) throws Exception {
+ CommandResult output = device.executeShellV2Command("dumpsys " + args);
+ if (output.getStatus() != CommandStatus.SUCCESS) {
+ throw new IllegalStateException(
+ String.format(
+ "Failed to get dumpsys for %s details, due to : %s",
+ args, output.toString()));
+ }
+ return output.getStdout();
+ }
+
+ /**
+ * Parse the dumpsys for the service using pattern
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param service the service name {@link String} to check status.
+ * @param args the argument {@link Map} to filter output
+ * @param pattern the pattern {@link String} to parse the dumpsys output
+ * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output
+ * @return the required value.
+ * @throws Exception
+ */
+ public static Matcher getParsedDumpsys(
+ ITestDevice device,
+ String service,
+ Map<String, String> args,
+ String pattern,
+ int matcherFlag)
+ throws Exception {
+ String arguments =
+ args == null
+ ? ""
+ : args.entrySet().stream()
+ .map(arg -> String.format("%s %s", arg.getKey(), arg.getValue()))
+ .collect(Collectors.joining(" "));
+ String rawOutput = getRawDumpsys(device, String.format("%s %s", service, arguments));
+ rawOutput =
+ String.join(
+ "",
+ Arrays.stream(rawOutput.split("\n"))
+ .map(e -> e.trim())
+ .toArray(String[]::new));
+ return Pattern.compile(pattern, matcherFlag).matcher(rawOutput);
+ }
+
+ /**
+ * Parse the dumpsys for the service using pattern
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param service the service name {@link String} to check status.
+ * @param pattern the pattern {@link String} to parse the dumpsys output
+ * @param matcherFlag the flags {@link String} to look while parsing the dumpsys output
+ * @return the required value.
+ * @throws Exception
+ */
+ public static Matcher getParsedDumpsys(
+ ITestDevice device, String service, String pattern, int matcherFlag) throws Exception {
+ return getParsedDumpsys(device, service, null /* args */, pattern, matcherFlag);
+ }
+
+ /**
+ * Check if output contains mResumed=true for the activity
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param activityName the activity name {@link String} to check status.
+ * @return true, if mResumed=true. Else false.
+ * @throws Exception
+ */
+ public static boolean hasActivityResumed(ITestDevice device, String activityName)
+ throws Exception {
+ return getParsedDumpsys(
+ device,
+ "activity" /* service */,
+ Map.of("-a", activityName) /* args */,
+ "mResumed=true" /* pattern */,
+ Pattern.CASE_INSENSITIVE /* matcherFlag */)
+ .find();
+ }
+
+ /**
+ * Check if output contains mVisible=true for the activity
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param activityName the activity name {@link String} to check status.
+ * @return true, if mVisible=true. Else false.
+ * @throws Exception
+ */
+ public static boolean isActivityVisible(ITestDevice device, String activityName)
+ throws Exception {
+ return getParsedDumpsys(
+ device,
+ "activity" /* service */,
+ Map.of("-a", activityName) /* args */,
+ "mVisible=true" /* pattern */,
+ Pattern.CASE_INSENSITIVE /* matcherFlag */)
+ .find();
+ }
+
+ /**
+ * Fetch the role-holder-name for the role-name under the userid
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param roleName the role name {@link String} to fetch role holder's name.
+ * @param userId the userid {@link int} to fetch role holder's name for the user.
+ * @return holder name, if exits. Else null.
+ * @throws Exception
+ */
+ public static String getRoleHolder(ITestDevice device, String roleName, int userId)
+ throws Exception {
+ // Fetch roles for the user
+ Matcher rolesMatcher =
+ getParsedDumpsys(
+ device,
+ "role" /* service */,
+ String.format("user_id=%d.+?roles=(?<roles>\\[.+?])", userId) /* pattern */,
+ Pattern.CASE_INSENSITIVE);
+ if (!rolesMatcher.find()) {
+ return null;
+ }
+
+ // Fetch the holder's name for the role
+ Matcher holderMatcher =
+ Pattern.compile(
+ String.format("\\{name=%sholders=(?<holders>.+?)}", roleName),
+ Pattern.CASE_INSENSITIVE)
+ .matcher(rolesMatcher.group("roles"));
+ if (!holderMatcher.find()) {
+ return null;
+ }
+ return holderMatcher.group("holders").trim();
+ }
+
+ /**
+ * Fetch the role-holder-name for the role-name
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @param roleName the role name {@link String} to fetch role holder's name for the current
+ * user.
+ * @return holder name, if exits. Else null.
+ * @throws Exception
+ */
+ public static String getRoleHolder(ITestDevice device, String roleName) throws Exception {
+ return getRoleHolder(device, roleName, device.getCurrentUser());
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
index 12665b2fb..214ccc0d6 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
@@ -17,9 +17,6 @@
package com.android.sts.common;
import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNoException;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
@@ -48,71 +45,89 @@ public class MallocDebug implements AutoCloseable {
Pattern.compile("^.*HAS INVALID TAG.*$", Pattern.MULTILINE),
};
- private ITestDevice device;
- private String processName;
- private AutoCloseable setMallocDebugOptionsProperty;
- private AutoCloseable setAttachedProgramProperty;
- private AutoCloseable killProcess;
+ private ITestDevice mDevice;
+ private String mProcessName;
+ private boolean mWasAdbRoot;
+ private AutoCloseable mSetMallocDebugOptionsProperty;
+ private AutoCloseable mSetAttachedProgramProperty;
+ private AutoCloseable mProcessKill;
private MallocDebug(
ITestDevice device, String mallocDebugOption, String processName, boolean isService)
throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException {
- this.device = device;
- this.processName = processName;
-
- // It's an error if this is called while something else is also doing malloc debug.
- assertNull(
- MALLOC_DEBUG_OPTIONS_PROP + " is already set!",
- device.getProperty(MALLOC_DEBUG_OPTIONS_PROP));
- CommandUtil.runAndCheck(device, "logcat -c");
+ mDevice = device;
+ mProcessName = processName;
+ mWasAdbRoot = device.isAdbRoot();
+
+ String previousProperty = device.getProperty(MALLOC_DEBUG_OPTIONS_PROP);
+ if (previousProperty != null) {
+ // log if this is called while something else is also doing malloc debug.
+ CLog.w("%s is already set! <%s>", MALLOC_DEBUG_OPTIONS_PROP, previousProperty);
+ }
try {
- this.setMallocDebugOptionsProperty =
+ mSetMallocDebugOptionsProperty =
SystemUtil.withProperty(device, MALLOC_DEBUG_OPTIONS_PROP, mallocDebugOption);
- this.setAttachedProgramProperty =
+ mSetAttachedProgramProperty =
SystemUtil.withProperty(device, MALLOC_DEBUG_PROGRAM_PROP, processName);
+ CommandUtil.runAndCheck(device, "logcat -c");
+
// Kill and wait for the process to come back if we're attaching to a service
- this.killProcess = null;
+ mProcessKill = null;
if (isService) {
- this.killProcess = ProcessUtil.withProcessKill(device, processName, null);
+ mProcessKill = ProcessUtil.withProcessKill(device, processName, null);
ProcessUtil.waitProcessRunning(device, processName);
}
} catch (Throwable e1) {
try {
- if (setMallocDebugOptionsProperty != null) {
- setMallocDebugOptionsProperty.close();
+ if (mSetMallocDebugOptionsProperty != null) {
+ mSetMallocDebugOptionsProperty.close();
}
- if (setAttachedProgramProperty != null) {
- setAttachedProgramProperty.close();
+ if (mSetAttachedProgramProperty != null) {
+ mSetAttachedProgramProperty.close();
}
} catch (Exception e2) {
CLog.e(e2);
- fail(
+ throw new IllegalStateException(
"Could not enable malloc debug. Additionally, there was an"
+ " exception while trying to reset device state. Tests after"
- + " this may not work as expected!\n"
- + e2);
+ + " this may not work as expected!",
+ e2);
}
- assumeNoException("Could not enable malloc debug", e1);
+ throw new IllegalStateException("Could not enable malloc debug", e1);
}
}
@Override
public void close() throws Exception {
- device.waitForDeviceAvailable();
- setMallocDebugOptionsProperty.close();
- setAttachedProgramProperty.close();
- if (killProcess != null) {
+ mDevice.waitForDeviceAvailable();
+ boolean isAdbRoot = mDevice.isAdbRoot();
+ if (mWasAdbRoot) {
+ // regain root permissions to teardown
+ mDevice.enableAdbRoot();
+ }
+ try {
+ mSetMallocDebugOptionsProperty.close();
+ mSetAttachedProgramProperty.close();
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not disable malloc debug", e);
+ }
+ if (mProcessKill != null) {
try {
- killProcess.close();
- ProcessUtil.waitProcessRunning(device, processName);
+ mProcessKill.close();
+ ProcessUtil.waitProcessRunning(mDevice, mProcessName);
} catch (TimeoutException e) {
- assumeNoException(
- "Could not restart '" + processName + "' after disabling malloc debug", e);
+ throw new IllegalStateException(
+ "Could not restart '" + mProcessName + "' after disabling malloc debug", e);
}
}
- String logcat = CommandUtil.runAndCheck(device, "logcat -d *:S malloc_debug:V").getStdout();
+ String logcat =
+ CommandUtil.runAndCheck(mDevice, "logcat -d *:S malloc_debug:V").getStdout();
+ if (!isAdbRoot) {
+ // restore nonroot status if the try-with-resources body unrooted
+ mDevice.disableAdbRoot();
+ }
assertNoMallocDebugErrors(logcat);
}
@@ -129,7 +144,7 @@ public class MallocDebug implements AutoCloseable {
public static AutoCloseable withLibcMallocDebugOnService(
ITestDevice device, String mallocDebugOptions, String processName)
throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
- ProcessUtil.KillException {
+ ProcessUtil.KillException {
if (processName == null || processName.isEmpty()) {
throw new IllegalArgumentException("Service processName can't be empty");
}
@@ -149,7 +164,7 @@ public class MallocDebug implements AutoCloseable {
public static AutoCloseable withLibcMallocDebugOnNewProcess(
ITestDevice device, String mallocDebugOptions, String processName)
throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
- ProcessUtil.KillException {
+ ProcessUtil.KillException {
if (processName == null || processName.isEmpty()) {
throw new IllegalArgumentException("processName can't be empty");
}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
index 87551a658..d8fd05fda 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
@@ -16,7 +16,6 @@
package com.android.sts.common;
-
import com.android.ddmlib.Log;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IFileEntry;
@@ -30,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -59,6 +59,9 @@ public final class ProcessUtil {
public static final long PROCESS_WAIT_TIMEOUT_MS = 10_000;
public static final long PROCESS_POLL_PERIOD_MS = 250;
+ public static final String[] INTENT_QUERY_CMDS = {
+ "resolve-activity", "query-activities", "query-services", "query-receivers"
+ };
private ProcessUtil() {}
@@ -180,8 +183,7 @@ public final class ProcessUtil {
* @param pid the id of the process to wait until exited
*/
public static void waitPidExited(ITestDevice device, int pid)
- throws TimeoutException, DeviceNotAvailableException,
- KillException {
+ throws TimeoutException, DeviceNotAvailableException, KillException {
waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS);
}
@@ -194,8 +196,7 @@ public final class ProcessUtil {
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void waitPidExited(ITestDevice device, int pid, long timeoutMs)
- throws TimeoutException, DeviceNotAvailableException,
- KillException {
+ throws TimeoutException, DeviceNotAvailableException, KillException {
long endTime = System.currentTimeMillis() + timeoutMs;
CommandResult res = null;
while (true) {
@@ -230,8 +231,7 @@ public final class ProcessUtil {
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void killPid(ITestDevice device, int pid, long timeoutMs)
- throws DeviceNotAvailableException, TimeoutException,
- KillException {
+ throws DeviceNotAvailableException, TimeoutException, KillException {
killPid(device, pid, 9, timeoutMs);
}
@@ -244,10 +244,8 @@ public final class ProcessUtil {
* @param timeoutMs how long to wait before throwing a TimeoutException
*/
public static void killPid(ITestDevice device, int pid, int signal, long timeoutMs)
- throws DeviceNotAvailableException, TimeoutException,
- KillException {
- CommandResult res =
- device.executeShellV2Command(String.format("kill -%d %d", signal, pid));
+ throws DeviceNotAvailableException, TimeoutException, KillException {
+ CommandResult res = device.executeShellV2Command(String.format("kill -%d %d", signal, pid));
if (res.getStatus() != CommandStatus.SUCCESS) {
String err = res.getStderr();
if (err.contains("invalid signal specification")) {
@@ -272,8 +270,7 @@ public final class ProcessUtil {
* @return whether any processes were killed
*/
public static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs)
- throws DeviceNotAvailableException, TimeoutException,
- KillException {
+ throws DeviceNotAvailableException, TimeoutException, KillException {
return killAll(device, pgrepRegex, timeoutMs, true);
}
@@ -289,8 +286,7 @@ public final class ProcessUtil {
*/
public static boolean killAll(
ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist)
- throws DeviceNotAvailableException, TimeoutException,
- KillException {
+ throws DeviceNotAvailableException, TimeoutException, KillException {
Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
if (!pids.isPresent()) {
// no pids to kill
@@ -350,8 +346,9 @@ public final class ProcessUtil {
{
try {
if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
- Log.d(LOG_TAG,
- String.format("did not kill any processes for %s", pgrepRegex));
+ Log.d(
+ LOG_TAG,
+ String.format("did not kill any processes for %s", pgrepRegex));
}
} catch (KillException e) {
Log.d(LOG_TAG, "failed to kill a process");
@@ -432,22 +429,63 @@ public final class ProcessUtil {
*
* @param device device to be run on
* @param process pgrep pattern of process to look for
- * @param filenameSubstr part of file name/path loaded by the process
+ * @param filenamePattern the filename pattern to find
* @return an Opotional of IFileEntry of the path of the file on the device if exists.
*/
public static Optional<IFileEntry> findFileLoadedByProcess(
- ITestDevice device, String process, String filenameSubstr)
+ ITestDevice device, String process, Pattern filenamePattern)
throws DeviceNotAvailableException {
Optional<Integer> pid = ProcessUtil.pidOf(device, process);
if (pid.isPresent()) {
- String cmd = "lsof -p " + pid.get().toString() + " | awk '{print $NF}'";
+ String cmd = "lsof -p " + pid.get().toString() + " | grep -o '/.*'";
String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n");
for (String f : openFiles) {
- if (f.contains(filenameSubstr)) {
+ if (f.contains("Permission denied")) {
+ throw new IllegalStateException("no permission to read open files for process");
+ }
+ if (filenamePattern.matcher(f).find()) {
return Optional.of(device.getFileEntry(f.trim()));
}
}
}
return Optional.empty();
}
+
+ /*
+ * To get application process pids of all applications that can handle the target intent
+ * @param queryCmd Query command to be used. One of the values present in INTENT_QUERY_CMDS
+ * @param intentOptions Map of intent option to value for target intent
+ * @param device device to be run on
+ * @return Optional Map of pid to process name of application processes that can handle the
+ target intent
+ */
+ public static Optional<Map<Integer, String>> getAllProcessIdsFromComponents(
+ String queryCmd, Map<String, String> intentOptions, ITestDevice device)
+ throws DeviceNotAvailableException, RuntimeException {
+ if (!Arrays.asList(INTENT_QUERY_CMDS).contains(queryCmd)) {
+ throw new RuntimeException("Unknown command " + queryCmd);
+ }
+ String cmd = "pm " + queryCmd + " ";
+ for (Map.Entry<String, String> entry : intentOptions.entrySet()) {
+ cmd += entry.getKey() + " " + entry.getValue() + " ";
+ }
+ CommandResult result = device.executeShellV2Command(cmd);
+ String resultString = result.getStdout();
+ Log.i(LOG_TAG, String.format("Executed cmd: %s \nOutput: %s", cmd, resultString));
+
+ // As target string (process name) is coming from system itself, regex here only checks for
+ // presence of valid characters in process name and not for the actual order of characters
+ Pattern processNamePattern = Pattern.compile("processName=(?<name>[a-zA-Z0-9_\\.:]+)");
+ Matcher matcher = processNamePattern.matcher(resultString);
+ Map<Integer, String> pidNameMap = new HashMap<Integer, String>();
+ while (matcher.find()) {
+ String process = matcher.group("name");
+ pidsOf(device, process)
+ .ifPresent(
+ (pids) -> {
+ pidNameMap.putAll(pids);
+ });
+ }
+ return pidNameMap.isEmpty() ? Optional.empty() : Optional.of(pidNameMap);
+ }
}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java
new file mode 100644
index 000000000..7f8d8bc86
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/UserUtils.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 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.sts.common;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Util to manage secondary user */
+public class UserUtils {
+
+ public static class SecondaryUser {
+ private ITestDevice mDevice;
+ private String mName; // Name of the new user
+ private boolean mIsDemo; // User type : --demo
+ private boolean mIsEphemeral; // User type : --ephemeral
+ private boolean mIsForTesting; // User type : --for-testing
+ private boolean mIsGuest; // User type : --guest
+ private boolean mIsManaged; // User type : --managed
+ private boolean mIsPreCreateOnly; // User type : --pre-created-only
+ private boolean mIsRestricted; // User type : --restricted
+ private boolean mSwitch; // Switch to newly created user
+ private int mProfileOf; // Userid associated with managed user
+ private int mTestUserId;
+ private Map<String, String> mUserRestrictions; // Map of user-restrictions for new user
+
+ /**
+ * Create an instance of secondary user.
+ *
+ * @param device the device {@link ITestDevice} to use.
+ * @throws Exception
+ */
+ public SecondaryUser(ITestDevice device) throws Exception {
+ // Device should not be null
+ if (device == null) {
+ throw new IllegalArgumentException("Device should not be null");
+ }
+
+ // Check if device supports multiple users
+ if (!device.isMultiUserSupported()) {
+ throw new IllegalStateException("Device does not support multiple users");
+ }
+
+ mDevice = device;
+ mName = "testUser"; /* Default username */
+ mUserRestrictions = new HashMap<String, String>();
+
+ // Set default value for all flags as false
+ mIsDemo = false;
+ mIsEphemeral = false;
+ mIsForTesting = false;
+ mIsGuest = false;
+ mIsManaged = false;
+ mIsPreCreateOnly = false;
+ mIsRestricted = false;
+ mSwitch = false;
+ }
+
+ /**
+ * Set the user type as demo.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser demo() {
+ mIsDemo = true;
+ return this;
+ }
+
+ /**
+ * Set the user type as ephemeral.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser ephemeral() {
+ mIsEphemeral = true;
+ return this;
+ }
+
+ /**
+ * Set the user type as for-testing.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser forTesting() {
+ mIsForTesting = true;
+ return this;
+ }
+
+ /**
+ * Set the user type as guest.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser guest() {
+ mIsGuest = true;
+ return this;
+ }
+
+ /**
+ * Set the user type as managed.
+ *
+ * @param profileOf value is set as the userid associated with managed user.
+ * @return this object for method chaining.
+ */
+ public SecondaryUser managed(int profileOf) {
+ mIsManaged = true;
+ mProfileOf = profileOf;
+ return this;
+ }
+
+ /**
+ * Set the user type as pre-created-only.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser preCreateOnly() {
+ mIsPreCreateOnly = true;
+ return this;
+ }
+
+ /**
+ * Set the user type as restricted.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser restricted() {
+ mIsRestricted = true;
+ return this;
+ }
+
+ /**
+ * Set the name of the new user.
+ *
+ * @param name value is set to name of the user.
+ * @return this object for method chaining.
+ * @throws IllegalArgumentException when {@code name} is null.
+ */
+ public SecondaryUser name(String name) throws IllegalArgumentException {
+ // The argument 'name' should not be null
+ if (mName == null) {
+ throw new IllegalArgumentException("The name of the user should not be null");
+ }
+
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Set if switching to newly created user is required.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser doSwitch() {
+ mSwitch = true;
+ return this;
+ }
+
+ /**
+ * Set user-restrictions on newly created secondary user.
+ * Note: Setting user-restrictions requires enabling root.
+ *
+ * @return this object for method chaining.
+ */
+ public SecondaryUser withUserRestrictions(Map<String, String> restrictions) {
+ mUserRestrictions.putAll(restrictions);
+ return this;
+ }
+
+ /**
+ * Create a secondary user and if required, switch to it. Returns an Autocloseable that
+ * removes the secondary user.
+ *
+ * @return AutoCloseable that switches back to the caller user if required, and removes the
+ * secondary user.
+ * @throws Exception
+ */
+ public AutoCloseable withUser() throws Exception {
+ // Fetch the caller's user id
+ final int callerUserId = mDevice.getCurrentUser();
+
+ // Command to create user
+ String command =
+ "pm create-user "
+ + (mIsDemo ? "--demo " : "")
+ + (mIsEphemeral ? "--ephemeral " : "")
+ + (mIsGuest ? "--guest " : "")
+ + (mIsManaged ? ("--profileOf " + mProfileOf + " --managed ") : "")
+ + (mIsPreCreateOnly ? "--pre-create-only " : "")
+ + (mIsRestricted ? "--restricted " : "")
+ + (mIsForTesting && mDevice.getApiLevel() >= 34 ? "--for-testing " : "")
+ + mName;
+
+ // Create a new user
+ final CommandResult output = mDevice.executeShellV2Command(command);
+ if (output.getStatus() != CommandStatus.SUCCESS) {
+ throw new IllegalStateException(
+ String.format("Failed to create user, due to : %s", output.toString()));
+ }
+ final String outputStdout = output.getStdout();
+ mTestUserId =
+ Integer.parseInt(outputStdout.substring(outputStdout.lastIndexOf(" ")).trim());
+
+ AutoCloseable asSecondaryUser =
+ () -> {
+ // Switch back to the caller user if required and the user type is
+ // neither managed nor pre-created-only
+ if (mSwitch && !mIsManaged && !mIsPreCreateOnly) {
+ mDevice.switchUser(callerUserId);
+ }
+
+ // Stop and remove the user if user type is not ephemeral
+ if (!mIsEphemeral) {
+ mDevice.stopUser(mTestUserId);
+ mDevice.removeUser(mTestUserId);
+ }
+ };
+
+ // Start the user
+ if (!mDevice.startUser(mTestUserId, true /* waitFlag */)) {
+ // Remove the user
+ asSecondaryUser.close();
+ throw new IllegalStateException(
+ String.format("Failed to start the user: %s", mTestUserId));
+ }
+
+ // Add user-restrictions to newly created secondary user
+ if (!mUserRestrictions.isEmpty()) {
+ if (!mDevice.isAdbRoot()) {
+ throw new IllegalStateException("Setting user-restriction requires root");
+ }
+
+ for (Map.Entry<String, String> entry : mUserRestrictions.entrySet()) {
+ final CommandResult cmdOutput =
+ mDevice.executeShellV2Command(
+ String.format(
+ "pm set-user-restriction --user %d %s %s",
+ mTestUserId, entry.getKey(), entry.getValue()));
+ if (cmdOutput.getStatus() != CommandStatus.SUCCESS) {
+ asSecondaryUser.close();
+ throw new IllegalStateException(
+ String.format(
+ "Failed to set user restriction %s value %s with"
+ + " message %s",
+ entry.getKey(), entry.getValue(), cmdOutput.toString()));
+ }
+ }
+ }
+
+ // Switch to the user if required and the user type is neither managed nor
+ // pre-created-only
+ if (mSwitch && !mIsManaged && !mIsPreCreateOnly && !mDevice.switchUser(mTestUserId)) {
+ // Stop and remove the user
+ asSecondaryUser.close();
+ throw new IllegalStateException(
+ String.format("Failed to switch the user: %s", mTestUserId));
+ }
+ return asSecondaryUser;
+ }
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java
new file mode 100644
index 000000000..25252d12a
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/KernelVersionHost.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 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.sts.common.util;
+
+import com.android.sts.common.CommandUtil;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+
+/** Tools for comparing kernel versions */
+public final class KernelVersionHost {
+
+ private KernelVersionHost() {}
+
+ /**
+ * Get the device kernel version
+ *
+ * @param device The device to collect the kernel version from
+ */
+ public static KernelVersion getKernelVersion(ITestDevice device)
+ throws DeviceNotAvailableException {
+ // https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
+ // uname is part of Android since 6.0 Marshmallow
+ CommandResult res = CommandUtil.runAndCheck(device, "uname -r");
+ return KernelVersion.parse(res.getStdout());
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) == 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionLessThan(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) < 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionLessThanEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) <= 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionGreaterThan(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) > 0;
+ }
+
+ /**
+ * Helper for BusinessLogic
+ *
+ * @param device The device to test against
+ * @param testVersion The kernel version string for comparison. "version.patchlevel.sublevel"
+ */
+ public static boolean isKernelVersionGreaterThanEqualTo(ITestDevice device, String testVersion)
+ throws DeviceNotAvailableException {
+ KernelVersion deviceKernelVersion = getKernelVersion(device);
+ KernelVersion testKernelVersion = KernelVersion.parse(testVersion);
+ return deviceKernelVersion.compareTo(testKernelVersion) >= 0;
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
index 03b19341d..f82a90756 100644
--- a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
@@ -16,9 +16,16 @@
package com.android.sts.common;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import org.junit.After;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,6 +52,26 @@ public class MallocDebugTest extends BaseHostJUnit4Test {
}
}
+ @Before
+ public void setUp() throws Exception {
+ assertWithMessage("libc.debug.malloc.options not empty before test")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isNull();
+ assertWithMessage("libc.debug.malloc.programs not empty before test")
+ .that(getDevice().getProperty("libc.debug.malloc.programs"))
+ .isNull();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertWithMessage("libc.debug.malloc.options not empty after test")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isNull();
+ assertWithMessage("libc.debug.malloc.programs not empty after test")
+ .that(getDevice().getProperty("libc.debug.malloc.programs"))
+ .isNull();
+ }
+
@Test(expected = Test.None.class /* no exception expected */)
public void testMallocDebugNoErrors() throws Exception {
MallocDebug.assertNoMallocDebugErrors(logcatWithoutErrors);
@@ -54,4 +81,63 @@ public class MallocDebugTest extends BaseHostJUnit4Test {
public void testMallocDebugWithErrors() throws Exception {
MallocDebug.assertNoMallocDebugErrors(logcatWithErrors);
}
+
+ @Test(expected = IllegalStateException.class)
+ public void testMallocDebugAutocloseableNonRoot() throws Exception {
+ assertTrue(getDevice().disableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ // empty
+ }
+ }
+
+ @Test
+ public void testMallocDebugAutocloseableRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ // empty
+ }
+ }
+
+ @Test
+ public void testMallocDebugAutocloseableNonRootCleanup() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(), "backtrace guard", "native-poc")) {
+ assertTrue("could not disable root", getDevice().disableAdbRoot());
+ }
+ assertFalse(
+ "device should not be root after autoclose if the body unrooted",
+ getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testMallocDebugAutoseablePriorValueNoException() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ final String oldValue = "TEST_VALUE_OLD";
+ final String newValue = "TEST_VALUE_NEW";
+ assertTrue(
+ "could not set libc.debug.malloc.options",
+ getDevice().setProperty("libc.debug.malloc.options", oldValue));
+ assertWithMessage("test property was not properly set on device")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isEqualTo(oldValue);
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(getDevice(), newValue, "native-poc")) {
+ assertWithMessage("new property was not set during malloc debug body")
+ .that(getDevice().getProperty("libc.debug.malloc.options"))
+ .isEqualTo(newValue);
+ }
+ String afterValue = getDevice().getProperty("libc.debug.malloc.options");
+ assertTrue(
+ "could not clear libc.debug.malloc.options",
+ getDevice().setProperty("libc.debug.malloc.options", ""));
+ assertWithMessage("prior property was not restored after test")
+ .that(afterValue)
+ .isEqualTo(oldValue);
+ }
}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java
new file mode 100644
index 000000000..c6f4c6963
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/ProcessUtilTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 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.sts.common;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/** Unit tests for {@link ProcessUtil}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ProcessUtilTest extends BaseHostJUnit4Test {
+
+ @Before
+ public void setUp() throws Exception {
+ assertTrue("could not unroot", getDevice().disableAdbRoot());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertTrue("could not unroot", getDevice().disableAdbRoot());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testFindLoadedByProcessNonRoot() throws Exception {
+ // expect failure because the shell user has no permission to read process info of other
+ // users
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFindLoadedByProcessMultipleProcesses() throws Exception {
+ // pattern 'android' has multiple (android.hardware.drm, android.hardware.gnss, etc)
+ ProcessUtil.findFileLoadedByProcess(getDevice(), "android", null);
+ }
+
+ @Test
+ public void testFindLoadedByProcessUtilRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ Optional<IFileEntry> fileEntryOptional =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "system_server", Pattern.compile(Pattern.quote("libc.so")));
+ assertWithMessage("file entry should not be empty")
+ .that(fileEntryOptional.isPresent())
+ .isTrue();
+ IFileEntry fileEntry = fileEntryOptional.get();
+ assertWithMessage("file entry should be a path to libc.so")
+ .that(fileEntry.getFullPath())
+ .contains("libc.so");
+ }
+
+ @Test
+ public void testFindLoadedByProcessUtilNoMatch() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ Optional<IFileEntry> fileEntryOptional =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(),
+ "system_server",
+ Pattern.compile(Pattern.quote("doesnotexist.foobar")));
+ assertWithMessage("file entry should be empty if no matches")
+ .that(fileEntryOptional.isPresent())
+ .isFalse();
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java
new file mode 100644
index 000000000..aa58f1022
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/UserUtilsTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.sts.common;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/** Unit tests for {@link UserUtils}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class UserUtilsTest extends BaseHostJUnit4Test {
+ private static final String TEST_USER_NAME = "TestUserForUserUtils";
+ private static final String CMD_PM_LIST_USERS = "pm list users";
+
+ @Before
+ public void setUp() throws Exception {
+ assertWithMessage("device already has the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .doesNotContain(TEST_USER_NAME);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ assertWithMessage("did not clean up the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .doesNotContain(TEST_USER_NAME);
+ }
+
+ @Test
+ public void testUserUtilsNonRoot() throws Exception {
+ assertTrue(getDevice().disableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) {
+ assertFalse(
+ "device should not implicitly root to create a user", getDevice().isAdbRoot());
+ assertWithMessage("did not create the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .contains(TEST_USER_NAME);
+ }
+ assertFalse("device should not implicitly root to cleanup", getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testUserUtilsRoot() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice()).name(TEST_USER_NAME).withUser()) {
+ assertTrue(
+ "device should still be root after user creation if started with root",
+ getDevice().isAdbRoot());
+ assertWithMessage("did not create the test user")
+ .that(CommandUtil.runAndCheck(getDevice(), CMD_PM_LIST_USERS).getStdout())
+ .contains(TEST_USER_NAME);
+ }
+ assertTrue(
+ "device should still be root after cleanup if started with root",
+ getDevice().isAdbRoot());
+ }
+
+ @Test
+ public void testUserUtilsUserRestriction() throws Exception {
+ assertTrue("must test with rootable device", getDevice().enableAdbRoot());
+ try (AutoCloseable user =
+ new UserUtils.SecondaryUser(getDevice())
+ .name(TEST_USER_NAME)
+ .withUserRestrictions(Map.of("test_restriction", "1"))
+ .withUser()) {
+ // Exception is thrown if any error occurs while setting user restriction above
+ }
+ assertTrue(
+ "device should still be root after cleanup if started with root",
+ getDevice().isAdbRoot());
+ }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java
new file mode 100644
index 000000000..3a4603978
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/util/KernelVersionHostTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.sts.common.util;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class KernelVersionHostTest extends BaseHostJUnit4Test {
+
+ @Test
+ public final void testGetKernelVersion() throws Exception {
+ KernelVersionHost.getKernelVersion(getDevice());
+ }
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/README.md b/libraries/sts-common-util/sts-sdk/package/README.md
index 663994aa3..41b399a14 100644
--- a/libraries/sts-common-util/sts-sdk/package/README.md
+++ b/libraries/sts-common-util/sts-sdk/package/README.md
@@ -1,2 +1,2 @@
-See https://source.android.com/docs/security/test/sts-sdK for instructions and
+See https://source.android.com/docs/security/test/sts-sdk for instructions and
documentation.
diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
index 2e27305ef..fe824cfd4 100644
--- a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
+++ b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
@@ -16,17 +16,30 @@
package android.security.sts;
-import static com.android.sts.common.CommandUtil.runAndCheck;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import com.android.sts.common.CommandUtil;
+import com.android.sts.common.MallocDebug;
import com.android.sts.common.NativePoc;
+import com.android.sts.common.NativePocCrashAsserter;
import com.android.sts.common.NativePocStatusAsserter;
+import com.android.sts.common.ProcessUtil;
+import com.android.sts.common.RegexUtils;
+import com.android.sts.common.SystemUtil;
+import com.android.sts.common.UserUtils;
import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
+import com.android.sts.common.util.TombstoneUtils;
+import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
@RunWith(DeviceJUnit4ClassRunner.class)
public class StsHostSideTestCase extends NonRootSecurityTestCase {
@@ -34,32 +47,126 @@ public class StsHostSideTestCase extends NonRootSecurityTestCase {
static final String TEST_PKG = "android.security.sts.sts_test_app_package";
static final String TEST_CLASS = TEST_PKG + "." + "DeviceTest";
+ /** An app test, which uses this host Java test to launch an Android instrumented test */
@Test
public void testWithApp() throws Exception {
- // Note: this test is for CVE-2020-0215
ITestDevice device = getDevice();
- device.enableAdbRoot();
+ assertTrue("could not disable root", device.disableAdbRoot());
uninstallPackage(device, TEST_PKG);
- runAndCheck(device, "input keyevent KEYCODE_WAKEUP");
- runAndCheck(device, "input keyevent KEYCODE_MENU");
- runAndCheck(device, "input keyevent KEYCODE_HOME");
-
installPackage(TEST_APP);
runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
}
+ /**
+ * A native PoC test, which uses this host Java test to push an executable with resources and
+ * execute with environment variables and more. This API uses a "NativePocAsserter" that handles
+ * the most common ways to retrieve data from the native PoC. It can be overloaded to handle the
+ * specific side-effect that your PoC generates. It also demonstrates how to add extra memory
+ * checking with Malloc Debug.
+ */
@Test
public void testWithNativePoc() throws Exception {
NativePoc.builder()
+ // the name of the PoC
.pocName("native-poc")
+ // extra files pushed to the device
.resources("res.txt")
+ // command-line arguments for the PoC
.args("res.txt", "arg2")
+ // other options allow different linker paths for library shims
.useDefaultLdLibraryPath(true)
+ // test ends with ASSUMPTION_FAILURE if not EXIT_OK
.assumePocExitSuccess(true)
+ // run code after the PoC is executed for cleanup or other
.after(r -> getDevice().executeShellV2Command("ls -l /"))
- .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode()) // not 113
+ // fail if the poc returns exit status 113
+ .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode())
+ .build()
+ .run(this);
+ }
+
+ /** Run native PoCs with Malloc Debug memory checking enabled */
+ @Test
+ public void testWithMallocDebug() throws Exception {
+ // Set up Malloc Debug for this test, which may be required if the vulnerability needs
+ // memory checking to crash. This is useful when an ASan/HWASan/MTE build is not available.
+ // https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
+ try (AutoCloseable mallocDebug =
+ MallocDebug.withLibcMallocDebugOnNewProcess(
+ getDevice(),
+ "backtrace guard", // malloc debug options
+ "native-poc" // process name
+ )) {
+ // run a native PoC
+ NativePoc.builder()
+ .pocName("native-poc")
+ .build() // add more as needed
+ .run(this);
+ }
+ }
+
+ /** Run code after applying device settings */
+ @Test
+ public void testWithSetting() throws Exception {
+ // allow reflection, which is not a security boundary
+ try (AutoCloseable setting =
+ SystemUtil.withSetting(getDevice(), "global", "hidden_api_policy", "1")) {
+ // run app
+ installPackage(TEST_APP);
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+ }
+ }
+
+ /** Link a native PoC against a vulnerable system library */
+ @Test
+ public void testWithVulnerableLibrary() throws Exception {
+ // get the path of the vulnerable library
+ Optional<IFileEntry> libFileEntry =
+ ProcessUtil.findFileLoadedByProcess(
+ getDevice(), "media.metrics", Pattern.quote("libmediametrics.so"));
+ assumeTrue("shared library not loaded by target process", libFileEntry.isPresent());
+
+ // attack the service
+ NativePoc.builder()
+ .pocName("native-poc")
+ // pass the library path to the PoC
+ .args(libFileEntry.get().getFullPath())
+ .asserter(
+ NativePocCrashAsserter.assertNoCrash(
+ new TombstoneUtils.Config()
+ // Because the vulnerability is in the shared library, the
+ // process crash is the PoC.
+ .setProcessPatterns(Pattern.compile("native-poc"))))
.build()
.run(this);
}
+
+ /** Match a log against a known vulnerable pattern regex */
+ @Test
+ public void testWithLogMessage() throws Exception {
+ // this is only for dmesg/logcat messages that are not controlled by the test.
+
+ // attack the device, which can be native poc, echo to socket, send intent, app, etc
+ NativePoc.builder()
+ .pocName("native-poc")
+ .build() // add more as needed
+ .run(this);
+
+ String dmesg = CommandUtil.runAndCheck(getDevice(), "dmesg -c").getStdout();
+
+ // It's preferred to use this for matching text because the regex has a timeout to
+ // protect against catastrophic backtracking. It also formats the test assert message.
+ RegexUtils.assertNotContainsMultiline(
+ "Call trace:.*?__arm_lpae_unmap.*?kgsl_iommu_unmap", dmesg);
+ }
+
+ /** Install and run an app as a secondary user */
+ @Test
+ public void testWithSecondaryUser() throws Exception {
+ try (AutoCloseable su = new UserUtils.SecondaryUser(getDevice()).restricted().withUser()) {
+ installPackage(TEST_APP);
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+ }
+ }
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
index b7f8ac87e..a16eccb9d 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
@@ -32,13 +32,5 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <receiver android:name=".PocReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="com.android.nfc.handover.action.ALLOW_CONNECT" />
- <action android:name="com.android.nfc.handover.action.DENY_CONNECT" />
- <action android:name="com.android.nfc.handover.action.TIMEOUT_CONNECT" />
- </intent-filter>
- </receiver>
</application>
</manifest>
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
index da1f7bf47..a218e81f6 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
@@ -18,56 +18,82 @@ package android.security.sts.sts_test_app_package;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeNoException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.SharedPreferences;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
-import androidx.annotation.IntegerRes;
-import androidx.annotation.StringRes;
-import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.UiDevice;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An example device test that starts an activity and uses broadcasts to wait for the artifact
+ * proving vulnerability
+ */
@RunWith(AndroidJUnit4.class)
public class DeviceTest {
+ private static final String TAG = DeviceTest.class.getSimpleName();
+ Context mContext;
- Context mAppContext;
+ /** Test broadcast action */
+ public static final String ACTION_BROADCAST = "action_security_test_broadcast";
+ /** Broadcast intent extra name for artifacts */
+ public static final String INTENT_ARTIFACT = "artifact";
- int getInteger(@IntegerRes int resId) {
- return mAppContext.getResources().getInteger(resId);
- }
+ /** Device test */
+ @Test
+ public void testDeviceSideMethod() throws Exception {
+ mContext = getApplicationContext();
- String getString(@StringRes int resId) {
- return mAppContext.getResources().getString(resId);
- }
+ AtomicReference<String> actual = new AtomicReference<>();
+ final Semaphore broadcastReceived = new Semaphore(0);
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ if (!intent.getAction().equals(ACTION_BROADCAST)) {
+ Log.i(
+ TAG,
+ String.format(
+ "got a broadcast that we didn't expect: %s",
+ intent.getAction()));
+ }
+ actual.set(intent.getStringExtra(INTENT_ARTIFACT));
+ broadcastReceived.release();
+ } catch (Exception e) {
+ Log.e(TAG, "got an exception when handling broadcast", e);
+ }
+ }
+ };
+ IntentFilter filter = new IntentFilter(); // see if there's a shorthand
+ filter.addAction(ACTION_BROADCAST); // what does this return?
+ mContext.registerReceiver(broadcastReceiver, filter);
- @Test
- public void testDeviceSideMethod() {
+ // start the target app
try {
- mAppContext = getApplicationContext();
- UiDevice device = UiDevice.getInstance(getInstrumentation());
- device.executeShellCommand(
- "am start -n com.android.nfc/.handover.ConfirmConnectActivity");
- long startTime = System.currentTimeMillis();
- while ((System.currentTimeMillis() - startTime)
- < getInteger(R.integer.MAX_WAIT_TIME_MS)) {
- SharedPreferences sh =
- mAppContext.getSharedPreferences(
- getString(R.string.SHARED_PREFERENCE), Context.MODE_APPEND);
- int result =
- sh.getInt(getString(R.string.RESULT_KEY), getInteger(R.integer.DEFAULT));
- assertNotEquals(
- "NFC Android App broadcasts Bluetooth device information!",
- result,
- getInteger(R.integer.FAIL));
- Thread.sleep(getInteger(R.integer.SLEEP_TIME_MS));
- }
- } catch (Exception e) {
- assumeNoException(e);
+ Log.d(TAG, "starting local activity");
+ Intent newActivityIntent = new Intent(mContext, PocActivity.class);
+ newActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // this could be startActivityForResult, but is generic for illustrative purposes
+ mContext.startActivity(newActivityIntent);
+ } finally {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
+ assertTrue(
+ "Timed out when getting result from other activity",
+ broadcastReceived.tryAcquire(/* TIMEOUT_MS */ 5000, TimeUnit.MILLISECONDS));
+ assertEquals("The target artifact should have been 'secure'", "secure", actual.get());
}
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
index 27d682d19..daeb76c8b 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
@@ -17,13 +17,26 @@
package android.security.sts.sts_test_app_package;
import android.app.Activity;
+import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
public class PocActivity extends Activity {
+ private static final String TAG = PocActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ Log.d(TAG, "poc activity started");
+
+ // Collect the artifact representing vulnerability here.
+ // Change this to whatever type best fits the vulnerable artifact; consider using a bundle
+ // if there are multiple artifacts necessary to prove the security vulnerability.
+ String artifact = "vulnerable";
+
+ Intent vulnerabilityDescriptionIntent = new Intent(DeviceTest.ACTION_BROADCAST);
+ vulnerabilityDescriptionIntent.putExtra(DeviceTest.INTENT_ARTIFACT, artifact);
+ this.sendBroadcast(vulnerabilityDescriptionIntent);
}
}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
deleted file mode 100644
index ac879258e..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2022 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.security.sts.sts_test_app_package;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-
-public class PocReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- SharedPreferences sh =
- context.getSharedPreferences(
- context.getResources().getString(R.string.SHARED_PREFERENCE),
- Context.MODE_PRIVATE);
- SharedPreferences.Editor edit = sh.edit();
- edit.putInt(
- context.getResources().getString(R.string.RESULT_KEY),
- context.getResources().getInteger(R.integer.FAIL));
- edit.commit();
- }
-}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml
deleted file mode 100644
index acdcd84b6..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/integers.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2022 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.
- -->
-
-<resources>
- <integer name="DEFAULT">0</integer>
- <integer name="FAIL">1</integer>
- <integer name="SLEEP_TIME_MS">500</integer>
- <integer name="MAX_WAIT_TIME_MS">10000</integer>
-</resources>
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
deleted file mode 100644
index 286e6fd69..000000000
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2022 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.
- -->
-
-<resources>
- <string name="RESULT_KEY">result</string>
- <string name="SHARED_PREFERENCE">sts_test_app_failure</string>
-</resources>
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java
new file mode 100644
index 000000000..c9329b969
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/KernelVersion.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.sts.common.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Tools for parsing kernel version strings */
+public final class KernelVersion implements Comparable<KernelVersion> {
+ public final int version;
+ public final int patchLevel;
+ public final int subLevel;
+
+ public KernelVersion(int version, int patchLevel, int subLevel) {
+ this.version = version;
+ this.patchLevel = patchLevel;
+ this.subLevel = subLevel;
+ }
+
+ /**
+ * Parse a kernel version string in the format "version.patchlevel.sublevel" - "5.4.123".
+ * Trailing values are ignored so `uname -r` can be parsed properly.
+ *
+ * @param versionString The version string to parse
+ */
+ public static KernelVersion parse(String versionString) {
+ Pattern kernelReleasePattern =
+ Pattern.compile("(?<version>\\d+)\\.(?<patchLevel>\\d+)\\.(?<subLevel>\\d+)(.*)");
+ Matcher matcher = kernelReleasePattern.matcher(versionString);
+ if (matcher.find()) {
+ return new KernelVersion(
+ Integer.parseInt(matcher.group("version")),
+ Integer.parseInt(matcher.group("patchLevel")),
+ Integer.parseInt(matcher.group("subLevel")));
+ }
+ throw new IllegalArgumentException(
+ String.format("Could not parse kernel version string (%s)", versionString));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ // 2147483647 (INT_MAX)
+ // vvppppssss
+ return version * 10000000 + patchLevel * 10000 + subLevel;
+ }
+
+ /** Compare by version, patchlevel, and sublevel in that order. */
+ public int compareTo(KernelVersion o) {
+ if (version != o.version) {
+ return Integer.compare(version, o.version);
+ }
+ if (patchLevel != o.patchLevel) {
+ return Integer.compare(patchLevel, o.patchLevel);
+ }
+ return Integer.compare(subLevel, o.subLevel);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof KernelVersion) {
+ return this.compareTo((KernelVersion) o) == 0;
+ }
+ return false;
+ }
+
+ /** Format as "version.patchlevel.sublevel" */
+ @Override
+ public String toString() {
+ return String.format("%d.%d.%d", version, patchLevel, subLevel);
+ }
+
+ /** Format as "version.patchlevel" */
+ public String toStringShort() {
+ return String.format("%d.%d", version, patchLevel);
+ }
+}
diff --git a/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java b/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java
new file mode 100644
index 000000000..5f227f012
--- /dev/null
+++ b/tests/automotive/health/rules/src/android/platform/scenario/ColdAppStartupRunRule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.platform.test.scenario;
+
+import android.platform.helpers.IAppHelper;
+import android.platform.test.rule.DropCachesRule;
+import android.platform.test.rule.KillAppsRule;
+
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class ColdAppStartupRunRule<T extends IAppHelper> implements TestRule {
+ private final RuleChain mRuleChain;
+
+ public ColdAppStartupRunRule(T appHelper) {
+ mRuleChain =
+ RuleChain.outerRule(new KillAppsRule(appHelper.getPackage()))
+ .around(new DropCachesRule())
+ .around(new SleepAtTestStartRule(3000))
+ .around(new SleepAtTestFinishRule(3000));
+ }
+
+ public Statement apply(final Statement base, final Description description) {
+ return mRuleChain.apply(base, description);
+ }
+}
diff --git a/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java b/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java
new file mode 100644
index 000000000..273a3b28c
--- /dev/null
+++ b/tests/automotive/health/rules/src/android/platform/scenario/HotAppStartupRunRule.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.platform.test.scenario;
+
+import android.platform.helpers.IAppHelper;
+import android.platform.test.rule.TestWatcher;
+import android.util.Log;
+
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class HotAppStartupRunRule<T extends IAppHelper> implements TestRule {
+ private final RuleChain mRuleChain;
+
+ public HotAppStartupRunRule(T appHelper) {
+ mRuleChain =
+ RuleChain.outerRule(new SwitchOutAppRule())
+ .around(new SleepAtTestStartRule(2000))
+ .around(new SleepAtTestFinishRule(3000));
+ }
+
+ public Statement apply(final Statement base, final Description description) {
+ return mRuleChain.apply(base, description);
+ }
+
+ // Custom rule to move away from app under test
+ private static class SwitchOutAppRule extends TestWatcher {
+ private static final String GO_HOME_PARAM_NAME = "go-home";
+ private static final String GO_HOME_DEFAULT = "true";
+ private static final String LAUNCHER_PARAM_NAME = "app-package";
+ private static final String CARLAUNCHER_PACKAGE = "com.android.car.carlauncher";
+ private static final String APP_ACTIVITY_PARAM_NAME = "app-activity";
+ private static final String APP_GRID_ACTIVITY =
+ "com.android.car.carlauncher.AppGridActivity";
+ private static final String LOG_TAG = SwitchOutAppRule.class.getSimpleName();
+
+ private boolean mGoHome;
+ private String mAppPackage;
+ private String mAppActivity;
+
+ @Override
+ protected void starting(Description description) {
+ mGoHome =
+ Boolean.parseBoolean(
+ getArguments().getString(GO_HOME_PARAM_NAME, GO_HOME_DEFAULT));
+ mAppPackage = getArguments().getString(LAUNCHER_PARAM_NAME, CARLAUNCHER_PACKAGE);
+ mAppActivity = getArguments().getString(APP_ACTIVITY_PARAM_NAME, APP_GRID_ACTIVITY);
+
+ // Default behavior is to press home
+ if (mGoHome) {
+ Log.v(LOG_TAG, "Pressing home");
+ getUiDevice().pressHome();
+ } else {
+ Log.i(LOG_TAG, String.format("Starting %s/%s", mAppPackage, mAppActivity));
+ String openAppGridCommand =
+ String.format("am start -n %s/%s", mAppPackage, mAppActivity);
+ executeShellCommand(openAppGridCommand);
+ }
+ }
+ }
+}
diff --git a/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java
new file mode 100644
index 000000000..8d8f8c540
--- /dev/null
+++ b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestStartRule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.platform.test.scenario;
+
+import android.os.SystemClock;
+import android.platform.test.rule.TestWatcher;
+import android.util.Log;
+
+import org.junit.runner.Description;
+
+/** This rule will sleep for a given amount of time at the start of each test method. */
+public class SleepAtTestStartRule extends TestWatcher {
+ private static final String LOG_TAG = SleepAtTestStartRule.class.getSimpleName();
+
+ private final long mMillis;
+
+ public SleepAtTestStartRule(long millis) {
+ mMillis = millis;
+ }
+
+ @Override
+ protected void starting(Description description) {
+ Log.v(LOG_TAG, String.format("Sleeping for %d ms", mMillis));
+ SystemClock.sleep(mMillis);
+ Log.v(LOG_TAG, String.format("Done sleeping for %d ms", mMillis));
+ }
+}
diff --git a/utils/shell-as/Android.bp b/utils/shell-as/Android.bp
new file mode 100644
index 000000000..96dc1c9d5
--- /dev/null
+++ b/utils/shell-as/Android.bp
@@ -0,0 +1,107 @@
+// Copyright (C) 2023 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.
+
+cc_binary {
+ name: "shell-as",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wextra",
+ ],
+ srcs: [
+ "*.cpp",
+ ":shell-as-test-app-apk-cpp",
+ ],
+ header_libs: ["libcutils_headers"],
+ static_executable: true,
+ static_libs: [
+ "libbase",
+ "libcap",
+ "liblog",
+ "libseccomp_policy",
+ "libselinux",
+ ],
+ arch: {
+ arm: {
+ srcs: ["shell-code/*-arm.S"]
+ },
+ arm64: {
+ srcs: ["shell-code/*-arm64.S"]
+ },
+ x86: {
+ srcs: ["shell-code/*-x86.S"]
+ },
+ x86_64: {
+ srcs: ["shell-code/*-x86_64.S"]
+ }
+ }
+}
+
+// A simple app that requests all non-system permissions and contains no other
+// functionality. This can be used as a target for shell-as to emulate the
+// security context of the most privileged possible non-system app.
+android_app {
+ name: "shell-as-test-app",
+ manifest: ":shell-as-test-app-manifest",
+ srcs: ["app/**/*.java"],
+ sdk_version: "9",
+ certificate: ":shell-as-test-app-cert",
+}
+
+// https://source.android.com/docs/core/ota/sign_builds#release-keys
+// Generated by running:
+// $ANDROID_BUILD_TOP/development/tools/make_key \
+// shell-as-test-app-key \
+// '/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com
+android_app_certificate {
+ name: "shell-as-test-app-cert",
+ certificate: "shell-as-test-app-key",
+}
+
+genrule {
+ name: "shell-as-test-app-manifest",
+ srcs: [
+ ":permission-list-normal",
+ "AndroidManifest.xml.template"
+ ],
+ cmd: "$(location gen-manifest.sh) " +
+ "$(location AndroidManifest.xml.template) " +
+ "$(location :permission-list-normal) " +
+ "$(out)",
+ out: ["AndroidManifest.xml"],
+ tool_files: ["gen-manifest.sh"],
+}
+
+// A source file that contains the contents of the above shell-as-test-app APK
+// embedded as an array.
+cc_genrule {
+ name: "shell-as-test-app-apk-cpp",
+ srcs: [":shell-as-test-app"],
+ cmd: "(" +
+ " echo '#include <stddef.h>';" +
+ " echo '#include <stdint.h>';" +
+ " echo '';" +
+ " echo 'namespace shell_as {';" +
+ " echo 'const uint8_t kTestAppApk[] = {';" +
+ " $(location toybox) xxd -i < $(in);" +
+ " echo '};';" +
+ " echo 'void GetTestApk(uint8_t **apk, size_t *length) {';" +
+ " echo ' *apk = (uint8_t*) kTestAppApk;';" +
+ " echo ' *length = sizeof(kTestAppApk);';" +
+ " echo '}';" +
+ " echo '} // namespace shell_as';" +
+ ") > $(out)",
+ out: ["test-app-apk.cpp"],
+ tools: ["toybox"]
+}
diff --git a/utils/shell-as/AndroidManifest.xml.template b/utils/shell-as/AndroidManifest.xml.template
new file mode 100644
index 000000000..07e89b186
--- /dev/null
+++ b/utils/shell-as/AndroidManifest.xml.template
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.google.tools.security.shell_as">
+
+ PERMISSIONS
+
+ <application
+ android:allowBackup="true"
+ android:label="Shell-As Test App">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/utils/shell-as/OWNERS b/utils/shell-as/OWNERS
new file mode 100644
index 000000000..431db9920
--- /dev/null
+++ b/utils/shell-as/OWNERS
@@ -0,0 +1,4 @@
+# Code owners for shell-as
+
+willcoster@google.com
+cdombroski@google.com
diff --git a/utils/shell-as/README.md b/utils/shell-as/README.md
new file mode 100644
index 000000000..e0f6f93f2
--- /dev/null
+++ b/utils/shell-as/README.md
@@ -0,0 +1,33 @@
+# shell-as
+
+shell-as is a utility that can be used to execute a binary in a less privileged
+security context. This can be useful for verifying the capabilities of a process
+on a running device or testing PoCs with different privilege levels.
+
+## Usage
+
+The security context can either be supplied explicitly, inferred from a process
+running on the device, or set to a predefined profile.
+
+For example, the following are equivalent and execute `/system/bin/id` in the
+context of the init process.
+
+```shell
+shell-as \
+ --uid 0 \
+ --gid 0 \
+ --selinux u:r:init:s0 \
+ --seccomp system \
+ /system/bin/id
+```
+
+```shell
+shell-as --pid 1 /system/bin/id
+```
+
+The "untrusted-app" profile can be used to execute a binary with all the
+possible privileges attainable by an untrusted app:
+
+```shell
+shell-as --profile untrusted-app /system/bin/id
+```
diff --git a/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java
new file mode 100644
index 000000000..d5d178c2f
--- /dev/null
+++ b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.google.tools.security.shell_as;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/** An empty activity for the shell-as test app. */
+public class MainActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/utils/shell-as/command-line.cpp b/utils/shell-as/command-line.cpp
new file mode 100644
index 000000000..9a893c375
--- /dev/null
+++ b/utils/shell-as/command-line.cpp
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./command-line.h"
+
+#include <getopt.h>
+
+#include <iostream>
+#include <string>
+
+#include "./context.h"
+#include "./string-utils.h"
+
+namespace shell_as {
+
+namespace {
+const std::string kUsage =
+ R"(Usage: shell-as [options] [<program> <arguments>...]
+
+shell-as executes a program in a specified Android security context. The default
+program that is executed if none is specified is `/bin/system/sh`.
+
+The following options can be used to define the target security context.
+
+--verbose, -v Enables verbose logging.
+--uid <uid>, -u <uid> The target real and effective user ID.
+--gid <gid>, -g <gid> The target real and effective group ID.
+--groups <gid1,2,..>, -G <1,2,..> A comma separated list of supplementary group
+ IDs.
+--nogroups Specifies that all supplementary groups should
+ be cleared.
+--selinux <context>, -s <context> The target SELinux context.
+--seccomp <filter>, -f <filter> The target seccomp filter. Valid values of
+ filter are 'none', 'uid-inferred', 'app',
+ 'app-zygote', and 'system'.
+--caps <capabilities> A libcap textual expression that describes
+ the desired capability sets. The only
+ capability set that matters is the permitted
+ set, the other sets are ignored.
+
+ Examples:
+
+ "=" - Clear all capabilities
+ "=p" - Raise all capabilities
+ "23,CAP_SYS_ADMIN+p" - Raise CAP_SYS_ADMIN
+ and capability 23.
+
+ For a full description of the possible values
+ see `man 3 cap_from_text` (the libcap-dev
+ package provides this man page).
+--pid <pid>, -p <pid> Infer the target security context from a
+ running process with the given process ID.
+ This option implies --seccomp uid_inferred.
+ This option infers the capability from the
+ target process's permitted capability set.
+--profile <profile>, -P <profile> Infer the target security context from a
+ predefined security profile. Using this
+ option will install and execute a test app on
+ the device. Currently, the only valid profile
+ is 'untrusted-app' which corresponds to an
+ untrusted app which has been granted every
+ non-system permission.
+
+Options are evaluated in the order that they are given. For example, the
+following will set the target context to that of process 1234 but override the
+user ID to 0:
+
+ shell-as --pid 1234 --uid 0
+)";
+
+const char* kShellExecvArgs[] = {"/system/bin/sh", nullptr};
+
+bool ParseGroups(char* line, std::vector<gid_t>* ids) {
+ // Allow a null line as a valid input since this method is used to handle both
+ // --groups and --nogroups.
+ if (line == nullptr) {
+ return true;
+ }
+ return SplitIdsAndSkip(line, ",", /*num_to_skip=*/0, ids);
+}
+} // namespace
+
+bool ParseOptions(const int argc, char* const argv[], bool* verbose,
+ SecurityContext* context, char* const* execv_args[]) {
+ char short_options[] = "+s:hp:u:g:G:f:c:vP:";
+ struct option long_options[] = {
+ {"selinux", true, nullptr, 's'}, {"help", false, nullptr, 'h'},
+ {"uid", true, nullptr, 'u'}, {"gid", true, nullptr, 'g'},
+ {"pid", true, nullptr, 'p'}, {"verbose", false, nullptr, 'v'},
+ {"groups", true, nullptr, 'G'}, {"nogroups", false, nullptr, 'G'},
+ {"seccomp", true, nullptr, 'f'}, {"caps", true, nullptr, 'c'},
+ {"profile", true, nullptr, 'P'},
+ };
+ int option;
+ bool infer_seccomp_filter = false;
+ SecurityContext working_context;
+ std::vector<gid_t> supplementary_group_ids;
+ uint32_t working_id = 0;
+ while ((option = getopt_long(argc, argv, short_options, long_options,
+ nullptr)) != -1) {
+ switch (option) {
+ case 'v':
+ *verbose = true;
+ break;
+ case 'h':
+ std::cerr << kUsage;
+ return false;
+ case 'u':
+ if (!StringToUInt32(optarg, &working_id)) {
+ return false;
+ }
+ working_context.user_id = working_id;
+ break;
+ case 'g':
+ if (!StringToUInt32(optarg, &working_id)) {
+ return false;
+ }
+ working_context.group_id = working_id;
+ break;
+ case 'c':
+ working_context.capabilities = cap_from_text(optarg);
+ if (working_context.capabilities.value() == nullptr) {
+ std::cerr << "Unable to parse capabilities" << std::endl;
+ return false;
+ }
+ break;
+ case 'G':
+ supplementary_group_ids.clear();
+ if (!ParseGroups(optarg, &supplementary_group_ids)) {
+ std::cerr << "Unable to parse supplementary groups" << std::endl;
+ return false;
+ }
+ working_context.supplementary_group_ids = supplementary_group_ids;
+ break;
+ case 's':
+ working_context.selinux_context = optarg;
+ break;
+ case 'f':
+ infer_seccomp_filter = false;
+ if (strcmp(optarg, "uid-inferred") == 0) {
+ infer_seccomp_filter = true;
+ } else if (strcmp(optarg, "app") == 0) {
+ working_context.seccomp_filter = kAppFilter;
+ } else if (strcmp(optarg, "app-zygote") == 0) {
+ working_context.seccomp_filter = kAppZygoteFilter;
+ } else if (strcmp(optarg, "system") == 0) {
+ working_context.seccomp_filter = kSystemFilter;
+ } else if (strcmp(optarg, "none") == 0) {
+ working_context.seccomp_filter.reset();
+ } else {
+ std::cerr << "Invalid value for --seccomp: " << optarg << std::endl;
+ return false;
+ }
+ break;
+ case 'p':
+ if (!SecurityContextFromProcess(atoi(optarg), &working_context)) {
+ return false;
+ }
+ infer_seccomp_filter = true;
+ break;
+ case 'P':
+ if (strcmp(optarg, "untrusted-app") == 0) {
+ if (!SecurityContextFromTestApp(&working_context)) {
+ return false;
+ }
+ } else {
+ std::cerr << "Invalid value for --profile: " << optarg << std::endl;
+ return false;
+ }
+ infer_seccomp_filter = true;
+ break;
+ default:
+ std::cerr << "Unknown option '" << (char)optopt << "'" << std::endl;
+ return false;
+ }
+ }
+
+ if (infer_seccomp_filter) {
+ if (!working_context.user_id.has_value()) {
+ std::cerr << "No user ID; unable to infer appropriate seccomp filter."
+ << std::endl;
+ return false;
+ }
+ working_context.seccomp_filter =
+ SeccompFilterFromUserId(working_context.user_id.value());
+ }
+
+ *context = working_context;
+ if (optind < argc) {
+ *execv_args = argv + optind;
+ } else {
+ *execv_args = (char**)kShellExecvArgs;
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/command-line.h b/utils/shell-as/command-line.h
new file mode 100644
index 000000000..4bf495f42
--- /dev/null
+++ b/utils/shell-as/command-line.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_COMMAND_LINE_H_
+#define SHELL_AS_COMMAND_LINE_H_
+
+#include "./context.h"
+
+namespace shell_as {
+
+// Parse command line options into a target security context and arguments that
+// can be passed to ExecuteInContext.
+//
+// The value of execv_args will either point to a sub-array of argv or to a
+// statically allocated default value. In both cases the caller should /not/
+// free the memory.
+//
+// Returns true on success and false if there is a problem parsing options.
+bool ParseOptions(const int argc, char* const argv[], bool* verbose,
+ SecurityContext* context, char* const* execv_args[]);
+} // namespace shell_as
+
+#endif // SHELL_AS_COMMAND_LINE_H_
diff --git a/utils/shell-as/context.cpp b/utils/shell-as/context.cpp
new file mode 100644
index 000000000..ea7979bab
--- /dev/null
+++ b/utils/shell-as/context.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./context.h"
+
+#include <private/android_filesystem_config.h> // For AID_APP_START.
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <iostream>
+#include <string>
+
+#include "./string-utils.h"
+#include "./test-app.h"
+
+namespace shell_as {
+
+namespace {
+
+bool ParseIdFromProcStatusLine(char* line, uid_t* id) {
+ // The user and group ID lines of the status file look like:
+ //
+ // Uid: <real> <effective> <saved> <filesystem>
+ // Gid: <real> <effective> <saved> <filesystem>
+ std::vector<uid_t> ids;
+ if (!SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, &ids) ||
+ ids.size() < 1) {
+ return false;
+ }
+ *id = ids[0];
+ return true;
+}
+
+bool ParseGroupsFromProcStatusLine(char* line, std::vector<gid_t>* ids) {
+ // The supplementary groups line of the status file looks like:
+ //
+ // Groups: <group1> <group2> <group3> ...
+ return SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, ids);
+}
+
+bool ParseProcStatusFile(const pid_t process_id, uid_t* real_user_id,
+ gid_t* real_group_id,
+ std::vector<gid_t>* supplementary_group_ids) {
+ std::string proc_status_path =
+ std::string("/proc/") + std::to_string(process_id) + "/status";
+ FILE* status_file = fopen(proc_status_path.c_str(), "r");
+ if (status_file == nullptr) {
+ std::cerr << "Unable to open '" << proc_status_path << "'" << std::endl;
+ }
+ bool parsed_user = false;
+ bool parsed_group = false;
+ bool parsed_supplementary_groups = false;
+ while (true) {
+ size_t line_length = 0;
+ char* line = nullptr;
+ if (getline(&line, &line_length, status_file) < 0) {
+ free(line);
+ break;
+ }
+ if (strncmp("Uid:", line, 4) == 0) {
+ parsed_user = ParseIdFromProcStatusLine(line, real_user_id);
+ } else if (strncmp("Gid:", line, 4) == 0) {
+ parsed_group = ParseIdFromProcStatusLine(line, real_group_id);
+ } else if (strncmp("Groups:", line, 7) == 0) {
+ parsed_supplementary_groups =
+ ParseGroupsFromProcStatusLine(line, supplementary_group_ids);
+ }
+ free(line);
+ }
+ fclose(status_file);
+ return parsed_user && parsed_group && parsed_supplementary_groups;
+}
+
+} // namespace
+
+bool SecurityContextFromProcess(const pid_t process_id,
+ SecurityContext* context) {
+ char* selinux_context;
+ if (getpidcon(process_id, &selinux_context) != 0) {
+ std::cerr << "Unable to obtain SELinux context from process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ cap_t capabilities = cap_get_pid(process_id);
+ if (capabilities == nullptr) {
+ std::cerr << "Unable to obtain capability set from process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ uid_t user_id = 0;
+ gid_t group_id = 0;
+ std::vector<gid_t> supplementary_group_ids;
+ if (!ParseProcStatusFile(process_id, &user_id, &group_id,
+ &supplementary_group_ids)) {
+ std::cerr << "Unable to obtain user and group IDs from process "
+ << process_id << std::endl;
+ return false;
+ }
+
+ context->selinux_context = selinux_context;
+ context->user_id = user_id;
+ context->group_id = group_id;
+ context->supplementary_group_ids = supplementary_group_ids;
+ context->capabilities = capabilities;
+ return true;
+}
+
+bool SecurityContextFromTestApp(SecurityContext* context) {
+ pid_t test_app_pid = 0;
+ if (!SetupAndStartTestApp(&test_app_pid)) {
+ std::cerr << "Unable to install test app." << std::endl;
+ return false;
+ }
+ return SecurityContextFromProcess(test_app_pid, context);
+}
+
+SeccompFilter SeccompFilterFromUserId(uid_t user_id) {
+ // Copied from:
+ // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
+ return user_id >= AID_APP_START ? kAppFilter : kSystemFilter;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/context.h b/utils/shell-as/context.h
new file mode 100644
index 000000000..17a8cca85
--- /dev/null
+++ b/utils/shell-as/context.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_CONTEXT_H_
+#define SHELL_AS_CONTEXT_H_
+
+#include <selinux/selinux.h>
+#include <sys/capability.h>
+
+#include <memory>
+#include <optional>
+#include <vector>
+
+namespace shell_as {
+
+// Enumeration of the possible seccomp filters that Android may apply to a
+// process.
+//
+// This should be kept in sync with the policies defined in:
+// bionic/libc/seccomp/include/seccomp_policy.h
+enum SeccompFilter {
+ kAppFilter = 0,
+ kAppZygoteFilter = 1,
+ kSystemFilter = 2,
+};
+
+typedef struct SecurityContext {
+ std::optional<uid_t> user_id;
+ std::optional<gid_t> group_id;
+ std::optional<std::vector<gid_t>> supplementary_group_ids;
+ std::optional<char *> selinux_context;
+ std::optional<SeccompFilter> seccomp_filter;
+ std::optional<cap_t> capabilities;
+} SecurityContext;
+
+// Infers the appropriate seccomp filter from a user ID.
+//
+// This mimics the behavior of the zygote process and provides a sane default
+// method of picking a filter. However, it is not 100% accurate since it does
+// not assign the app zygote filter and would not return an appropriate value
+// for processes not started by the zygote.
+SeccompFilter SeccompFilterFromUserId(uid_t user_id);
+
+// Derives a complete security context from a given process.
+//
+// If unable to determine any field of the context this method will return false
+// and not modify the given context.
+bool SecurityContextFromProcess(pid_t process_id, SecurityContext* context);
+
+// Derives a complete security context from the bundled test app.
+//
+// If unable to determine any field of the context this method will return false
+// and not modify the given context.
+bool SecurityContextFromTestApp(SecurityContext* context);
+
+} // namespace shell_as
+
+#endif // SHELL_AS_CONTEXT_H_
diff --git a/utils/shell-as/elf-utils.cpp b/utils/shell-as/elf-utils.cpp
new file mode 100644
index 000000000..8a82555be
--- /dev/null
+++ b/utils/shell-as/elf-utils.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include <elf.h>
+#include <stdio.h>
+
+#include <iostream>
+#include <string>
+
+#include "./elf.h"
+
+namespace shell_as {
+
+namespace {
+// The base address of a PIE binary when loaded with ASLR disabled.
+#if defined(__arm__) || defined(__aarch64__)
+constexpr uint64_t k32BitImageBase = 0xAAAAA000;
+constexpr uint64_t k64BitImageBase = 0x5555555000;
+#else
+constexpr uint64_t k32BitImageBase = 0x56555000;
+constexpr uint64_t k64BitImageBase = 0x555555554000;
+#endif
+} // namespace
+
+bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address,
+ bool* is_arm_mode) {
+ uint8_t elf_header_buffer[sizeof(Elf64_Ehdr)];
+ std::string exe_path = "/proc/" + std::to_string(process_id) + "/exe";
+ FILE* exe_file = fopen(exe_path.c_str(), "rb");
+ if (exe_file == nullptr) {
+ std::cerr << "Unable to open executable of process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ int read_size =
+ fread(elf_header_buffer, sizeof(elf_header_buffer), 1, exe_file);
+ fclose(exe_file);
+ if (read_size <= 0) {
+ std::cerr << "Unable to read executable of process " << process_id
+ << std::endl;
+ return false;
+ }
+
+ const Elf32_Ehdr* file_header_32 = (Elf32_Ehdr*)elf_header_buffer;
+ const Elf64_Ehdr* file_header_64 = (Elf64_Ehdr*)elf_header_buffer;
+ // The first handful of bytes of a header do not depend on whether the file is
+ // 32bit vs 64bit.
+ const bool is_pie_binary = file_header_32->e_type == ET_DYN;
+
+ if (file_header_32->e_ident[EI_CLASS] == ELFCLASS32) {
+ *entry_address =
+ file_header_32->e_entry + (is_pie_binary ? k32BitImageBase : 0);
+ } else if (file_header_32->e_ident[EI_CLASS] == ELFCLASS64) {
+ *entry_address =
+ file_header_64->e_entry + (is_pie_binary ? k64BitImageBase : 0);
+ } else {
+ return false;
+ }
+
+ *is_arm_mode = false;
+#if defined(__arm__)
+ if ((*entry_address & 1) == 0) {
+ *is_arm_mode = true;
+ }
+ // The entry address for ARM Elf binaries is branched to using a BX
+ // instruction. The low bit of these instructions indicates the instruction
+ // set of the code that is being jumped to. A low bit of 1 indicates thumb
+ // mode while a low bit of 0 indicates ARM mode.
+ *entry_address &= ~1;
+#endif
+
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/elf-utils.h b/utils/shell-as/elf-utils.h
new file mode 100644
index 000000000..eba40f303
--- /dev/null
+++ b/utils/shell-as/elf-utils.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_ELF_H_
+#define SHELL_AS_ELF_H_
+
+#include <sys/types.h>
+
+namespace shell_as {
+
+// Sets entry_address to the process's entry point.
+//
+// This method assumes that PIE binaries are executing with ADDR_NO_RANDOMIZE.
+//
+// The is_arm_mode flag is set to true IFF the architecture is 32bit ARM and the
+// expected instruction set for code located at the entry address is not-thumb.
+// It is false for all other cases.
+bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address,
+ bool* is_arm_mode);
+} // namespace shell_as
+
+#endif // SHELL_AS_ELF_H_
diff --git a/utils/shell-as/execute.cpp b/utils/shell-as/execute.cpp
new file mode 100644
index 000000000..3ef529252
--- /dev/null
+++ b/utils/shell-as/execute.cpp
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./execute.h"
+
+#include <linux/securebits.h>
+#include <linux/uio.h>
+#include <seccomp_policy.h>
+#include <sys/capability.h>
+#include <sys/personality.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <memory>
+
+#include "./elf-utils.h"
+#include "./registers.h"
+#include "./shell-code.h"
+
+namespace shell_as {
+
+namespace {
+
+// Capabilities are implemented as a 64-bit bit-vector. Therefore the maximum
+// number of capabilities supported by a kernel is 64.
+constexpr cap_value_t kMaxCapabilities = 64;
+
+bool DropPreExecPrivileges(const shell_as::SecurityContext* context) {
+ // The ordering here is important:
+ // (1) The platform's seccomp filters disallow setresgiud, so it must come
+ // before the seccomp drop.
+ // (2) Adding seccomp filters must happen before setresuid because setresuid
+ // drops some capabilities which are required for seccomp.
+ if (context->group_id.has_value() &&
+ setresgid(context->group_id.value(), context->group_id.value(),
+ context->group_id.value()) != 0) {
+ std::cerr << "Unable to set group id: " << context->group_id.value()
+ << std::endl;
+ return false;
+ }
+ if (context->supplementary_group_ids.has_value() &&
+ setgroups(context->supplementary_group_ids.value().size(),
+ context->supplementary_group_ids.value().data()) != 0) {
+ std::cerr << "Unable to set supplementary groups." << std::endl;
+ return false;
+ }
+
+ if (context->seccomp_filter.has_value()) {
+ switch (context->seccomp_filter.value()) {
+ case shell_as::kAppFilter:
+ set_app_seccomp_filter();
+ break;
+ case shell_as::kAppZygoteFilter:
+ set_app_zygote_seccomp_filter();
+ break;
+ case shell_as::kSystemFilter:
+ set_system_seccomp_filter();
+ break;
+ }
+ }
+
+ // This must be set prior to setresuid, otherwise that call will drop the
+ // permitted set of capabilities.
+ if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) {
+ std::cerr << "Unable to set keep capabilities." << std::endl;
+ return false;
+ }
+
+ if (context->user_id.has_value() &&
+ setresuid(context->user_id.value(), context->user_id.value(),
+ context->user_id.value()) != 0) {
+ std::cerr << "Unable to set user id: " << context->user_id.value()
+ << std::endl;
+ return false;
+ }
+
+ // Capabilities must be reacquired after setresuid since it still modifies
+ // capabilities, but it leaves the permitted set intact.
+ if (context->capabilities.has_value()) {
+ // The first step is to raise all the capabilities possible in all sets
+ // including the inheritable set. This defines the superset of possible
+ // capabilities that can be passed on after calling execve.
+ //
+ // The reason that all capabilities are raised in the inheritable set is due
+ // to a limitation of libcap. libcap may not contain a capability definition
+ // for all capabilities supported by the kernel. If this occurs, it will
+ // silently ignore requests to raise unknown capabilities via cap_set_flag.
+ //
+ // However, when parsing a cap_t from a text value, libcap will treat "all"
+ // as all possible 64 capability bits as set.
+ cap_t all_capabilities = cap_from_text("all+pie");
+ if (cap_set_proc(all_capabilities) != 0) {
+ std::cerr << "Unable to raise inheritable capability set." << std::endl;
+ cap_free(all_capabilities);
+ return false;
+ }
+ cap_free(all_capabilities);
+
+ // The second step is to raise the /desired/ capability subset in the
+ // ambient capability set. These are the capabilities that will actually be
+ // passed to the process after execve.
+ if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) != 0) {
+ std::cerr << "Unable to clear ambient capabilities." << std::endl;
+ return false;
+ }
+ cap_t desired_capabilities = context->capabilities.value();
+ for (cap_value_t cap = 0; cap < kMaxCapabilities; cap++) {
+ // Skip capability values not supported by the kernel.
+ if (!CAP_IS_SUPPORTED(cap)) {
+ continue;
+ }
+ cap_flag_value_t value = CAP_CLEAR;
+ if (cap_get_flag(desired_capabilities, cap, CAP_PERMITTED, &value) == 0 &&
+ value == CAP_SET) {
+ if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) != 0) {
+ std::cerr << "Unable to raise capability " << cap
+ << " in the ambient set." << std::endl;
+ return false;
+ }
+ }
+ }
+
+ // The final step is to raise the SECBIT_NOROOT flag. The kernel has special
+ // case logic that treats root calling execve differently than other users.
+ //
+ // By default all bits in the permitted set prior to calling execve will be
+ // raised after calling execve. This would ignore the work above and result
+ // in the process to have all capabilities.
+ //
+ // Setting the SECBIT_NOROOT disables this special casing for root and
+ // causes the kernel to treat it as any other UID.
+ int64_t secure_bits = prctl(PR_GET_SECUREBITS, 0, 0, 0, 0);
+ if (secure_bits < 0 ||
+ prctl(PR_SET_SECUREBITS, secure_bits | SECBIT_NOROOT, 0, 0, 0) != 0) {
+ std::cerr << "Unable to raise SECBIT_NOROOT." << std::endl;
+ return false;
+ }
+ }
+ return true;
+}
+
+uint8_t ReadChildByte(const pid_t process, const uintptr_t address) {
+ uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr);
+ return ((uint8_t*)&data)[0];
+}
+
+void WriteChildByte(const pid_t process, const uintptr_t address,
+ const uint8_t value) {
+ // This is not the most efficient way to write data to a process. However, it
+ // reduces code complexity of handling different word sizes and reading and
+ // writing memory that is not a multiple of the native word size.
+ uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr);
+ ((uint8_t*)&data)[0] = value;
+ ptrace(PTRACE_POKEDATA, process, address, data);
+}
+
+void ReadChildMemory(const pid_t process, uintptr_t process_address,
+ uint8_t* bytes, size_t byte_count) {
+ for (; byte_count != 0; byte_count--, bytes++, process_address++) {
+ *bytes = ReadChildByte(process, process_address);
+ }
+}
+
+void WriteChildMemory(const pid_t process, uintptr_t process_address,
+ uint8_t const* bytes, size_t byte_count) {
+ for (; byte_count != 0; byte_count--, bytes++, process_address++) {
+ WriteChildByte(process, process_address, *bytes);
+ }
+}
+
+// Executes shell code in a target process.
+//
+// The following assumptions are made:
+// * The process is currently being ptraced and that the process has already
+// stopped.
+// * The shell code will raise SIGSTOP when it has finished as signal that
+// control flow should be handed back to the original code.
+// * The shell code only alters registers and pushes values onto the stack.
+//
+// Execution is performed by overwriting the memory under the current
+// instruction pointer with the shell code. After the shell code signals
+// completion the original register state and memory are restored.
+//
+// If the above assumptions are met, then this function will leave the process
+// in a stopped state that is equivalent to the original state.
+bool ExecuteShellCode(const pid_t process, const uint8_t* shell_code,
+ const size_t shell_code_size) {
+ REGISTER_STRUCT registers;
+ struct iovec registers_iovec;
+ registers_iovec.iov_base = &registers;
+ registers_iovec.iov_len = sizeof(REGISTER_STRUCT);
+ ptrace(PTRACE_GETREGSET, process, 1, &registers_iovec);
+
+ std::unique_ptr<uint8_t[]> memory_backup(new uint8_t[shell_code_size]);
+ ReadChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(),
+ shell_code_size);
+ WriteChildMemory(process, PROGRAM_COUNTER(registers), shell_code,
+ shell_code_size);
+
+ // Execute the shell code and wait for the signal that it has finished.
+ ptrace(PTRACE_CONT, process, NULL, NULL);
+ int status;
+ waitpid(process, &status, 0);
+ if (status >> 8 != SIGSTOP) {
+ std::cerr << "Failed to execute SELinux shellcode." << std::endl;
+ return false;
+ }
+
+ ptrace(PTRACE_SETREGSET, process, 1, &registers_iovec);
+ WriteChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(),
+ shell_code_size);
+ return true;
+}
+
+bool SetProgramCounter(const pid_t process_id, uint64_t program_counter) {
+ REGISTER_STRUCT registers;
+ struct iovec registers_iovec;
+ registers_iovec.iov_base = &registers;
+ registers_iovec.iov_len = sizeof(REGISTER_STRUCT);
+ if (ptrace(PTRACE_GETREGSET, process_id, 1, &registers_iovec) != 0) {
+ return false;
+ }
+ PROGRAM_COUNTER(registers) = program_counter;
+ if ((ptrace(PTRACE_SETREGSET, process_id, 1, &registers_iovec)) != 0) {
+ return false;
+ }
+ return true;
+}
+
+bool StepToEntryPoint(const pid_t process_id) {
+ bool is_arm_mode;
+ uint64_t entry_address;
+ if (!GetElfEntryPoint(process_id, &entry_address, &is_arm_mode)) {
+ std::cerr << "Not able to determine Elf entry point." << std::endl;
+ return false;
+ }
+ if (is_arm_mode) {
+ // TODO(willcoster): If there is a need to handle ARM mode instructions in
+ // addition to thumb instructions update this with ARM mode shell code.
+ std::cerr << "Attempting to run an ARM-mode binary. "
+ << "shell-as currently only supports thumb-mode. "
+ << "Bug willcoster@ if you run into this error." << std::endl;
+ return false;
+ }
+
+ int expected_signal = 0;
+ size_t trap_code_size = 0;
+ std::unique_ptr<uint8_t[]> trap_code =
+ GetTrapShellCode(&expected_signal, &trap_code_size);
+ std::unique_ptr<uint8_t[]> backup(new uint8_t[trap_code_size]);
+
+ // Set a break point at the entry point declared by the Elf file. When a
+ // statically linked binary is executed this will be the first instruction
+ // executed.
+ //
+ // When a dynamically linked binary is executed, the dynamic linker is
+ // executed first. This brings .so files into memory and resolves shared
+ // symbols. Once this process is finished, it jumps to the entry point
+ // declared in the Elf file.
+ ReadChildMemory(process_id, entry_address, backup.get(), trap_code_size);
+ WriteChildMemory(process_id, entry_address, trap_code.get(), trap_code_size);
+ ptrace(PTRACE_CONT, process_id, NULL, NULL);
+ int status;
+ waitpid(process_id, &status, 0);
+ if (status >> 8 != expected_signal) {
+ std::cerr << "Program exited unexpectedly while stepping to entry point."
+ << std::endl;
+ std::cerr << "Expected status " << expected_signal << " but encountered "
+ << (status >> 8) << std::endl;
+ return false;
+ }
+
+ if (!SetProgramCounter(process_id, entry_address)) {
+ return false;
+ }
+ WriteChildMemory(process_id, entry_address, backup.get(), trap_code_size);
+ return true;
+}
+
+} // namespace
+
+bool ExecuteInContext(char* const executable_and_args[],
+ const shell_as::SecurityContext* context) {
+ // Getting an executable running in a lower privileged context is tricky with
+ // SELinux. The recommended approach in the documentation is to use setexeccon
+ // which sets the context on the next execve call.
+ //
+ // However, this doesn't work for unprivileged processes like untrusted apps
+ // in Android because they are not allowed to execute most binaries.
+ //
+ // To work around this, ptrace is used to inject shell code into the new
+ // process just after it has executed an execve syscall. This shell code then
+ // sets the desired SELinux context.
+ pid_t child = fork();
+ if (child == 0) {
+ // Disabling ASLR makes it easier to determine the entry point of the target
+ // executable.
+ personality(ADDR_NO_RANDOMIZE);
+
+ // Drop the privileges that can be dropped before executing the new binary
+ // and exit early if there is an issue.
+ if (!DropPreExecPrivileges(context)) {
+ exit(1);
+ }
+
+ ptrace(PTRACE_TRACEME, 0, NULL, NULL);
+ raise(SIGSTOP); // Wait for the parent process to attach.
+ execv(executable_and_args[0], executable_and_args);
+ } else {
+ // Wait for the child to reach the SIGSTOP line above.
+ int status;
+ waitpid(child, &status, 0);
+ if ((status >> 8) != SIGSTOP) {
+ // If the first status is not SIGSTOP, then the child aborted early
+ // because it was not able to set the user and group IDs.
+ return false;
+ }
+
+ // Break inside the child's execv call.
+ ptrace(PTRACE_SETOPTIONS, child, NULL,
+ PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL);
+ ptrace(PTRACE_CONT, child, NULL, NULL);
+ waitpid(child, &status, 0);
+ if (status >> 8 != (SIGTRAP | PTRACE_EVENT_EXEC << 8)) {
+ std::cerr << "Failed to execute " << executable_and_args[0] << std::endl;
+ return false;
+ }
+
+ // Allow the dynamic linker to run before dropping to a lower SELinux
+ // context. This is required for executing in some very constrained domains
+ // like mediacodec.
+ //
+ // If the context was dropped before the dynamic linker runs, then when the
+ // linker attempts to read /proc/self/exe to determine dynamic symbol
+ // information, SELinux will kill the binary if the domain is not allowed to
+ // read the binary's executable file.
+ //
+ // This happens for example, when attempting to run any toybox binary (id,
+ // sh, etc) as mediacodec.
+ if (!StepToEntryPoint(child)) {
+ std::cerr << "Something bad happened stepping to the entry point."
+ << std::endl;
+ return false;
+ }
+
+ // Run the SELinux shellcode in the child process before the child can
+ // execute any instructions in the newly loaded executable.
+ if (context->selinux_context.has_value()) {
+ size_t shell_code_size;
+ std::unique_ptr<uint8_t[]> shell_code = GetSELinuxShellCode(
+ context->selinux_context.value(), &shell_code_size);
+ bool success = ExecuteShellCode(child, shell_code.get(), shell_code_size);
+ if (!success) {
+ return false;
+ }
+ }
+
+ // Resume and detach from the child now that the SELinux context has been
+ // updated.
+ ptrace(PTRACE_DETACH, child, NULL, NULL);
+ waitpid(child, nullptr, 0);
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/execute.h b/utils/shell-as/execute.h
new file mode 100644
index 000000000..2e8f51189
--- /dev/null
+++ b/utils/shell-as/execute.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_EXECUTE_H_
+#define SHELL_AS_EXECUTE_H_
+
+#include "context.h"
+
+namespace shell_as {
+
+// Executes a command in the given security context.
+//
+// The executable_and_args parameter must contain at least two values. The first
+// value is the path to the executable to run and the last value must be null.
+// Additional arguments are passed to the executable as command line options.
+//
+// Returns true if the executable was run and false otherwise.
+bool ExecuteInContext(char* const executable_and_args[],
+ const SecurityContext* context);
+} // namespace shell_as
+
+#endif // SHELL_AS_EXECUTE_H_
diff --git a/utils/shell-as/gen-manifest.sh b/utils/shell-as/gen-manifest.sh
new file mode 100755
index 000000000..9dc4d1142
--- /dev/null
+++ b/utils/shell-as/gen-manifest.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+# Copyright (C) 2023 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.
+
+# Generates an AndroidManifest.xml file from a template by replacing the line
+# containing the substring, 'PERMISSIONS', with a list of permissions defined in
+# another text file.
+
+set -e
+
+if [ "$#" != 3 ];
+then
+ echo "usage: gen-manifest.sh AndroidManifest.xml.template" \
+ "permissions.txt AndroidManifest.xml"
+ exit 1
+fi
+
+readonly template="$1"
+readonly permissions="$2"
+readonly output="$3"
+
+echo "template = $1"
+
+# Print the XML template file before the line containing PERMISSIONS.
+sed -e '/PERMISSIONS/,$d' "$template" > "$output"
+
+# Print the permissions formatted as XML.
+sed -r 's!(.*)! <uses-permission android:name="\1"/>!g' "$permissions" >> "$output"
+
+# Print the XML template file after the line containing PERMISSIONS.
+sed -e '1,/PERMISSIONS/d' "$template" >> "$output"
diff --git a/utils/shell-as/registers.h b/utils/shell-as/registers.h
new file mode 100644
index 000000000..6f7af6c54
--- /dev/null
+++ b/utils/shell-as/registers.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_REGISTERS_H_
+#define SHELL_AS_REGISTERS_H_
+
+#if defined(__aarch64__)
+
+#define REGISTER_STRUCT struct user_pt_regs
+#define PROGRAM_COUNTER(regs) (regs.pc)
+
+#elif defined(__i386__)
+
+#include "sys/user.h"
+#define REGISTER_STRUCT struct user_regs_struct
+#define PROGRAM_COUNTER(regs) (regs.eip)
+
+#elif defined(__x86_64__)
+
+#include "sys/user.h"
+#define REGISTER_STRUCT struct user_regs_struct
+#define PROGRAM_COUNTER(regs) (regs.rip)
+
+#elif defined(__arm__)
+
+#define REGISTER_STRUCT struct user_regs
+#define PROGRAM_COUNTER(regs) (regs.ARM_pc)
+
+#endif
+
+#endif // SHELL_AS_REGISTERS_H_
diff --git a/utils/shell-as/shell-as-main.cpp b/utils/shell-as/shell-as-main.cpp
new file mode 100644
index 000000000..880cf1c91
--- /dev/null
+++ b/utils/shell-as/shell-as-main.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include <iostream>
+#include <memory>
+#include <string>
+
+#include "./command-line.h"
+#include "./context.h"
+#include "./execute.h"
+
+int main(const int argc, char* const argv[]) {
+ bool verbose = false;
+ auto context = std::make_unique<shell_as::SecurityContext>();
+ char* const* execute_arguments = nullptr;
+ if (!shell_as::ParseOptions(argc, argv, &verbose, context.get(),
+ &execute_arguments)) {
+ return 1;
+ }
+
+ if (verbose) {
+ std::cerr << "Dropping privileges to:" << std::endl;
+ std::cerr << "\tuser ID = "
+ << (context->user_id.has_value()
+ ? std::to_string(context->user_id.value())
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tgroup ID = "
+ << (context->group_id.has_value()
+ ? std::to_string(context->group_id.value())
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tsupplementary group IDs = ";
+ if (!context->supplementary_group_ids.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ for (auto& id : context->supplementary_group_ids.value()) {
+ std::cerr << id << " ";
+ }
+ }
+ std::cerr << std::endl;
+
+ std::cerr << "\tSELinux = "
+ << (context->selinux_context.has_value()
+ ? context->selinux_context.value()
+ : "<no value>")
+ << std::endl;
+
+ std::cerr << "\tseccomp = ";
+ if (!context->seccomp_filter.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ switch (context->seccomp_filter.value()) {
+ case shell_as::kAppFilter:
+ std::cerr << "app";
+ break;
+ case shell_as::kAppZygoteFilter:
+ std::cerr << "app-zygote";
+ break;
+ case shell_as::kSystemFilter:
+ std::cerr << "system";
+ break;
+ }
+ }
+ std::cerr << std::endl;
+
+ std::cerr << "\tcapabilities = ";
+ if (!context->capabilities.has_value()) {
+ std::cerr << "<no value>";
+ } else {
+ std::cerr << "'" << cap_to_text(context->capabilities.value(), nullptr)
+ << "'";
+ }
+ std::cerr << std::endl;
+ }
+
+ return !shell_as::ExecuteInContext(execute_arguments, context.get());
+}
diff --git a/utils/shell-as/shell-as-test-app-key.pk8 b/utils/shell-as/shell-as-test-app-key.pk8
new file mode 100644
index 000000000..df9254543
--- /dev/null
+++ b/utils/shell-as/shell-as-test-app-key.pk8
Binary files differ
diff --git a/utils/shell-as/shell-as-test-app-key.x509.pem b/utils/shell-as/shell-as-test-app-key.x509.pem
new file mode 100644
index 000000000..4e5efc980
--- /dev/null
+++ b/utils/shell-as/shell-as-test-app-key.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIECzCCAvOgAwIBAgIUNyI1+/ZDui4r+jp6uy/aVRBpeR0wDQYJKoZIhvcNAQEL
+BQAwgZQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFu
+ZHJvaWQuY29tMB4XDTE5MTIwNTIyNDEwMloXDTQ3MDQyMjIyNDEwMlowgZQxCzAJ
+BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFp
+biBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRyb2lkMRAwDgYD
+VQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyzHGxhfDk4VzImAGQyFV
+5+tb7Dgl9TTg57t8/LwMQX4abjB9o6tPwtZl757m4oLP8HCpjbI/kX5Wk4hLmNQ/
+I4AHG+LhCJGlz3nqBAJJxYoM//+3tUSLrq0ypuHMXNDPI5HGgE3hhzZbA9iWuGNB
+7in+bHuhPFUq8e5og6piy3s3f77GB8QXzJEKyO2FhQR1Do8t4UdRji7TWR+USHqw
+WBj/CyrpLJMwbr4Mx4YRN0JXUlFX1X/66ENonX4QZXofeiWDv5qgwFbbzgu9FLFN
+imDeeCzU0mtYEKQpmZOdEaWclkT8IzUPwPMdawEq3Wj8nutoma5CztYl+OO9BgJC
+LQIDAQABo1MwUTAdBgNVHQ4EFgQUqyxwI0Khq+xKbEGG3NCpN01wsaMwHwYDVR0j
+BBgwFoAUqyxwI0Khq+xKbEGG3NCpN01wsaMwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAQEAIx4k1g1jDQs3ekseXMvz0V+O9AArWOEmwkIcA6EISvfC
+dJ0DpmgRbZyvi0FowzOGYIZJ0Uwh4uwxETTHBQkvKoFdByukaasfX0p8axYVslT1
+87RrQDSA8fDp9K7d4kG3iXX16H5WJ0O/sI3UkZevZzVjXcoqSHA2CltGZv/EXPAh
+dwGL5OupiiJcCV4ISSgh9PHswH1tGASdg3nqFqQLZrCYZE3pyLdsiDTQADlBMpZ4
+dH7kbh8McSA/OM2Fp1y05oecYVzKOzJ/I4SLhbSGLRLHvSg9fNiJPoKm46leQtFV
+OVtzzBt6TKITRIhA8VVo45U0gVGUwlj/4BCKQLsJpA==
+-----END CERTIFICATE-----
diff --git a/utils/shell-as/shell-code.cpp b/utils/shell-as/shell-code.cpp
new file mode 100644
index 000000000..bdadf6c5a
--- /dev/null
+++ b/utils/shell-as/shell-code.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./shell-code.h"
+
+#include <sys/mman.h>
+#include <sys/types.h>
+
+#define PAGE_START(addr) ((uintptr_t)addr & ~(PAGE_SIZE - 1))
+
+// Shell code that sets the SELinux context of the current process.
+//
+// The shell code expects a null-terminated SELinux context string to be placed
+// immediately after it in memory. After the SELinux context has been changed
+// the shell code will stop the current process with SIGSTOP.
+//
+// This shell code must be self-contained and position-independent.
+extern "C" void __setcon_shell_code_start();
+extern "C" void __setcon_shell_code_end();
+
+// Shell code that stops execution of the current process by raising a signal.
+// The specific signal that is raised is given in __trap_shell_code_signal.
+//
+// This shell code can be used to inject break points into a traced process.
+//
+// The shell code must not modify any registers other than the program counter.
+extern "C" void __trap_shell_code_start();
+extern "C" void __trap_shell_code_end();
+extern "C" int __trap_shell_code_signal;
+
+namespace shell_as {
+
+namespace {
+void EnsureShellcodeReadable(void (*start)(), void (*end)()) {
+ mprotect((void*)PAGE_START(start),
+ PAGE_START(end) - PAGE_START(start) + PAGE_SIZE,
+ PROT_READ | PROT_EXEC);
+}
+} // namespace
+
+std::unique_ptr<uint8_t[]> GetSELinuxShellCode(
+ char* selinux_context, size_t* total_size) {
+ EnsureShellcodeReadable(&__setcon_shell_code_start, &__setcon_shell_code_end);
+
+ size_t shell_code_size = (uintptr_t)&__setcon_shell_code_end -
+ (uintptr_t)&__setcon_shell_code_start;
+ size_t selinux_context_size = strlen(selinux_context) + 1 /* null byte */;
+ *total_size = shell_code_size + selinux_context_size;
+
+ std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]);
+ memcpy(shell_code.get(), (void*)&__setcon_shell_code_start, shell_code_size);
+ memcpy(shell_code.get() + shell_code_size, selinux_context,
+ selinux_context_size);
+ return shell_code;
+}
+
+std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal,
+ size_t* total_size) {
+ EnsureShellcodeReadable(&__trap_shell_code_start, &__trap_shell_code_end);
+
+ *expected_signal = __trap_shell_code_signal;
+
+ *total_size =
+ (uintptr_t)&__trap_shell_code_end - (uintptr_t)&__trap_shell_code_start;
+ std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]);
+ memcpy(shell_code.get(), (void*)&__trap_shell_code_start, *total_size);
+ return shell_code;
+}
+} // namespace shell_as
diff --git a/utils/shell-as/shell-code.h b/utils/shell-as/shell-code.h
new file mode 100644
index 000000000..9c88d165a
--- /dev/null
+++ b/utils/shell-as/shell-code.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_SHELL_CODE_H_
+#define SHELL_AS_SHELL_CODE_H_
+
+#include <selinux/selinux.h>
+#include <sys/types.h>
+
+#include <memory>
+
+#include "context.h"
+
+namespace shell_as {
+
+// Returns shell code that when executed will set the current process's SElinux
+// context to the given value and then SIGSTOP itself.
+std::unique_ptr<uint8_t[]> GetSELinuxShellCode(
+ char* selinux_context, size_t* total_size);
+
+// Returns shell code that when executed will halt the current process and raise
+// a signal. The specific signal is returned in the expected_signal argument.
+std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal,
+ size_t* total_size);
+} // namespace shell_as
+
+#endif // SHELL_AS_SHELL_CODE_H_
diff --git a/utils/shell-as/shell-code/constants-arm.S b/utils/shell-as/shell-code/constants-arm.S
new file mode 100644
index 000000000..10db630d9
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-arm.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Arm specific constants.
+
+.equ SYS_OPEN, 0x000005
+.equ SYS_CLOSE, 0x000006
+.equ SYS_WRITE, 0x000004
+.equ SYS_KILL, 0x000025
+.equ SYS_GETPID, 0x000014
+.equ SYS_MPROTECT, 0x00007d
diff --git a/utils/shell-as/shell-code/constants-arm64.S b/utils/shell-as/shell-code/constants-arm64.S
new file mode 100644
index 000000000..a9dec758b
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-arm64.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Arm64 specific constants.
+
+.equ SYS_OPENAT, 0x38
+.equ SYS_CLOSE, 0x39
+.equ SYS_WRITE, 0x40
+.equ SYS_KILL, 0x81
+.equ SYS_GETPID, 0xAC
+.equ SYS_MPROTECT, 0xE2
diff --git a/utils/shell-as/shell-code/constants-x86.S b/utils/shell-as/shell-code/constants-x86.S
new file mode 100644
index 000000000..afa9d1472
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-x86.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// x86 specific constants.
+
+.equ SYS_WRITE, 0x04
+.equ SYS_OPEN, 0x05
+.equ SYS_CLOSE, 0x06
+.equ SYS_GETPID, 0x14
+.equ SYS_KILL, 0x25
+.equ SYS_MPROTECT, 0x7d
diff --git a/utils/shell-as/shell-code/constants-x86_64.S b/utils/shell-as/shell-code/constants-x86_64.S
new file mode 100644
index 000000000..0bf95cc75
--- /dev/null
+++ b/utils/shell-as/shell-code/constants-x86_64.S
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// x86-64 specific constants.
+
+.equ SYS_WRITE, 0x01
+.equ SYS_OPEN, 0x02
+.equ SYS_CLOSE, 0x03
+.equ SYS_GETPID, 0x27
+.equ SYS_KILL, 0x3e
+.equ SYS_MPROTECT, 0x0a
diff --git a/utils/shell-as/shell-code/constants.S b/utils/shell-as/shell-code/constants.S
new file mode 100644
index 000000000..9e2a238d5
--- /dev/null
+++ b/utils/shell-as/shell-code/constants.S
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Architecture independent constants.
+
+.equ AT_FDCWD, -100
+.equ O_WRONLY, 1
+
+// Possible memory access modes for mprotect.
+.equ PROT_NONE, 0
+.equ PROT_READ, 1
+.equ PROT_WRITE, 2
+.equ PROT_EXEC, 4
+
+.equ SIGILL, 4
+.equ SIGTRAP, 5
+.equ SIGSTOP, 19
diff --git a/utils/shell-as/shell-code/selinux-arm.S b/utils/shell-as/shell-code/selinux-arm.S
new file mode 100644
index 000000000..0c9480f9b
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-arm.S
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-arm.S"
+
+.thumb
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov r7, SYS_MPROTECT
+ adr r0, context
+ movw r2, 0xF000
+ movt r2, 0xFFFF
+ and r0, r0, r2
+ mov r1, 0x2000
+ mov r2, (PROT_READ | PROT_EXEC)
+ swi 0
+
+ // r10 = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov r7, SYS_OPEN
+ adr r0, selinux_proc_file
+ mov r1, O_WRONLY
+ mov r2, O_WRONLY
+ swi 0
+ mov r10, r0
+
+ // r11 = strlen(context)
+ mov r11, 0
+ adr r0, context
+strlen_start:
+ ldrb r1, [r0, r11]
+ cmp r1, 0
+ beq strlen_done
+ add r11, r11, 1
+ b strlen_start
+strlen_done:
+
+ // write(r10, context, r11)
+ mov r7, SYS_WRITE
+ mov r0, r10
+ adr r1, context
+ mov r2, r11
+ swi 0
+
+ // close(r10)
+ mov r7, SYS_CLOSE
+ mov r0, r10
+ swi 0
+
+ // r0 = getpid()
+ mov r7, SYS_GETPID
+ swi 0
+
+ // kill(r0, SIGSTOP)
+ mov r7, SYS_KILL
+ mov r1, SIGSTOP
+ swi 0
+
+selinux_proc_file:
+ .asciz "/proc/thread-self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-arm64.S b/utils/shell-as/shell-code/selinux-arm64.S
new file mode 100644
index 000000000..4e8c49296
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-arm64.S
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-arm64.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov x8, SYS_MPROTECT
+ adr X0, __setcon_shell_code_end
+ and x0, x0, ~0xFFF
+ mov x1, 0x2000
+ mov x2, (PROT_READ | PROT_EXEC)
+ svc 0
+
+ // x10 = openat(AT_FDCWD, "/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov x8, SYS_OPENAT
+ mov x0, AT_FDCWD
+ adr x1, selinux_proc_file
+ mov x2, O_WRONLY
+ mov x3, O_WRONLY
+ svc 0
+ mov x10, x0
+
+ // x11 = strlen(context)
+ mov x11, 0
+ adr x0, context
+strlen_start:
+ ldrb w1, [x0, x11]
+ cmp w1, 0
+ b.eq strlen_done
+ add x11, x11, 1
+ b strlen_start
+strlen_done:
+
+ // write(x10, context, x11)
+ mov x8, SYS_WRITE
+ mov x0, x10
+ adr x1, context
+ mov x2, x11
+ svc 0
+
+ // close(x10)
+ mov x8, SYS_CLOSE
+ mov x0, x10
+ svc 0
+
+ // x0 = getpid()
+ mov x8, SYS_GETPID
+ svc 0
+
+ // kill(x0, SIGSTOP)
+ mov x8, SYS_KILL
+ mov x1, SIGSTOP
+ svc 0
+
+selinux_proc_file:
+ .asciz "/proc/thread-self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-x86.S b/utils/shell-as/shell-code/selinux-x86.S
new file mode 100644
index 000000000..81c150f13
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-x86.S
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-x86.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+
+ // x86 does not have RIP relative addressing. To work around this, relative
+ // calls are used to obtain the runtime address of a label. Once the location
+ // of one label is known, other labels can be addressed relative to the known
+ // label.
+ call constant_relative_address
+constant_relative_address:
+ pop %esi
+
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov $SYS_MPROTECT, %eax
+ mov $~0xFFF, %ebx
+ and %esi, %ebx
+ mov $0x2000, %ecx
+ mov $(PROT_READ | PROT_EXEC), %edx
+ int $0x80
+
+ // ebx = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov $SYS_OPEN, %eax
+ lea (selinux_proc_file - constant_relative_address)(%esi), %ebx
+ mov $O_WRONLY, %ecx
+ mov $O_WRONLY, %edx
+ int $0x80
+ mov %eax, %ebx
+
+ // write(ebx, context, strlen(context))
+ xor %edx, %edx
+ leal (context - constant_relative_address)(%esi), %ecx
+strlen_start:
+ movb (%ecx, %edx), %al
+ test %al, %al
+ jz strlen_done
+ inc %edx
+ jmp strlen_start
+strlen_done:
+ mov $SYS_WRITE, %eax
+ int $0x80
+
+ // close(ebx)
+ mov $SYS_CLOSE, %eax
+ int $0x80
+
+ // ebx = getpid()
+ mov $SYS_GETPID, %eax
+ int $0x80
+ mov %eax, %ebx
+
+ // kill(ebx, SIGSTOP)
+ mov $SYS_KILL, %eax
+ mov $SIGSTOP, %ecx
+ int $0x80
+
+selinux_proc_file:
+ .asciz "/proc/self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/selinux-x86_64.S b/utils/shell-as/shell-code/selinux-x86_64.S
new file mode 100644
index 000000000..94fc876c6
--- /dev/null
+++ b/utils/shell-as/shell-code/selinux-x86_64.S
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Shell code that sets the current SELinux context to a given string.
+//
+// The desired SELinux context is appended to the payload as a null-terminated
+// string.
+//
+// After the SELinux context has been updated the current process will raise
+// SIGSTOP.
+
+#include "./shell-code/constants.S"
+#include "./shell-code/constants-x86_64.S"
+
+.globl __setcon_shell_code_start
+.globl __setcon_shell_code_end
+
+__setcon_shell_code_start:
+
+ // Ensure that the context and SELinux /proc file are readable. This assumes
+ // that the max length of these two strings is shorter than 0x1000.
+ //
+ // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC)
+ mov $SYS_MPROTECT, %rax
+ lea context(%rip), %rdi
+ and $~0xFFF, %rdi
+ mov $0x2000, %rsi
+ mov $(PROT_READ | PROT_EXEC), %rdx
+ syscall
+
+ // rdi = open("/proc/self/attr/current", O_WRONLY, O_WRONLY)
+ mov $SYS_OPEN, %eax
+ lea selinux_proc_file(%rip), %rdi
+ mov $O_WRONLY, %rsi
+ mov $O_WRONLY, %rdx
+ syscall
+ mov %rax, %rdi
+
+ // write(rdi, context, strlen(context))
+ xor %rdx, %rdx
+ lea context(%rip), %rsi
+strlen_start:
+ movb (%rsi, %rdx), %al
+ test %al, %al
+ jz strlen_done
+ inc %rdx
+ jmp strlen_start
+strlen_done:
+ mov $SYS_WRITE, %rax
+ syscall
+
+ // close(rdi)
+ mov $SYS_CLOSE, %rax
+ syscall
+
+ // rdi = getpid()
+ mov $SYS_GETPID, %rax
+ syscall
+ mov %rax, %rdi
+
+ // kill(rdi, SIGSTOP)
+ mov $SYS_KILL, %rax
+ mov $SIGSTOP, %rsi
+ syscall
+
+selinux_proc_file:
+ .asciz "/proc/self/attr/current"
+
+context:
+__setcon_shell_code_end:
diff --git a/utils/shell-as/shell-code/trap-arm.S b/utils/shell-as/shell-code/trap-arm.S
new file mode 100644
index 000000000..8bb347411
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-arm.S
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./shell-code/constants.S"
+
+.thumb
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+bkpt
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/shell-code/trap-arm64.S b/utils/shell-as/shell-code/trap-arm64.S
new file mode 100644
index 000000000..90063ffee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-arm64.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+hlt 0
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGILL
diff --git a/utils/shell-as/shell-code/trap-x86.S b/utils/shell-as/shell-code/trap-x86.S
new file mode 100644
index 000000000..1669bb8ee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-x86.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+int $0x03
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/shell-code/trap-x86_64.S b/utils/shell-as/shell-code/trap-x86_64.S
new file mode 100644
index 000000000..1669bb8ee
--- /dev/null
+++ b/utils/shell-as/shell-code/trap-x86_64.S
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./shell-code/constants.S"
+
+.globl __trap_shell_code_start
+.globl __trap_shell_code_end
+.globl __trap_shell_code_signal
+
+__trap_shell_code_start:
+int $0x03
+__trap_shell_code_end:
+
+__trap_shell_code_signal:
+.int SIGTRAP
diff --git a/utils/shell-as/string-utils.cpp b/utils/shell-as/string-utils.cpp
new file mode 100644
index 000000000..8977f73e0
--- /dev/null
+++ b/utils/shell-as/string-utils.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./string-utils.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+namespace shell_as {
+
+bool StringToUInt32(const char* s, uint32_t* i) {
+ uint64_t value = 0;
+ if (!StringToUInt64(s, &value)) {
+ return false;
+ }
+ if (value > UINT_MAX) {
+ return false;
+ }
+ *i = value;
+ return true;
+}
+
+bool StringToUInt64(const char* s, uint64_t* i) {
+ char* endptr = nullptr;
+ // Reset errno to a non-error value since strtoul does not clear errno.
+ errno = 0;
+ *i = strtoul(s, &endptr, 10);
+ // strtoul will return 0 if the value cannot be parsed as an unsigned long. If
+ // this occurs, ensure that the ID actually was zero. This is done by ensuring
+ // that the end pointer was advanced and that it now points to the end of the
+ // string (a null byte).
+ return errno == 0 && (*i != 0 || (endptr != s && *endptr == '\0'));
+}
+
+bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip,
+ std::vector<uid_t>* ids) {
+ if (line == nullptr) {
+ return false;
+ }
+
+ ids->clear();
+ for (char* id_string = strtok(line, separators); id_string != nullptr;
+ id_string = strtok(nullptr, separators)) {
+ if (num_to_skip > 0) {
+ num_to_skip--;
+ continue;
+ }
+
+ gid_t id;
+ if (!StringToUInt32(id_string, &id)) {
+ return false;
+ }
+ ids->push_back(id);
+ }
+ return true;
+}
+
+} // namespace shell_as
diff --git a/utils/shell-as/string-utils.h b/utils/shell-as/string-utils.h
new file mode 100644
index 000000000..f4910894a
--- /dev/null
+++ b/utils/shell-as/string-utils.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_STRING_UTILS_H_
+#define SHELL_AS_STRING_UTILS_H_
+
+#include <unistd.h>
+
+#include <vector>
+
+namespace shell_as {
+
+// Parses a string into an unsigned 32bit int value. Returns true on success and
+// false otherwise.
+bool StringToUInt32(const char* s, uint32_t* i);
+
+// Parses a string into a unsigned 64bit int value. Returns true on success and
+// false otherwise.
+bool StringToUInt64(const char* s, uint64_t* i);
+
+// Splits a line of uid_t/guid_t values by a given separator and returns the
+// integer values in a vector.
+//
+// The separators string may contain multiple characters and is treated as a set
+// of possible separating characters.
+//
+// If num_to_skip is non-zero, then that many entries will be skipped after
+// splitting the line and before parsing the values as integers. This is useful
+// if the line has a prefix such as "Gid: 1 2 3 4".
+//
+// Returns true on success and false otherwise.
+bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip,
+ std::vector<uid_t>* ids);
+
+} // namespace shell_as
+
+#endif // SHELL_AS_STRING_UTILS_H_
diff --git a/utils/shell-as/test-app.cpp b/utils/shell-as/test-app.cpp
new file mode 100644
index 000000000..84fedcab6
--- /dev/null
+++ b/utils/shell-as/test-app.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include "./test-app.h"
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <string>
+
+#include "./string-utils.h"
+
+namespace shell_as {
+
+// Returns a pointer to bytes of the test app APK along with the length in bytes
+// of the APK.
+//
+// This function is defined by the shell-as-test-app-apk-cpp genrule.
+void GetTestApk(uint8_t **apk, size_t *length);
+
+namespace {
+
+// The staging path for the test app APK.
+const char kTestAppApkStagingPath[] = "/data/local/tmp/shell-as-test-app.apk";
+
+// Writes the test app to a staging location and then installs the APK via the
+// 'pm' utility. The app is granted runtime permissions on installation. Returns
+// true if the app is installed successfully.
+bool InstallTestApp() {
+ uint8_t *apk = nullptr;
+ size_t apk_size = 0;
+ GetTestApk(&apk, &apk_size);
+
+ int staging_file = open(kTestAppApkStagingPath, O_WRONLY | O_CREAT | O_TRUNC,
+ S_IRUSR | S_IWUSR);
+ if (staging_file == -1) {
+ std::cerr << "Unable to open staging APK path." << std::endl;
+ return false;
+ }
+
+ size_t bytes_written = write(staging_file, apk, apk_size);
+ close(staging_file);
+ if (bytes_written != apk_size) {
+ std::cerr << "Unable to write entire test app APK." << std::endl;
+ return false;
+ }
+
+ const char cmd_template[] = "pm install -g %s > /dev/null 2> /dev/null";
+ char system_cmd[sizeof(cmd_template) + sizeof(kTestAppApkStagingPath) + 1] =
+ {};
+ sprintf(system_cmd, cmd_template, kTestAppApkStagingPath);
+ return system(system_cmd) == 0;
+}
+
+// Uninstalls the test app if it is installed. This method is a no-op if the app
+// is not installed.
+void UninstallTestApp() {
+ system(
+ "pm uninstall com.android.google.tools.security.shell_as"
+ " > /dev/null 2> /dev/null");
+}
+
+// Starts the main activity of the test app. This is necessary as some aspects
+// of the security context can only be inferred from a running process.
+bool StartTestApp() {
+ return system(
+ "am start-activity "
+ "com.android.google.tools.security.shell_as/"
+ ".MainActivity"
+ " > /dev/null 2> /dev/null") == 0;
+}
+
+// Obtain the process ID of the test app and returns true if it is running.
+// Returns false otherwise.
+bool GetTestAppProcessId(pid_t *test_app_pid) {
+ FILE *pgrep = popen(
+ "pgrep -f "
+ "com.android.google.tools.security.shell_as",
+ "r");
+ if (!pgrep) {
+ std::cerr << "Unable to execute pgrep." << std::endl;
+ return false;
+ }
+
+ char pgrep_output[128];
+ memset(pgrep_output, 0, sizeof(pgrep_output));
+ int bytes_read = fread(pgrep_output, 1, sizeof(pgrep_output) - 1, pgrep);
+ pclose(pgrep);
+ if (bytes_read <= 0) {
+ // Unable to find the process. This may happen if the app is still starting
+ // up.
+ return false;
+ }
+ return StringToUInt32(pgrep_output, (uint32_t *)test_app_pid);
+}
+} // namespace
+
+bool SetupAndStartTestApp(pid_t *test_app_pid) {
+ UninstallTestApp();
+
+ if (!InstallTestApp()) {
+ std::cerr << "Unable to install test app." << std::endl;
+ return false;
+ }
+
+ if (!StartTestApp()) {
+ std::cerr << "Unable to start and obtain test app PID." << std::endl;
+ return false;
+ }
+
+ for (int i = 0; i < 5; i++) {
+ if (GetTestAppProcessId(test_app_pid)) {
+ return true;
+ }
+ sleep(1);
+ }
+ return false;
+}
+} // namespace shell_as
diff --git a/utils/shell-as/test-app.h b/utils/shell-as/test-app.h
new file mode 100644
index 000000000..866bbfb33
--- /dev/null
+++ b/utils/shell-as/test-app.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#ifndef SHELL_AS_TEST_APP_H_
+#define SHELL_AS_TEST_APP_H_
+
+#include <sys/types.h>
+
+namespace shell_as {
+
+// Installs and launches the embedded shell-as test app. The test app requests
+// and is granted all non-system permissions defined by the OS. The test_app_pid
+// parameter is set to the process ID of the running test app. Returns true if
+// successful.
+bool SetupAndStartTestApp(pid_t *test_app_pid);
+}
+
+#endif // SHELL_AS_TEST_APP_H_