Add payload linker namespace test

Bug: 261110181
Test: atest MicrodroidTestApp
Change-Id: Id95c91a1ac75f7df5ae6e38980c626f82cdf5248
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index bd5b180..72a0090 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -132,6 +132,8 @@
         private OptionalLong mKernelStartedNanoTime = OptionalLong.empty();
         private OptionalLong mInitStartedNanoTime = OptionalLong.empty();
         private OptionalLong mPayloadStartedNanoTime = OptionalLong.empty();
+        private StringBuilder mConsoleOutput = new StringBuilder();
+        private StringBuilder mLogOutput = new StringBuilder();
 
         private void processBootEvents(String log) {
             if (!mVcpuStartedNanoTime.isPresent()) {
@@ -149,44 +151,50 @@
             }
         }
 
-        private void logVmOutputAndMonitorBootEvents(String tag,
+        private void logVmOutputAndMonitorBootEvents(
+                String tag,
                 InputStream vmOutputStream,
                 String name,
+                StringBuilder result,
                 boolean monitorEvents) {
             new Thread(
-                    () -> {
-                        try {
-                            BufferedReader reader =
-                                    new BufferedReader(new InputStreamReader(vmOutputStream));
-                            String line;
-                            while ((line = reader.readLine()) != null
-                                    && !Thread.interrupted()) {
-                                if (monitorEvents) processBootEvents(line);
-                                Log.i(tag, name + ": " + line);
-                            }
-                        } catch (Exception e) {
-                            Log.w(tag, name, e);
-                        }
-                    }).start();
+                            () -> {
+                                try {
+                                    BufferedReader reader =
+                                            new BufferedReader(
+                                                    new InputStreamReader(vmOutputStream));
+                                    String line;
+                                    while ((line = reader.readLine()) != null
+                                            && !Thread.interrupted()) {
+                                        if (monitorEvents) processBootEvents(line);
+                                        Log.i(tag, name + ": " + line);
+                                        result.append(line + "\n");
+                                    }
+                                } catch (Exception e) {
+                                    Log.w(tag, name, e);
+                                }
+                            })
+                    .start();
         }
 
-        private void logVmOutputAndMonitorBootEvents(String tag,
-                InputStream vmOutputStream,
-                String name) {
-            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, true);
+        private void logVmOutputAndMonitorBootEvents(
+                String tag, InputStream vmOutputStream, String name, StringBuilder result) {
+            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, result, true);
         }
 
         /** Copy output from the VM to logcat. This is helpful when things go wrong. */
-        protected void logVmOutput(String tag, InputStream vmOutputStream, String name) {
-            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, false);
+        protected void logVmOutput(
+                String tag, InputStream vmOutputStream, String name, StringBuilder result) {
+            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, result, false);
         }
 
         public void runToFinish(String logTag, VirtualMachine vm)
                 throws VirtualMachineException, InterruptedException {
             vm.setCallback(mExecutorService, this);
             vm.run();
-            logVmOutputAndMonitorBootEvents(logTag, vm.getConsoleOutput(), "Console");
-            logVmOutput(logTag, vm.getLogOutput(), "Log");
+            logVmOutputAndMonitorBootEvents(
+                    logTag, vm.getConsoleOutput(), "Console", mConsoleOutput);
+            logVmOutput(logTag, vm.getLogOutput(), "Log", mLogOutput);
             mExecutorService.awaitTermination(300, TimeUnit.SECONDS);
         }
 
@@ -206,6 +214,14 @@
             return mPayloadStartedNanoTime;
         }
 
+        public String getConsoleOutput() {
+            return mConsoleOutput.toString();
+        }
+
+        public String getLogOutput() {
+            return mLogOutput.toString();
+        }
+
         protected void forceStop(VirtualMachine vm) {
             try {
                 vm.stop();
@@ -248,14 +264,20 @@
         public final OptionalLong initStartedNanoTime;
         public final OptionalLong payloadStartedNanoTime;
 
-        BootResult(boolean payloadStarted,
+        public final String consoleOutput;
+        public final String logOutput;
+
+        BootResult(
+                boolean payloadStarted,
                 int deathReason,
                 long apiCallNanoTime,
                 long endToEndNanoTime,
                 OptionalLong vcpuStartedNanoTime,
                 OptionalLong kernelStartedNanoTime,
                 OptionalLong initStartedNanoTime,
-                OptionalLong payloadStartedNanoTime) {
+                OptionalLong payloadStartedNanoTime,
+                String consoleOutput,
+                String logOutput) {
             this.apiCallNanoTime = apiCallNanoTime;
             this.payloadStarted = payloadStarted;
             this.deathReason = deathReason;
@@ -264,6 +286,8 @@
             this.kernelStartedNanoTime = kernelStartedNanoTime;
             this.initStartedNanoTime = initStartedNanoTime;
             this.payloadStartedNanoTime = payloadStartedNanoTime;
+            this.consoleOutput = consoleOutput;
+            this.logOutput = logOutput;
         }
 
         private long getVcpuStartedNanoTime() {
@@ -332,7 +356,9 @@
                 listener.getVcpuStartedNanoTime(),
                 listener.getKernelStartedNanoTime(),
                 listener.getInitStartedNanoTime(),
-                listener.getPayloadStartedNanoTime());
+                listener.getPayloadStartedNanoTime(),
+                listener.getConsoleOutput(),
+                listener.getLogOutput());
     }
 
     /** Execute a command. Returns stdout. */
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 4dc9489..edb4759 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -23,6 +23,8 @@
     jni_libs: [
         "MicrodroidTestNativeLib",
         "MicrodroidIdleNativeLib",
+        "MicrodroidEmptyNativeLib",
+        "MicrodroidPrivateLinkingNativeLib",
     ],
     jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
@@ -62,3 +64,21 @@
     header_libs: ["vm_payload_headers"],
     stl: "libc++_static",
 }
+
+// An empty payload missing AVmPayload_main
+cc_library_shared {
+    name: "MicrodroidEmptyNativeLib",
+    srcs: ["src/native/emptybinary.cpp"],
+    stl: "none",
+}
+
+// A payload which tries to link against libselinux, one of private libraries
+cc_library_shared {
+    name: "MicrodroidPrivateLinkingNativeLib",
+    srcs: ["src/native/idlebinary.cpp"],
+    header_libs: ["vm_payload_headers"],
+    // HACK: linking against "libselinux" will embed libselinux.so into the apk
+    // link against a stub to prevent libselinux.so from being embedded
+    shared_libs: ["libselinux#latest"],
+    stl: "libc++_static",
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index fa8be93..95b4760 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -828,6 +828,50 @@
                 VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR);
     }
 
+    // Checks whether microdroid_launcher started but payload failed. reason must be recorded in the
+    // console output.
+    private void assertThatPayloadFailsDueTo(VirtualMachine vm, String reason) throws Exception {
+        final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+        final CompletableFuture<Integer> exitCodeFuture = new CompletableFuture<>();
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        payloadStarted.complete(true);
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        exitCodeFuture.complete(exitCode);
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+
+        assertThat(payloadStarted.getNow(false)).isTrue();
+        assertThat(exitCodeFuture.getNow(0)).isNotEqualTo(0);
+        assertThat(listener.getConsoleOutput()).contains(reason);
+    }
+
+    @Test
+    public void bootFailsWhenBinaryIsMissingEntryFunction() throws Exception {
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder().setPayloadBinaryPath("MicrodroidEmptyNativeLib.so");
+        VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_missing_entry", normalConfig);
+
+        assertThatPayloadFailsDueTo(vm, "Failed to find entrypoint");
+    }
+
+    @Test
+    public void bootFailsWhenBinaryTriesToLinkAgainstPrivateLibs() throws Exception {
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder().setPayloadBinaryPath("MicrodroidPrivateLinkingNativeLib.so");
+        VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_private_linking", normalConfig);
+
+        assertThatPayloadFailsDueTo(vm, "Failed to dlopen");
+    }
+
     @Test
     public void sameInstancesShareTheSameVmObject() throws Exception {
         VirtualMachineConfig config =
diff --git a/tests/testapk/src/native/emptybinary.cpp b/tests/testapk/src/native/emptybinary.cpp
new file mode 100644
index 0000000..15f3f4d
--- /dev/null
+++ b/tests/testapk/src/native/emptybinary.cpp
@@ -0,0 +1 @@
+// a binary without AVmPayload_main