Merge changes from topic "mount-vendor-in-microdroid" into main

* changes:
  Propagate to Microdroid whether it should mount vendor partition
  Add test api for setting vendor disk image
  Add a way to add a vendor disk image when launching Microdroid VM
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 1298000..cf95770 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -13,6 +13,7 @@
 
   public static final class VirtualMachineConfig.Builder {
     method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
   }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b400eeb..4cad2e3 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -76,6 +76,7 @@
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -167,6 +168,8 @@
     /** Whether the app can write console input to the VM */
     private final boolean mVmConsoleInputSupported;
 
+    @Nullable private final File mVendorDiskImage;
+
     private VirtualMachineConfig(
             @Nullable String packageName,
             @Nullable String apkPath,
@@ -178,7 +181,8 @@
             @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
-            boolean vmConsoleInputSupported) {
+            boolean vmConsoleInputSupported,
+            @Nullable File vendorDiskImage) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -191,6 +195,7 @@
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
+        mVendorDiskImage = vendorDiskImage;
     }
 
     /** Loads a config from a file. */
@@ -267,6 +272,11 @@
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
         builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
 
+        String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
+        if (vendorDiskImagePath != null) {
+            builder.setVendorDiskImage(new File(vendorDiskImagePath));
+        }
+
         return builder.build();
     }
 
@@ -302,6 +312,9 @@
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
         b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        if (mVendorDiskImage != null) {
+            b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
+        }
         b.writeToStream(output);
     }
 
@@ -501,6 +514,20 @@
                 vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
                 break;
         }
+        if (mVendorDiskImage != null) {
+            VirtualMachineAppConfig.CustomConfig customConfig =
+                    new VirtualMachineAppConfig.CustomConfig();
+            customConfig.taskProfiles = new String[0];
+            try {
+                customConfig.vendorImage =
+                        ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
+            } catch (FileNotFoundException e) {
+                throw new VirtualMachineException(
+                        "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
+                        e);
+            }
+            vsConfig.customConfig = customConfig;
+        }
         return vsConfig;
     }
 
@@ -572,6 +599,7 @@
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
+        @Nullable private File mVendorDiskImage;
 
         /**
          * Creates a builder for the given context.
@@ -645,7 +673,8 @@
                     mCpuTopology,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
-                    mVmConsoleInputSupported);
+                    mVmConsoleInputSupported,
+                    mVendorDiskImage);
         }
 
         /**
@@ -863,5 +892,18 @@
             mVmConsoleInputSupported = supported;
             return this;
         }
+
+        /**
+         * Sets the path to the disk image with vendor-specific modules.
+         *
+         * @hide
+         */
+        @TestApi
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setVendorDiskImage(@NonNull File vendorDiskImage) {
+            mVendorDiskImage = vendorDiskImage;
+            return this;
+        }
     }
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 5440695..2d3f084 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -23,6 +23,10 @@
     "apex",
     "linkerconfig",
     "second_stage_resources",
+
+    // Ideally we should only create the /vendor for Microdroid VMs that will mount /vendor, but
+    // for the time being we will just create it unconditionally.
+    "vendor",
 ]
 
 microdroid_symlinks = [
diff --git a/microdroid/bootconfig.x86_64 b/microdroid/bootconfig.x86_64
index 6076889..eed9212 100644
--- a/microdroid/bootconfig.x86_64
+++ b/microdroid/bootconfig.x86_64
@@ -1 +1 @@
-androidboot.boot_devices = pci0000:00/0000:00:04.0,pci0000:00/0000:00:05.0,pci0000:00/0000:00:06.0
+androidboot.boot_devices = pci0000:00/0000:00:04.0,pci0000:00/0000:00:05.0,pci0000:00/0000:00:06.0,pci0000:00/0000:00:07.0
diff --git a/microdroid/fstab.microdroid b/microdroid/fstab.microdroid
index 9478c7c..da000b9 100644
--- a/microdroid/fstab.microdroid
+++ b/microdroid/fstab.microdroid
@@ -1 +1,7 @@
 system /system ext4 noatime,ro,errors=panic wait,slotselect,avb=vbmeta,first_stage_mount,logical
+# This is a temporary solution to unblock other devs that depend on /vendor partition in Microdroid
+# The /vendor partition will only be mounted if the kernel cmdline contains
+# androidboot.microdroid.mount_vendor=1.
+# TODO(b/285855430): this should probably be defined in the DT
+# TODO(b/285855436): should be mounted on top of dm-verity device
+/dev/block/by-name/microdroid-vendor /vendor ext4 noatime,ro,errors=panic wait,first_stage_mount
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index fe8f5c9..8a31c21 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -43,7 +43,10 @@
     ],
     min_sdk_version: "33",
     // Defined in ../vmshareapp/Android.bp
-    data: [":MicrodroidVmShareApp"],
+    data: [
+        ":MicrodroidVmShareApp",
+        ":test_microdroid_vendor_image",
+    ],
 }
 
 // Defaults shared between MicrodroidTestNativeLib and MicrodroidPayloadInOtherAppNativeLib shared
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index 2ea3f6c..d6e6004 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -22,8 +22,7 @@
     <queries>
         <package android:name="com.android.microdroid.vmshare_app" />
     </queries>
-    <application>
-    </application>
+    <application />
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.microdroid.test"
         android:label="Microdroid Test" />
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 929dd31..e72a2e3 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -23,6 +23,14 @@
         <option name="test-file-name" value="MicrodroidTestApp.apk" />
         <option name="test-file-name" value="MicrodroidVmShareApp.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="mkdir -p /data/local/tmp/cts/microdroid" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/cts/microdroid" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="test_microdroid_vendor_image.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.test" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
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 5ec4ca8..15cb450 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1975,8 +1975,10 @@
                 .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR);
     }
 
-    // Taken from bionic/libs/kernel/uapi/linux/mounth.h.
+    // Taken from bionic/libc/kernel/uapi/linux/mount.h
+    private static final int MS_RDONLY = 1;
     private static final int MS_NOEXEC = 8;
+    private static final int MS_NOATIME = 1024;
 
     @Test
     @CddTest(requirements = {"9.17/C-1-5"})
@@ -2050,6 +2052,60 @@
         }
     }
 
+    @Test
+    public void configuringVendorDiskImageRequiresCustomPermission() throws Exception {
+        assumeSupportedDevice();
+
+        File vendorDiskImage =
+                new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setVendorDiskImage(vendorDiskImage)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("test_vendor_image_req_custom_permission", config);
+
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
+        assertThat(e)
+                .hasMessageThat()
+                .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
+    }
+
+    @Test
+    public void bootsWithVendorPartition() throws Exception {
+        assumeSupportedDevice();
+
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+
+        File vendorDiskImage =
+                new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setVendorDiskImage(vendorDiskImage)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_boot_with_vendor", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mMountFlags = ts.getMountFlags("/vendor");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        int expectedFlags = MS_NOATIME | MS_RDONLY;
+        assertThat(testResults.mMountFlags & expectedFlags).isEqualTo(expectedFlags);
+    }
+
     private static class VmShareServiceConnection implements ServiceConnection {
 
         private final CountDownLatch mLatch = new CountDownLatch(1);
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
new file mode 100644
index 0000000..09c657c
--- /dev/null
+++ b/tests/vendor_images/Android.bp
@@ -0,0 +1,9 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_filesystem {
+    name: "test_microdroid_vendor_image",
+    type: "ext4",
+    file_contexts: ":microdroid_vendor_file_contexts.gen",
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index dd74d55..d0a8e85 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -20,7 +20,7 @@
 use crate::composite::make_composite_image;
 use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
-use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images};
+use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
 use crate::selinux::{getfilecon, SeContext};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::{
@@ -579,6 +579,15 @@
     Ok(DiskFile { image, writable: disk.writable })
 }
 
+fn append_kernel_param(param: &str, vm_config: &mut VirtualMachineRawConfig) {
+    if let Some(ref mut params) = vm_config.params {
+        params.push(' ');
+        params.push_str(param)
+    } else {
+        vm_config.params = Some(param.to_owned())
+    }
+}
+
 fn load_app_config(
     config: &VirtualMachineAppConfig,
     debug_config: &DebugConfig,
@@ -620,6 +629,11 @@
         }
         vm_config.taskProfiles = custom_config.taskProfiles.clone();
         vm_config.gdbPort = custom_config.gdbPort;
+
+        if let Some(file) = custom_config.vendorImage.as_ref() {
+            add_microdroid_vendor_image(clone_file(file)?, &mut vm_config);
+            append_kernel_param("androidboot.microdroid.mount_vendor=1", &mut vm_config)
+        }
     }
 
     if config.memoryMib > 0 {
@@ -1349,4 +1363,19 @@
         assert!(modified_orig == modified_new, "idsig file was updated unnecessarily");
         Ok(())
     }
+
+    #[test]
+    fn test_append_kernel_param_first_param() {
+        let mut vm_config = VirtualMachineRawConfig { ..Default::default() };
+        append_kernel_param("foo=1", &mut vm_config);
+        assert_eq!(vm_config.params, Some("foo=1".to_owned()))
+    }
+
+    #[test]
+    fn test_append_kernel_param() {
+        let mut vm_config =
+            VirtualMachineRawConfig { params: Some("foo=5".to_owned()), ..Default::default() };
+        append_kernel_param("bar=42", &mut vm_config);
+        assert_eq!(vm_config.params, Some("foo=5 bar=42".to_owned()))
+    }
 }
diff --git a/virtualizationmanager/src/payload.rs b/virtualizationmanager/src/payload.rs
index 733add6..ab6f31c 100644
--- a/virtualizationmanager/src/payload.rs
+++ b/virtualizationmanager/src/payload.rs
@@ -398,6 +398,18 @@
         .collect()
 }
 
+pub fn add_microdroid_vendor_image(vendor_image: File, vm_config: &mut VirtualMachineRawConfig) {
+    vm_config.disks.push(DiskImage {
+        image: None,
+        writable: false,
+        partitions: vec![Partition {
+            label: "microdroid-vendor".to_owned(),
+            image: Some(ParcelFileDescriptor::new(vendor_image)),
+            writable: false,
+        }],
+    })
+}
+
 pub fn add_microdroid_system_images(
     config: &VirtualMachineAppConfig,
     instance_file: File,
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 6a0bf7c..2b762c4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -105,6 +105,9 @@
          * List of task profile names to apply for the VM
          */
         String[] taskProfiles;
+
+        /** A disk image containing vendor specific modules. */
+        @nullable ParcelFileDescriptor vendorImage;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 0800f57..d7c2c4d 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -115,6 +115,10 @@
         /// Path to custom kernel image to use when booting Microdroid.
         #[clap(long)]
         kernel: Option<PathBuf>,
+
+        /// Path to disk image containing vendor-specific modules.
+        #[clap(long)]
+        vendor: Option<PathBuf>,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
@@ -179,6 +183,10 @@
         /// Path to custom kernel image to use when booting Microdroid.
         #[clap(long)]
         kernel: Option<PathBuf>,
+
+        /// Path to disk image containing vendor-specific modules.
+        #[clap(long)]
+        vendor: Option<PathBuf>,
     },
     /// Run a virtual machine
     Run {
@@ -299,6 +307,7 @@
             extra_idsigs,
             gdb,
             kernel,
+            vendor,
         } => command_run_app(
             name,
             get_service()?.as_ref(),
@@ -320,6 +329,7 @@
             &extra_idsigs,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
         Opt::RunMicrodroid {
             name,
@@ -336,6 +346,7 @@
             task_profiles,
             gdb,
             kernel,
+            vendor,
         } => command_run_microdroid(
             name,
             get_service()?.as_ref(),
@@ -352,6 +363,7 @@
             task_profiles,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
         Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
             command_run(
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 84072ca..64da2d9 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -65,6 +65,7 @@
     extra_idsigs: &[PathBuf],
     gdb: Option<NonZeroU16>,
     kernel: Option<&Path>,
+    vendor: Option<&Path>,
 ) -> Result<(), Error> {
     let apk_file = File::open(apk).context("Failed to open APK file")?;
 
@@ -122,6 +123,8 @@
 
     let kernel = kernel.map(|p| open_parcel_file(p, false)).transpose()?;
 
+    let vendor = vendor.map(|p| open_parcel_file(p, false)).transpose()?;
+
     let extra_idsig_files: Result<Vec<File>, _> = extra_idsigs.iter().map(File::open).collect();
     let extra_idsig_fds = extra_idsig_files?.into_iter().map(ParcelFileDescriptor::new).collect();
 
@@ -144,6 +147,7 @@
         customKernelImage: kernel,
         gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
         taskProfiles: task_profiles,
+        vendorImage: vendor,
     };
 
     let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
@@ -203,6 +207,7 @@
     task_profiles: Vec<String>,
     gdb: Option<NonZeroU16>,
     kernel: Option<&Path>,
+    vendor: Option<&Path>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
@@ -236,6 +241,7 @@
         &extra_sig,
         gdb,
         kernel,
+        vendor,
     )
 }