Merge "Changes for Rust 1.72" into main
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 386036d..698972a 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -139,6 +139,10 @@
 |  offset = (SECOND - HEAD)     |
 |  size = (SECOND_END - SECOND) |
 +-------------------------------+
+|           [Entry 2]           | <-- Entry 2 is present since version 1.1
+|  offset = (THIRD - HEAD)      |
+|  size = (THIRD_END - SECOND)  |
++-------------------------------+
 |              ...              |
 +-------------------------------+
 |           [Entry n]           |
@@ -152,6 +156,10 @@
 |        {Second blob: DP}      |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ <-- SECOND_END
 | (Padding to 8-byte alignment) |
++===============================+ <-- THIRD
+|        {Third blob: VM DTBO}  |
++~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ <-- THIRD_END
+| (Padding to 8-byte alignment) |
 +===============================+
 |              ...              |
 +===============================+ <-- TAIL
@@ -177,6 +185,11 @@
 - entry 1 may point to a [DTBO] to be applied to the pVM device tree. See
   [debug policy][debug_policy] for an example.
 
+In version 1.1, new blob is added.
+
+- entry 2 may point to a [DTBO] that describes VM DTBO for device assignment.
+  pvmfw will provision assigned devices with the VM DTBO.
+
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/master/Documentation/dt-object-internal.txt
 [debug_policy]: ../docs/debug/README.md#debug-policy
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index ec5c6dc..db27001 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -18,7 +18,9 @@
 use core::mem;
 use core::ops::Range;
 use core::result;
-use vmbase::util::unchecked_align_up;
+use log::info;
+use static_assertions::const_assert_eq;
+use vmbase::util::RangeExt;
 use zerocopy::{FromBytes, LayoutVerified};
 
 /// Configuration data header.
@@ -33,8 +35,6 @@
     total_size: u32,
     /// Feature flags; currently reserved and must be zero.
     flags: u32,
-    /// (offset, size) pairs used to locate individual entries appended to the header.
-    entries: [HeaderEntry; Entry::COUNT],
 }
 
 #[derive(Debug)]
@@ -53,8 +53,8 @@
     InvalidSize(usize),
     /// Header entry is missing.
     MissingEntry(Entry),
-    /// Header entry is invalid.
-    InvalidEntry(Entry, EntryError),
+    /// Range described by entry does not fit within config data.
+    EntryOutOfBounds(Entry, Range<usize>, Range<usize>),
 }
 
 impl fmt::Display for Error {
@@ -67,98 +67,61 @@
             Self::InvalidFlags(v) => write!(f, "Flags value {v:#x} is incorrect or reserved"),
             Self::InvalidSize(sz) => write!(f, "Total size ({sz:#x}) overflows reserved region"),
             Self::MissingEntry(entry) => write!(f, "Mandatory {entry:?} entry is missing"),
-            Self::InvalidEntry(entry, e) => write!(f, "Invalid {entry:?} entry: {e}"),
+            Self::EntryOutOfBounds(entry, range, limits) => {
+                write!(
+                    f,
+                    "Entry {entry:?} out of bounds: {range:#x?} must be within range {limits:#x?}"
+                )
+            }
         }
     }
 }
 
 pub type Result<T> = result::Result<T, Error>;
 
-#[derive(Debug)]
-pub enum EntryError {
-    /// Offset isn't between the fixed minimum value and size of configuration data.
-    InvalidOffset(usize),
-    /// Size must be zero when offset is and not be when it isn't.
-    InvalidSize(usize),
-    /// Entry isn't fully within the configuration data structure.
-    OutOfBounds { offset: usize, size: usize, limit: usize },
-}
-
-impl fmt::Display for EntryError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::InvalidOffset(offset) => write!(f, "Invalid offset: {offset:#x?}"),
-            Self::InvalidSize(sz) => write!(f, "Invalid size: {sz:#x?}"),
-            Self::OutOfBounds { offset, size, limit } => {
-                let range = Header::PADDED_SIZE..*limit;
-                let entry = *offset..(*offset + *size);
-                write!(f, "Out of bounds: {entry:#x?} must be within range {range:#x?}")
-            }
-        }
-    }
-}
-
 impl Header {
     const MAGIC: u32 = u32::from_ne_bytes(*b"pvmf");
     const VERSION_1_0: Version = Version { major: 1, minor: 0 };
-    const PADDED_SIZE: usize = unchecked_align_up(mem::size_of::<Self>(), mem::size_of::<u64>());
+    const VERSION_1_1: Version = Version { major: 1, minor: 1 };
 
     pub fn total_size(&self) -> usize {
         self.total_size as usize
     }
 
-    pub fn body_size(&self) -> usize {
-        self.total_size() - Self::PADDED_SIZE
+    pub fn body_offset(&self) -> Result<usize> {
+        let entries_offset = mem::size_of::<Self>();
+
+        // Ensure that the entries are properly aligned and do not require padding.
+        const_assert_eq!(mem::align_of::<Header>() % mem::align_of::<HeaderEntry>(), 0);
+        const_assert_eq!(mem::size_of::<Header>() % mem::align_of::<HeaderEntry>(), 0);
+
+        let entries_size = self.entry_count()?.checked_mul(mem::size_of::<HeaderEntry>()).unwrap();
+
+        Ok(entries_offset.checked_add(entries_size).unwrap())
     }
 
-    fn get_body_range(&self, entry: Entry) -> Result<Option<Range<usize>>> {
-        let e = self.entries[entry as usize];
-        let offset = e.offset as usize;
-        let size = e.size as usize;
+    pub fn entry_count(&self) -> Result<usize> {
+        let last_entry = match self.version {
+            Self::VERSION_1_0 => Entry::DebugPolicy,
+            Self::VERSION_1_1 => Entry::VmDtbo,
+            v => return Err(Error::UnsupportedVersion(v)),
+        };
 
-        match self._get_body_range(offset, size) {
-            Ok(r) => Ok(r),
-            Err(EntryError::InvalidSize(0)) => {
-                // As our bootloader currently uses this (non-compliant) case, permit it for now.
-                log::warn!("Config entry {entry:?} uses non-zero offset with zero size");
-                // TODO(b/262181812): Either make this case valid or fix the bootloader.
-                Ok(None)
-            }
-            Err(e) => Err(Error::InvalidEntry(entry, e)),
-        }
-    }
-
-    fn _get_body_range(
-        &self,
-        offset: usize,
-        size: usize,
-    ) -> result::Result<Option<Range<usize>>, EntryError> {
-        match (offset, size) {
-            (0, 0) => Ok(None),
-            (0, size) | (_, size @ 0) => Err(EntryError::InvalidSize(size)),
-            _ => {
-                let start = offset
-                    .checked_sub(Header::PADDED_SIZE)
-                    .ok_or(EntryError::InvalidOffset(offset))?;
-                let end = start
-                    .checked_add(size)
-                    .filter(|x| *x <= self.body_size())
-                    .ok_or(EntryError::OutOfBounds { offset, size, limit: self.total_size() })?;
-
-                Ok(Some(start..end))
-            }
-        }
+        Ok(last_entry as usize + 1)
     }
 }
 
 #[derive(Clone, Copy, Debug)]
 pub enum Entry {
-    Bcc = 0,
-    DebugPolicy = 1,
+    Bcc,
+    DebugPolicy,
+    VmDtbo,
+    #[allow(non_camel_case_types)] // TODO: Use mem::variant_count once stable.
+    _VARIANT_COUNT,
 }
 
 impl Entry {
-    const COUNT: usize = 2;
+    const COUNT: usize = Self::_VARIANT_COUNT as usize;
 }
 
 #[repr(packed)]
@@ -168,6 +131,19 @@
     size: u32,
 }
 
+impl HeaderEntry {
+    pub fn as_range(&self) -> Option<Range<usize>> {
+        let size = usize::try_from(self.size).unwrap();
+        if size != 0 {
+            let offset = self.offset.try_into().unwrap();
+            // Allow overflows here for the Range to properly describe the entry (validated later).
+            Some(offset..(offset + size))
+        } else {
+            None
+        }
+    }
+}
+
 #[repr(C, packed)]
 #[derive(Clone, Copy, Debug, Eq, FromBytes, PartialEq)]
 pub struct Version {
@@ -186,55 +162,79 @@
 #[derive(Debug)]
 pub struct Config<'a> {
     body: &'a mut [u8],
-    bcc_range: Range<usize>,
-    dp_range: Option<Range<usize>>,
+    ranges: [Option<Range<usize>>; Entry::COUNT],
 }
 
 impl<'a> Config<'a> {
     /// Take ownership of a pvmfw configuration consisting of its header and following entries.
-    pub fn new(data: &'a mut [u8]) -> Result<Self> {
-        let header = data.get(..Header::PADDED_SIZE).ok_or(Error::BufferTooSmall)?;
+    pub fn new(bytes: &'a mut [u8]) -> Result<Self> {
+        const HEADER_SIZE: usize = mem::size_of::<Header>();
+        if bytes.len() < HEADER_SIZE {
+            return Err(Error::BufferTooSmall);
+        }
 
-        let (header, _) =
-            LayoutVerified::<_, Header>::new_from_prefix(header).ok_or(Error::HeaderMisaligned)?;
+        let (header, rest) =
+            LayoutVerified::<_, Header>::new_from_prefix(bytes).ok_or(Error::HeaderMisaligned)?;
         let header = header.into_ref();
 
         if header.magic != Header::MAGIC {
             return Err(Error::InvalidMagic);
         }
 
-        if header.version != Header::VERSION_1_0 {
-            return Err(Error::UnsupportedVersion(header.version));
-        }
-
         if header.flags != 0 {
             return Err(Error::InvalidFlags(header.flags));
         }
 
-        let bcc_range =
-            header.get_body_range(Entry::Bcc)?.ok_or(Error::MissingEntry(Entry::Bcc))?;
-        let dp_range = header.get_body_range(Entry::DebugPolicy)?;
+        info!("pvmfw config version: {}", header.version);
 
-        let body_size = header.body_size();
+        // Validate that we won't get an invalid alignment in the following due to padding to u64.
+        const_assert_eq!(HEADER_SIZE % mem::size_of::<u64>(), 0);
+
+        // Ensure that Header::total_size isn't larger than anticipated by the caller and resize
+        // the &[u8] to catch OOB accesses to entries/blobs.
         let total_size = header.total_size();
-        let body = data
-            .get_mut(Header::PADDED_SIZE..)
-            .ok_or(Error::BufferTooSmall)?
-            .get_mut(..body_size)
-            .ok_or(Error::InvalidSize(total_size))?;
+        let rest = if let Some(rest_size) = total_size.checked_sub(HEADER_SIZE) {
+            rest.get_mut(..rest_size).ok_or(Error::InvalidSize(total_size))?
+        } else {
+            return Err(Error::InvalidSize(total_size));
+        };
 
-        Ok(Self { body, bcc_range, dp_range })
+        let (header_entries, body) =
+            LayoutVerified::<_, [HeaderEntry]>::new_slice_from_prefix(rest, header.entry_count()?)
+                .ok_or(Error::BufferTooSmall)?;
+
+        // Validate that we won't get an invalid alignment in the following due to padding to u64.
+        const_assert_eq!(mem::size_of::<HeaderEntry>() % mem::size_of::<u64>(), 0);
+
+        let limits = header.body_offset()?..total_size;
+        let ranges = [
+            // TODO: Find a way to do this programmatically even if the trait
+            // `core::marker::Copy` is not implemented for `core::ops::Range<usize>`.
+            Self::validated_body_range(Entry::Bcc, &header_entries, &limits)?,
+            Self::validated_body_range(Entry::DebugPolicy, &header_entries, &limits)?,
+            Self::validated_body_range(Entry::VmDtbo, &header_entries, &limits)?,
+        ];
+
+        Ok(Self { body, ranges })
     }
 
     /// Get slice containing the platform BCC.
-    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
-        let bcc_start = self.bcc_range.start;
-        let bcc_end = self.bcc_range.len();
+    pub fn get_entries(&mut self) -> Result<(&mut [u8], Option<&mut [u8]>)> {
+        // This assumes that the blobs are in-order w.r.t. the entries.
+        let bcc_range = self.get_entry_range(Entry::Bcc).ok_or(Error::MissingEntry(Entry::Bcc))?;
+        let dp_range = self.get_entry_range(Entry::DebugPolicy);
+        let vm_dtbo_range = self.get_entry_range(Entry::VmDtbo);
+        // TODO(b/291191157): Provision device assignment with this.
+        if let Some(vm_dtbo_range) = vm_dtbo_range {
+            info!("Found VM DTBO at {:?}", vm_dtbo_range);
+        }
+        let bcc_start = bcc_range.start;
+        let bcc_end = bcc_range.len();
         let (_, rest) = self.body.split_at_mut(bcc_start);
         let (bcc, rest) = rest.split_at_mut(bcc_end);
 
-        let dp = if let Some(dp_range) = &self.dp_range {
-            let dp_start = dp_range.start.checked_sub(self.bcc_range.end).unwrap();
+        let dp = if let Some(dp_range) = dp_range {
+            let dp_start = dp_range.start.checked_sub(bcc_range.end).unwrap();
             let dp_end = dp_range.len();
             let (_, rest) = rest.split_at_mut(dp_start);
             let (dp, _) = rest.split_at_mut(dp_end);
@@ -243,6 +243,31 @@
             None
         };
 
-        (bcc, dp)
+        Ok((bcc, dp))
+    }
+
+    pub fn get_entry_range(&self, entry: Entry) -> Option<Range<usize>> {
+        self.ranges[entry as usize].clone()
+    }
+
+    fn validated_body_range(
+        entry: Entry,
+        header_entries: &[HeaderEntry],
+        limits: &Range<usize>,
+    ) -> Result<Option<Range<usize>>> {
+        if let Some(header_entry) = header_entries.get(entry as usize) {
+            if let Some(r) = header_entry.as_range() {
+                return if r.start <= r.end && r.is_within(limits) {
+                    let start = r.start - limits.start;
+                    let end = r.end - limits.start;
+
+                    Ok(Some(start..end))
+                } else {
+                    Err(Error::EntryOutOfBounds(entry, r, limits.clone()))
+                };
+            }
+        }
+
+        Ok(None)
     }
 }
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 9c929a9..3efa61e 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -207,7 +207,10 @@
         RebootReason::InvalidConfig
     })?;
 
-    let (bcc_slice, debug_policy) = appended.get_entries();
+    let (bcc_slice, debug_policy) = appended.get_entries().map_err(|e| {
+        error!("Failed to obtained the config entries: {e}");
+        RebootReason::InvalidConfig
+    })?;
 
     // Up to this point, we were using the built-in static (from .rodata) page tables.
     MEMORY.lock().replace(MemoryTracker::new(
@@ -427,10 +430,10 @@
         }
     }
 
-    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    fn get_entries(&mut self) -> config::Result<(&mut [u8], Option<&mut [u8]>)> {
         match self {
             Self::Config(ref mut cfg) => cfg.get_entries(),
-            Self::LegacyBcc(ref mut bcc) => (bcc, None),
+            Self::LegacyBcc(ref mut bcc) => Ok((bcc, None)),
         }
     }
 }
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
index 95eaa58..d752108 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
@@ -33,20 +33,24 @@
     private static final int SIZE_8B = 8; // 8 bytes
     private static final int SIZE_4K = 4 << 10; // 4 KiB, PAGE_SIZE
     private static final int BUFFER_SIZE = 1024;
-    private static final int HEADER_SIZE = Integer.BYTES * 8; // Header has 8 integers.
     private static final int HEADER_MAGIC = 0x666d7670;
-    private static final int HEADER_VERSION = getVersion(1, 0);
+    private static final int HEADER_DEFAULT_VERSION = getVersion(1, 0);
     private static final int HEADER_FLAGS = 0;
 
     @NonNull private final File mPvmfwBinFile;
     @NonNull private final File mBccFile;
     @Nullable private final File mDebugPolicyFile;
+    private final int mVersion;
 
     private Pvmfw(
-            @NonNull File pvmfwBinFile, @NonNull File bccFile, @Nullable File debugPolicyFile) {
+            @NonNull File pvmfwBinFile,
+            @NonNull File bccFile,
+            @Nullable File debugPolicyFile,
+            int version) {
         mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
         mBccFile = Objects.requireNonNull(bccFile);
         mDebugPolicyFile = debugPolicyFile;
+        mVersion = version;
     }
 
     /**
@@ -56,17 +60,22 @@
     public void serialize(@NonNull File outFile) throws IOException {
         Objects.requireNonNull(outFile);
 
-        int bccOffset = HEADER_SIZE;
+        int headerSize = alignTo(getHeaderSize(mVersion), SIZE_8B);
+        int bccOffset = headerSize;
         int bccSize = (int) mBccFile.length();
 
         int debugPolicyOffset = alignTo(bccOffset + bccSize, SIZE_8B);
         int debugPolicySize = mDebugPolicyFile == null ? 0 : (int) mDebugPolicyFile.length();
 
         int totalSize = debugPolicyOffset + debugPolicySize;
+        if (hasVmDtbo(mVersion)) {
+            // Add VM DTBO size as well.
+            totalSize += Integer.BYTES * 2;
+        }
 
-        ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE).order(LITTLE_ENDIAN);
+        ByteBuffer header = ByteBuffer.allocate(headerSize).order(LITTLE_ENDIAN);
         header.putInt(HEADER_MAGIC);
-        header.putInt(HEADER_VERSION);
+        header.putInt(mVersion);
         header.putInt(totalSize);
         header.putInt(HEADER_FLAGS);
         header.putInt(bccOffset);
@@ -74,11 +83,18 @@
         header.putInt(debugPolicyOffset);
         header.putInt(debugPolicySize);
 
+        if (hasVmDtbo(mVersion)) {
+            // Add placeholder entry for VM DTBO.
+            // TODO(b/291191157): Add a real DTBO and test.
+            header.putInt(0);
+            header.putInt(0);
+        }
+
         try (FileOutputStream pvmfw = new FileOutputStream(outFile)) {
             appendFile(pvmfw, mPvmfwBinFile);
             padTo(pvmfw, SIZE_4K);
             pvmfw.write(header.array());
-            padTo(pvmfw, HEADER_SIZE);
+            padTo(pvmfw, SIZE_8B);
             appendFile(pvmfw, mBccFile);
             if (mDebugPolicyFile != null) {
                 padTo(pvmfw, SIZE_8B);
@@ -110,6 +126,19 @@
         }
     }
 
+    private static int getHeaderSize(int version) {
+        if (version == getVersion(1, 0)) {
+            return Integer.BYTES * 8; // Header has 8 integers.
+        }
+        return Integer.BYTES * 10; // Default + VM DTBO (offset, size)
+    }
+
+    private static boolean hasVmDtbo(int version) {
+        int major = getMajorVersion(version);
+        int minor = getMinorVersion(version);
+        return major > 1 || (major == 1 && minor >= 1);
+    }
+
     private static int alignTo(int x, int size) {
         return (x + size - 1) & ~(size - 1);
     }
@@ -118,15 +147,25 @@
         return ((major & 0xFFFF) << 16) | (minor & 0xFFFF);
     }
 
+    private static int getMajorVersion(int version) {
+        return (version >> 16) & 0xFFFF;
+    }
+
+    private static int getMinorVersion(int version) {
+        return version & 0xFFFF;
+    }
+
     /** Builder for {@link Pvmfw}. */
     public static final class Builder {
         @NonNull private final File mPvmfwBinFile;
         @NonNull private final File mBccFile;
         @Nullable private File mDebugPolicyFile;
+        private int mVersion;
 
         public Builder(@NonNull File pvmfwBinFile, @NonNull File bccFile) {
             mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
             mBccFile = Objects.requireNonNull(bccFile);
+            mVersion = HEADER_DEFAULT_VERSION;
         }
 
         @NonNull
@@ -136,8 +175,14 @@
         }
 
         @NonNull
+        public Builder setVersion(int major, int minor) {
+            mVersion = getVersion(major, minor);
+            return this;
+        }
+
+        @NonNull
         public Pvmfw build() {
-            return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile);
+            return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile, mVersion);
         }
     }
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java b/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java
new file mode 100644
index 0000000..30ad9e2
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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.microdroid.test;
+
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.microdroid.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Objects;
+
+/** Tests pvmfw.img and pvmfw */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PvmfwImgTest extends MicrodroidHostTestCaseBase {
+    @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+    @NonNull private static final String BCC_FILE_NAME = "bcc.dat";
+    @NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
+    @NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
+    @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
+    @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+    private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+    private static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
+
+    @NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
+    @NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
+
+    @Nullable private static File mPvmfwBinFileOnHost;
+    @Nullable private static File mBccFileOnHost;
+
+    @Nullable private TestDevice mAndroidDevice;
+    @Nullable private ITestDevice mMicrodroidDevice;
+    @Nullable private File mCustomPvmfwBinFileOnHost;
+
+    @Before
+    public void setUp() throws Exception {
+        mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+
+        // Check device capabilities
+        assumeDeviceIsCapable(mAndroidDevice);
+        assumeTrue(
+                "Skip if protected VMs are not supported",
+                mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+        assumeFalse("Test requires setprop for using custom pvmfw and adb root", isUserBuild());
+
+        assumeTrue("Skip if adb root fails", mAndroidDevice.enableAdbRoot());
+
+        // tradefed copies the test artfacts under /tmp when running tests,
+        // so we should *find* the artifacts with the file name.
+        mPvmfwBinFileOnHost =
+                getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+        mBccFileOnHost =
+                getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+        // Prepare for system properties for custom pvmfw.img.
+        // File will be prepared later in individual test and then pushed to device
+        // when launching with launchProtectedVmAndWaitForBootCompleted().
+        mCustomPvmfwBinFileOnHost =
+                FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+
+        // Prepare for launching microdroid
+        mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+        prepareVirtualizationTestSetup(mAndroidDevice);
+        mMicrodroidDevice = null;
+    }
+
+    @After
+    public void shutdown() throws Exception {
+        if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+            return;
+        }
+        if (mMicrodroidDevice != null) {
+            mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+            mMicrodroidDevice = null;
+        }
+        mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+        // Cleanup for custom pvmfw.img
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, "");
+        FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+        cleanUpVirtualizationTestSetup(mAndroidDevice);
+
+        mAndroidDevice.disableAdbRoot();
+    }
+
+    @Test
+    public void testConfigVersion1_0_boots() throws Exception {
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 0).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+    }
+
+    @Test
+    public void testConfigVersion1_1_boots() throws Exception {
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 1).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+    }
+
+    @Test
+    public void testInvalidConfigVersion_doesNotBoot() throws Exception {
+        // Disclaimer: Update versions when it becomes valid
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 100).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        assertThrows(
+                "pvmfw shouldn't boot with invalid version",
+                DeviceRuntimeException.class,
+                () -> launchProtectedVmAndWaitForBootCompleted(BOOT_FAILURE_WAIT_TIME_MS));
+    }
+
+    private ITestDevice launchProtectedVmAndWaitForBootCompleted(long adbTimeoutMs)
+            throws DeviceNotAvailableException {
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(
+                                getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+                        .debugLevel(MICRODROID_DEBUG_FULL)
+                        .protectedVm(true)
+                        .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+                        .setAdbConnectTimeoutMs(adbTimeoutMs)
+                        .build(mAndroidDevice);
+        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
+        return mMicrodroidDevice;
+    }
+}