Merge "Add config of audio device" into main
diff --git a/README.md b/README.md
index 7560a45..f417b00 100644
--- a/README.md
+++ b/README.md
@@ -32,4 +32,5 @@
 * [Debugging](docs/debug)
 * [Using custom VM](docs/custom_vm.md)
 * [Device assignment](docs/device_assignment.md)
+* [Microdroid vendor modules](docs/microdroid_vendor_modules.md)
 * [Huge Pages](docs/hugepages.md)
diff --git a/docs/abl.md b/docs/abl.md
index b08464e..7139d26 100644
--- a/docs/abl.md
+++ b/docs/abl.md
@@ -22,7 +22,9 @@
 * DICE chain (also known as BCC Handover)
 * DTBO describing [debug policy](debug/README.md#debug-policy) (if available)
 * DTBO describing [assignable devices](device_assignment.md) (if available)
-* Reference DT carrying extra information that needs to be passed to the guest VM
+* Reference DT carrying extra information that needs to be passed to the guest VM, e.g.:
+    * Hashtree digest of the
+      [microdroid-vendor.img](microdroid_vendor_modules.md#changes-in-abl).
 
 See [Configuration Data](../pvmfw/README.md#configuration-data) for more detail.
 
diff --git a/docs/microdroid_vendor_modules.md b/docs/microdroid_vendor_modules.md
new file mode 100644
index 0000000..ef55225
--- /dev/null
+++ b/docs/microdroid_vendor_modules.md
@@ -0,0 +1,178 @@
+# Microdroid vendor modules
+
+Starting with Android V it is possible to start a Microdroid VM with a
+vendor-prodived kernel modules. This feature is part of the bigger
+[device assignment](device_assignmnent.md) effort.
+
+The vendor kernel modules should be packaged inside a `microdroid-vendor.img`
+dm-verity protected partition, inside a Microdroid VM this will be mounted as
+`/vendor` partition.
+
+Currently the following features are supported:
+* Kernel modules;
+* init .rc scripts with basic triggers (e.g. `on early-init`);
+* `ueventd.rc` file;
+* `/vendor/etc/selinux/vendor_file_contexts` file.
+
+
+Additionallity, starting with android15-6.6 it is possible to start a Microdroid
+VM with GKI as guest kernel. This is **required** when launching a Microdroid VM with
+vendor provided kernel modules.
+
+**Note:** in Android V, the 'Microdroid vendor modules' is considered an experimental
+feature to provide our partners a reference implementation that they can start
+integrating with to flesh out missing pieces.
+We **do not recommened** launching user-facing features that depend on using
+vendor modules in a pVM.
+
+
+## Integrating into a product {#build-system-integration}
+
+You can define microdroid vendor partition using `android_filesystem` soong
+module, here is an example:
+
+```
+android_filesystem {
+    name: "microdroid_vendor_example_image",
+    partition_name: "microdroid-vendor",
+    type: "ext4",
+    file_contexts: "file_contexts",
+    use_avb: true,
+    avb_private_key: ":microdroid_vendor_example_sign_key",
+    mount_point: "vendor",
+    deps: [
+        "microdroid_vendor_example_ueventd",
+        "microdroid_vendor_example_file_contexts",
+        "microdroid_vendor_example_kernel_modules",
+        "microdroid_vendor_example.rc",
+    ],
+}
+
+prebuilt_etc {
+    name: "microdroid_vendor_example",
+    src: ":microdroid_vendor_example_image",
+    relative_install_path: "avf/microdroid",
+    filename: "microdroid_vendor.img",
+    vendor: true,
+}
+```
+
+In order to integrate the microdroid vendor partition into a product, add the
+following lines to the corresponding device makefile:
+
+```
+PRODUCT_PACKAGES += microdroid_vendor_example
+MICRODROID_VENDOR_IMAGE_MODULE := microdroid_vendor_example
+```
+
+**Note**: it is important that the microdroid-vendor.img is installed into
+`/vendor/etc/avf/microdroid/microdroid_vendor.img` on the device.
+
+
+## Launching a Microdroid VM wirth vendor partition
+
+### Non-protected VMs
+
+You can launch a non-protected Microdroid VM with vendor partition by adding the
+`--vendor` argument to the `/apex/com.android.virt/bin/vm run-app` or
+`/apex/com.android.virt/bin/vm run-microdroid` CLI commands, e.g.:
+
+```
+adb shell /apex/com.android.virt/bin/vm/run-microdroid \
+  --debug full \
+  --vendor /vendor/etc/avf/microdroid/microdroid_vendor.img
+```
+
+On the Android host side, the `virtmgr` will append the
+`vendor_hashtree_descriptor_root_digest` property to the `/avf` node of the
+guest device tree overlay. Value of this property will contain the hashtree
+digest of the `microdroid_vendor.img` provided via the `--vendor` argument.
+
+Inside the Microdroid guest VM, the `first_stage_init` will use the
+`/proc/device-tree/avf/vendor_hashtree_descriptor_root_digest` to create a
+`dm-verity` device on top of the `/dev/block/by-name/microdroid-vendor` block
+device. The `/vendor` partition will be mounted on top of the created
+`dm-verity` device.
+
+TODO(ioffe): create drawings and add them here.
+
+
+### Protected VMs
+
+As of now, only **debuggable** Microdroid pVMs support running with the
+Microdroid vendor partition, e.g.:
+
+```
+adb shell /apex/com.android.virt/bin/vm/run-microdroid \
+  --debug full \
+  --protected \
+  --vendor /vendor/etc/avf/microdroid/microdroid_vendor.img
+```
+
+The execution flow is very similar to the non-protected case above, however
+there is one important addition. The `pvmfw` binary will use the
+[VM reference DT blob](#../pvmfw/README.md#pvmfw-data-v1-2) passed from the
+Android Bootloader (ABL), to validate the guest DT overlay passed from the host.
+
+See [Changes in Android Bootloader](#changes-in-abl) section below for more
+details.
+
+### Reflecting microdroid vendor partition in the guest DICE chain
+
+The microdroid vendor partition will be reflected as a separate
+`Microdroid vendor` node in the Microdroid DICE chain.
+
+TODO(ioffe): drawing of DICE chain here.
+
+This node derivation happens in the `derive_microdroid_vendor_dice_node`, which
+is executed by `first_stage_init`. The binary will write the new DICE chain into
+the `/microdroid_resources/dice_chain.raw` file, which will be then read by
+`microdroid_manager` to derive the final `Microdroid payload` DICE node.
+
+TODO(ioffe): another drawing here.
+
+## Changes in the Android Bootloader {#changes-in-abl}
+
+In order for a Microdroid pVM with the
+`/vendor/etc/avf/microdroid/microdroid_vendor.img` to successfully boot, the
+ABL is required to pass the correct value of the
+`/vendor/etc/avf/microdroid/microdroid_vendor.img` hashtree digest in the
+`vendor_hashtree_descriptor_root_digest` property of `the /avf/reference` node.
+
+The `MICRODROID_VENDOR_IMAGE_MODULE` make variable mentioned in the
+[section above](#build-system-integration) configures build system to inject
+the value of the `microdroid-vendor.img` hashtree digest into the
+`com.android.build.microdroid-vendor.root_digest ` property of the footer of
+the host's `vendor.img`.
+
+The Android Bootloader can read that property when construction the
+[VM reference DT blob](#../pvmfw/README.md#pvmfw-data-v1-2) passed to pvmfw.
+
+## GKI as Microdroid guest kernel
+
+In order to enable running Microdroid with GKI as guest kernel, specify the
+`PRODUCT_AVF_MICRODROID_GUEST_GKI_VERSION ` variable in a product makefile:
+
+```
+PRODUCT_AVF_MICRODROID_GUEST_GKI_VERSION := android15_66
+```
+
+Note: currently this will alter the content of the `com.android.virt` APEX by
+installing the corresponding GKI image into it. In the future, the GKI image
+will be installed on the `/system_ext` partition.
+
+The following changes to the `gki_defconfig` were made to support running as
+guest kernel:
+
+```
+CONFIG_VIRTIO_VSOCKETS=m
+CONFIG_VIRTIO_BLK=m
+CONFIG_OPEN_DICE=m
+CONFIG_VCPU_STALL_DETECTOR=m
+CONFIG_VIRTIO_CONSOLE=m
+CONFIG_HW_RANDOM_CCTRNG=m
+CONFIG_VIRTIO_PCI=m
+CONFIG_VIRTIO_BALLOON=m
+CONFIG_DMA_RESTRICTED_POOL=y
+```
+
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 053e4f7..7a03f0b 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -187,18 +187,26 @@
 of the array. The header uses the endianness of the virtual machine.
 
 The header format itself is agnostic of the internal format of the individual
-blos it refers to. In version 1.0, it describes two blobs:
+blos it refers to.
+
+##### Version 1.0 {#pvmfw-data-v1-0}
+
+In version 1.0, it describes two blobs:
 
 - entry 0 must point to a valid DICE chain handover (see below)
 - entry 1 may point to a [DTBO] to be applied to the pVM device tree. See
   [debug policy][debug_policy] for an example.
 
+##### Version 1.1 {#pvmfw-data-v1-1}
+
 In version 1.1, a third blob is added.
 
 - entry 2 may point to a [DTBO] that describes VM DA DTBO for
   [device assignment][device_assignment].
   pvmfw will provision assigned devices with the VM DTBO.
 
+#### Version 1.2 {#pvmfw-data-v1-2}
+
 In version 1.2, a fourth blob is added.
 
 - entry 3 if present contains the VM reference DT. This defines properties that
diff --git a/pvmfw/avb/tests/api_test.rs b/pvmfw/avb/tests/api_test.rs
index c6f26ac..8683e69 100644
--- a/pvmfw/avb/tests/api_test.rs
+++ b/pvmfw/avb/tests/api_test.rs
@@ -20,7 +20,11 @@
 use avb::{DescriptorError, SlotVerifyError};
 use avb_bindgen::{AvbFooter, AvbVBMetaImageHeader};
 use pvmfw_avb::{verify_payload, Capability, DebugLevel, PvmfwVerifyError, VerifiedBootData};
-use std::{fs, mem::size_of, ptr};
+use std::{
+    fs,
+    mem::{offset_of, size_of},
+    ptr,
+};
 use utils::*;
 
 const TEST_IMG_WITH_ONE_HASHDESC_PATH: &str = "test_image_with_one_hashdesc.img";
@@ -243,32 +247,20 @@
 fn kernel_footer_with_vbmeta_offset_overwritten_fails_verification() -> Result<()> {
     // Arrange.
     let mut kernel = load_latest_signed_kernel()?;
-    let total_len = kernel.len() as u64;
-    let footer = extract_avb_footer(&kernel)?;
-    assert!(footer.vbmeta_offset < total_len);
-    // TODO: use core::mem::offset_of once stable.
-    let footer_addr = ptr::addr_of!(footer) as *const u8;
-    let vbmeta_offset_addr = ptr::addr_of!(footer.vbmeta_offset) as *const u8;
-    let vbmeta_offset_start =
-        // SAFETY:
-        // - both raw pointers `vbmeta_offset_addr` and `footer_addr` are not null;
-        // - they are both derived from the `footer` object;
-        // - the offset is known from the struct definition to be a small positive number of bytes.
-        unsafe { vbmeta_offset_addr.offset_from(footer_addr) };
-    let footer_start = kernel.len() - size_of::<AvbFooter>();
-    let vbmeta_offset_start = footer_start + usize::try_from(vbmeta_offset_start)?;
+    let footer_offset = get_avb_footer_offset(&kernel)?;
+    let vbmeta_offset_offset = footer_offset + offset_of!(AvbFooter, vbmeta_offset);
+    let vbmeta_offset_bytes = vbmeta_offset_offset..(vbmeta_offset_offset + size_of::<u64>());
 
-    let wrong_offsets = [total_len, u64::MAX];
-    for &wrong_offset in wrong_offsets.iter() {
+    let test_values = [kernel.len(), usize::MAX];
+    for value in test_values {
+        let value = u64::try_from(value).unwrap();
         // Act.
-        kernel[vbmeta_offset_start..(vbmeta_offset_start + size_of::<u64>())]
-            .copy_from_slice(&wrong_offset.to_be_bytes());
+        kernel[vbmeta_offset_bytes.clone()].copy_from_slice(&value.to_be_bytes());
+        // footer is unaligned; copy vbmeta_offset to local variable
+        let vbmeta_offset = extract_avb_footer(&kernel)?.vbmeta_offset;
+        assert_eq!(vbmeta_offset, value);
 
         // Assert.
-        let footer = extract_avb_footer(&kernel)?;
-        // footer is unaligned; copy vbmeta_offset to local variable
-        let vbmeta_offset = footer.vbmeta_offset;
-        assert_eq!(wrong_offset, vbmeta_offset);
         assert_payload_verification_with_initrd_fails(
             &kernel,
             &load_latest_initrd_normal()?,
diff --git a/pvmfw/avb/tests/utils.rs b/pvmfw/avb/tests/utils.rs
index cf37fcf..e989579 100644
--- a/pvmfw/avb/tests/utils.rs
+++ b/pvmfw/avb/tests/utils.rs
@@ -72,8 +72,14 @@
     Ok(fs::read(PUBLIC_KEY_RSA4096_PATH)?)
 }
 
+pub fn get_avb_footer_offset(signed_kernel: &[u8]) -> Result<usize> {
+    let offset = signed_kernel.len().checked_sub(size_of::<AvbFooter>());
+
+    offset.ok_or_else(|| anyhow!("Kernel too small to be AVB-signed"))
+}
+
 pub fn extract_avb_footer(kernel: &[u8]) -> Result<AvbFooter> {
-    let footer_start = kernel.len() - size_of::<AvbFooter>();
+    let footer_start = get_avb_footer_offset(kernel)?;
     // SAFETY: The slice is the same size as the struct which only contains simple data types.
     let mut footer = unsafe {
         transmute::<[u8; size_of::<AvbFooter>()], AvbFooter>(kernel[footer_start..].try_into()?)
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 43822a5..0ff7270 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -30,9 +30,9 @@
 use log::LevelFilter;
 use vmbase::util::RangeExt as _;
 use vmbase::{
-    configure_heap, console,
+    configure_heap, console_writeln,
     hyp::{get_mem_sharer, get_mmio_guard},
-    layout::{self, crosvm},
+    layout::{self, crosvm, UART_PAGE_ADDR},
     main,
     memory::{min_dcache_line_size, MemoryTracker, MEMORY, SIZE_128KB, SIZE_4KB},
     power::reboot,
@@ -59,6 +59,21 @@
     SecretDerivationError,
 }
 
+impl RebootReason {
+    pub fn as_avf_reboot_string(&self) -> &'static str {
+        match self {
+            Self::InvalidBcc => "PVM_FIRMWARE_INVALID_BCC",
+            Self::InvalidConfig => "PVM_FIRMWARE_INVALID_CONFIG_DATA",
+            Self::InternalError => "PVM_FIRMWARE_INTERNAL_ERROR",
+            Self::InvalidFdt => "PVM_FIRMWARE_INVALID_FDT",
+            Self::InvalidPayload => "PVM_FIRMWARE_INVALID_PAYLOAD",
+            Self::InvalidRamdisk => "PVM_FIRMWARE_INVALID_RAMDISK",
+            Self::PayloadVerificationError => "PVM_FIRMWARE_PAYLOAD_VERIFICATION_FAILED",
+            Self::SecretDerivationError => "PVM_FIRMWARE_SECRET_DERIVATION_FAILED",
+        }
+    }
+}
+
 main!(start);
 configure_heap!(SIZE_128KB);
 
@@ -66,11 +81,15 @@
 pub fn start(fdt_address: u64, payload_start: u64, payload_size: u64, _arg3: u64) {
     // Limitations in this function:
     // - can't access non-pvmfw memory (only statically-mapped memory)
-    // - can't access MMIO (therefore, no logging)
+    // - can't access MMIO (except the console, already configured by vmbase)
 
     match main_wrapper(fdt_address as usize, payload_start as usize, payload_size as usize) {
         Ok((entry, bcc)) => jump_to_payload(fdt_address, entry.try_into().unwrap(), bcc),
-        Err(_) => reboot(), // TODO(b/220071963) propagate the reason back to the host.
+        Err(e) => {
+            const REBOOT_REASON_CONSOLE: usize = 1;
+            console_writeln!(REBOOT_REASON_CONSOLE, "{}", e.as_avf_reboot_string());
+            reboot()
+        }
     }
 
     // if we reach this point and return, vmbase::entry::rust_entry() will call power::shutdown().
@@ -256,7 +275,7 @@
     // Call unshare_all_memory here (instead of relying on the dtor) while UART is still mapped.
     MEMORY.lock().as_mut().unwrap().unshare_all_memory();
     if let Some(mmio_guard) = get_mmio_guard() {
-        mmio_guard.unmap(console::BASE_ADDRESS).map_err(|e| {
+        mmio_guard.unmap(UART_PAGE_ADDR).map_err(|e| {
             error!("Failed to unshare the UART: {e}");
             RebootReason::InternalError
         })?;
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
index 5a3735e..8d12b57 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -51,7 +51,7 @@
     page_table.map_code(&layout::text_range().into())?;
     page_table.map_rodata(&layout::rodata_range().into())?;
     page_table.map_data_dbm(&appended_payload_range().into())?;
-    if let Err(e) = page_table.map_device(&layout::console_uart_range().into()) {
+    if let Err(e) = page_table.map_device(&layout::console_uart_page().into()) {
         error!("Failed to remap the UART as a dynamic page table entry: {e}");
         return Err(e);
     }
diff --git a/rialto/src/exceptions.rs b/rialto/src/exceptions.rs
index b806b08..e87e0d3 100644
--- a/rialto/src/exceptions.rs
+++ b/rialto/src/exceptions.rs
@@ -15,7 +15,6 @@
 //! Exception handlers.
 
 use vmbase::{
-    console::emergency_write_str,
     eprintln,
     exceptions::{ArmException, Esr, HandleExceptionError},
     logger,
@@ -49,45 +48,45 @@
 
 #[no_mangle]
 extern "C" fn irq_current() {
-    emergency_write_str("irq_current\n");
+    eprintln!("irq_current");
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn fiq_current() {
-    emergency_write_str("fiq_current\n");
+    eprintln!("fiq_current");
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn serr_current() {
-    emergency_write_str("serr_current\n");
+    eprintln!("serr_current");
     print_esr();
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn sync_lower() {
-    emergency_write_str("sync_lower\n");
+    eprintln!("sync_lower");
     print_esr();
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn irq_lower() {
-    emergency_write_str("irq_lower\n");
+    eprintln!("irq_lower");
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn fiq_lower() {
-    emergency_write_str("fiq_lower\n");
+    eprintln!("fiq_lower");
     reboot();
 }
 
 #[no_mangle]
 extern "C" fn serr_lower() {
-    emergency_write_str("serr_lower\n");
+    eprintln!("serr_lower");
     print_esr();
     reboot();
 }
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index 864f5e4..701a287 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -48,7 +48,7 @@
     configure_heap,
     fdt::SwiotlbInfo,
     hyp::{get_mem_sharer, get_mmio_guard},
-    layout::{self, crosvm},
+    layout::{self, crosvm, UART_PAGE_ADDR},
     main,
     memory::{MemoryTracker, PageTable, MEMORY, PAGE_SIZE, SIZE_128KB},
     power::reboot,
@@ -78,7 +78,7 @@
     page_table.map_data(&layout::stack_range(40 * PAGE_SIZE).into())?;
     page_table.map_code(&layout::text_range().into())?;
     page_table.map_rodata(&layout::rodata_range().into())?;
-    page_table.map_device(&layout::console_uart_range().into())?;
+    page_table.map_device(&layout::console_uart_page().into())?;
 
     Ok(page_table)
 }
@@ -205,7 +205,7 @@
 
     // No logging after unmapping UART.
     if let Some(mmio_guard) = get_mmio_guard() {
-        mmio_guard.unmap(vmbase::console::BASE_ADDRESS)?;
+        mmio_guard.unmap(UART_PAGE_ADDR)?;
     }
     // Unshares all memory and deactivates page table.
     drop(MEMORY.lock().take());
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 72b5433..210548a 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -27,6 +27,7 @@
 FECR_BOOT_COMPLETED_LOG="Have fun and send patches!"
 FECR_BOOT_TIMEOUT="300" # 5 minutes (300 seconds)
 ACTION_NAME="android.virtualization.VM_LAUNCHER"
+TRY_UNLOCK_MAX=10
 
 fecr_clean_up() {
   trap - INT
@@ -132,6 +133,35 @@
   adb push ${fecr_script_path}/assets/vm_config.json ${FECR_CONFIG_PATH}
 fi
 
+echo "Ensure screen unlocked"
+
+try_unlock=0
+while [[ "${try_unlock}" -le "${TRY_UNLOCK_MAX}" ]]; do
+  screen_state=$(adb shell dumpsys nfc | sed -n 's/^mScreenState=\(.*\)$/\1/p')
+  case "${screen_state}" in
+    "ON_UNLOCKED")
+      break
+      ;;
+    "ON_LOCKED")
+      # Disclaimer: This can unlock phone only if unlock method is swipe (default after FDR)
+      adb shell input keyevent KEYCODE_MENU
+      ;;
+    "OFF_LOCKED"|"OFF_UNLOCKED")
+      adb shell input keyevent KEYCODE_WAKEUP
+      ;;
+    *)
+      echo "Unknown screen state. Continue to boot, but may fail"
+      break
+      ;;
+  esac
+  sleep 1
+  try_unlock=$((try_unlock+1))
+done
+if [[ "${try_unlock}" -gt "${TRY_UNLOCK_MAX}" ]]; then
+  >&2 echo "Failed to unlock screen. Try again after manual unlock"
+  exit 1
+fi
+
 echo "Starting ferrochrome"
 adb shell am start-activity -a ${ACTION_NAME} > /dev/null
 
diff --git a/vmbase/README.md b/vmbase/README.md
index 280d7e1..28d930a 100644
--- a/vmbase/README.md
+++ b/vmbase/README.md
@@ -76,10 +76,10 @@
 must use the C ABI, and have the expected names. For example, to log sync exceptions and reboot:
 
 ```rust
-use vmbase::{console::emergency_write_str, power::reboot};
+use vmbase::power::reboot;
 
 extern "C" fn sync_exception_current() {
-    emergency_write_str("sync_exception_current\n");
+    eprintln!("sync_exception_current");
 
     let mut esr: u64;
     unsafe {
@@ -93,14 +93,9 @@
 
 The `println!` macro shouldn't be used in exception handlers, because it relies on a global instance
 of the UART driver which might be locked when the exception happens, which would result in deadlock.
-Instead you can use `emergency_write_str` and `eprintln!`, which will re-initialize the UART every
-time to ensure that it can be used. This should still be used with care, as it may interfere with
-whatever the rest of the program is doing with the UART.
-
-Note also that in some cases when the system is in a bad state resulting in the stack not working
-properly, `eprintln!` may hang. `emergency_write_str` may be more reliable as it seems to avoid
-any stack allocation. This is why the example above uses `emergency_write_str` first to ensure that
-at least something is logged, before trying `eprintln!` to print more details.
+Instead you can use `eprintln!`, which will re-initialize the UART every time to ensure that it can
+be used. This should still be used with care, as it may interfere with whatever the rest of the
+program is doing with the UART.
 
 See [example/src/exceptions.rs](examples/src/exceptions.rs) for a complete example.
 
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index 9fe3a4a..6ea8d60 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -14,8 +14,6 @@
 
 //! Low-level compatibility layer between baremetal Rust and Bionic C functions.
 
-use crate::console;
-use crate::eprintln;
 use crate::rand::fill_with_entropy;
 use crate::read_sysreg;
 use core::ffi::c_char;
@@ -27,6 +25,8 @@
 use core::str;
 
 use cstr::cstr;
+use log::error;
+use log::info;
 
 const EOF: c_int = -1;
 const EIO: c_int = 5;
@@ -119,7 +119,7 @@
 
     if let (Ok(prefix), Ok(format)) = (prefix.to_str(), format.to_str()) {
         // We don't bother with printf formatting.
-        eprintln!("FATAL BIONIC ERROR: {prefix}: \"{format}\" (unformatted)");
+        error!("FATAL BIONIC ERROR: {prefix}: \"{format}\" (unformatted)");
     }
 }
 
@@ -130,6 +130,23 @@
     Stderr = 0x9d118200,
 }
 
+impl File {
+    fn write_lines(&self, s: &str) {
+        for line in s.split_inclusive('\n') {
+            let (line, ellipsis) = if let Some(stripped) = line.strip_suffix('\n') {
+                (stripped, "")
+            } else {
+                (line, " ...")
+            };
+
+            match self {
+                Self::Stdout => info!("{line}{ellipsis}"),
+                Self::Stderr => error!("{line}{ellipsis}"),
+            }
+        }
+    }
+}
+
 impl TryFrom<usize> for File {
     type Error = &'static str;
 
@@ -152,8 +169,8 @@
     // SAFETY: Just like libc, we need to assume that `s` is a valid NULL-terminated string.
     let c_str = unsafe { CStr::from_ptr(c_str) };
 
-    if let (Ok(s), Ok(_)) = (c_str.to_str(), File::try_from(stream)) {
-        console::write_str(s);
+    if let (Ok(s), Ok(f)) = (c_str.to_str(), File::try_from(stream)) {
+        f.write_lines(s);
         0
     } else {
         set_errno(EOF);
@@ -168,8 +185,8 @@
     // SAFETY: Just like libc, we need to assume that `ptr` is valid.
     let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, length) };
 
-    if let (Ok(s), Ok(_)) = (str::from_utf8(bytes), File::try_from(stream)) {
-        console::write_str(s);
+    if let (Ok(s), Ok(f)) = (str::from_utf8(bytes), File::try_from(stream)) {
+        f.write_lines(s);
         length
     } else {
         0
@@ -198,9 +215,9 @@
     let error = cstr_error(get_errno()).to_str().unwrap();
 
     if let Some(prefix) = prefix {
-        eprintln!("{prefix}: {error}");
+        error!("{prefix}: {error}");
     } else {
-        eprintln!("{error}");
+        error!("{error}");
     }
 }
 
diff --git a/vmbase/src/console.rs b/vmbase/src/console.rs
index a7d37b4..bbbcb07 100644
--- a/vmbase/src/console.rs
+++ b/vmbase/src/console.rs
@@ -15,91 +15,111 @@
 //! Console driver for 8250 UART.
 
 use crate::uart::Uart;
-use core::fmt::{write, Arguments, Write};
+use core::{
+    cell::OnceCell,
+    fmt::{write, Arguments, Write},
+};
 use spin::mutex::SpinMutex;
 
-/// Base memory-mapped address of the primary UART device.
-pub const BASE_ADDRESS: usize = 0x3f8;
+// Arbitrary limit on the number of consoles that can be registered.
+//
+// Matches the UART count in crosvm.
+const MAX_CONSOLES: usize = 4;
 
-static CONSOLE: SpinMutex<Option<Uart>> = SpinMutex::new(None);
+static CONSOLES: [SpinMutex<Option<Uart>>; MAX_CONSOLES] =
+    [SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None)];
+static ADDRESSES: [SpinMutex<OnceCell<usize>>; MAX_CONSOLES] = [
+    SpinMutex::new(OnceCell::new()),
+    SpinMutex::new(OnceCell::new()),
+    SpinMutex::new(OnceCell::new()),
+    SpinMutex::new(OnceCell::new()),
+];
 
-/// Initialises a new instance of the UART driver and returns it.
-fn create() -> Uart {
-    // SAFETY: BASE_ADDRESS is the base of the MMIO region for a UART and is mapped as device
-    // memory.
-    unsafe { Uart::new(BASE_ADDRESS) }
-}
+/// Index of the console used by default for logging.
+pub const DEFAULT_CONSOLE_INDEX: usize = 0;
 
-/// Initialises the global instance of the UART driver. This must be called before using
-/// the `print!` and `println!` macros.
-pub fn init() {
-    let uart = create();
-    CONSOLE.lock().replace(uart);
-}
+/// Index of the console used by default for emergency logging.
+pub const DEFAULT_EMERGENCY_CONSOLE_INDEX: usize = DEFAULT_CONSOLE_INDEX;
 
-/// Writes a string to the console.
+/// Initialises the global instance(s) of the UART driver.
 ///
-/// Panics if [`init`] was not called first.
-pub(crate) fn write_str(s: &str) {
-    CONSOLE.lock().as_mut().unwrap().write_str(s).unwrap();
-}
-
-/// Writes a formatted string to the console.
+/// This must be called before using the `print!` and `println!` macros.
 ///
-/// Panics if [`init`] was not called first.
-pub(crate) fn write_args(format_args: Arguments) {
-    write(CONSOLE.lock().as_mut().unwrap(), format_args).unwrap();
+/// # Safety
+///
+/// This must be called once with the bases of UARTs, mapped as device memory and (if necessary)
+/// shared with the host as MMIO, to which no other references must be held.
+pub unsafe fn init(base_addresses: &[usize]) {
+    for (i, &base_address) in base_addresses.iter().enumerate() {
+        // Remember the valid address, for emergency console accesses.
+        ADDRESSES[i].lock().set(base_address).expect("console::init() called more than once");
+
+        // Initialize the console driver, for normal console accesses.
+        let mut console = CONSOLES[i].lock();
+        assert!(console.is_none(), "console::init() called more than once");
+        // SAFETY: base_address must be the base of a mapped UART.
+        console.replace(unsafe { Uart::new(base_address) });
+    }
 }
 
-/// Reinitializes the UART driver and writes a string to it.
+/// Writes a formatted string followed by a newline to the n-th console.
+///
+/// Panics if the n-th console was not initialized by calling [`init`] first.
+pub fn writeln(n: usize, format_args: Arguments) {
+    let mut guard = CONSOLES[n].lock();
+    let uart = guard.as_mut().unwrap();
+
+    write(uart, format_args).unwrap();
+    let _ = uart.write_str("\n");
+}
+
+/// Reinitializes the n-th UART driver and writes a formatted string followed by a newline to it.
 ///
 /// This is intended for use in situations where the UART may be in an unknown state or the global
 /// instance may be locked, such as in an exception handler or panic handler.
-pub fn emergency_write_str(s: &str) {
-    let mut uart = create();
-    let _ = uart.write_str(s);
-}
+pub fn ewriteln(n: usize, format_args: Arguments) {
+    let Some(cell) = ADDRESSES[n].try_lock() else { return };
+    let Some(addr) = cell.get() else { return };
 
-/// Reinitializes the UART driver and writes a formatted string to it.
-///
-/// This is intended for use in situations where the UART may be in an unknown state or the global
-/// instance may be locked, such as in an exception handler or panic handler.
-pub fn emergency_write_args(format_args: Arguments) {
-    let mut uart = create();
+    // SAFETY: addr contains the base of a mapped UART, passed in init().
+    let mut uart = unsafe { Uart::new(*addr) };
+
     let _ = write(&mut uart, format_args);
+    let _ = uart.write_str("\n");
 }
 
+/// Prints the given formatted string to the n-th console, followed by a newline.
+///
+/// Panics if the console has not yet been initialized. May hang if used in an exception context;
+/// use `eprintln!` instead.
+#[macro_export]
+macro_rules! console_writeln {
+    ($n:expr, $($arg:tt)*) => ({
+        $crate::console::writeln($n, format_args!($($arg)*))
+    })
+}
+
+pub(crate) use console_writeln;
+
 /// Prints the given formatted string to the console, followed by a newline.
 ///
 /// Panics if the console has not yet been initialized. May hang if used in an exception context;
 /// use `eprintln!` instead.
 macro_rules! println {
-    () => ($crate::console::write_str("\n"));
     ($($arg:tt)*) => ({
-        $crate::console::write_args(format_args!($($arg)*))};
-        $crate::console::write_str("\n");
-    );
+        $crate::console::console_writeln!($crate::console::DEFAULT_CONSOLE_INDEX, $($arg)*)
+    })
 }
 
 pub(crate) use println; // Make it available in this crate.
 
-/// Prints the given string to the console in an emergency, such as an exception handler.
-///
-/// Never panics.
-#[macro_export]
-macro_rules! eprint {
-    ($($arg:tt)*) => ($crate::console::emergency_write_args(format_args!($($arg)*)));
-}
-
 /// Prints the given string followed by a newline to the console in an emergency, such as an
 /// exception handler.
 ///
 /// Never panics.
 #[macro_export]
 macro_rules! eprintln {
-    () => ($crate::console::emergency_write_str("\n"));
     ($($arg:tt)*) => ({
-        $crate::console::emergency_write_args(format_args!($($arg)*))};
-        $crate::console::emergency_write_str("\n");
-    );
+        $crate::console::ewriteln($crate::console::DEFAULT_EMERGENCY_CONSOLE_INDEX, format_args!($($arg)*))
+    })
 }
diff --git a/vmbase/src/entry.rs b/vmbase/src/entry.rs
index bb5ccef..ad633ed 100644
--- a/vmbase/src/entry.rs
+++ b/vmbase/src/entry.rs
@@ -15,8 +15,10 @@
 //! Rust entry point.
 
 use crate::{
-    bionic, console, heap, hyp, logger,
-    memory::{page_4kb_of, SIZE_16KB, SIZE_4KB},
+    bionic, console, heap, hyp,
+    layout::{UART_ADDRESSES, UART_PAGE_ADDR},
+    logger,
+    memory::{SIZE_16KB, SIZE_4KB},
     power::{reboot, shutdown},
     rand,
 };
@@ -24,8 +26,6 @@
 use static_assertions::const_assert_eq;
 
 fn try_console_init() -> Result<(), hyp::Error> {
-    console::init();
-
     if let Some(mmio_guard) = hyp::get_mmio_guard() {
         mmio_guard.enroll()?;
 
@@ -43,10 +43,13 @@
             granule == SIZE_4KB || granule == SIZE_16KB
         });
         // Validate the assumption above by ensuring that the UART is not moved to another page:
-        const_assert_eq!(page_4kb_of(console::BASE_ADDRESS), 0);
-        mmio_guard.map(console::BASE_ADDRESS)?;
+        const_assert_eq!(UART_PAGE_ADDR, 0);
+        mmio_guard.map(UART_PAGE_ADDR)?;
     }
 
+    // SAFETY: UART_PAGE is mapped at stage-1 (see entry.S) and was just MMIO-guarded.
+    unsafe { console::init(&UART_ADDRESSES) };
+
     Ok(())
 }
 
diff --git a/vmbase/src/exceptions.rs b/vmbase/src/exceptions.rs
index 7833334..11fcd93 100644
--- a/vmbase/src/exceptions.rs
+++ b/vmbase/src/exceptions.rs
@@ -15,15 +15,14 @@
 //! Helper functions and structs for exception handlers.
 
 use crate::{
-    console, eprintln,
+    eprintln,
+    layout::UART_PAGE_ADDR,
     memory::{page_4kb_of, MemoryTrackerError},
     read_sysreg,
 };
 use aarch64_paging::paging::VirtualAddress;
 use core::fmt;
 
-const UART_PAGE: usize = page_4kb_of(console::BASE_ADDRESS);
-
 /// Represents an error that can occur while handling an exception.
 #[derive(Debug)]
 pub enum HandleExceptionError {
@@ -134,6 +133,6 @@
     }
 
     fn is_uart_exception(&self) -> bool {
-        self.esr == Esr::DataAbortSyncExternalAbort && page_4kb_of(self.far.0) == UART_PAGE
+        self.esr == Esr::DataAbortSyncExternalAbort && page_4kb_of(self.far.0) == UART_PAGE_ADDR
     }
 }
diff --git a/vmbase/src/layout.rs b/vmbase/src/layout.rs
index f7e8170..5ac435f 100644
--- a/vmbase/src/layout.rs
+++ b/vmbase/src/layout.rs
@@ -16,15 +16,28 @@
 
 pub mod crosvm;
 
-use crate::console::BASE_ADDRESS;
 use crate::linker::__stack_chk_guard;
+use crate::memory::{page_4kb_of, PAGE_SIZE};
 use aarch64_paging::paging::VirtualAddress;
 use core::ops::Range;
 use core::ptr::addr_of;
+use static_assertions::const_assert_eq;
 
 /// First address that can't be translated by a level 1 TTBR0_EL1.
 pub const MAX_VIRT_ADDR: usize = 1 << 40;
 
+/// Base memory-mapped addresses of the UART devices.
+///
+/// See SERIAL_ADDR in https://crosvm.dev/book/appendix/memory_layout.html#common-layout.
+pub const UART_ADDRESSES: [usize; 4] = [0x3f8, 0x2f8, 0x3e8, 0x2e8];
+
+/// Address of the single page containing all the UART devices.
+pub const UART_PAGE_ADDR: usize = 0;
+const_assert_eq!(UART_PAGE_ADDR, page_4kb_of(UART_ADDRESSES[0]));
+const_assert_eq!(UART_PAGE_ADDR, page_4kb_of(UART_ADDRESSES[1]));
+const_assert_eq!(UART_PAGE_ADDR, page_4kb_of(UART_ADDRESSES[2]));
+const_assert_eq!(UART_PAGE_ADDR, page_4kb_of(UART_ADDRESSES[3]));
+
 /// Get an address from a linker-defined symbol.
 #[macro_export]
 macro_rules! linker_addr {
@@ -86,11 +99,9 @@
     linker_region!(eh_stack_limit, bss_end)
 }
 
-/// UART console range.
-pub fn console_uart_range() -> Range<VirtualAddress> {
-    const CONSOLE_LEN: usize = 1; // `uart::Uart` only uses one u8 register.
-
-    VirtualAddress(BASE_ADDRESS)..VirtualAddress(BASE_ADDRESS + CONSOLE_LEN)
+/// Range of the page at UART_PAGE_ADDR of PAGE_SIZE.
+pub fn console_uart_page() -> Range<VirtualAddress> {
+    VirtualAddress(UART_PAGE_ADDR)..VirtualAddress(UART_PAGE_ADDR + PAGE_SIZE)
 }
 
 /// Read-write data (original).
diff --git a/vmbase/src/memory/shared.rs b/vmbase/src/memory/shared.rs
index 5a25d9f..d869b16 100644
--- a/vmbase/src/memory/shared.rs
+++ b/vmbase/src/memory/shared.rs
@@ -17,11 +17,11 @@
 use super::dbm::{flush_dirty_range, mark_dirty_block, set_dbm_enabled};
 use super::error::MemoryTrackerError;
 use super::page_table::{PageTable, MMIO_LAZY_MAP_FLAG};
-use super::util::{page_4kb_of, virt_to_phys};
-use crate::console;
+use super::util::virt_to_phys;
 use crate::dsb;
 use crate::exceptions::HandleExceptionError;
 use crate::hyp::{self, get_mem_sharer, get_mmio_guard};
+use crate::layout;
 use crate::util::unchecked_align_down;
 use crate::util::RangeExt as _;
 use aarch64_paging::paging::{
@@ -412,7 +412,7 @@
         let base = unchecked_align_down(phys, self.granule);
 
         // TODO(ptosi): Share the UART using this method and remove the hardcoded check.
-        if self.frames.contains(&base) || base == page_4kb_of(console::BASE_ADDRESS) {
+        if self.frames.contains(&base) || base == layout::UART_PAGE_ADDR {
             return Err(MemoryTrackerError::DuplicateMmioShare(base));
         }