Snap for 11219529 from 4d7d96a648941d07ef912c6becf1bbab822c41dd to mainline-tzdata4-release

Change-Id: I7b752d5d13adedfe82ef7290ae178511528bd1d5
diff --git a/libraries/audio-test-harness/client-lib/Android.bp b/libraries/audio-test-harness/client-lib/Android.bp
index 5b14d10..554e901 100644
--- a/libraries/audio-test-harness/client-lib/Android.bp
+++ b/libraries/audio-test-harness/client-lib/Android.bp
@@ -65,7 +65,6 @@
 
 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 @@
     ],
     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 b8e458f..65e5c8f 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 @@
 
 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 @@
                             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 0000000..e4997d9
--- /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 12665b2..214ccc0 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 @@
         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;
+        mDevice = device;
+        mProcessName = processName;
+        mWasAdbRoot = device.isAdbRoot();
 
-        // 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");
+        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 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 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 87551a6..d8fd05f 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.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 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 @@
      * @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 @@
      * @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 @@
      * @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 @@
      * @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 @@
      * @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 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 @@
             {
                 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 @@
      *
      * @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 0000000..7f8d8bc
--- /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 0000000..25252d1
--- /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 03b1934..f82a907 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 @@
         }
     }
 
+    @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 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 0000000..c6f4c69
--- /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 0000000..aa58f10
--- /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 0000000..3a46039
--- /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 663994a..41b399a 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 2e27305..fe824cf 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 @@
     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 b7f8ac8..a16eccb 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 da1f7bf..a218e81 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 @@
 
 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);
-    }
-
-    String getString(@StringRes int resId) {
-        return mAppContext.getResources().getString(resId);
-    }
-
+    /** Device test */
     @Test
-    public void testDeviceSideMethod() {
+    public void testDeviceSideMethod() throws Exception {
+        mContext = getApplicationContext();
+
+        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);
+
+        // 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 27d682d..daeb76c 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 ac87925..0000000
--- 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 acdcd84..0000000
--- 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 286e6fd..0000000
--- 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 0000000..c9329b9
--- /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 0000000..5f227f0
--- /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 0000000..273a3b2
--- /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 0000000..8d8f8c5
--- /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 0000000..96dc1c9
--- /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 0000000..07e89b1
--- /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 0000000..431db99
--- /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 0000000..e0f6f93
--- /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 0000000..d5d178c
--- /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 0000000..9a893c3
--- /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 0000000..4bf495f
--- /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 0000000..ea7979b
--- /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 0000000..17a8cca
--- /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 0000000..8a82555
--- /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 0000000..eba40f3
--- /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 0000000..3ef5292
--- /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 0000000..2e8f511
--- /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 0000000..9dc4d11
--- /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 0000000..6f7af6c
--- /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 0000000..880cf1c
--- /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 0000000..df92545
--- /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 0000000..4e5efc9
--- /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 0000000..bdadf6c
--- /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 0000000..9c88d16
--- /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 0000000..10db630
--- /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 0000000..a9dec75
--- /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 0000000..afa9d14
--- /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 0000000..0bf95cc
--- /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 0000000..9e2a238
--- /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 0000000..0c9480f
--- /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 0000000..4e8c492
--- /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 0000000..81c150f
--- /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 0000000..94fc876
--- /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 0000000..8bb3474
--- /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 0000000..90063ff
--- /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 0000000..1669bb8
--- /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 0000000..1669bb8
--- /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 0000000..8977f73
--- /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 0000000..f491089
--- /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 0000000..84fedca
--- /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 0000000..866bbfb
--- /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_