Merge "Mark ComposHostTestCases as needing API 33"
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index 592a751..30e437b 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -3,19 +3,19 @@
 
   public class VirtualMachine implements java.lang.AutoCloseable {
     method public void clearCallback();
-    method public void close();
-    method @NonNull public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig getConfig();
-    method @NonNull public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void close();
+    method @NonNull @WorkerThread public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig getConfig();
+    method @NonNull @WorkerThread public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public String getName();
-    method public int getStatus();
-    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public void run() throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public int getStatus();
+    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public void run() throws android.system.virtualmachine.VirtualMachineException;
     method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.system.virtualmachine.VirtualMachineCallback);
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method public void stop() throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void stop() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
     field public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION = "android.permission.MANAGE_VIRTUAL_MACHINE";
     field public static final long MAX_VSOCK_PORT = 4294967295L; // 0xffffffffL
     field public static final long MIN_VSOCK_PORT = 1024L; // 0x400L
@@ -91,12 +91,12 @@
   }
 
   public class VirtualMachineManager {
-    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
-    method @Nullable public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @Nullable @WorkerThread public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
     method public int getCapabilities();
-    method @NonNull public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
     field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
     field public static final int CAPABILITY_PROTECTED_VM = 1; // 0x1
   }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index b040cc0..b38e0e4 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -52,6 +52,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.annotation.WorkerThread;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -108,6 +109,15 @@
  * received using {@link #setCallback}. The app can communicate with the VM using {@link
  * #connectToVsockServer} or {@link #connectVsock}.
  *
+ * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
+ * href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/Virtualization/vm_payload/README.md">README
+ * file</a> for details.
+ *
+ * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
+ * VM configuration, and a random per-instance salt. The secret can be accessed by the payload code
+ * running inside the VM (using {@code AVmPayload_getVmInstanceSecret}) but is not made available
+ * outside it.
+ *
  * @hide
  */
 @SystemApi
@@ -512,8 +522,8 @@
             // Once we explicitly delete a VM it must remain permanently in the deleted state;
             // if a new VM is created with the same name (and files) that's unrelated.
             mWasDeleted = true;
-            deleteVmDirectory(context, name);
         }
+        deleteVmDirectory(context, name);
     }
 
     static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
@@ -569,13 +579,16 @@
     /**
      * Returns the currently selected config of this virtual machine. There can be multiple virtual
      * machines sharing the same config. Even in that case, the virtual machines are completely
-     * isolated from each other; one cannot share its secret to another virtual machine even if they
-     * share the same config. It is also possible that a virtual machine can switch its config,
-     * which can be done by calling {@link #setConfig(VirtualMachineConfig)}.
+     * isolated from each other; they have different secrets. It is also possible that a virtual
+     * machine can change its config, which can be done by calling {@link
+     * #setConfig(VirtualMachineConfig)}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineConfig getConfig() {
         synchronized (mLock) {
@@ -586,9 +599,12 @@
     /**
      * Returns the current status of this virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Status
     public int getStatus() {
         IVirtualMachine virtualMachine;
@@ -727,11 +743,14 @@
      * registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)} before
      * calling {@code run()}.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine is not stopped or could not be
      *     started.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public void run() throws VirtualMachineException {
         synchronized (mLock) {
@@ -872,10 +891,13 @@
     /**
      * Returns the stream object representing the console output from the virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public InputStream getConsoleOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -887,10 +909,13 @@
     /**
      * Returns the stream object representing the log output from the virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public InputStream getLogOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -904,11 +929,14 @@
      * computer; the machine halts immediately. Software running on the virtual machine is not
      * notified of the event. A stopped virtual machine can be re-started by calling {@link #run()}.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine is not running or could not be
      *     stopped.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     public void stop() throws VirtualMachineException {
         synchronized (mLock) {
             if (mVirtualMachine == null) {
@@ -928,10 +956,13 @@
     /**
      * Stops this virtual machine, if it is running.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @see #stop()
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Override
     public void close() {
         synchronized (mLock) {
@@ -982,12 +1013,15 @@
      * <p>The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
      * existing config.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @return the old config
      * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
      *     incompatible.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
             throws VirtualMachineException {
@@ -1016,11 +1050,14 @@
      * VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to establish a
      * connection to the guest VM.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine is not running or the connection
      *     failed.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public IBinder connectToVsockServer(
             @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
@@ -1039,10 +1076,13 @@
     /**
      * Opens a vsock connection to the VM on the given port.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if connecting fails.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public ParcelFileDescriptor connectVsock(
             @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
@@ -1088,12 +1128,15 @@
      * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
      * started until that operation is complete.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
      * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
      *     be captured.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
         synchronized (mLock) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 75e5414..7555dec 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -349,9 +349,10 @@
 
     /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
-     * can be interchangeably used for the same virtual machine. Compatible changes includes the
-     * number of CPUs and the size of the RAM. All other changes (e.g. using a different payload,
-     * change of the debug mode, etc.) are considered as incompatible.
+     * can be interchangeably used for the same virtual machine; they do not change the VM identity
+     * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
+     * that would alter the identity of the VM (e.g. using a different payload or changing the debug
+     * mode) are considered incompatible.
      *
      * @hide
      */
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 5b30617..6aa8133 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -24,6 +24,7 @@
 import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.WorkerThread;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.sysprop.HypervisorProperties;
@@ -123,12 +124,15 @@
      * the name and the config are the same as a deleted one. The new virtual machine will initially
      * be stopped.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the VM cannot be created, or there is an existing VM with
      *     the given name.
      * @hide
      */
     @SystemApi
     @NonNull
+    @WorkerThread
     @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public VirtualMachine create(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
@@ -154,12 +158,15 @@
      * machine instance. Multiple calls to get() passing the same name will get the same object
      * returned, until the virtual machine is deleted (via {@link #delete}) and then recreated.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @see #getOrCreate
      * @throws VirtualMachineException if the virtual machine exists but could not be successfully
      *     retrieved.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Nullable
     public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
@@ -186,11 +193,14 @@
      *
      * <p>The new virtual machine will be in the same state as the descriptor indicates.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the VM cannot be imported.
      * @hide
      */
     @NonNull
     @SystemApi
+    @WorkerThread
     public VirtualMachine importFromDescriptor(
             @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
             throws VirtualMachineException {
@@ -205,10 +215,13 @@
      * Returns an existing {@link VirtualMachine} if it exists, or create a new one. The config
      * parameter is used only when a new virtual machine is created.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachine getOrCreate(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
@@ -229,11 +242,14 @@
      * with the same name is different from an already deleted virtual machine even if it has the
      * same config.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine does not exist, is not stopped, or
      *     cannot be deleted.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     public void delete(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
             VirtualMachine vm = getVmByName(name);
diff --git a/libs/fdtpci/Android.bp b/libs/fdtpci/Android.bp
new file mode 100644
index 0000000..f650385
--- /dev/null
+++ b/libs/fdtpci/Android.bp
@@ -0,0 +1,14 @@
+rust_library_rlib {
+    name: "libfdtpci",
+    edition: "2021",
+    no_stdlibs: true,
+    host_supported: false,
+    crate_name: "fdtpci",
+    srcs: ["src/lib.rs"],
+    rustlibs: [
+        "liblibfdt",
+        "liblog_rust_nostd",
+        "libvirtio_drivers",
+    ],
+    apex_available: ["com.android.virt"],
+}
diff --git a/libs/fdtpci/TEST_MAPPING b/libs/fdtpci/TEST_MAPPING
new file mode 100644
index 0000000..c315b4a
--- /dev/null
+++ b/libs/fdtpci/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit": [
+    {
+      "name": "vmbase_example.integration_test"
+    }
+  ]
+}
diff --git a/libs/fdtpci/src/lib.rs b/libs/fdtpci/src/lib.rs
new file mode 100644
index 0000000..a63e05b
--- /dev/null
+++ b/libs/fdtpci/src/lib.rs
@@ -0,0 +1,231 @@
+// Copyright 2022, 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.
+
+//! Library for working with (VirtIO) PCI devices discovered from a device tree.
+
+#![no_std]
+
+use core::{
+    ffi::CStr,
+    fmt::{self, Display, Formatter},
+    ops::Range,
+};
+use libfdt::{AddressRange, Fdt, FdtError, FdtNode};
+use log::debug;
+use virtio_drivers::pci::bus::{Cam, PciRoot};
+
+/// PCI MMIO configuration region size.
+const PCI_CFG_SIZE: usize = 0x100_0000;
+
+/// An error parsing a PCI node from an FDT.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum PciError {
+    /// Error getting PCI node from FDT.
+    FdtErrorPci(FdtError),
+    /// Failed to find PCI bus in FDT.
+    FdtNoPci,
+    /// Error getting `reg` property from PCI node.
+    FdtErrorReg(FdtError),
+    /// PCI node missing `reg` property.
+    FdtMissingReg,
+    /// Empty `reg property on PCI node.
+    FdtRegEmpty,
+    /// PCI `reg` property missing size.
+    FdtRegMissingSize,
+    /// PCI CAM size reported by FDT is not what we expected.
+    CamWrongSize(usize),
+    /// Error getting `ranges` property from PCI node.
+    FdtErrorRanges(FdtError),
+    /// PCI node missing `ranges` property.
+    FdtMissingRanges,
+    /// Bus address is not equal to CPU physical address in `ranges` property.
+    RangeAddressMismatch {
+        /// A bus address from the `ranges` property.
+        bus_address: u64,
+        /// The corresponding CPU physical address from the `ranges` property.
+        cpu_physical: u64,
+    },
+    /// No suitable PCI memory range found.
+    NoSuitableRange,
+}
+
+impl Display for PciError {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self {
+            Self::FdtErrorPci(e) => write!(f, "Error getting PCI node from FDT: {}", e),
+            Self::FdtNoPci => write!(f, "Failed to find PCI bus in FDT."),
+            Self::FdtErrorReg(e) => write!(f, "Error getting reg property from PCI node: {}", e),
+            Self::FdtMissingReg => write!(f, "PCI node missing reg property."),
+            Self::FdtRegEmpty => write!(f, "Empty reg property on PCI node."),
+            Self::FdtRegMissingSize => write!(f, "PCI reg property missing size."),
+            Self::CamWrongSize(cam_size) => write!(
+                f,
+                "FDT says PCI CAM is {} bytes but we expected {}.",
+                cam_size, PCI_CFG_SIZE
+            ),
+            Self::FdtErrorRanges(e) => {
+                write!(f, "Error getting ranges property from PCI node: {}", e)
+            }
+            Self::FdtMissingRanges => write!(f, "PCI node missing ranges property."),
+            Self::RangeAddressMismatch { bus_address, cpu_physical } => {
+                write!(
+                    f,
+                    "bus address {:#018x} != CPU physical address {:#018x}",
+                    bus_address, cpu_physical
+                )
+            }
+            Self::NoSuitableRange => write!(f, "No suitable PCI memory range found."),
+        }
+    }
+}
+
+/// Information about the PCI bus parsed from the device tree.
+#[derive(Debug)]
+pub struct PciInfo {
+    /// The MMIO range used by the memory-mapped PCI CAM.
+    pub cam_range: Range<usize>,
+    /// The MMIO range from which 32-bit PCI BARs should be allocated.
+    pub bar_range: Range<u32>,
+}
+
+impl PciInfo {
+    /// Finds the PCI node in the FDT, parses its properties and validates it.
+    pub fn from_fdt(fdt: &Fdt) -> Result<Self, PciError> {
+        let pci_node = pci_node(fdt)?;
+
+        let cam_range = parse_cam_range(&pci_node)?;
+        let bar_range = parse_ranges(&pci_node)?;
+
+        Ok(Self { cam_range, bar_range })
+    }
+
+    /// Returns the `PciRoot` for the memory-mapped CAM found in the FDT. The CAM should be mapped
+    /// before this is called, by calling [`PciInfo::map`].
+    ///
+    /// # Safety
+    ///
+    /// To prevent concurrent access, only one `PciRoot` should exist in the program. Thus this
+    /// method must only be called once, and there must be no other `PciRoot` constructed using the
+    /// same CAM.
+    pub unsafe fn make_pci_root(&self) -> PciRoot {
+        PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam)
+    }
+}
+
+/// Finds an FDT node with compatible=pci-host-cam-generic.
+fn pci_node(fdt: &Fdt) -> Result<FdtNode, PciError> {
+    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
+        .map_err(PciError::FdtErrorPci)?
+        .next()
+        .ok_or(PciError::FdtNoPci)
+}
+
+/// Parses the "reg" property of the given PCI FDT node to find the MMIO CAM range.
+fn parse_cam_range(pci_node: &FdtNode) -> Result<Range<usize>, PciError> {
+    let pci_reg = pci_node
+        .reg()
+        .map_err(PciError::FdtErrorReg)?
+        .ok_or(PciError::FdtMissingReg)?
+        .next()
+        .ok_or(PciError::FdtRegEmpty)?;
+    let cam_addr = pci_reg.addr as usize;
+    let cam_size = pci_reg.size.ok_or(PciError::FdtRegMissingSize)? as usize;
+    debug!("Found PCI CAM at {:#x}-{:#x}", cam_addr, cam_addr + cam_size);
+    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
+    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
+    // access it; maybe there is some new version of PCI we don't know about.
+    if cam_size != PCI_CFG_SIZE {
+        return Err(PciError::CamWrongSize(cam_size));
+    }
+
+    Ok(cam_addr..cam_addr + cam_size)
+}
+
+/// Parses the "ranges" property of the given PCI FDT node, and returns the largest suitable range
+/// to use for non-prefetchable 32-bit memory BARs.
+fn parse_ranges(pci_node: &FdtNode) -> Result<Range<u32>, PciError> {
+    let mut memory_address = 0;
+    let mut memory_size = 0;
+
+    for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
+        .ranges::<(u32, u64), u64, u64>()
+        .map_err(PciError::FdtErrorRanges)?
+        .ok_or(PciError::FdtMissingRanges)?
+    {
+        let flags = PciMemoryFlags(flags);
+        let prefetchable = flags.prefetchable();
+        let range_type = flags.range_type();
+        debug!(
+            "range: {:?} {}prefetchable bus address: {:#018x} CPU physical address: {:#018x} size: {:#018x}",
+            range_type,
+            if prefetchable { "" } else { "non-" },
+            bus_address,
+            cpu_physical,
+            size,
+        );
+
+        // Use a 64-bit range for 32-bit memory, if it is low enough, because crosvm doesn't
+        // currently provide any 32-bit ranges.
+        if !prefetchable
+            && matches!(range_type, PciRangeType::Memory32 | PciRangeType::Memory64)
+            && size > memory_size.into()
+            && bus_address + size < u32::MAX.into()
+        {
+            if bus_address != cpu_physical {
+                return Err(PciError::RangeAddressMismatch { bus_address, cpu_physical });
+            }
+            memory_address = u32::try_from(cpu_physical).unwrap();
+            memory_size = u32::try_from(size).unwrap();
+        }
+    }
+
+    if memory_size == 0 {
+        return Err(PciError::NoSuitableRange);
+    }
+
+    Ok(memory_address..memory_address + memory_size)
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+struct PciMemoryFlags(u32);
+
+impl PciMemoryFlags {
+    pub fn prefetchable(self) -> bool {
+        self.0 & 0x80000000 != 0
+    }
+
+    pub fn range_type(self) -> PciRangeType {
+        PciRangeType::from((self.0 & 0x3000000) >> 24)
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+enum PciRangeType {
+    ConfigurationSpace,
+    IoSpace,
+    Memory32,
+    Memory64,
+}
+
+impl From<u32> for PciRangeType {
+    fn from(value: u32) -> Self {
+        match value {
+            0 => Self::ConfigurationSpace,
+            1 => Self::IoSpace,
+            2 => Self::Memory32,
+            3 => Self::Memory64,
+            _ => panic!("Tried to convert invalid range type {}", value),
+        }
+    }
+}
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 2912c91..f5e214e 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -15,6 +15,7 @@
         "libaarch64_paging",
         "libbuddy_system_allocator",
         "libdice_nostd",
+        "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
         "libpvmfw_avb_nostd",
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 7d64bf0..e610e31 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -37,9 +37,10 @@
     avb::PUBLIC_KEY,
     entry::RebootReason,
     memory::MemoryTracker,
-    pci::{find_virtio_devices, PciError, PciInfo},
+    pci::{find_virtio_devices, map_mmio},
 };
 use dice::bcc;
+use fdtpci::{PciError, PciInfo};
 use libfdt::Fdt;
 use log::{debug, error, info, trace};
 use pvmfw_avb::verify_payload;
@@ -64,7 +65,7 @@
     // Set up PCI bus for VirtIO devices.
     let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
     debug!("PCI: {:#x?}", pci_info);
-    pci_info.map(memory)?;
+    map_mmio(&pci_info, memory)?;
     // Safety: This is the only place where we call make_pci_root, and this main function is only
     // called once.
     let mut pci_root = unsafe { pci_info.make_pci_root() };
diff --git a/pvmfw/src/pci.rs b/pvmfw/src/pci.rs
index 301ecfc..e9ac45b 100644
--- a/pvmfw/src/pci.rs
+++ b/pvmfw/src/pci.rs
@@ -14,225 +14,26 @@
 
 //! Functions to scan the PCI bus for VirtIO devices.
 
-use crate::{
-    entry::RebootReason,
-    memory::{MemoryRange, MemoryTracker},
-};
-use core::{
-    ffi::CStr,
-    fmt::{self, Display, Formatter},
-    ops::Range,
-};
-use libfdt::{AddressRange, Fdt, FdtError, FdtNode};
+use crate::{entry::RebootReason, memory::MemoryTracker};
+use fdtpci::{PciError, PciInfo};
 use log::{debug, error};
-use virtio_drivers::pci::{
-    bus::{Cam, PciRoot},
-    virtio_device_type,
-};
+use virtio_drivers::pci::{bus::PciRoot, virtio_device_type};
 
-/// PCI MMIO configuration region size.
-const PCI_CFG_SIZE: usize = 0x100_0000;
+/// Maps the CAM and BAR range in the page table and MMIO guard.
+pub fn map_mmio(pci_info: &PciInfo, memory: &mut MemoryTracker) -> Result<(), RebootReason> {
+    memory.map_mmio_range(pci_info.cam_range.clone()).map_err(|e| {
+        error!("Failed to map PCI CAM: {}", e);
+        RebootReason::InternalError
+    })?;
 
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum PciError {
-    FdtErrorPci(FdtError),
-    FdtNoPci,
-    FdtErrorReg(FdtError),
-    FdtMissingReg,
-    FdtRegEmpty,
-    FdtRegMissingSize,
-    CamWrongSize(usize),
-    FdtErrorRanges(FdtError),
-    FdtMissingRanges,
-    RangeAddressMismatch { bus_address: u64, cpu_physical: u64 },
-    NoSuitableRange,
-}
-
-impl Display for PciError {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        match self {
-            Self::FdtErrorPci(e) => write!(f, "Error getting PCI node from FDT: {}", e),
-            Self::FdtNoPci => write!(f, "Failed to find PCI bus in FDT."),
-            Self::FdtErrorReg(e) => write!(f, "Error getting reg property from PCI node: {}", e),
-            Self::FdtMissingReg => write!(f, "PCI node missing reg property."),
-            Self::FdtRegEmpty => write!(f, "Empty reg property on PCI node."),
-            Self::FdtRegMissingSize => write!(f, "PCI reg property missing size."),
-            Self::CamWrongSize(cam_size) => write!(
-                f,
-                "FDT says PCI CAM is {} bytes but we expected {}.",
-                cam_size, PCI_CFG_SIZE
-            ),
-            Self::FdtErrorRanges(e) => {
-                write!(f, "Error getting ranges property from PCI node: {}", e)
-            }
-            Self::FdtMissingRanges => write!(f, "PCI node missing ranges property."),
-            Self::RangeAddressMismatch { bus_address, cpu_physical } => {
-                write!(
-                    f,
-                    "bus address {:#018x} != CPU physical address {:#018x}",
-                    bus_address, cpu_physical
-                )
-            }
-            Self::NoSuitableRange => write!(f, "No suitable PCI memory range found."),
-        }
-    }
-}
-
-/// Information about the PCI bus parsed from the device tree.
-#[derive(Debug)]
-pub struct PciInfo {
-    /// The MMIO range used by the memory-mapped PCI CAM.
-    cam_range: MemoryRange,
-    /// The MMIO range from which 32-bit PCI BARs should be allocated.
-    bar_range: Range<u32>,
-}
-
-impl PciInfo {
-    /// Finds the PCI node in the FDT, parses its properties and validates it.
-    pub fn from_fdt(fdt: &Fdt) -> Result<Self, PciError> {
-        let pci_node = pci_node(fdt)?;
-
-        let cam_range = parse_cam_range(&pci_node)?;
-        let bar_range = parse_ranges(&pci_node)?;
-
-        Ok(Self { cam_range, bar_range })
-    }
-
-    /// Maps the CAM and BAR range in the page table and MMIO guard.
-    pub fn map(&self, memory: &mut MemoryTracker) -> Result<(), RebootReason> {
-        memory.map_mmio_range(self.cam_range.clone()).map_err(|e| {
-            error!("Failed to map PCI CAM: {}", e);
+    memory
+        .map_mmio_range(pci_info.bar_range.start as usize..pci_info.bar_range.end as usize)
+        .map_err(|e| {
+            error!("Failed to map PCI MMIO range: {}", e);
             RebootReason::InternalError
         })?;
 
-        memory.map_mmio_range(self.bar_range.start as usize..self.bar_range.end as usize).map_err(
-            |e| {
-                error!("Failed to map PCI MMIO range: {}", e);
-                RebootReason::InternalError
-            },
-        )?;
-
-        Ok(())
-    }
-
-    /// Returns the `PciRoot` for the memory-mapped CAM found in the FDT. The CAM should be mapped
-    /// before this is called, by calling [`PciInfo::map`].
-    ///
-    /// # Safety
-    ///
-    /// To prevent concurrent access, only one `PciRoot` should exist in the program. Thus this
-    /// method must only be called once, and there must be no other `PciRoot` constructed using the
-    /// same CAM.
-    pub unsafe fn make_pci_root(&self) -> PciRoot {
-        PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam)
-    }
-}
-
-/// Finds an FDT node with compatible=pci-host-cam-generic.
-fn pci_node(fdt: &Fdt) -> Result<FdtNode, PciError> {
-    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
-        .map_err(PciError::FdtErrorPci)?
-        .next()
-        .ok_or(PciError::FdtNoPci)
-}
-
-/// Parses the "reg" property of the given PCI FDT node to find the MMIO CAM range.
-fn parse_cam_range(pci_node: &FdtNode) -> Result<MemoryRange, PciError> {
-    let pci_reg = pci_node
-        .reg()
-        .map_err(PciError::FdtErrorReg)?
-        .ok_or(PciError::FdtMissingReg)?
-        .next()
-        .ok_or(PciError::FdtRegEmpty)?;
-    let cam_addr = pci_reg.addr as usize;
-    let cam_size = pci_reg.size.ok_or(PciError::FdtRegMissingSize)? as usize;
-    debug!("Found PCI CAM at {:#x}-{:#x}", cam_addr, cam_addr + cam_size);
-    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
-    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
-    // access it; maybe there is some new version of PCI we don't know about.
-    if cam_size != PCI_CFG_SIZE {
-        return Err(PciError::CamWrongSize(cam_size));
-    }
-
-    Ok(cam_addr..cam_addr + cam_size)
-}
-
-/// Parses the "ranges" property of the given PCI FDT node, and returns the largest suitable range
-/// to use for non-prefetchable 32-bit memory BARs.
-fn parse_ranges(pci_node: &FdtNode) -> Result<Range<u32>, PciError> {
-    let mut memory_address = 0;
-    let mut memory_size = 0;
-
-    for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
-        .ranges::<(u32, u64), u64, u64>()
-        .map_err(PciError::FdtErrorRanges)?
-        .ok_or(PciError::FdtMissingRanges)?
-    {
-        let flags = PciMemoryFlags(flags);
-        let prefetchable = flags.prefetchable();
-        let range_type = flags.range_type();
-        debug!(
-            "range: {:?} {}prefetchable bus address: {:#018x} CPU physical address: {:#018x} size: {:#018x}",
-            range_type,
-            if prefetchable { "" } else { "non-" },
-            bus_address,
-            cpu_physical,
-            size,
-        );
-
-        // Use a 64-bit range for 32-bit memory, if it is low enough, because crosvm doesn't
-        // currently provide any 32-bit ranges.
-        if !prefetchable
-            && matches!(range_type, PciRangeType::Memory32 | PciRangeType::Memory64)
-            && size > memory_size.into()
-            && bus_address + size < u32::MAX.into()
-        {
-            if bus_address != cpu_physical {
-                return Err(PciError::RangeAddressMismatch { bus_address, cpu_physical });
-            }
-            memory_address = u32::try_from(cpu_physical).unwrap();
-            memory_size = u32::try_from(size).unwrap();
-        }
-    }
-
-    if memory_size == 0 {
-        return Err(PciError::NoSuitableRange);
-    }
-
-    Ok(memory_address..memory_address + memory_size)
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-struct PciMemoryFlags(u32);
-
-impl PciMemoryFlags {
-    pub fn prefetchable(self) -> bool {
-        self.0 & 0x80000000 != 0
-    }
-
-    pub fn range_type(self) -> PciRangeType {
-        PciRangeType::from((self.0 & 0x3000000) >> 24)
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum PciRangeType {
-    ConfigurationSpace,
-    IoSpace,
-    Memory32,
-    Memory64,
-}
-
-impl From<u32> for PciRangeType {
-    fn from(value: u32) -> Self {
-        match value {
-            0 => Self::ConfigurationSpace,
-            1 => Self::IoSpace,
-            2 => Self::Memory32,
-            3 => Self::Memory64,
-            _ => panic!("Tried to convert invalid range type {}", value),
-        }
-    }
+    Ok(())
 }
 
 /// Finds VirtIO PCI devices.
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index fbad8f4..94eb21a 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -12,6 +12,7 @@
         "libaarch64_paging",
         "libbuddy_system_allocator",
         "libdice_nostd",
+        "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
         "libvirtio_drivers",
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index 888f273..ec28a11 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -28,18 +28,16 @@
     bionic_tls, dtb_range, print_addresses, rodata_range, stack_chk_guard, text_range,
     writable_region, DEVICE_REGION,
 };
-use crate::pci::{check_pci, pci_node, PciMemory32Allocator};
+use crate::pci::{check_pci, get_bar_region};
 use aarch64_paging::{idmap::IdMap, paging::Attributes};
 use alloc::{vec, vec::Vec};
 use buddy_system_allocator::LockedHeap;
 use core::ffi::CStr;
+use fdtpci::PciInfo;
 use libfdt::Fdt;
 use log::{debug, info, trace, LevelFilter};
 use vmbase::{logger, main, println};
 
-/// PCI MMIO configuration region size.
-const AARCH64_PCI_CFG_SIZE: u64 = 0x1000000;
-
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
 static mut ZEROED_DATA: [u32; 10] = [0; 10];
 static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
@@ -73,16 +71,8 @@
     info!("FDT passed verification.");
     check_fdt(fdt);
 
-    let pci_node = pci_node(fdt);
-    // Parse reg property to find CAM.
-    let pci_reg = pci_node.reg().unwrap().unwrap().next().unwrap();
-    debug!("Found PCI CAM at {:#x}-{:#x}", pci_reg.addr, pci_reg.addr + pci_reg.size.unwrap());
-    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
-    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
-    // access it; maybe there is some new version of PCI we don't know about.
-    assert_eq!(pci_reg.size.unwrap(), AARCH64_PCI_CFG_SIZE);
-    // Parse ranges property to find memory ranges from which to allocate PCI BARs.
-    let pci_allocator = PciMemory32Allocator::for_pci_ranges(&pci_node);
+    let pci_info = PciInfo::from_fdt(fdt).unwrap();
+    debug!("Found PCI CAM at {:#x}-{:#x}", pci_info.cam_range.start, pci_info.cam_range.end);
 
     modify_fdt(fdt);
 
@@ -125,10 +115,7 @@
         )
         .unwrap();
     idmap
-        .map_range(
-            &pci_allocator.get_region(),
-            Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER,
-        )
+        .map_range(&get_bar_region(&pci_info), Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER)
         .unwrap();
 
     info!("Activating IdMap...");
@@ -139,7 +126,8 @@
     check_data();
     check_dice();
 
-    check_pci(pci_reg);
+    let mut pci_root = unsafe { pci_info.make_pci_root() };
+    check_pci(&mut pci_root);
 }
 
 fn check_stack_guard() {
diff --git a/vmbase/example/src/pci.rs b/vmbase/example/src/pci.rs
index bd5b5ba..a204b90 100644
--- a/vmbase/example/src/pci.rs
+++ b/vmbase/example/src/pci.rs
@@ -16,14 +16,11 @@
 
 use aarch64_paging::paging::MemoryRegion;
 use alloc::alloc::{alloc, dealloc, Layout};
-use core::{ffi::CStr, mem::size_of};
-use libfdt::{AddressRange, Fdt, FdtNode, Reg};
+use core::mem::size_of;
+use fdtpci::PciInfo;
 use log::{debug, info};
 use virtio_drivers::{
-    pci::{
-        bus::{Cam, PciRoot},
-        virtio_device_type, PciTransport,
-    },
+    pci::{bus::PciRoot, virtio_device_type, PciTransport},
     DeviceType, Hal, PhysAddr, Transport, VirtAddr, VirtIOBlk, PAGE_SIZE,
 };
 
@@ -33,24 +30,14 @@
 /// The size in sectors of the test block device we expect.
 const EXPECTED_SECTOR_COUNT: usize = 4;
 
-/// Finds an FDT node with compatible=pci-host-cam-generic.
-pub fn pci_node(fdt: &Fdt) -> FdtNode {
-    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
-        .unwrap()
-        .next()
-        .unwrap()
-}
-
-pub fn check_pci(reg: Reg<u64>) {
-    let mut pci_root = unsafe { PciRoot::new(reg.addr as *mut u8, Cam::MmioCam) };
+pub fn check_pci(pci_root: &mut PciRoot) {
     let mut checked_virtio_device_count = 0;
     for (device_function, info) in pci_root.enumerate_bus(0) {
         let (status, command) = pci_root.get_status_command(device_function);
         info!("Found {} at {}, status {:?} command {:?}", info, device_function, status, command);
         if let Some(virtio_type) = virtio_device_type(&info) {
             info!("  VirtIO {:?}", virtio_type);
-            let mut transport =
-                PciTransport::new::<HalImpl>(&mut pci_root, device_function).unwrap();
+            let mut transport = PciTransport::new::<HalImpl>(pci_root, device_function).unwrap();
             info!(
                 "Detected virtio PCI device with device type {:?}, features {:#018x}",
                 transport.device_type(),
@@ -88,89 +75,9 @@
     }
 }
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-struct PciMemoryFlags(u32);
-
-impl PciMemoryFlags {
-    pub fn prefetchable(self) -> bool {
-        self.0 & 0x80000000 != 0
-    }
-
-    pub fn range_type(self) -> PciRangeType {
-        PciRangeType::from((self.0 & 0x3000000) >> 24)
-    }
-}
-
-/// Allocates 32-bit memory addresses for PCI BARs.
-pub struct PciMemory32Allocator {
-    start: u32,
-    end: u32,
-}
-
-impl PciMemory32Allocator {
-    /// Creates a new allocator based on the ranges property of the given PCI node.
-    pub fn for_pci_ranges(pci_node: &FdtNode) -> Self {
-        let mut memory_32_address = 0;
-        let mut memory_32_size = 0;
-        for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
-            .ranges::<(u32, u64), u64, u64>()
-            .expect("Error getting ranges property from PCI node")
-            .expect("PCI node missing ranges property.")
-        {
-            let flags = PciMemoryFlags(flags);
-            let prefetchable = flags.prefetchable();
-            let range_type = flags.range_type();
-            info!(
-                "range: {:?} {}prefetchable bus address: {:#018x} host physical address: {:#018x} size: {:#018x}",
-                range_type,
-                if prefetchable { "" } else { "non-" },
-                bus_address,
-                cpu_physical,
-                size,
-            );
-            if !prefetchable
-                && ((range_type == PciRangeType::Memory32 && size > memory_32_size.into())
-                    || (range_type == PciRangeType::Memory64
-                        && size > memory_32_size.into()
-                        && bus_address + size < u32::MAX.into()))
-            {
-                // Use the 64-bit range for 32-bit memory, if it is low enough.
-                assert_eq!(bus_address, cpu_physical);
-                memory_32_address = u32::try_from(cpu_physical).unwrap();
-                memory_32_size = u32::try_from(size).unwrap();
-            }
-        }
-        if memory_32_size == 0 {
-            panic!("No PCI memory regions found.");
-        }
-
-        Self { start: memory_32_address, end: memory_32_address + memory_32_size }
-    }
-
-    /// Gets a memory region covering the address space from which this allocator will allocate.
-    pub fn get_region(&self) -> MemoryRegion {
-        MemoryRegion::new(self.start as usize, self.end as usize)
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum PciRangeType {
-    ConfigurationSpace,
-    IoSpace,
-    Memory32,
-    Memory64,
-}
-
-impl From<u32> for PciRangeType {
-    fn from(value: u32) -> Self {
-        match value {
-            0 => Self::ConfigurationSpace,
-            1 => Self::IoSpace,
-            2 => Self::Memory32,
-            3 => Self::Memory64,
-            _ => panic!("Tried to convert invalid range type {}", value),
-        }
-    }
+/// Gets the memory region in which BARs are allocated.
+pub fn get_bar_region(pci_info: &PciInfo) -> MemoryRegion {
+    MemoryRegion::new(pci_info.bar_range.start as usize, pci_info.bar_range.end as usize)
 }
 
 struct HalImpl;