Merge "If rollback_index is specified use it in the dice chain of pVM" into main
diff --git a/docs/mainline_module_payload.md b/docs/mainline_module_payload.md
index 84617f0..a7601f6 100644
--- a/docs/mainline_module_payload.md
+++ b/docs/mainline_module_payload.md
@@ -1,5 +1,7 @@
 # Delivery Microdroid pVM payload via Mainline modules
 
+Note: this feature is under development, use it with cauition!
+
 There are several additional challenges when a Microdroid pVM payload is
 delivered inside a Mainline module.
 
@@ -13,7 +15,20 @@
 To work around this challenge, payloads delivered via Mainline modules are
 expected to request
 `android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION` privileged
-permission.
+permission. Additionally they need to specify a
+`android.system.virtualmachine.ROLLBACK_INDEX` property in their manifest, e.g.:
 
-TODO(ioffe): add more context on how permission is used once the implementation
-is done.
+```xml
+<uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
+<application>
+  <property android:name="android.system.virtualmachine.ROLLBACK_INDEX" android:value="1" />
+</application>
+```
+
+If apk manifest has both permission and the property specified then the value of
+the `android.system.virtualmachine.ROLLBACK_INDEX` property is used by
+`microdroid_manager` when constructing the payload node of the dice chain.
+
+Please check the tests prefixed with `relaxedRollbackProtectionScheme` to get
+more context on the behaviour.
+
diff --git a/guest/microdroid_manager/src/dice.rs b/guest/microdroid_manager/src/dice.rs
index dd5375f..7952210 100644
--- a/guest/microdroid_manager/src/dice.rs
+++ b/guest/microdroid_manager/src/dice.rs
@@ -100,7 +100,10 @@
     fn for_apk(apk: &ApkData) -> Self {
         Self {
             name: format!("apk:{}", apk.package_name),
-            version: apk.version_code,
+            // Ideally we would want to log both rollback_index and apk version code in dice. There
+            // is even a separate field called security_version_code, but it looks like it is not
+            // used in subcomponents, so for now log the rollback index as version code.
+            version: apk.rollback_index.map(u64::from).unwrap_or(apk.version_code),
             code_hash: apk.root_hash.clone(),
             authority_hash: apk.cert_hash.clone(),
         }
diff --git a/guest/microdroid_manager/src/instance.rs b/guest/microdroid_manager/src/instance.rs
index d3a597a..a5f0d66 100644
--- a/guest/microdroid_manager/src/instance.rs
+++ b/guest/microdroid_manager/src/instance.rs
@@ -290,6 +290,7 @@
     pub cert_hash: Vec<u8>,
     pub package_name: String,
     pub version_code: u64,
+    pub rollback_index: Option<u32>,
 }
 
 impl ApkData {
diff --git a/guest/microdroid_manager/src/verify.rs b/guest/microdroid_manager/src/verify.rs
index ec8d66e..2d46b1f 100644
--- a/guest/microdroid_manager/src/verify.rs
+++ b/guest/microdroid_manager/src/verify.rs
@@ -203,6 +203,7 @@
         cert_hash,
         package_name: manifest_info.package,
         version_code: manifest_info.version_code,
+        rollback_index: manifest_info.rollback_index,
     })
 }
 
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 94f7ced..67249b4 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -538,6 +538,11 @@
     public BootResult tryBootVm(String logTag, String vmName)
             throws VirtualMachineException, InterruptedException {
         VirtualMachine vm = getVirtualMachineManager().get(vmName);
+        return tryBootVm(logTag, vm);
+    }
+
+    public BootResult tryBootVm(String logTag, VirtualMachine vm)
+            throws VirtualMachineException, InterruptedException {
         final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
         final CompletableFuture<Integer> deathReason = new CompletableFuture<>();
         final CompletableFuture<Long> endTime = new CompletableFuture<>();
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 859007a..d9f74dc 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -64,7 +64,9 @@
 
 DATA = [
     ":MicrodroidTestAppUpdated",
-    ":MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5",
+    ":MicrodroidTestHelperAppRelaxedRollbackProtection_V5",
+    ":MicrodroidTestHelperAppRelaxedRollbackProtection_V6",
+    ":MicrodroidTestHelperAppRelaxedRollbackProtection_V7_inc_rollback_version",
     ":MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission",
     ":MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index",
     ":MicrodroidVmShareApp",
@@ -73,7 +75,7 @@
 ]
 
 android_test_helper_app {
-    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5",
+    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_V5",
     defaults: ["MicrodroidTestAppsDefaults"],
     manifest: "AndroidManifestV5_relaxed_rollback_protection.xml",
     jni_libs: [
@@ -84,6 +86,28 @@
 }
 
 android_test_helper_app {
+    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_V6",
+    defaults: ["MicrodroidTestAppsDefaults"],
+    manifest: "AndroidManifestV6_relaxed_rollback_protection.xml",
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+        "MicrodroidTestNativeLibWithLibIcu",
+    ],
+    min_sdk_version: "33",
+}
+
+android_test_helper_app {
+    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_V7_inc_rollback_version",
+    defaults: ["MicrodroidTestAppsDefaults"],
+    manifest: "AndroidManifestV7_relaxed_rollback_protection_inc_rollback_version.xml",
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+        "MicrodroidTestNativeLibWithLibIcu",
+    ],
+    min_sdk_version: "33",
+}
+
+android_test_helper_app {
     name: "MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index",
     defaults: ["MicrodroidTestAppsDefaults"],
     manifest: "AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml",
diff --git a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml
index 91de2a0..c165a2d 100644
--- a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml
+++ b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+<!-- Copyright (C) 2025 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.
diff --git a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml
index 3d6d734..432da67 100644
--- a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml
+++ b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+<!-- Copyright (C) 2025 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.
diff --git a/tests/testapk/AndroidManifestV6_relaxed_rollback_protection.xml b/tests/testapk/AndroidManifestV6_relaxed_rollback_protection.xml
new file mode 100644
index 0000000..3cc4dd9
--- /dev/null
+++ b/tests/testapk/AndroidManifestV6_relaxed_rollback_protection.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_relaxed_rollback_protection_scheme"
+      android:versionCode="6" >
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application>
+        <property android:name="android.system.virtualmachine.ROLLBACK_INDEX" android:value="1" />
+    </application>
+</manifest>
diff --git a/tests/testapk/AndroidManifestV7_relaxed_rollback_protection_inc_rollback_version.xml b/tests/testapk/AndroidManifestV7_relaxed_rollback_protection_inc_rollback_version.xml
new file mode 100644
index 0000000..40be01a
--- /dev/null
+++ b/tests/testapk/AndroidManifestV7_relaxed_rollback_protection_inc_rollback_version.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_relaxed_rollback_protection_scheme"
+      android:versionCode="7" >
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application>
+        <property android:name="android.system.virtualmachine.ROLLBACK_INDEX" android:value="2" />
+    </application>
+</manifest>
diff --git a/tests/testapk/AndroidTestTemplate.xml b/tests/testapk/AndroidTestTemplate.xml
index 64ddbbe..78d8b15 100644
--- a/tests/testapk/AndroidTestTemplate.xml
+++ b/tests/testapk/AndroidTestTemplate.xml
@@ -30,7 +30,9 @@
         <option name="cleanup" value="true" />
         <option name="push" value="test_microdroid_vendor_image.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img" />
         <option name="push" value="test_microdroid_vendor_image_unsigned.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image_unsigned.img" />
-        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_V5.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_V5.apk" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_V6.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_V6.apk" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_V7_inc_rollback_version.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_V7_inc_rollback_version.apk" />
         <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission.apk" />
         <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index.apk" />
     </target_preparer>
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 521ee9a..e4a3ff6 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -2861,6 +2861,116 @@
                         VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED);
     }
 
+    @Test
+    public void relaxedRollbackProtectionScheme_rollbackVersionDoesNotChange() throws Exception {
+        assumeSupportedDevice();
+        // Relaxed rollback protection scheme only makes sense if VM updates are supported.
+        assumeTrue("Missing Updatable VM support", isUpdatableVmSupported());
+
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_V6.apk");
+
+        Context testHelperAppCtx =
+                getContext()
+                        .createPackageContext(
+                                RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(testHelperAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setProtectedVm(isProtectedVm())
+                        .setOs(os())
+                        .setEncryptedStorageBytes(1 * 1024 * 1024)
+                        .build();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("test_rollback_version_does_not_change", config);
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            ts.writeToFile(
+                                    /* content= */ EXAMPLE_STRING,
+                                    /* path= */ "/mnt/encryptedstore/test_file");
+                        });
+        testResults.assertNoException();
+
+        // Simulate a rollback by installing a downgraded version of the helper apk.
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_V5.apk", "-d");
+
+        testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
+    }
+
+    @Test
+    public void relaxedRollbackProtectionScheme_rollbackVersionChanges() throws Exception {
+        assumeSupportedDevice();
+        // Relaxed rollback protection scheme only makes sense if VM updates are supported.
+        assumeTrue("Missing Updatable VM support", isUpdatableVmSupported());
+        assumeProtectedVM();
+
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_V5.apk");
+
+        Context testHelperAppCtx =
+                getContext()
+                        .createPackageContext(
+                                RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(testHelperAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setProtectedVm(isProtectedVm())
+                        .setOs(os())
+                        .setEncryptedStorageBytes(1 * 1024 * 1024)
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_rollback_version_changes", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            ts.writeToFile(
+                                    /* content= */ EXAMPLE_STRING,
+                                    /* path= */ "/mnt/encryptedstore/test_file");
+                        });
+        testResults.assertNoException();
+
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_V7_inc_rollback_version.apk");
+
+        testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
+
+        assertThat(vm.getStatus()).isEqualTo(VirtualMachine.STATUS_STOPPED);
+
+        // Simulate a rollback by installing a downgraded version of the helper apk.
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_V6.apk", "-d");
+
+        // Now pVM shouldn't boot.
+        BootResult bootResult = tryBootVm(TAG, vm);
+        assertThat(bootResult.deathReason)
+                .isEqualTo(
+                        // TODO(ioffe): this should probably be payload verification error?
+                        VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR);
+    }
+
     private static class VmShareServiceConnection implements ServiceConnection {
 
         private final CountDownLatch mLatch = new CountDownLatch(1);
@@ -2956,14 +3066,15 @@
         assertThat(e).hasMessageThat().contains(expectedContents);
     }
 
-    private void installApp(String apkName) throws Exception {
+    private void installApp(String apkName, String... additionalArgs) throws Exception {
         String apkFile = new File("/data/local/tmp/cts/microdroid/", apkName).getAbsolutePath();
         UiAutomation uai = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         Log.i(TAG, "Installing apk " + apkFile);
         // We read the output of the shell command not only to see if it succeeds, but also to make
         // sure that the installation finishes. This avoids a race condition when test tries to
         // create a context of the installed package before the installation finished.
-        try (ParcelFileDescriptor pfd = uai.executeShellCommand("pm install " + apkFile)) {
+        String installCmd = "pm install " + String.join(" ", additionalArgs) + " " + apkFile;
+        try (ParcelFileDescriptor pfd = uai.executeShellCommand(installCmd)) {
             try (InputStream is = new FileInputStream(pfd.getFileDescriptor())) {
                 try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
                     String line;