Merge "Revert^2 "Adjust tests to account for new DICE mode check"" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index f5f39e3..3ae014e 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -10,8 +10,10 @@
         "vm_launcher_lib",
     ],
     sdk_version: "system_current",
-    product_specific: true,
     optimize: {
         shrink_resources: true,
     },
+    apex_available: [
+        "com.android.virt",
+    ],
 }
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 07e6147..27b2b46 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -11,7 +11,8 @@
         <activity android:name=".MainActivity"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
-                  android:exported="true">
+                  android:exported="true"
+                  android:enabled="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index a21ee6c..4828057 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -55,6 +55,7 @@
         "libregex",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libsemver",
         "libselinux_bindgen",
         "libserde",
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 144524f..7a357f3 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -73,6 +73,7 @@
 use nix::unistd::pipe;
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
+use safe_ownedfd::take_fd_ownership;
 use semver::VersionReq;
 use std::collections::HashSet;
 use std::convert::TryInto;
@@ -82,7 +83,7 @@
 use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
 use std::iter;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
+use std::os::unix::io::{AsRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
@@ -1274,7 +1275,7 @@
         let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
             .context("Failed to connect")
             .or_service_specific_exception(-1)?;
-        Ok(vsock_stream_to_pfd(stream))
+        vsock_stream_to_pfd(stream)
     }
 
     fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
@@ -1433,10 +1434,12 @@
 }
 
 /// Converts a `VsockStream` to a `ParcelFileDescriptor`.
-fn vsock_stream_to_pfd(stream: VsockStream) -> ParcelFileDescriptor {
-    // SAFETY: ownership is transferred from stream to f
-    let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
-    ParcelFileDescriptor::new(f)
+fn vsock_stream_to_pfd(stream: VsockStream) -> binder::Result<ParcelFileDescriptor> {
+    let owned_fd = take_fd_ownership(stream.into_raw_fd())
+        .context("Failed to take ownership of the vsock stream")
+        .with_log()
+        .or_service_specific_exception(-1)?;
+    Ok(ParcelFileDescriptor::new(owned_fd))
 }
 
 /// Parses the platform version requirement string.
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index cb2ad2d..08a9e47 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -25,7 +25,6 @@
 use log::{debug, error, info};
 use semver::{Version, VersionReq};
 use nix::{fcntl::OFlag, unistd::pipe2, unistd::Uid, unistd::User};
-use nix::unistd::dup;
 use regex::{Captures, Regex};
 use rustutils::system_properties;
 use shared_child::SharedChild;
@@ -36,7 +35,6 @@
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::fd::FromRawFd;
 use std::os::unix::io::{AsRawFd, OwnedFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
@@ -59,7 +57,6 @@
 use rpcbinder::RpcServer;
 
 /// external/crosvm
-use base::AsRawDescriptor;
 use base::UnixSeqpacketListener;
 use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
 
@@ -1042,14 +1039,7 @@
 
     let control_sock = UnixSeqpacketListener::bind(crosvm_control_socket_path)
         .context("failed to create control server")?;
-    command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, {
-        let dup_fd = dup(control_sock.as_raw_descriptor())?;
-        // SAFETY: UnixSeqpacketListener doesn't provide a way to convert it into a RawFd or
-        // OwnedFd. In order to provide a OwnedFd for add_preserved_fd, dup the control socket
-        // and create a OwnedFd from the duped fd. This is fine as the original fd is still
-        // closed when control_socket is dropped.
-        unsafe { OwnedFd::from_raw_fd(dup_fd) }
-    }));
+    command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
 
     if let Some(dt_overlay) = config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
diff --git a/android/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index 445260f..7d29685 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -25,16 +25,16 @@
 
 use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use binder::{BinderFeatures, ProcessState};
 use lazy_static::lazy_static;
 use log::{info, LevelFilter};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{AsFd, RawFd};
 use clap::Parser;
-use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{write, Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
+use safe_ownedfd::take_fd_ownership;
 
 const LOG_TAG: &str = "virtmgr";
 
@@ -73,32 +73,6 @@
     ready_fd: RawFd,
 }
 
-fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
-    // Basic check that the integer value does correspond to a file descriptor.
-    fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
-
-    // The file descriptor had CLOEXEC disabled to be inherited from the parent.
-    // Re-enable it to make sure it is not accidentally inherited further.
-    fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
-        .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-
-    // Reject RawFds that already have a corresponding OwnedFd.
-    if owned_fds.contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} already owned");
-    }
-    owned_fds.push(raw_fd);
-
-    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
-    // We checked that the integer value corresponds to a valid FD and that this
-    // is the first argument to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 fn check_vm_support() -> Result<()> {
     if hypervisor_props::is_any_vm_supported()? {
         Ok(())
@@ -122,11 +96,9 @@
 
     let args = Args::parse();
 
-    let mut owned_fds = vec![];
-    let rpc_server_fd = take_fd_ownership(args.rpc_server_fd, &mut owned_fds)
-        .expect("Failed to take ownership of rpc_server_fd");
-    let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
-        .expect("Failed to take ownership of ready_fd");
+    let rpc_server_fd =
+        take_fd_ownership(args.rpc_server_fd).expect("Failed to take ownership of rpc_server_fd");
+    let ready_fd = take_fd_ownership(args.ready_fd).expect("Failed to take ownership of ready_fd");
 
     // Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
     ProcessState::start_thread_pool();
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 0a2b2de..f493202 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -7,12 +7,28 @@
     "microdroid_vbmeta",
 ]
 
-apex {
+soong_config_module_type {
+    name: "virt_apex",
+    module_type: "apex",
+    config_namespace: "ANDROID",
+    bool_variables: [
+        "avf_enabled",
+    ],
+    properties: [
+        "defaults",
+    ],
+}
+
+virt_apex {
     name: "com.android.virt",
-    defaults: select(soong_config_variable("ANDROID", "avf_enabled"), {
-        "true": ["com.android.virt_avf_enabled"],
-        default: ["com.android.virt_avf_disabled"],
-    }),
+    soong_config_variables: {
+        avf_enabled: {
+            defaults: ["com.android.virt_avf_enabled"],
+            conditions_default: {
+                defaults: ["com.android.virt_avf_disabled"],
+            },
+        },
+    },
 }
 
 apex_defaults {
@@ -30,7 +46,10 @@
     apps: [
         "android.system.virtualmachine.res",
     ] + select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
-        true: ["VmLauncherApp"],
+        true: [
+            "VmLauncherApp",
+            "VmTerminalApp",
+        ],
         default: [],
     }),
 
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 6a1cc00..b02fbf7 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -110,12 +110,11 @@
 ## Graphical VMs
 
 To run OSes with graphics support, simply
-`packages/modules/Virtualization/tests/ferrochrome/ferrochrome.sh`. It prepares
-and launches the ChromiumOS, which is the only officially supported guest
-payload. We will be adding more OSes in the future.
+`packages/modules/Virtualization/tests/ferrochrome/ferrochrome.sh --forever`.
+It prepares and launches the ChromiumOS, which is the only officially supported
+guest payload. We will be adding more OSes in the future.
 
-If you want to do so by yourself (e.g. boot with your build), follow the
-instruction below.
+If you want to do so by yourself, follow the instruction below.
 
 ### Prepare a guest image
 
@@ -306,9 +305,6 @@
 ```
 
 To see console logs only, check
-`/data/data/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log`
-
-For HSUM enabled devices,
 `/data/user/${current_user_id}/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log`
 
 You can monitor console out as follows
diff --git a/guest/microdroid_manager/Android.bp b/guest/microdroid_manager/Android.bp
index 9c9a3d0..82e26b7 100644
--- a/guest/microdroid_manager/Android.bp
+++ b/guest/microdroid_manager/Android.bp
@@ -48,6 +48,7 @@
         "libprotobuf",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libsecretkeeper_client",
         "libsecretkeeper_comm_nostd",
         "libscopeguard",
@@ -59,6 +60,7 @@
         "libvsock",
         "librand",
         "libzeroize",
+        "libsafe_ownedfd",
     ],
     init_rc: ["microdroid_manager.rc"],
     multilib: {
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 990d27a..8b676b8 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -50,13 +50,14 @@
 use rustutils::sockets::android_get_control_socket;
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
+use safe_ownedfd::take_fd_ownership;
 use secretkeeper_comm::data_types::ID_SIZE;
 use std::borrow::Cow::{Borrowed, Owned};
 use std::env;
 use std::ffi::CString;
 use std::fs::{self, create_dir, File, OpenOptions};
 use std::io::{Read, Write};
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::Path;
@@ -199,13 +200,7 @@
     );
     info!("started.");
 
-    // SAFETY: This is the only place we take the ownership of the fd of the vm payload service.
-    //
-    // To ensure that the CLOEXEC flag is set on the file descriptor as early as possible,
-    // it is necessary to fetch the socket corresponding to vm_payload_service at the
-    // very beginning, as android_get_control_socket() sets the CLOEXEC flag on the file
-    // descriptor.
-    let vm_payload_service_fd = unsafe { prepare_vm_payload_service_socket()? };
+    let vm_payload_service_fd = prepare_vm_payload_service_socket()?;
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
@@ -487,22 +482,9 @@
 }
 
 /// Prepares a socket file descriptor for the vm payload service.
-///
-/// # Safety
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
+fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
     let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+    Ok(take_fd_ownership(raw_fd)?)
 }
 
 fn is_strict_boot() -> bool {
diff --git a/libs/libsafe_ownedfd/Android.bp b/libs/libsafe_ownedfd/Android.bp
new file mode 100644
index 0000000..53e14dc
--- /dev/null
+++ b/libs/libsafe_ownedfd/Android.bp
@@ -0,0 +1,38 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libsafe_ownedfd.defaults",
+    crate_name: "safe_ownedfd",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libnix",
+        "libthiserror",
+    ],
+}
+
+rust_library {
+    name: "libsafe_ownedfd",
+    defaults: ["libsafe_ownedfd.defaults"],
+    apex_available: [
+        "com.android.compos",
+        "com.android.microfuchsia",
+        "com.android.virt",
+    ],
+}
+
+rust_test {
+    name: "libsafe_ownedfd.test",
+    defaults: ["libsafe_ownedfd.defaults"],
+    rustlibs: [
+        "libanyhow",
+        "libtempfile",
+    ],
+    host_supported: true,
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/libs/libsafe_ownedfd/src/lib.rs b/libs/libsafe_ownedfd/src/lib.rs
new file mode 100644
index 0000000..52ae180
--- /dev/null
+++ b/libs/libsafe_ownedfd/src/lib.rs
@@ -0,0 +1,127 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Library for a safer conversion from `RawFd` to `OwnedFd`
+
+use nix::fcntl::{fcntl, FdFlag, F_DUPFD, F_GETFD, F_SETFD};
+use nix::libc;
+use nix::unistd::close;
+use std::os::fd::FromRawFd;
+use std::os::fd::OwnedFd;
+use std::os::fd::RawFd;
+use std::sync::Mutex;
+use thiserror::Error;
+
+/// Errors that can occur while taking an ownership of `RawFd`
+#[derive(Debug, PartialEq, Error)]
+pub enum Error {
+    /// RawFd is not a valid file descriptor
+    #[error("{0} is not a file descriptor")]
+    Invalid(RawFd),
+
+    /// RawFd is either stdio, stdout, or stderr
+    #[error("standard IO descriptors cannot be owned")]
+    StdioNotAllowed,
+
+    /// Generic UNIX error
+    #[error("UNIX error")]
+    Errno(#[from] nix::errno::Errno),
+}
+
+static LOCK: Mutex<()> = Mutex::new(());
+
+/// Takes the ownership of `RawFd` and converts it to `OwnedFd`. It is important to know that
+/// `RawFd` is closed when this function successfully returns. The raw file descriptor of the
+/// returned `OwnedFd` is different from `RawFd`. The returned file descriptor is CLOEXEC set.
+pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
+    fcntl(raw_fd, F_GETFD).map_err(|_| Error::Invalid(raw_fd))?;
+
+    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+        return Err(Error::StdioNotAllowed);
+    }
+
+    // sync is needed otherwise we can create multiple OwnedFds out of the same RawFd
+    let lock = LOCK.lock().unwrap();
+    let new_fd = fcntl(raw_fd, F_DUPFD(raw_fd))?;
+    close(raw_fd)?;
+    drop(lock);
+
+    // This is not essential, but let's follow the common practice in the Rust ecosystem
+    fcntl(new_fd, F_SETFD(FdFlag::FD_CLOEXEC)).map_err(Error::Errno)?;
+
+    // SAFETY: In this function, we have checked that RawFd is actually an open file descriptor and
+    // this is the first time to claim its ownership because we just created it by duping.
+    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::Result;
+    use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
+    use std::os::fd::AsRawFd;
+    use std::os::fd::IntoRawFd;
+    use tempfile::tempfile;
+
+    #[test]
+    fn good_fd() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+        assert!(take_fd_ownership(raw_fd).is_ok());
+        Ok(())
+    }
+
+    #[test]
+    fn invalid_fd() -> Result<()> {
+        let raw_fd = 12345; // randomly chosen
+        assert_eq!(take_fd_ownership(raw_fd).unwrap_err(), Error::Invalid(raw_fd));
+        Ok(())
+    }
+
+    #[test]
+    fn original_fd_closed() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+        let owned_fd = take_fd_ownership(raw_fd)?;
+        assert_ne!(raw_fd, owned_fd.as_raw_fd());
+        assert!(fcntl(raw_fd, F_GETFD).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn cannot_use_same_rawfd_multiple_times() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+
+        let owned_fd = take_fd_ownership(raw_fd); // once
+        let owned_fd2 = take_fd_ownership(raw_fd); // twice
+
+        assert!(owned_fd.is_ok());
+        assert!(owned_fd2.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn cloexec() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+
+        // intentionally clear cloexec to see if it is set by take_fd_ownership
+        fcntl(raw_fd, F_SETFD(FdFlag::empty()))?;
+        let flags = fcntl(raw_fd, F_GETFD)?;
+        assert_eq!(flags, FdFlag::empty().bits());
+
+        let owned_fd = take_fd_ownership(raw_fd)?;
+        let flags = fcntl(owned_fd.as_raw_fd(), F_GETFD)?;
+        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
+        drop(owned_fd);
+        Ok(())
+    }
+}
diff --git a/microfuchsia/OWNERS b/microfuchsia/OWNERS
new file mode 100644
index 0000000..8092be7
--- /dev/null
+++ b/microfuchsia/OWNERS
@@ -0,0 +1,2 @@
+awolter@google.com
+jamesr@google.com
diff --git a/microfuchsia/microfuchsiad/Android.bp b/microfuchsia/microfuchsiad/Android.bp
index ddf360d..ab3f865 100644
--- a/microfuchsia/microfuchsiad/Android.bp
+++ b/microfuchsia/microfuchsiad/Android.bp
@@ -15,8 +15,9 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
-        "liblog_rust",
         "liblibc",
+        "liblog_rust",
+        "libsafe_ownedfd",
         "libvmclient",
     ],
     apex_available: [
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
index 15fcc06..6688447 100644
--- a/microfuchsia/microfuchsiad/src/instance_starter.rs
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -23,9 +23,10 @@
 use anyhow::{ensure, Context, Result};
 use binder::{LazyServiceGuard, ParcelFileDescriptor};
 use log::info;
+use safe_ownedfd::take_fd_ownership;
 use std::ffi::CStr;
 use std::fs::File;
-use std::os::fd::FromRawFd;
+use std::os::fd::AsRawFd;
 use vmclient::VmInstance;
 
 pub struct MicrofuchsiaInstance {
@@ -133,6 +134,7 @@
             "failed to openpty"
         );
     }
+    let leader = take_fd_ownership(leader)?;
 
     // SAFETY: calling these libc functions with valid+initialized variables is safe.
     unsafe {
@@ -145,24 +147,25 @@
             c_line: 0,
             c_cc: [0u8; 19],
         };
-        ensure!(libc::tcgetattr(leader, &mut attr) == 0, "failed to get termios attributes");
+        ensure!(
+            libc::tcgetattr(leader.as_raw_fd(), &mut attr) == 0,
+            "failed to get termios attributes"
+        );
 
         // Force it to be a raw pty and re-set it.
         libc::cfmakeraw(&mut attr);
         ensure!(
-            libc::tcsetattr(leader, libc::TCSANOW, &attr) == 0,
+            libc::tcsetattr(leader.as_raw_fd(), libc::TCSANOW, &attr) == 0,
             "failed to set termios attributes"
         );
     }
 
     // Construct the return value.
-    // SAFETY: The file descriptors are valid because openpty returned without error (above).
-    let leader = unsafe { File::from_raw_fd(leader) };
     let follower_name: Vec<u8> = follower_name.iter_mut().map(|x| *x as _).collect();
     let follower_name = CStr::from_bytes_until_nul(&follower_name)
         .context("pty filename missing NUL")?
         .to_str()
         .context("pty filename invalid utf8")?
         .to_string();
-    Ok(Pty { leader, follower_name })
+    Ok(Pty { leader: File::from(leader), follower_name })
 }
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index c68e8a9..03630dd 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -65,6 +65,7 @@
   echo "  --version \${version}: ferrochrome version to be downloaded"
   echo "  --keep: Keep downloaded ferrochrome image"
   echo "  --test: Download test image instead"
+  echo "  --forever: Keep ferrochrome running forever. Used for manual test"
 }
 
 fecr_version="${FECR_DEFAULT_VERSION}"
@@ -76,6 +77,7 @@
 fecr_image="${FECR_DEFAULT_IMAGE}"
 fecr_boot_completed_log="${FECR_DEFAULT_BOOT_COMPLETED_LOG}"
 fecr_screenshot_dir="${FECR_DEFAULT_SCREENSHOT_DIR}"
+fecr_forever=""
 
 # Parse parameters
 while (( "${#}" )); do
@@ -102,6 +104,9 @@
       fecr_image="${FECR_TEST_IMAGE}"
       fecr_boot_completed_log="${FECR_TEST_IMAGE_BOOT_COMPLETED_LOG}"
       ;;
+    --forever)
+      fecr_forever="true"
+      ;;
     -h|--help)
       print_usage
       exit 0
@@ -131,9 +136,12 @@
 fi
 
 pkg_name=$(dirname ${resolved_activities})
+current_user=$(adb shell am get-current-user)
 
-adb shell pm grant ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
-adb shell pm clear ${pkg_name} > /dev/null
+echo "Reset app & granting permission"
+adb shell pm clear --user ${current_user} ${pkg_name} > /dev/null
+adb shell pm grant --user ${current_user} ${pkg_name} android.permission.RECORD_AUDIO
+adb shell pm grant --user ${current_user} ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
 
 if [[ -z "${fecr_skip}" ]]; then
   if [[ -z "${fecr_dir}" ]]; then
@@ -155,26 +163,30 @@
 adb shell svc power stayon true
 adb shell wm dismiss-keyguard
 
-echo "Granting runtime permissions to ensure VmLauncher is focused"
-adb shell pm grant ${pkg_name} android.permission.RECORD_AUDIO
-
 echo "Starting ferrochrome"
 adb shell am start-activity -a ${ACTION_NAME} > /dev/null
 
 # HSUM aware log path
-current_user=$(adb shell am get-current-user)
 log_path="/data/user/${current_user}/${pkg_name}/${FECR_CONSOLE_LOG_PATH}"
 fecr_start_time=${EPOCHSECONDS}
 
-adb shell mkdir -p "${fecr_screenshot_dir}"
-while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
-  adb shell screencap -p "${fecr_screenshot_dir}/screenshot-${EPOCHSECONDS}.png"
-  adb shell grep -soF \""${fecr_boot_completed_log}"\" "${log_path}" && exit 0 || true
-  sleep 10
-done
+echo "Check ${log_path} on device for console log"
 
->&2 echo "Ferrochrome failed to boot. Dumping console log"
->&2 adb shell cat ${log_path}
+if [[ "${fecr_forever}" == "true" ]]; then
+  echo "Ctrl+C to stop running"
+  echo "To open interactive serial console, use following command:"
+  echo "adb shell -t /apex/com.android.virt/bin/vm console"
+else
+  adb shell mkdir -p "${fecr_screenshot_dir}"
+  while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
+    adb shell screencap -p "${fecr_screenshot_dir}/screenshot-${EPOCHSECONDS}.png"
+    adb shell grep -soF \""${fecr_boot_completed_log}"\" "${log_path}" && exit 0 || true
+    sleep 10
+  done
 
-exit 1
+  >&2 echo "Ferrochrome failed to boot. Dumping console log"
+  >&2 adb shell cat ${log_path}
+
+  exit 1
+fi
 
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 3d67a04..d38af45 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1305,7 +1305,7 @@
     }
 
     @Test
-    @VsrTest(requirements = {"VSR-7.1-001.005"})
+    @VsrTest(requirements = {"VSR-7.1-001.004"})
     public void protectedVmHasValidDiceChain() throws Exception {
         // This test validates two things regarding the pVM DICE chain:
         // 1. The DICE chain is well-formed that all the entries conform to the DICE spec.
@@ -1313,7 +1313,7 @@
         assumeSupportedDevice();
         assumeProtectedVM();
         assumeVsrCompliant();
-        assumeTrue("Vendor API must be at least 202404", getVendorApiLevel() >= 202404);
+        assumeTrue("Vendor API must be newer than 202404", getVendorApiLevel() > 202404);
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =