Add a test ensuring multiple namespaces for APEX

This change adds a test to CtsApexTestCases to ensure that mount
namespaces are correctly configured. Specifically, for the devices
supporting APEX updates (i.e. ro.apex.updatable == true), the pre-apexd
processes (e.g. vold) and post-apexd processes (e.g. init, zygote, etc.)
must be running in different mount namespaces. In case of legacy
devices, they should be in the same mount namespace.

The test compares the mount namespaces indirectly by comparing the mount
ID of the "root" (/) mount point. Directly comparing the mount namespace
ID (/proc/<pid>/ns/mnt) is not available in user builds..

Bug: 139337320
Test: atest CtsApexTestCases

Merged-In: Ia8ec88b4b0e84db9bd1bff82ce6d86845bd1a4a2
(cherry picked from commit 45e0824d597f592a07a711cf5b5a0cae36e1929d)
Change-Id: Ia8ec88b4b0e84db9bd1bff82ce6d86845bd1a4a2
diff --git a/hostsidetests/apex/src/android/apex/cts/ApexTest.java b/hostsidetests/apex/src/android/apex/cts/ApexTest.java
index fb81760..5c9df98 100644
--- a/hostsidetests/apex/src/android/apex/cts/ApexTest.java
+++ b/hostsidetests/apex/src/android/apex/cts/ApexTest.java
@@ -24,8 +24,13 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Arrays;
+
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class ApexTest extends BaseHostJUnit4Test {
+  private boolean isApexUpdatable() throws Exception {
+    return Boolean.parseBoolean(getDevice().getProperty("ro.apex.updatable"));
+  }
 
   /**
    * Ensures that the built-in APEXes are all with flattened APEXes
@@ -51,7 +56,7 @@
         "No APEX found",
         (numFlattenedApexes + numNonFlattenedApexes) != 0);
 
-    if (Boolean.parseBoolean(getDevice().getProperty("ro.apex.updatable"))) {
+    if (isApexUpdatable()) {
       Assert.assertTrue(numFlattenedApexes +
           " flattened APEX(es) found on a device supporting updatable APEX",
           numFlattenedApexes == 0);
@@ -79,4 +84,50 @@
         CTS_SHIM_APEX_NAME + ".apex\" | wc -l");
     return result.getExitCode() == 0 ? Integer.parseInt(result.getStdout().trim()) : 0;
   }
+
+  /**
+   * Ensures that pre-apexd processes (e.g. vold) and post-apexd processes (e.g. init) are using
+   * different mount namespaces (in case of ro.apexd.updatable is true), or not.
+   */
+  @Test
+  public void testMountNamespaces() throws Exception {
+    final int rootMountIdOfInit = getMountEntry("1", "/").mountId;
+    final int rootMountIdOfVold = getMountEntry("$(pidof vold)", "/").mountId;
+    if (isApexUpdatable()) {
+      Assert.assertNotEquals("device supports updatable APEX, but is not using multiple mount namespaces",
+          rootMountIdOfInit, rootMountIdOfVold);
+    } else {
+      Assert.assertEquals("device doesn't support updatable APEX, but is using multiple mount namespaces",
+          rootMountIdOfInit, rootMountIdOfVold);
+    }
+  }
+
+  private static class MountEntry {
+    public final int mountId;
+    public final String mountPoint;
+
+    public MountEntry(String mountInfoLine) {
+      String[] tokens = mountInfoLine.split(" ");
+      if (tokens.length < 5) {
+        throw new RuntimeException(mountInfoLine + " doesn't seem to be from mountinfo");
+      }
+      mountId = Integer.parseInt(tokens[0]);
+      mountPoint = tokens[4];
+    }
+  }
+
+  private String[] readMountInfo(String pidExpression) throws Exception {
+    CommandResult result = getDevice().executeShellV2Command(
+        "cat /proc/" + pidExpression + "/mountinfo");
+    if (result.getExitCode() != 0) {
+      throw new RuntimeException("failed to read mountinfo for " + pidExpression);
+    }
+    return result.getStdout().trim().split("\n");
+  }
+
+  private MountEntry getMountEntry(String pidExpression, String mountPoint) throws Exception {
+    return Arrays.asList(readMountInfo(pidExpression)).stream()
+        .map(MountEntry::new)
+        .filter(entry -> mountPoint.equals(entry.mountPoint)).findAny().get();
+  }
 }