pvmfw: Support com.android.virt.page_size

Replace the assumption that guests run with 4KiB page tables by adding
support for a new VBMeta descriptor property describing the size in use.
When absent, default to the previous assumption of 4KiB.

Set the property for the Micrdroid kernels with 16KiB PAGE_SIZE.

Add test coverage & document this in the README.

Bug: 339779843
Bug: 339782511
Test: atest libpvmfw_avb.integration_test
Change-Id: Ib3c2b87fd507046578cc95d892d01fa9b04bc5c7
diff --git a/build/microdroid/Android.bp b/build/microdroid/Android.bp
index 7f23ae6..20980e0 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -560,6 +560,12 @@
             src: ":microdroid_kernel_prebuilt-x86_64",
         },
     },
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "16",
+        },
+    ],
     include_descriptors_from_images: [
         ":microdroid_16k_initrd_normal_hashdesc",
         ":microdroid_16k_initrd_debug_hashdesc",
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 8c8314d..766a923 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -461,6 +461,7 @@
   - `secretkeeper_protection`: pvmfw defers rollback protection to the guest
   - `supports_uefi_boot`: pvmfw boots the VM as a EFI payload (experimental)
   - `trusty_security_vm`: pvmfw skips rollback protection
+- `"com.android.virt.page_size"`: the guest page size in KiB (optional, defaults to 4)
 
 ## Development
 
diff --git a/guest/pvmfw/avb/Android.bp b/guest/pvmfw/avb/Android.bp
index 10c7841..0294322 100644
--- a/guest/pvmfw/avb/Android.bp
+++ b/guest/pvmfw/avb/Android.bp
@@ -37,6 +37,14 @@
         ":test_image_with_one_hashdesc",
         ":test_image_with_non_initrd_hashdesc",
         ":test_image_with_initrd_and_non_initrd_desc",
+        ":test_image_with_invalid_page_size",
+        ":test_image_with_negative_page_size",
+        ":test_image_with_overflow_page_size",
+        ":test_image_with_0k_page_size",
+        ":test_image_with_1k_page_size",
+        ":test_image_with_4k_page_size",
+        ":test_image_with_9k_page_size",
+        ":test_image_with_16k_page_size",
         ":test_image_with_service_vm_prop",
         ":test_image_with_unknown_vm_type_prop",
         ":test_image_with_duplicated_capability",
@@ -115,6 +123,118 @@
 }
 
 avb_add_hash_footer {
+    name: "test_image_with_invalid_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "invalid",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_negative_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "-16",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_overflow_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "18014398509481983",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_0k_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "0",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_1k_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "1",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_4k_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "4",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_9k_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "9",
+        },
+    ],
+}
+
+avb_add_hash_footer {
+    name: "test_image_with_16k_page_size",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "2134",
+    props: [
+        {
+            name: "com.android.virt.page_size",
+            value: "16",
+        },
+    ],
+}
+
+avb_add_hash_footer {
     name: "test_image_with_service_vm_prop",
     src: ":unsigned_test_image",
     partition_name: "boot",
diff --git a/guest/pvmfw/avb/src/error.rs b/guest/pvmfw/avb/src/error.rs
index 2e1950a..1307e15 100644
--- a/guest/pvmfw/avb/src/error.rs
+++ b/guest/pvmfw/avb/src/error.rs
@@ -28,6 +28,8 @@
     InvalidDescriptors(DescriptorError),
     /// Unknown vbmeta property.
     UnknownVbmetaProperty,
+    /// VBMeta has invalid page_size property.
+    InvalidPageSize,
 }
 
 impl From<SlotVerifyError<'_>> for PvmfwVerifyError {
@@ -51,6 +53,7 @@
                 write!(f, "VBMeta has invalid descriptors. Error: {:?}", e)
             }
             Self::UnknownVbmetaProperty => write!(f, "Unknown vbmeta property"),
+            Self::InvalidPageSize => write!(f, "Invalid page_size property"),
         }
     }
 }
diff --git a/guest/pvmfw/avb/src/verify.rs b/guest/pvmfw/avb/src/verify.rs
index 2a7eed2..8810696 100644
--- a/guest/pvmfw/avb/src/verify.rs
+++ b/guest/pvmfw/avb/src/verify.rs
@@ -22,6 +22,7 @@
     Descriptor, DescriptorError, DescriptorResult, HashDescriptor, PartitionData, SlotVerifyError,
     SlotVerifyNoDataResult, VbmetaData,
 };
+use core::str;
 
 // We use this for the rollback_index field if SlotVerifyData has empty rollback_indexes
 const DEFAULT_ROLLBACK_INDEX: u64 = 0;
@@ -220,6 +221,23 @@
     Ok(digest)
 }
 
+/// Returns the indicated payload page size, if present.
+fn read_page_size(vbmeta_data: &VbmetaData) -> Result<Option<usize>, PvmfwVerifyError> {
+    let Some(property) = vbmeta_data.get_property_value("com.android.virt.page_size") else {
+        return Ok(None);
+    };
+    let size = str::from_utf8(property)
+        .or(Err(PvmfwVerifyError::InvalidPageSize))?
+        .parse::<usize>()
+        .or(Err(PvmfwVerifyError::InvalidPageSize))?
+        .checked_mul(1024)
+        // TODO(stable(unsigned_is_multiple_of)): use .is_multiple_of()
+        .filter(|sz| sz % (4 << 10) == 0 && *sz != 0)
+        .ok_or(PvmfwVerifyError::InvalidPageSize)?;
+
+    Ok(Some(size))
+}
+
 /// Verifies the given initrd partition, and checks that the resulting contents looks like expected.
 fn verify_initrd(
     ops: &mut Ops,
@@ -256,7 +274,7 @@
     let descriptors = vbmeta_image.descriptors()?;
     let hash_descriptors = HashDescriptors::get(&descriptors)?;
     let capabilities = Capability::get_capabilities(vbmeta_image)?;
-    let page_size = None; // TODO(ptosi): Read from payload.
+    let page_size = read_page_size(vbmeta_image)?;
 
     if initrd.is_none() {
         hash_descriptors.verify_no_initrd()?;
diff --git a/guest/pvmfw/avb/tests/api_test.rs b/guest/pvmfw/avb/tests/api_test.rs
index 1a4e247..0ed0279 100644
--- a/guest/pvmfw/avb/tests/api_test.rs
+++ b/guest/pvmfw/avb/tests/api_test.rs
@@ -28,6 +28,14 @@
 use utils::*;
 
 const TEST_IMG_WITH_ONE_HASHDESC_PATH: &str = "test_image_with_one_hashdesc.img";
+const TEST_IMG_WITH_INVALID_PAGE_SIZE_PATH: &str = "test_image_with_invalid_page_size.img";
+const TEST_IMG_WITH_NEGATIVE_PAGE_SIZE_PATH: &str = "test_image_with_negative_page_size.img";
+const TEST_IMG_WITH_OVERFLOW_PAGE_SIZE_PATH: &str = "test_image_with_overflow_page_size.img";
+const TEST_IMG_WITH_0K_PAGE_SIZE_PATH: &str = "test_image_with_0k_page_size.img";
+const TEST_IMG_WITH_1K_PAGE_SIZE_PATH: &str = "test_image_with_1k_page_size.img";
+const TEST_IMG_WITH_4K_PAGE_SIZE_PATH: &str = "test_image_with_4k_page_size.img";
+const TEST_IMG_WITH_9K_PAGE_SIZE_PATH: &str = "test_image_with_9k_page_size.img";
+const TEST_IMG_WITH_16K_PAGE_SIZE_PATH: &str = "test_image_with_16k_page_size.img";
 const TEST_IMG_WITH_ROLLBACK_INDEX_5: &str = "test_image_with_rollback_index_5.img";
 const TEST_IMG_WITH_SERVICE_VM_PROP_PATH: &str = "test_image_with_service_vm_prop.img";
 const TEST_IMG_WITH_UNKNOWN_VM_TYPE_PROP_PATH: &str = "test_image_with_unknown_vm_type_prop.img";
@@ -240,6 +248,60 @@
 }
 
 #[test]
+fn kernel_has_expected_page_size_invalid() {
+    let kernel = fs::read(TEST_IMG_WITH_INVALID_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_negative() {
+    let kernel = fs::read(TEST_IMG_WITH_NEGATIVE_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_overflow() {
+    let kernel = fs::read(TEST_IMG_WITH_OVERFLOW_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_none() {
+    let kernel = fs::read(TEST_IMG_WITH_ONE_HASHDESC_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Ok(None));
+}
+
+#[test]
+fn kernel_has_expected_page_size_0k() {
+    let kernel = fs::read(TEST_IMG_WITH_0K_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_1k() {
+    let kernel = fs::read(TEST_IMG_WITH_1K_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_4k() {
+    let kernel = fs::read(TEST_IMG_WITH_4K_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Ok(Some(4usize << 10)));
+}
+
+#[test]
+fn kernel_has_expected_page_size_9k() {
+    let kernel = fs::read(TEST_IMG_WITH_9K_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Err(PvmfwVerifyError::InvalidPageSize));
+}
+
+#[test]
+fn kernel_has_expected_page_size_16k() {
+    let kernel = fs::read(TEST_IMG_WITH_16K_PAGE_SIZE_PATH).unwrap();
+    assert_eq!(read_page_size(&kernel), Ok(Some(16usize << 10)));
+}
+
+#[test]
 fn kernel_footer_with_vbmeta_offset_overwritten_fails_verification() -> Result<()> {
     // Arrange.
     let mut kernel = load_latest_signed_kernel()?;
diff --git a/guest/pvmfw/avb/tests/utils.rs b/guest/pvmfw/avb/tests/utils.rs
index 86efbba..79552b5 100644
--- a/guest/pvmfw/avb/tests/utils.rs
+++ b/guest/pvmfw/avb/tests/utils.rs
@@ -173,6 +173,16 @@
     Ok(())
 }
 
+pub fn read_page_size(kernel: &[u8]) -> Result<Option<usize>, PvmfwVerifyError> {
+    let public_key = load_trusted_public_key().unwrap();
+    let verified_boot_data = verify_payload(
+        kernel,
+        None, // initrd
+        &public_key,
+    )?;
+    Ok(verified_boot_data.page_size)
+}
+
 pub fn hash(inputs: &[&[u8]]) -> Digest {
     let mut digester = sha::Sha256::new();
     inputs.iter().for_each(|input| digester.update(input));