Merge "Add an explicit user root for accessor_demo" into main
diff --git a/android/LinuxInstaller/linux_image_builder/.gitignore b/android/LinuxInstaller/linux_image_builder/.gitignore
new file mode 100644
index 0000000..f082413
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/.gitignore
@@ -0,0 +1,2 @@
+debian.img
+ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/commands b/android/LinuxInstaller/linux_image_builder/commands
new file mode 100644
index 0000000..8b2da45
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/commands
@@ -0,0 +1,11 @@
+upload init.sh:/root
+upload vsock.py:/usr/local/bin
+upload ttyd:/usr/local/bin
+upload ttyd.service:/etc/systemd/system
+upload vsockip.service:/etc/systemd/system
+chmod 0777:/root/init.sh
+firstboot-command "/root/init.sh"
+chmod 0644:/etc/systemd/system/vsockip.service
+chmod 0644:/etc/systemd/system/ttyd.service
+chmod 0777:/usr/local/bin/vsock.py
+chmod 0777:/usr/local/bin/ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/init.sh b/android/LinuxInstaller/linux_image_builder/init.sh
new file mode 100644
index 0000000..bec5ac5
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/init.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+systemctl daemon-reload
+systemctl start ttyd && sudo systemctl enable ttyd
+systemctl start vsockip && sudo systemctl enable vsockip
diff --git a/android/LinuxInstaller/linux_image_builder/setup.sh b/android/LinuxInstaller/linux_image_builder/setup.sh
new file mode 100755
index 0000000..a9aa77d
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/setup.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O debian.img
+wget  https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ttyd
+virt-customize --commands-from-file commands -a debian.img
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
new file mode 100644
index 0000000..3a8f181
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/ttyd.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=TTYD
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/local/bin/ttyd -W login
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/linux_image_builder/vsock.py b/android/LinuxInstaller/linux_image_builder/vsock.py
new file mode 100644
index 0000000..292d953
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsock.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import socket
+
+# Constants for vsock (from linux/vm_sockets.h)
+AF_VSOCK = 40
+SOCK_STREAM = 1
+VMADDR_CID_ANY = -1
+
+def get_local_ip():
+    """Retrieves the first IPv4 address found on the system.
+
+    Returns:
+        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
+    """
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.connect(('8.8.8.8', 80))
+        ip = s.getsockname()[0]
+    except Exception:
+        ip = '127.0.0.1'
+    finally:
+        s.close()
+    return ip
+
+def main():
+    PORT = 1024
+
+    # Create a vsock socket
+    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
+
+    # Bind the socket to the server address
+    server_address = (VMADDR_CID_ANY, PORT)
+    server_socket.bind(server_address)
+
+    # Listen for incoming connections
+    server_socket.listen(1)
+    print(f"VSOCK server listening on port {PORT}...")
+
+    while True:
+        # Accept a connection
+        connection, client_address = server_socket.accept()
+        print(f"Connection from: {client_address}")
+
+        try:
+            # Get the local IP address
+            local_ip = get_local_ip()
+
+            # Send the IP address to the client
+            connection.sendall(local_ip.encode())
+        finally:
+            # Close the connection
+            connection.close()
+
+if __name__ == "__main__":
+    main()
diff --git a/android/LinuxInstaller/linux_image_builder/vsockip.service b/android/LinuxInstaller/linux_image_builder/vsockip.service
new file mode 100644
index 0000000..a29020b
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsockip.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=vsock ip service
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index e6e56d9..2112f89 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -20,6 +20,8 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
@@ -53,6 +55,12 @@
                 });
     }
 
+    @Override
+    protected void onDestroy() {
+        VmLauncherServices.stopVmLauncherService(this);
+        super.onDestroy();
+    }
+
     private void gotoURL(String url) {
         runOnUiThread(() -> mWebView.loadUrl(url));
     }
@@ -79,4 +87,21 @@
         new Handler(Looper.getMainLooper())
                 .postDelayed(() -> gotoURL("http://" + mVmIpAddr + ":7681"), 2000);
     }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.main_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.stop_vm:
+                VmLauncherServices.stopVmLauncherService(this);
+                return true;
+            default:
+                return super.onMenuItemSelected(featureId, item);
+        }
+    }
 }
diff --git a/android/TerminalApp/res/menu/main_menu.xml b/android/TerminalApp/res/menu/main_menu.xml
new file mode 100644
index 0000000..cc65098
--- /dev/null
+++ b/android/TerminalApp/res/menu/main_menu.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/stop_vm"
+        android:title="Stop the existing VM instance"/>
+</menu>
\ No newline at end of file
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index ec98f4c..5e78f99 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -76,6 +76,10 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isVmRunning()) {
+            Log.d(TAG, "there is already the running VM instance");
+            return START_NOT_STICKY;
+        }
         mExecutorService = Executors.newCachedThreadPool();
 
         ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
@@ -85,7 +89,9 @@
         try {
             runner = Runner.create(this, config);
         } catch (VirtualMachineException e) {
-            throw new RuntimeException(e);
+            Log.e(TAG, "cannot create runner", e);
+            stopSelf();
+            return START_NOT_STICKY;
         }
         mVirtualMachine = runner.getVm();
         mResultReceiver =
@@ -117,13 +123,32 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        mExecutorService.shutdownNow();
+        if (isVmRunning()) {
+            try {
+                mVirtualMachine.stop();
+                stopForeground(STOP_FOREGROUND_REMOVE);
+            } catch (VirtualMachineException e) {
+                Log.e(TAG, "failed to stop a VM instance", e);
+            }
+            mExecutorService.shutdownNow();
+            mExecutorService = null;
+            mVirtualMachine = null;
+        }
+    }
+
+    private boolean isVmRunning() {
+        return mVirtualMachine != null
+                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
     }
 
     // TODO(b/359523803): Use AVF API to get ip addr when it exists
     private void gatherIpAddrFromVm(Handler handler) {
         handler.postDelayed(
                 () -> {
+                    if (!isVmRunning()) {
+                        Log.d(TAG, "A virtual machine instance isn't running");
+                        return;
+                    }
                     int INTERNAL_VSOCK_SERVER_PORT = 1024;
                     try (ParcelFileDescriptor pfd =
                             mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
diff --git a/android/fd_server/Android.bp b/android/fd_server/Android.bp
index b02c104..32a8fec 100644
--- a/android/fd_server/Android.bp
+++ b/android/fd_server/Android.bp
@@ -18,6 +18,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "libsafe_ownedfd",
     ],
     prefer_rlib: true,
     apex_available: ["com.android.virt"],
@@ -39,6 +40,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "libsafe_ownedfd",
     ],
     prefer_rlib: true,
     test_suites: ["general-tests"],
diff --git a/android/fd_server/src/aidl.rs b/android/fd_server/src/aidl.rs
index 5f91987..2f3697c 100644
--- a/android/fd_server/src/aidl.rs
+++ b/android/fd_server/src/aidl.rs
@@ -14,20 +14,21 @@
  * limitations under the License.
  */
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use log::error;
 use nix::{
     errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::fchmod, sys::stat::mkdirat,
     sys::stat::mode_t, sys::stat::Mode, sys::statvfs::statvfs, sys::statvfs::Statvfs,
     unistd::unlinkat, unistd::UnlinkatFlags,
 };
+use safe_ownedfd::take_fd_ownership;
 use std::cmp::min;
 use std::collections::{btree_map, BTreeMap};
 use std::convert::TryInto;
 use std::fs::File;
 use std::io;
 use std::os::unix::fs::FileExt;
-use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
 use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
 use std::sync::{Arc, RwLock};
 
@@ -38,7 +39,8 @@
     get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
 };
 use binder::{
-    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
+    BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Status,
+    StatusCode, Strong,
 };
 
 /// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
@@ -299,9 +301,11 @@
                     mode,
                 )
                 .map_err(new_errno_error)?;
-                // SAFETY: new_fd is just created and not an error.
-                let new_file = unsafe { File::from_raw_fd(new_fd) };
-                Ok((new_fd, FdConfig::ReadWrite(new_file)))
+                let new_fd = take_fd_ownership(new_fd)
+                    .context("Failed to take ownership of fd for file")
+                    .or_service_specific_exception(-1)?;
+                let new_file = File::from(new_fd);
+                Ok((new_file.as_raw_fd(), FdConfig::ReadWrite(new_file)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -327,9 +331,10 @@
                     Mode::empty(),
                 )
                 .map_err(new_errno_error)?;
-                // SAFETY: new_dir_fd is just created and not an error.
-                let fd_owner = unsafe { OwnedFd::from_raw_fd(new_dir_fd) };
-                Ok((new_dir_fd, FdConfig::OutputDir(fd_owner)))
+                let fd_owner = take_fd_ownership(new_dir_fd)
+                    .context("Failed to take ownership of the fd for directory")
+                    .or_service_specific_exception(-1)?;
+                Ok((fd_owner.as_raw_fd(), FdConfig::OutputDir(fd_owner)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -408,9 +413,11 @@
 
 fn open_readonly_at(dir_fd: BorrowedFd, path: &Path) -> nix::Result<File> {
     let new_fd = openat(Some(dir_fd.as_raw_fd()), path, OFlag::O_RDONLY, Mode::empty())?;
-    // SAFETY: new_fd is just created successfully and not owned.
-    let new_file = unsafe { File::from_raw_fd(new_fd) };
-    Ok(new_file)
+    let new_fd = take_fd_ownership(new_fd).map_err(|e| match e {
+        safe_ownedfd::Error::Errno(e) => e,
+        _ => Errno::UnknownErrno,
+    })?;
+    Ok(File::from(new_fd))
 }
 
 fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
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/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/product_packages.mk b/build/apex/product_packages.mk
index e710021..efa8dd5 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -62,3 +62,11 @@
     $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
   endif
 endif
+
+ifdef RELEASE_AVF_ENABLE_EARLY_VM
+  # We can't query TARGET_RELEASE from here, so we use RELEASE_AIDL_USE_UNFROZEN as a proxy value of
+  # whether we are building -next release.
+  ifneq ($(RELEASE_AIDL_USE_UNFROZEN),true)
+    $(error RELEASE_AVF_ENABLE_EARLY_VM can only be enabled in trunk_staging until b/357025924 is fixed)
+  endif
+endif
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/authfs_service/Android.bp b/guest/authfs_service/Android.bp
index 2101a36..e508c17 100644
--- a/guest/authfs_service/Android.bp
+++ b/guest/authfs_service/Android.bp
@@ -18,6 +18,7 @@
         "libnix",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libshared_child",
     ],
     prefer_rlib: true,
diff --git a/guest/authfs_service/src/main.rs b/guest/authfs_service/src/main.rs
index 97e684d..ff2f770 100644
--- a/guest/authfs_service/src/main.rs
+++ b/guest/authfs_service/src/main.rs
@@ -26,9 +26,10 @@
 use log::*;
 use rpcbinder::RpcServer;
 use rustutils::sockets::android_get_control_socket;
+use safe_ownedfd::take_fd_ownership;
 use std::ffi::OsString;
 use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 use std::sync::atomic::{AtomicUsize, Ordering};
 
 use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
@@ -109,22 +110,9 @@
 }
 
 /// Prepares a socket file descriptor for the authfs service.
-///
-/// # Safety requirement
-///
-/// 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_authfs_service_socket() -> Result<OwnedFd> {
+fn prepare_authfs_service_socket() -> Result<OwnedFd> {
     let raw_fd = android_get_control_socket(AUTHFS_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)?)
 }
 
 #[allow(clippy::eq_op)]
@@ -137,8 +125,7 @@
 
     clean_up_working_directory()?;
 
-    // SAFETY: This is the only place we take the ownership of the fd of the authfs service.
-    let socket_fd = unsafe { prepare_authfs_service_socket()? };
+    let socket_fd = prepare_authfs_service_socket()?;
     let service = AuthFsService::new_binder(debuggable).as_binder();
     debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
     let server = RpcServer::new_bound_socket(service, socket_fd)?;
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/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index 7c0d9dc..582b69e 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -68,14 +68,9 @@
 
 #[test]
 fn process_requests_in_non_protected_vm() -> Result<()> {
-    check_processing_requests(VmType::NonProtectedVm, None)
-}
-
-#[ignore] // TODO(b/360077974): Figure out why this is flaky.
-#[test]
-fn process_requests_in_non_protected_vm_with_extra_ram() -> Result<()> {
     const MEMORY_MB: i32 = 300;
-    check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))
+    check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))?;
+    check_processing_requests(VmType::NonProtectedVm, None)
 }
 
 fn check_processing_requests(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<()> {
@@ -288,7 +283,9 @@
 }
 
 fn check_csr(csr: Vec<u8>) -> Result<()> {
-    let _csr = rkp::Csr::from_cbor(&Session::default(), &csr[..]).context("Failed to parse CSR")?;
+    let mut session = Session::default();
+    session.set_allow_any_mode(true);
+    let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
     Ok(())
 }
 
diff --git a/libs/libsafe_ownedfd/Android.bp b/libs/libsafe_ownedfd/Android.bp
index 1f14578..53e14dc 100644
--- a/libs/libsafe_ownedfd/Android.bp
+++ b/libs/libsafe_ownedfd/Android.bp
@@ -18,6 +18,7 @@
     defaults: ["libsafe_ownedfd.defaults"],
     apex_available: [
         "com.android.compos",
+        "com.android.microfuchsia",
         "com.android.virt",
     ],
 }
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
index c5bc5fb..565b793 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -59,6 +59,11 @@
         return i;
     }
 
+    public static void stopVmLauncherService(Context context) {
+        Intent i = buildVmLauncherServiceIntent(context);
+        context.stopService(i);
+    }
+
     public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
         Intent i = buildVmLauncherServiceIntent(context);
         if (i == null) {
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/ComposHostTestCases/java/android/compos/test/ComposTestCase.java b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
index 7a35829..6e583c0 100644
--- a/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
+++ b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
@@ -200,6 +200,7 @@
                                 10000,
                                 validator.getAbsolutePath(),
                                 "dice-chain",
+                                "--allow-any-mode",
                                 bcc_file.getAbsolutePath());
         assertWithMessage("hwtrust failed").about(command_results()).that(result).isSuccess();
     }
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 6e23204..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
@@ -134,9 +139,9 @@
 current_user=$(adb shell am get-current-user)
 
 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
-adb shell pm clear --user ${current_user} ${pkg_name} > /dev/null
 
 if [[ -z "${fecr_skip}" ]]; then
   if [[ -z "${fecr_dir}" ]]; then
@@ -165,15 +170,23 @@
 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/HwTrustJni.java b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
index 3b237aa..0cf0606 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
@@ -25,7 +25,8 @@
      * Validates a DICE chain.
      *
      * @param diceChain The dice chain to validate.
+     * @param allowAnyMode Allow the chain's certificates to have any mode.
      * @return true if the dice chain is valid, false otherwise.
      */
-    public static native boolean validateDiceChain(byte[] diceChain);
+    public static native boolean validateDiceChain(byte[] diceChain, boolean allowAnyMode);
 }
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 7089b33..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,12 +1313,12 @@
         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 =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
-                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm_for_vsr", config);
         TestResults testResults =
@@ -1331,7 +1331,11 @@
         testResults.assertNoException();
         byte[] bccBytes = testResults.mBcc;
         assertThat(bccBytes).isNotNull();
-        assertThat(HwTrustJni.validateDiceChain(bccBytes)).isTrue();
+
+        String buildType = SystemProperties.get("ro.build.type");
+        boolean nonUserBuild = !buildType.isEmpty() && buildType != "user";
+
+        assertThat(HwTrustJni.validateDiceChain(bccBytes, nonUserBuild)).isTrue();
     }
 
     @Test
diff --git a/tests/testapk/src/native/hwtrust_jni.rs b/tests/testapk/src/native/hwtrust_jni.rs
index 3b00364..058b1a6 100644
--- a/tests/testapk/src/native/hwtrust_jni.rs
+++ b/tests/testapk/src/native/hwtrust_jni.rs
@@ -29,6 +29,7 @@
     env: JNIEnv,
     _class: JClass,
     dice_chain: JByteArray,
+    allow_any_mode: jboolean,
 ) -> jboolean {
     android_logger::init_once(
         android_logger::Config::default()
@@ -36,7 +37,7 @@
             .with_max_level(log::LevelFilter::Debug),
     );
     debug!("Starting the DICE chain validation ...");
-    match validate_dice_chain(env, dice_chain) {
+    match validate_dice_chain(env, dice_chain, allow_any_mode) {
         Ok(_) => {
             info!("DICE chain validated successfully");
             true
@@ -49,9 +50,14 @@
     .into()
 }
 
-fn validate_dice_chain(env: JNIEnv, jdice_chain: JByteArray) -> Result<()> {
+fn validate_dice_chain(
+    env: JNIEnv,
+    jdice_chain: JByteArray,
+    allow_any_mode: jboolean,
+) -> Result<()> {
     let dice_chain = env.convert_byte_array(jdice_chain)?;
-    let session = Session::default();
+    let mut session = Session::default();
+    session.set_allow_any_mode(allow_any_mode == jboolean::from(true));
     let _chain = dice::Chain::from_cbor(&session, &dice_chain)?;
     Ok(())
 }