Merge "RaResponder: add support for NS/NA for tests"
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 353fe69..1ad9c35 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -228,6 +228,35 @@
     public static final int TAG_SYSTEM_PROBE = 0xFFFFFF81;
     public static final int TAG_SYSTEM_DNS = 0xFFFFFF82;
 
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTPS URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTPS_URL = "test_captive_portal_https_url";
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTP URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTP_URL = "test_captive_portal_http_url";
+    /**
+     * Expiration time of the test URL, in ms, relative to {@link System#currentTimeMillis()}.
+     *
+     * <p>After this expiration time, test URLs will be ignored. They will also be ignored if
+     * the expiration time is more than 10 minutes in the future, to avoid misconfiguration
+     * following test runs.
+     */
+    public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
+
     // TODO: Move to Inet4AddressUtils
     // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
     // for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 8bc8eb1..a21e685 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -37,6 +37,9 @@
 // Android T / 13 (api level 33) - support for shared/selinux_context/pindir
 #define BPFLOADER_T_VERSION 19u
 
+// BpfLoader v0.25+ support obj@ver.o files
+#define BPFLOADER_OBJ_AT_VER_VERSION 25u
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -179,20 +182,39 @@
         return bpf_map_delete_elem_unsafe(&the_map, k);                                          \
     };
 
+#ifndef DEFAULT_BPF_MAP_SELINUX_CONTEXT
+#define DEFAULT_BPF_MAP_SELINUX_CONTEXT ""
+#endif
+
+#ifndef DEFAULT_BPF_MAP_PIN_SUBDIR
+#define DEFAULT_BPF_MAP_PIN_SUBDIR ""
+#endif
+
+#ifndef DEFAULT_BPF_MAP_UID
+#define DEFAULT_BPF_MAP_UID AID_ROOT
+#elif BPFLOADER_MIN_VER < 28u
+#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
+#endif
+
 #define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, "", "", false)
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false)
 
 #define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT, 0600)
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, AID_ROOT, 0600)
 
 #define DEFINE_BPF_MAP_GWO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, gid, 0620)
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0620)
 
 #define DEFINE_BPF_MAP_GRO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, gid, 0640)
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0640)
 
 #define DEFINE_BPF_MAP_GRW(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, gid, 0660)
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
+                       DEFAULT_BPF_MAP_UID, gid, 0660)
 
 static int (*bpf_probe_read)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read;
 static int (*bpf_probe_read_str)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_str;
@@ -221,9 +243,18 @@
     SECTION(SECTION_NAME)                                                                          \
     int the_prog
 
+#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
+#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
+#endif
+
+#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
+#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
+#endif
+
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
                                        opt) \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, opt, "", "")
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, opt, \
+                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR)
 
 // Programs (here used in the sense of functions/sections) marked optional are allowed to fail
 // to load (for example due to missing kernel patches).
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index 14a0295..2d47426 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -143,7 +143,7 @@
     //   unsigned int inner_map_idx;
     //   unsigned int numa_node;
 
-    unsigned int uid;   // uid_t
+    unsigned int zero;  // uid_t, for compat with old (buggy) bpfloader must be AID_ROOT == 0
     unsigned int gid;   // gid_t
     unsigned int mode;  // mode_t
 
@@ -171,13 +171,15 @@
 
     bool shared;  // use empty string as 'file' component of pin path - allows cross .o map sharing
     char pad0[3];  // manually pad up to 4 byte alignment, may be used for extensions in the future
+
+    unsigned int uid;   // uid_t
 };
 
 _Static_assert(sizeof(((struct bpf_map_def *)0)->selinux_context) == 32, "must be 32 bytes");
 _Static_assert(sizeof(((struct bpf_map_def *)0)->pin_subdir) == 32, "must be 32 bytes");
 
 // This needs to be updated whenever the above structure definition is expanded.
-_Static_assert(sizeof(struct bpf_map_def) == 116, "sizeof struct bpf_map_def != 116");
+_Static_assert(sizeof(struct bpf_map_def) == 120, "sizeof struct bpf_map_def != 120");
 _Static_assert(__alignof__(struct bpf_map_def) == 4, "__alignof__ struct bpf_map_def != 4");
 _Static_assert(_Alignof(struct bpf_map_def) == 4, "_Alignof struct bpf_map_def != 4");
 
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
index 446e881..aa1bfee 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -17,6 +17,8 @@
 package com.android.net.module.util;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import androidx.test.filters.SmallTest;
@@ -27,16 +29,24 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class SharedLogTest {
     private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
     private static final String TIMESTAMP = "HH:MM:SS";
+    private static final String TAG = "top";
 
     @Test
     public void testBasicOperation() {
-        final SharedLog logTop = new SharedLog("top");
+        final SharedLog logTop = new SharedLog(TAG);
+        assertTrue(TAG.equals(logTop.getTag()));
+
         logTop.mark("first post!");
 
         final SharedLog logLevel2a = logTop.forSubComponent("twoA");
@@ -48,8 +58,10 @@
 
         final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
         logTop.log("still logging");
-        logLevel3.log("3 >> 2");
+        logLevel2b.e(new Exception("Got another exception"));
+        logLevel3.i("3 >> 2");
         logLevel2a.mark("ok: last post");
+        logTop.logf("finished!");
 
         final String[] expected = {
             " - MARK first post!",
@@ -59,29 +71,53 @@
             " - [twoB] ERROR Wait, here's one: Test",
             " - [twoA] WARN second post?",
             " - still logging",
+            " - [twoB] ERROR java.lang.Exception: Got another exception",
             " - [twoA.three] 3 >> 2",
             " - [twoA] MARK ok: last post",
+            " - finished!",
         };
         // Verify the logs are all there and in the correct order.
-        verifyLogLines(expected, logTop);
+        assertDumpLogs(expected, logTop);
 
         // In fact, because they all share the same underlying LocalLog,
         // every subcomponent SharedLog's dump() is identical.
-        verifyLogLines(expected, logLevel2a);
-        verifyLogLines(expected, logLevel2b);
-        verifyLogLines(expected, logLevel3);
+        assertDumpLogs(expected, logLevel2a);
+        assertDumpLogs(expected, logLevel2b);
+        assertDumpLogs(expected, logLevel3);
     }
 
-    private static void verifyLogLines(String[] expected, SharedLog log) {
+    private static void assertDumpLogs(String[] expected, SharedLog log) {
+        verifyLogLines(expected, dump(log));
+        verifyLogLines(reverse(expected), reverseDump(log));
+    }
+
+    private static String dump(SharedLog log) {
+        return getSharedLogString(pw -> log.dump(null /* fd */, pw, null /* args */));
+    }
+
+    private static String reverseDump(SharedLog log) {
+        return getSharedLogString(pw -> log.reverseDump(pw));
+    }
+
+    private static String[] reverse(String[] ary) {
+        final List<String> ls = new ArrayList<>(Arrays.asList(ary));
+        Collections.reverse(ls);
+        return ls.toArray(new String[ary.length]);
+    }
+
+    private static String getSharedLogString(Consumer<PrintWriter> functor) {
         final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
         final PrintWriter pw = new PrintWriter(ostream, true);
-        log.dump(null, pw, null);
+        functor.accept(pw);
 
         final String dumpOutput = ostream.toString();
-        assertTrue(dumpOutput != null);
-        assertTrue(!"".equals(dumpOutput));
+        assertNotNull(dumpOutput);
+        assertFalse("".equals(dumpOutput));
+        return dumpOutput;
+    }
 
-        final String[] lines = dumpOutput.split("\n");
+    private static void verifyLogLines(String[] expected, String gottenLogs) {
+        final String[] lines = gottenLogs.split("\n");
         assertEquals(expected.length, lines.length);
 
         for (int i = 0; i < expected.length; i++) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
index ff8c668..82f1d9b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
@@ -17,7 +17,7 @@
 package com.android.testutils
 
 import android.net.NetworkSpecifier
-import android.os.Build
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
 
 /**
  * Test utility to create [NetworkSpecifier]s on different SDK versions.
@@ -26,7 +26,7 @@
     @JvmStatic
     fun makeTestNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no TestNetworkSpecifier, StringNetworkSpecifier was used instead
-        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+        if (!isAtLeastS()) {
             return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // TestNetworkSpecifier is not part of the SDK in some branches using this utility
@@ -37,7 +37,7 @@
     @JvmStatic
     fun makeEthernetNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no EthernetNetworkSpecifier, StringNetworkSpecifier was used instead
-        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+        if (!isAtLeastS()) {
             return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // EthernetNetworkSpecifier is not part of the SDK in some branches using this utility
@@ -50,4 +50,4 @@
         return Class.forName(clazz)
                 .getConstructor(String::class.java).newInstance(specifier) as NetworkSpecifier
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 3fcf801..21e84da 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -18,58 +18,41 @@
 
 import android.os.Build
 import androidx.test.InstrumentationRegistry
-import com.android.modules.utils.build.SdkLevel
-import kotlin.test.fail
+import com.android.modules.utils.build.UnboundedSdkLevel
 import org.junit.Assume.assumeTrue
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 import java.util.regex.Pattern
 
-// TODO: Remove it when Build.VERSION_CODES.SC_V2 is available
-const val SC_V2 = 32
+@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
+const val SC_V2 = Build.VERSION_CODES.S_V2
 
 private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
 private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
 
+private fun isDevSdkInRange(minExclusive: String?, maxInclusive: String?): Boolean {
+    return (minExclusive == null || !UnboundedSdkLevel.isAtMost(minExclusive)) &&
+            (maxInclusive == null || UnboundedSdkLevel.isAtMost(maxInclusive))
+}
+
 /**
- * Returns true if the development SDK version of the device is in the provided range.
+ * Returns true if the development SDK version of the device is in the provided annotation range.
  *
- * If the device is not using a release SDK, the development SDK is considered to be higher than
- * [Build.VERSION.SDK_INT].
+ * If the device is not using a release SDK, the development SDK differs from
+ * [Build.VERSION.SDK_INT], and is indicated by the device codenames; see [UnboundedSdkLevel].
  */
-fun isDevSdkInRange(minExclusive: Int?, maxInclusive: Int?): Boolean {
-    return (minExclusive == null || isDevSdkAfter(minExclusive)) &&
-            (maxInclusive == null || isDevSdkUpTo(maxInclusive))
-}
-
-private fun isDevSdkAfter(minExclusive: Int): Boolean {
-    // A development build for T typically has SDK_INT = 30 (R) or SDK_INT = 31 (S), so SDK_INT
-    // alone cannot be used to check the SDK version.
-    // For recent SDKs that still have development builds used for testing, use SdkLevel utilities
-    // instead of SDK_INT.
-    return when (minExclusive) {
-        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
-        SC_V2 -> SdkLevel.isAtLeastT()
-        // TODO: To use SdkLevel.isAtLeastSv2 when available
-        Build.VERSION_CODES.S -> fail("Do you expect to ignore the test until T? Use SC_V2 instead")
-        Build.VERSION_CODES.R -> SdkLevel.isAtLeastS()
-        // Development builds of SDK versions <= R are not used anymore
-        else -> Build.VERSION.SDK_INT > minExclusive
-    }
-}
-
-private fun isDevSdkUpTo(maxInclusive: Int): Boolean {
-    return when (maxInclusive) {
-        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
-        SC_V2 -> !SdkLevel.isAtLeastT()
-        // TODO: To use SdkLevel.isAtLeastSv2 when available
-        Build.VERSION_CODES.S ->
-                fail("Do you expect to ignore the test before T? Use SC_V2 instead")
-        Build.VERSION_CODES.R -> !SdkLevel.isAtLeastS()
-        // Development builds of SDK versions <= R are not used anymore
-        else -> Build.VERSION.SDK_INT <= maxInclusive
-    }
+fun isDevSdkInRange(
+    ignoreUpTo: DevSdkIgnoreRule.IgnoreUpTo?,
+    ignoreAfter: DevSdkIgnoreRule.IgnoreAfter?
+): Boolean {
+    val minExclusive =
+            if (ignoreUpTo?.value == 0) ignoreUpTo.codename
+            else ignoreUpTo?.value?.toString()
+    val maxInclusive =
+            if (ignoreAfter?.value == 0) ignoreAfter.codename
+            else ignoreAfter?.value?.toString()
+    return isDevSdkInRange(minExclusive, maxInclusive)
 }
 
 private fun getMaxTargetSdk(description: Description): Int? {
@@ -86,13 +69,23 @@
  * If the device is not using a release SDK, the development SDK is considered to be higher than
  * [Build.VERSION.SDK_INT].
  *
- * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
- * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+ * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this codename or
+ *                        SDK level.
+ * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this codename or
+ *                         SDK level.
  */
 class DevSdkIgnoreRule @JvmOverloads constructor(
-    private val ignoreClassUpTo: Int? = null,
-    private val ignoreClassAfter: Int? = null
+    private val ignoreClassUpTo: String? = null,
+    private val ignoreClassAfter: String? = null
 ) : TestRule {
+    /**
+     * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
+     * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+     */
+    @JvmOverloads
+    constructor(ignoreClassUpTo: Int?, ignoreClassAfter: Int? = null) : this(
+            ignoreClassUpTo?.toString(), ignoreClassAfter?.toString())
+
     override fun apply(base: Statement, description: Description): Statement {
         return IgnoreBySdkStatement(base, description)
     }
@@ -103,7 +96,7 @@
      * If the device is not using a release SDK, the development SDK is considered to be higher
      * than [Build.VERSION.SDK_INT].
      */
-    annotation class IgnoreAfter(val value: Int)
+    annotation class IgnoreAfter(val value: Int = 0, val codename: String = "")
 
     /**
      * Ignore the test for any development SDK that lower than or equal to [value].
@@ -111,7 +104,7 @@
      * If the device is not using a release SDK, the development SDK is considered to be higher
      * than [Build.VERSION.SDK_INT].
      */
-    annotation class IgnoreUpTo(val value: Int)
+    annotation class IgnoreUpTo(val value: Int = 0, val codename: String = "")
 
     private inner class IgnoreBySdkStatement(
         private val base: Statement,
@@ -124,7 +117,7 @@
             val devSdkMessage = "Skipping test for build ${Build.VERSION.CODENAME} " +
                     "with SDK ${Build.VERSION.SDK_INT}"
             assumeTrue(devSdkMessage, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
-            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo, ignoreAfter))
 
             val maxTargetSdk = getMaxTargetSdk(description)
             if (maxTargetSdk != null) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index da7bf97..2e73666 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -52,7 +52,7 @@
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
 
-        if (isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value)) AndroidJUnit4(klass) else null
+        if (isDevSdkInRange(ignoreUpTo, ignoreAfter)) AndroidJUnit4(klass) else null
     }
 
     override fun run(notifier: RunNotifier) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
index f557f18..a4dbd9a 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
@@ -19,6 +19,7 @@
 package com.android.testutils
 
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.ExceptionUtils.ThrowingRunnable
 import com.android.testutils.ExceptionUtils.ThrowingSupplier
 
@@ -31,6 +32,23 @@
  */
 fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
     val autom = InstrumentationRegistry.getInstrumentation().uiAutomation
+
+    // Calls to adoptShellPermissionIdentity do not nest, and dropShellPermissionIdentity drops all
+    // permissions. Thus, nesting calls will almost certainly cause test bugs, On S+, where we can
+    // detect this, refuse to do it.
+    //
+    // TODO: when R is deprecated, we could try to make this work instead.
+    // - Get the list of previously-adopted permissions.
+    // - Adopt the union of the previously-adopted and newly-requested permissions.
+    // - Run the task.
+    // - Adopt the previously-adopted permissions, dropping the ones just adopted.
+    //
+    // This would allow tests (and utility classes, such as the TestCarrierConfigReceiver attempted
+    // in aosp/2106007) to call runAsShell even within a test that has already adopted permissions.
+    if (SdkLevel.isAtLeastS() && !autom.getAdoptedShellPermissions().isNullOrEmpty()) {
+        throw IllegalStateException("adoptShellPermissionIdentity calls must not be nested")
+    }
+
     autom.adoptShellPermissionIdentity(*permissions)
     try {
         return task()