Merge tag 'android-security-13.0.0_r8' into int/13/fp3

Android Security 13.0.0 Release 8 (10286630)

* tag 'android-security-13.0.0_r8':
  Set avb_hash_algorithm=sha256 for system & vendor img

Change-Id: Ibe61f576819644274f0ff4440c7cbe5e3b085527
diff --git a/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb b/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb
index ea17dfc..d54adb6 100644
--- a/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb
+++ b/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "8597076"
+    build_id: "9106195"
     target: "u-boot_pvmfw"
     source_file: "pvmfw.img"
   }
@@ -8,5 +8,6 @@
   version: ""
   version_group: ""
   git_project: "platform/packages/modules/Virtualization"
-  git_branch: "master"
+  git_branch: "tm-qpr-dev"
+  transform: TRANSFORM_NONE
 }
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 0e660f1..b18fbae 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -125,6 +125,11 @@
         ITestDevice androidDevice = testInfo.getDevice();
         sAndroid = new CommandRunner(androidDevice);
 
+        if (isCuttlefish(androidDevice)) {
+            sAssumptionFailed = true;
+            return;
+        }
+
         try {
             testIfDeviceIsCapable(androidDevice);
         } catch (AssumptionViolatedException e) {
diff --git a/pvmfw/pvmfw.img b/pvmfw/pvmfw.img
index 9afb092..12e4c70 100644
--- a/pvmfw/pvmfw.img
+++ b/pvmfw/pvmfw.img
Binary files differ
diff --git a/tests/Android.bp b/tests/Android.bp
index 2c36a62..71a73f7 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -90,11 +90,3 @@
     ],
     type: "cpio",
 }
-
-genrule {
-    name: "test-payload-metadata",
-    tools: ["mk_payload"],
-    cmd: "$(location mk_payload) --metadata-only $(in) $(out)",
-    srcs: ["test-payload-metadata-config.json"],
-    out: ["test-payload-metadata.img"],
-}
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index dfc2f2b..a7852d0 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -21,9 +21,6 @@
         ":microdroid_general_sepolicy.conf",
         ":test.com.android.virt.pem",
         ":test2.com.android.virt.pem",
-        ":test-payload-metadata",
-        ":com.android.adbd{.apex}",
-        ":com.android.os.statsd{.apex}",
     ],
     data_native_bins: [
         "sepolicy-analyze",
@@ -32,6 +29,7 @@
         "img2simg",
         "lpmake",
         "lpunpack",
+        "mk_payload",
         "sign_virt_apex",
         "simg2img",
     ],
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 440ae18..8e2d0ee 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -399,4 +399,17 @@
         // Check if it actually booted by reading a sysprop.
         assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
     }
+
+    protected boolean isCuttlefish() throws Exception {
+        return isCuttlefish(getDevice());
+    }
+
+    protected static boolean isCuttlefish(ITestDevice device) throws Exception {
+        String productName = device.getProperty("ro.product.name");
+        return (null != productName)
+                && (productName.startsWith("aosp_cf_x86")
+                        || productName.startsWith("aosp_cf_arm")
+                        || productName.startsWith("cf_x86")
+                        || productName.startsWith("cf_arm"));
+    }
 }
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 67f48a3..bcf0a0d 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -28,6 +28,8 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
+import static java.util.stream.Collectors.toList;
+
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
@@ -37,9 +39,9 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.xml.AbstractXmlParser;
 
 import org.json.JSONArray;
-import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
@@ -47,9 +49,11 @@
 import org.junit.Test;
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
+import org.xml.sax.Attributes;
+import org.xml.sax.helpers.DefaultHandler;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -73,16 +77,6 @@
     @Rule public TestLogData mTestLogs = new TestLogData();
     @Rule public TestName mTestName = new TestName();
 
-    // TODO(b/176805428): remove this
-    private boolean isCuttlefish() throws Exception {
-        String productName = getDevice().getProperty("ro.product.name");
-        return (null != productName)
-                && (productName.startsWith("aosp_cf_x86")
-                        || productName.startsWith("aosp_cf_arm")
-                        || productName.startsWith("cf_x86")
-                        || productName.startsWith("cf_arm"));
-    }
-
     private int minMemorySize() throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(getDevice());
         String abi = android.run("getprop", "ro.product.cpu.abi");
@@ -133,6 +127,47 @@
         return new JSONObject(Map.of("label", label, "path", path));
     }
 
+    private void createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)
+            throws Exception {
+        // mk_payload's config
+        File configFile = new File(payloadMetadata.getParentFile(), "payload_config.json");
+        JSONObject config = new JSONObject();
+        config.put("apk",
+                new JSONObject(Map.of("name", "microdroid-apk", "path", "", "idsig_path", "")));
+        config.put("payload_config_path", "/mnt/apk/assets/vm_config.json");
+        config.put("apexes",
+                new JSONArray(
+                        apexes.stream()
+                            .map(apex -> new JSONObject(Map.of("name", apex.name, "path", "")))
+                            .collect(toList())));
+        FileUtil.writeToFile(config.toString(), configFile);
+
+        File mkPayload = findTestFile("mk_payload");
+        RunUtil runUtil = new RunUtil();
+        // Set the parent dir on the PATH (e.g. <workdir>/bin)
+        String separator = System.getProperty("path.separator");
+        String path = mkPayload.getParentFile().getPath() + separator + System.getenv("PATH");
+        runUtil.setEnvVariable("PATH", path);
+
+        List<String> command = new ArrayList<String>();
+        command.add("mk_payload");
+        command.add("--metadata-only");
+        command.add(configFile.toString());
+        command.add(payloadMetadata.toString());
+
+        CommandResult result = runUtil.runTimedCmd(
+                                    // mk_payload should run fast enough
+                                    5 * 1000,
+                                    "/bin/bash",
+                                    "-c",
+                                    String.join(" ", command));
+        String out = result.getStdout();
+        String err = result.getStderr();
+        assertEquals(
+                "creating payload metadata failed:\n\tout: " + out + "\n\terr: " + err + "\n",
+                CommandStatus.SUCCESS, result.getStatus());
+    }
+
     private void resignVirtApex(File virtApexDir, File signingKey, Map<String, File> keyOverrides) {
         File signVirtApex = findTestFile("sign_virt_apex");
 
@@ -183,9 +218,62 @@
         }
     }
 
+    static class ActiveApexInfo {
+        public String name;
+        public String path;
+        public boolean provideSharedApexLibs;
+        ActiveApexInfo(String name, String path, boolean provideSharedApexLibs) {
+            this.name = name;
+            this.path = path;
+            this.provideSharedApexLibs = provideSharedApexLibs;
+        }
+    }
+
+    static class ActiveApexInfoList {
+        private List<ActiveApexInfo> mList;
+        ActiveApexInfoList(List<ActiveApexInfo> list) {
+            this.mList = list;
+        }
+        ActiveApexInfo get(String apexName) {
+            for (ActiveApexInfo info: mList) {
+                if (info.name.equals(apexName)) {
+                    return info;
+                }
+            }
+            return null;
+        }
+        List<ActiveApexInfo> getSharedLibApexes() {
+            return mList.stream().filter(info -> info.provideSharedApexLibs).collect(toList());
+        }
+    }
+
+    private ActiveApexInfoList getActiveApexInfoList() throws Exception {
+        String apexInfoListXml = getDevice().pullFileContents("/apex/apex-info-list.xml");
+        List<ActiveApexInfo> list = new ArrayList<>();
+        new AbstractXmlParser() {
+            @Override
+            protected DefaultHandler createXmlHandler() {
+                return new DefaultHandler() {
+                    @Override
+                    public void startElement(String uri, String localName, String qName,
+                            Attributes attributes) {
+                        if (localName.equals("apex-info")
+                                && attributes.getValue("isActive").equals("true")) {
+                            String name = attributes.getValue("moduleName");
+                            String path = attributes.getValue("modulePath");
+                            String sharedApex = attributes.getValue("provideSharedApexLibs");
+                            list.add(new ActiveApexInfo(name, path, "true".equals(sharedApex)));
+                        }
+                    }
+                };
+            }
+        }.parse(new ByteArrayInputStream(apexInfoListXml.getBytes()));
+        return new ActiveApexInfoList(list);
+    }
+
     private String runMicrodroidWithResignedImages(File key, Map<String, File> keyOverrides,
             boolean isProtected, boolean daemonize, String consolePath)
-            throws DeviceNotAvailableException, IOException, JSONException {
+            throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
 
         File virtApexDir = FileUtil.createTempDir("virt_apex");
@@ -211,15 +299,11 @@
         android.run(VIRT_APEX + "bin/vm", "create-partition", "--type instance",
                 instanceImgPath, Integer.toString(10 * 1024 * 1024));
 
-        // payload-metadata is prepared on host with the two APEXes and APK
+        // payload-metadata is created on device
         final String payloadMetadataPath = TEST_ROOT + "payload-metadata.img";
-        getDevice().pushFile(findTestFile("test-payload-metadata.img"), payloadMetadataPath);
 
-        // push APEXes required for the VM.
-        final String statsdApexPath = TEST_ROOT + "com.android.os.statsd.apex";
-        final String adbdApexPath = TEST_ROOT + "com.android.adbd.apex";
-        getDevice().pushFile(findTestFile("com.android.os.statsd.apex"), statsdApexPath);
-        getDevice().pushFile(findTestFile("com.android.adbd.apex"), adbdApexPath);
+        // Load /apex/apex-info-list.xml to get paths to APEXes required for the VM.
+        ActiveApexInfoList list = getActiveApexInfoList();
 
         // Since Java APP can't start a VM with a custom image, here, we start a VM using `vm run`
         // command with a VM Raw config which is equiv. to what virtualizationservice creates with
@@ -259,14 +343,28 @@
 
         // Add payload image disk with partitions:
         // - payload-metadata
-        // - apexes: com.android.os.statsd, com.android.adbd
+        // - apexes: com.android.os.statsd, com.android.adbd, [sharedlib apex](optional)
         // - apk and idsig
-        disks.put(new JSONObject().put("writable", false).put("partitions", new JSONArray()
-                .put(newPartition("payload-metadata", payloadMetadataPath))
-                .put(newPartition("microdroid-apex-0", statsdApexPath))
-                .put(newPartition("microdroid-apex-1", adbdApexPath))
+        List<ActiveApexInfo> apexesForVm = new ArrayList<>();
+        apexesForVm.add(list.get("com.android.os.statsd"));
+        apexesForVm.add(list.get("com.android.adbd"));
+        apexesForVm.addAll(list.getSharedLibApexes());
+
+        final JSONArray partitions = new JSONArray();
+        partitions.put(newPartition("payload-metadata", payloadMetadataPath));
+        int apexIndex = 0;
+        for (ActiveApexInfo apex : apexesForVm) {
+            partitions.put(
+                    newPartition(String.format("microdroid-apex-%d", apexIndex++), apex.path));
+        }
+        partitions
                 .put(newPartition("microdroid-apk", apkPath))
-                .put(newPartition("microdroid-apk-idsig", idSigPath))));
+                .put(newPartition("microdroid-apk-idsig", idSigPath));
+        disks.put(new JSONObject().put("writable", false).put("partitions", partitions));
+
+        final File localPayloadMetadata = new File(virtApexDir, "payload-metadata.img");
+        createPayloadMetadata(apexesForVm, localPayloadMetadata);
+        getDevice().pushFile(localPayloadMetadata, payloadMetadataPath);
 
         config.put("protected", isProtected);
 
diff --git a/tests/test-payload-metadata-config.json b/tests/test-payload-metadata-config.json
deleted file mode 100644
index 3c56e5f..0000000
--- a/tests/test-payload-metadata-config.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "_comment": "This file is to create a payload-metadata partition for payload.img which is for MicrodroidTestApp to run with assets/vm_config.json",
-  "apexes": [
-    {
-      "name": "com.android.os.statsd",
-      "path": ""
-    },
-    {
-      "name": "com.android.adbd",
-      "path": ""
-    }
-  ],
-  "apk": {
-    "name": "microdroid-apk",
-    "path": "",
-    "idsig_path": ""
-  },
-  "payload_config_path": "/mnt/apk/assets/vm_config.json"
-}
\ No newline at end of file
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 f7261d3..df26ebe 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -71,6 +71,7 @@
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
 
     private static final String KERNEL_VERSION = SystemProperties.get("ro.kernel.version");
+    private static final String PRODUCT_NAME = SystemProperties.get("ro.product.name");
 
     private static class Inner {
         public boolean mProtectedVm;
@@ -143,6 +144,14 @@
         mInner.mVm.delete();
     }
 
+    private boolean isCuttlefish() {
+        return (null != PRODUCT_NAME)
+               && (PRODUCT_NAME.startsWith("aosp_cf_x86")
+                       || PRODUCT_NAME.startsWith("aosp_cf_arm")
+                       || PRODUCT_NAME.startsWith("cf_x86")
+                       || PRODUCT_NAME.startsWith("cf_arm"));
+    }
+
     private abstract static class VmEventListener implements VirtualMachineCallback {
         private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
 
@@ -361,6 +370,7 @@
             .withMessage("SKip on 5.4 kernel. b/218303240")
             .that(KERNEL_VERSION)
             .isNotEqualTo("5.4");
+        assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
 
         VmCdis first_boot_cdis = launchVmAndGetCdis("test_vm");
         VmCdis second_boot_cdis = launchVmAndGetCdis("test_vm");
@@ -509,6 +519,8 @@
     @Test
     public void bootFailsWhenMicrodroidDataIsCompromised()
             throws VirtualMachineException, InterruptedException, IOException {
+        assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
+
         assertThatBootFailsAfterCompromisingPartition(MICRODROID_PARTITION_UUID);
     }
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 7a8da96..8d27325 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -67,5 +67,8 @@
 rust_test {
     name: "virtualizationservice_device_test",
     defaults: ["virtualizationservice_defaults"],
+    rustlibs: [
+        "libtempfile",
+    ],
     test_suites: ["general-tests"],
 }
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index 7b8cb7f..cd80efd 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -26,7 +26,9 @@
 use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
 use once_cell::sync::OnceCell;
-use packagemanager_aidl::aidl::android::content::pm::IPackageManagerNative::IPackageManagerNative;
+use packagemanager_aidl::aidl::android::content::pm::{
+    IPackageManagerNative::IPackageManagerNative, StagedApexInfo::StagedApexInfo,
+};
 use regex::Regex;
 use serde::Deserialize;
 use serde_xml_rs::from_reader;
@@ -47,7 +49,7 @@
 const PACKAGE_MANAGER_NATIVE_SERVICE: &str = "package_native";
 
 /// Represents the list of APEXes
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
 struct ApexInfoList {
     #[serde(rename = "apex-info")]
     list: Vec<ApexInfo>,
@@ -57,6 +59,8 @@
 struct ApexInfo {
     #[serde(rename = "moduleName")]
     name: String,
+    #[serde(rename = "versionCode")]
+    version: u64,
     #[serde(rename = "modulePath")]
     path: PathBuf,
 
@@ -100,6 +104,40 @@
             Ok(apex_info_list)
         })
     }
+
+    // Override apex info with the staged one
+    fn override_staged_apex(&mut self, staged_apex_info: &StagedApexInfo) -> Result<()> {
+        let mut need_to_add: Option<ApexInfo> = None;
+        for apex_info in self.list.iter_mut() {
+            if staged_apex_info.moduleName == apex_info.name {
+                if apex_info.is_active && apex_info.is_factory {
+                    // Copy the entry to the end as factory/non-active after the loop
+                    // to keep the factory version. Typically this step is unncessary,
+                    // but some apexes (like sharedlibs) need to be kept even if it's inactive.
+                    need_to_add.replace(ApexInfo { is_active: false, ..apex_info.clone() });
+                    // And make this one as non-factory. Note that this one is still active
+                    // and overridden right below.
+                    apex_info.is_factory = false;
+                }
+                // Active one is overridden with the staged one.
+                if apex_info.is_active {
+                    apex_info.version = staged_apex_info.versionCode as u64;
+                    apex_info.path = PathBuf::from(&staged_apex_info.diskImagePath);
+                    apex_info.has_classpath_jar = staged_apex_info.hasClassPathJars;
+                    apex_info.last_update_seconds = last_updated(&apex_info.path)?;
+                }
+            }
+        }
+        if let Some(info) = need_to_add {
+            self.list.push(info);
+        }
+        Ok(())
+    }
+}
+
+fn last_updated<P: AsRef<Path>>(path: P) -> Result<u64> {
+    let metadata = metadata(path)?;
+    Ok(metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs())
 }
 
 impl ApexInfo {
@@ -134,18 +172,13 @@
             let pm =
                 wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
                     .context("Failed to get service when prefer_staged is set.")?;
-            let staged = pm.getStagedApexModuleNames()?;
-            for apex_info in list.list.iter_mut() {
-                if staged.contains(&apex_info.name) {
-                    if let Some(staged_apex_info) = pm.getStagedApexInfo(&apex_info.name)? {
-                        apex_info.path = PathBuf::from(staged_apex_info.diskImagePath);
-                        apex_info.has_classpath_jar = staged_apex_info.hasClassPathJars;
-                        let metadata = metadata(&apex_info.path)?;
-                        apex_info.last_update_seconds =
-                            metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
-                        // by definition, staged apex can't be a factory apex.
-                        apex_info.is_factory = false;
-                    }
+            let staged =
+                pm.getStagedApexModuleNames().context("getStagedApexModuleNames failed")?;
+            for name in staged {
+                if let Some(staged_apex_info) =
+                    pm.getStagedApexInfo(&name).context("getStagedApexInfo failed")?
+                {
+                    list.override_staged_apex(&staged_apex_info)?;
                 }
             }
         }
@@ -229,8 +262,13 @@
     let apex_list = pm.get_apex_list(vm_payload_config.prefer_staged)?;
 
     // collect APEXes from config
-    let apex_infos =
+    let mut apex_infos =
         collect_apex_infos(&apex_list, &vm_payload_config.apexes, app_config.debugLevel);
+
+    // Pass sorted list of apexes. Sorting key shouldn't use `path` because it will change after
+    // reboot with prefer_staged. `last_update_seconds` is added to distinguish "samegrade"
+    // update.
+    apex_infos.sort_by_key(|info| (&info.name, &info.version, &info.last_update_seconds));
     info!("Microdroid payload APEXes: {:?}", apex_infos.iter().map(|ai| &ai.name));
 
     let metadata_file =
@@ -391,6 +429,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use tempfile::NamedTempFile;
 
     #[test]
     fn test_find_apex_names_in_classpath() {
@@ -529,4 +568,90 @@
             ]
         );
     }
+
+    #[test]
+    fn test_prefer_staged_apex_with_factory_active_apex() {
+        let single_apex = ApexInfo {
+            name: "foo".to_string(),
+            version: 1,
+            path: PathBuf::from("foo.apex"),
+            is_factory: true,
+            is_active: true,
+            ..Default::default()
+        };
+        let mut apex_info_list = ApexInfoList { list: vec![single_apex.clone()] };
+
+        let staged = NamedTempFile::new().unwrap();
+        apex_info_list
+            .override_staged_apex(&StagedApexInfo {
+                moduleName: "foo".to_string(),
+                versionCode: 2,
+                diskImagePath: staged.path().to_string_lossy().to_string(),
+                ..Default::default()
+            })
+            .expect("should be ok");
+
+        assert_eq!(
+            apex_info_list,
+            ApexInfoList {
+                list: vec![
+                    ApexInfo {
+                        version: 2,
+                        is_factory: false,
+                        path: staged.path().to_owned(),
+                        last_update_seconds: last_updated(staged.path()).unwrap(),
+                        ..single_apex.clone()
+                    },
+                    ApexInfo { is_active: false, ..single_apex },
+                ],
+            }
+        );
+    }
+
+    #[test]
+    fn test_prefer_staged_apex_with_factory_and_inactive_apex() {
+        let factory_apex = ApexInfo {
+            name: "foo".to_string(),
+            version: 1,
+            path: PathBuf::from("foo.apex"),
+            is_factory: true,
+            ..Default::default()
+        };
+        let active_apex = ApexInfo {
+            name: "foo".to_string(),
+            version: 2,
+            path: PathBuf::from("foo.downloaded.apex"),
+            is_active: true,
+            ..Default::default()
+        };
+        let mut apex_info_list =
+            ApexInfoList { list: vec![factory_apex.clone(), active_apex.clone()] };
+
+        let staged = NamedTempFile::new().unwrap();
+        apex_info_list
+            .override_staged_apex(&StagedApexInfo {
+                moduleName: "foo".to_string(),
+                versionCode: 3,
+                diskImagePath: staged.path().to_string_lossy().to_string(),
+                ..Default::default()
+            })
+            .expect("should be ok");
+
+        assert_eq!(
+            apex_info_list,
+            ApexInfoList {
+                list: vec![
+                    // factory apex isn't touched
+                    factory_apex,
+                    // update active one
+                    ApexInfo {
+                        version: 3,
+                        path: staged.path().to_owned(),
+                        last_update_seconds: last_updated(staged.path()).unwrap(),
+                        ..active_apex
+                    },
+                ],
+            }
+        );
+    }
 }