Snap for 12260156 from 51b75f07529d3a4d0256152c33df1cbcd6c31687 to 24Q4-release

Change-Id: I19a8f18839b7f9bd7e22ac77473074c2b2a60428
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
index c32d017..433e89c 100644
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -30,8 +30,8 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        boolean isRoot = isTaskRoot();
         finish();
+
         if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
             return;
         }
@@ -49,16 +49,6 @@
             return;
         }
         Log.i(TAG, "Sending " + scheme + " URL to VM");
-        if (isRoot) {
-            Log.w(
-                    TAG,
-                    "Cannot open URL without starting "
-                            + FerrochromeActivity.class.getSimpleName()
-                            + " first, starting it now");
-            startActivity(
-                    new Intent(this, FerrochromeActivity.class).setAction(Intent.ACTION_MAIN));
-            return;
-        }
         startActivity(
                 new Intent(ACTION_VM_OPEN_URL)
                         .setFlags(
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
index 828d923..def464e 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
@@ -34,7 +34,7 @@
         mVmAgent = vmAgent;
     }
 
-    private VmAgent.Connection getConnection() {
+    private VmAgent.Connection getConnection() throws InterruptedException {
         return mVmAgent.connect();
     }
 
@@ -53,7 +53,7 @@
 
         try {
             getConnection().sendData(VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, data);
-        } catch (RuntimeException e) {
+        } catch (InterruptedException | RuntimeException e) {
             Log.e(TAG, "Failed to write clipboard data to VM", e);
         }
     }
@@ -63,7 +63,7 @@
         VmAgent.Data data;
         try {
             data = getConnection().sendAndReceive(VmAgent.READ_CLIPBOARD_FROM_VM, null);
-        } catch (RuntimeException e) {
+        } catch (InterruptedException | RuntimeException e) {
             Log.e(TAG, "Failed to read clipboard data from VM", e);
             return;
         }
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 54543b0..fb75533 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -52,16 +52,12 @@
     private DisplayProvider mDisplayProvider;
     private VmAgent mVmAgent;
     private ClipboardHandler mClipboardHandler;
+    private OpenUrlHandler mOpenUrlHandler;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        String action = getIntent().getAction();
-        if (!ACTION_VM_LAUNCHER.equals(action)) {
-            finish();
-            Log.e(TAG, "onCreate unsupported intent action: " + action);
-            return;
-        }
+        Log.d(TAG, "onCreate intent: " + getIntent());
         checkAndRequestRecordAudioPermission();
         mExecutorService = Executors.newCachedThreadPool();
 
@@ -98,6 +94,8 @@
 
         mVmAgent = new VmAgent(mVirtualMachine);
         mClipboardHandler = new ClipboardHandler(this, mVmAgent);
+        mOpenUrlHandler = new OpenUrlHandler(mVmAgent);
+        handleIntent(getIntent());
     }
 
     private void makeFullscreen() {
@@ -146,6 +144,7 @@
         super.onDestroy();
         mExecutorService.shutdownNow();
         mInputForwarder.cleanUp();
+        mOpenUrlHandler.shutdown();
         Log.d(TAG, "destroyed");
     }
 
@@ -172,18 +171,16 @@
 
     @Override
     protected void onNewIntent(Intent intent) {
-        String action = intent.getAction();
-        if (!ACTION_VM_OPEN_URL.equals(action)) {
-            Log.e(TAG, "onNewIntent unsupported intent action: " + action);
-            return;
-        }
-        Log.d(TAG, "onNewIntent intent action: " + action);
-        String text = intent.getStringExtra(Intent.EXTRA_TEXT);
-        if (text != null) {
-            mExecutorService.execute(
-                    () -> {
-                        mVmAgent.connect().sendData(VmAgent.OPEN_URL, text.getBytes());
-                    });
+        Log.d(TAG, "onNewIntent intent: " + intent);
+        handleIntent(intent);
+    }
+
+    private void handleIntent(Intent intent) {
+        if (ACTION_VM_OPEN_URL.equals(intent.getAction())) {
+            String url = intent.getStringExtra(Intent.EXTRA_TEXT);
+            if (url != null) {
+                mOpenUrlHandler.sendUrlToVm(url);
+            }
         }
     }
 
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
new file mode 100644
index 0000000..fb0c6bf
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.virtualization.vmlauncher;
+
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+class OpenUrlHandler {
+    private static final String TAG = MainActivity.TAG;
+
+    private final VmAgent mVmAgent;
+    private final ExecutorService mExecutorService;
+
+    OpenUrlHandler(VmAgent vmAgent) {
+        mVmAgent = vmAgent;
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    void shutdown() {
+        mExecutorService.shutdownNow();
+    }
+
+    void sendUrlToVm(String url) {
+        mExecutorService.execute(
+                () -> {
+                    try {
+                        mVmAgent.connect().sendData(VmAgent.OPEN_URL, url.getBytes());
+                        Log.d(TAG, "Successfully sent URL to the VM");
+                    } catch (InterruptedException | RuntimeException e) {
+                        Log.e(TAG, "Failed to send URL to the VM", e);
+                    }
+                });
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
index 78da6c0..af1d298 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
@@ -17,8 +17,10 @@
 package com.android.virtualization.vmlauncher;
 
 import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
 
 import libcore.io.Streams;
 
@@ -39,6 +41,7 @@
     private static final int DATA_SHARING_SERVICE_PORT = 3580;
     private static final int HEADER_SIZE = 8; // size of the header
     private static final int SIZE_OFFSET = 4; // offset of the size field in the header
+    private static final long RETRY_INTERVAL_MS = 1_000;
 
     static final byte READ_CLIPBOARD_FROM_VM = 0;
     static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
@@ -51,13 +54,26 @@
         mVirtualMachine = vm;
     }
 
-    /** Connect to the agent and returns the communication channel established. This can block. */
-    Connection connect() {
-        try {
-            // TODO: wait until the VM is up and the agent is running
-            return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
-        } catch (VirtualMachineException e) {
-            throw new RuntimeException("Failed to connect to the VM agent", e);
+    /**
+     * Connects to the agent and returns the established communication channel. This can block.
+     *
+     * @throws InterruptedException If the current thread was interrupted
+     */
+    Connection connect() throws InterruptedException {
+        boolean shouldLog = true;
+        while (true) {
+            if (Thread.interrupted()) {
+                throw new InterruptedException();
+            }
+            try {
+                return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
+            } catch (VirtualMachineException e) {
+                if (shouldLog) {
+                    shouldLog = false;
+                    Log.d(TAG, "Still waiting for VM agent to start", e);
+                }
+            }
+            SystemClock.sleep(RETRY_INTERVAL_MS);
         }
     }
 
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index f1bfd8c..144524f 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -411,9 +411,9 @@
 
         let state = &mut *self.state.lock().unwrap();
         let console_out_fd =
-            clone_or_prepare_logger_fd(&debug_config, console_out_fd, format!("Console({})", cid))?;
+            clone_or_prepare_logger_fd(console_out_fd, format!("Console({})", cid))?;
         let console_in_fd = console_in_fd.map(clone_file).transpose()?;
-        let log_fd = clone_or_prepare_logger_fd(&debug_config, log_fd, format!("Log({})", cid))?;
+        let log_fd = clone_or_prepare_logger_fd(log_fd, format!("Log({})", cid))?;
 
         // Counter to generate unique IDs for temporary image files.
         let mut next_temporary_image_id = 0;
@@ -1563,7 +1563,6 @@
 }
 
 fn clone_or_prepare_logger_fd(
-    debug_config: &DebugConfig,
     fd: Option<&ParcelFileDescriptor>,
     tag: String,
 ) -> Result<Option<File>, Status> {
@@ -1571,10 +1570,6 @@
         return Ok(Some(clone_file(fd)?));
     }
 
-    if !debug_config.should_prepare_console_output() {
-        return Ok(None);
-    };
-
     let (read_fd, write_fd) =
         pipe().context("Failed to create pipe").or_service_specific_exception(-1)?;
 
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index f9fbd16..37618c7 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -25,6 +25,7 @@
 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;
@@ -35,7 +36,8 @@
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::fd::FromRawFd;
+use std::os::unix::io::{AsRawFd, OwnedFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -872,26 +874,6 @@
     }
 }
 
-fn append_platform_devices(
-    command: &mut Command,
-    preserved_fds: &mut Vec<RawFd>,
-    config: &CrosvmConfig,
-) -> Result<(), Error> {
-    if config.vfio_devices.is_empty() {
-        return Ok(());
-    }
-
-    let Some(dtbo) = &config.dtbo else {
-        bail!("VFIO devices assigned but no DTBO available");
-    };
-    command.arg(format!("--device-tree-overlay={},filter", add_preserved_fd(preserved_fds, dtbo)));
-
-    for device in &config.vfio_devices {
-        command.arg(vfio_argument_for_platform_device(device)?);
-    }
-    Ok(())
-}
-
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -986,7 +968,7 @@
     }
 
     // Keep track of what file descriptors should be mapped to the crosvm process.
-    let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
+    let mut preserved_fds = config.indirect_files.into_iter().map(|f| f.into()).collect();
 
     // Setup the serial devices.
     // 1. uart device: used as the output device by bootloaders and as early console by linux
@@ -997,15 +979,14 @@
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
     // written there is discarded.
-    let console_out_arg = format_serial_out_arg(&mut preserved_fds, &config.console_out_fd);
+    let console_out_arg = format_serial_out_arg(&mut preserved_fds, config.console_out_fd);
     let console_in_arg = config
         .console_in_fd
-        .as_ref()
         .map(|fd| format!(",input={}", add_preserved_fd(&mut preserved_fds, fd)))
         .unwrap_or_default();
-    let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
-    let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
-    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
+    let log_arg = format_serial_out_arg(&mut preserved_fds, config.log_fd);
+    let failure_serial_path = add_preserved_fd(&mut preserved_fds, failure_pipe_write);
+    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, config.ramdump);
     let console_input_device = config.console_input_device.as_deref().unwrap_or(CONSOLE_HVC0);
     match console_input_device {
         CONSOLE_HVC0 | CONSOLE_TTYS0 => {}
@@ -1035,11 +1016,11 @@
     // /dev/hvc2
     command.arg(format!("--serial={},hardware=virtio-console,num=3", &log_arg));
 
-    if let Some(bootloader) = &config.bootloader {
+    if let Some(bootloader) = config.bootloader {
         command.arg("--bios").arg(add_preserved_fd(&mut preserved_fds, bootloader));
     }
 
-    if let Some(initrd) = &config.initrd {
+    if let Some(initrd) = config.initrd {
         command.arg("--initrd").arg(add_preserved_fd(&mut preserved_fds, initrd));
     }
 
@@ -1047,25 +1028,30 @@
         command.arg("--params").arg(params);
     }
 
-    for disk in &config.disks {
+    for disk in config.disks {
         command.arg("--block").arg(format!(
             "path={},ro={}",
-            add_preserved_fd(&mut preserved_fds, &disk.image),
+            add_preserved_fd(&mut preserved_fds, disk.image),
             !disk.writable,
         ));
     }
 
-    if let Some(kernel) = &config.kernel {
+    if let Some(kernel) = config.kernel {
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
-    let control_server_socket = UnixSeqpacketListener::bind(crosvm_control_socket_path)
+    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, &control_server_socket.as_raw_descriptor()));
+    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) }
+    }));
 
-    if let Some(dt_overlay) = &config.device_tree_overlay {
+    if let Some(dt_overlay) = config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
 
@@ -1116,15 +1102,15 @@
     }
 
     if cfg!(network) {
-        if let Some(tap) = &config.tap {
-            let tap_fd = tap.as_raw_fd();
-            preserved_fds.push(tap_fd);
-            command.arg("--net").arg(format!("tap-fd={}", tap_fd));
+        if let Some(tap) = config.tap {
+            command
+                .arg("--net")
+                .arg(format!("tap-fd={}", add_preserved_fd(&mut preserved_fds, tap)));
         }
     }
 
     if cfg!(paravirtualized_devices) {
-        for input_device_option in config.input_device_options.iter() {
+        for input_device_option in config.input_device_options.into_iter() {
             command.arg("--input");
             command.arg(match input_device_option {
                 InputDeviceOption::EvDev(file) => {
@@ -1172,7 +1158,19 @@
         command.arg("--boost-uclamp");
     }
 
-    append_platform_devices(&mut command, &mut preserved_fds, &config)?;
+    if !config.vfio_devices.is_empty() {
+        if let Some(dtbo) = config.dtbo {
+            command.arg(format!(
+                "--device-tree-overlay={},filter",
+                add_preserved_fd(&mut preserved_fds, dtbo)
+            ));
+        } else {
+            bail!("VFIO devices assigned but no DTBO available");
+        }
+    };
+    for device in config.vfio_devices {
+        command.arg(vfio_argument_for_platform_device(&device)?);
+    }
 
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
@@ -1242,15 +1240,16 @@
 
 /// Adds the file descriptor for `file` to `preserved_fds`, and returns a string of the form
 /// "/proc/self/fd/N" where N is the file descriptor.
-fn add_preserved_fd(preserved_fds: &mut Vec<RawFd>, file: &dyn AsRawFd) -> String {
-    let fd = file.as_raw_fd();
+fn add_preserved_fd<F: Into<OwnedFd>>(preserved_fds: &mut Vec<OwnedFd>, file: F) -> String {
+    let fd = file.into();
+    let raw_fd = fd.as_raw_fd();
     preserved_fds.push(fd);
-    format!("/proc/self/fd/{}", fd)
+    format!("/proc/self/fd/{}", raw_fd)
 }
 
 /// Adds the file descriptor for `file` (if any) to `preserved_fds`, and returns the appropriate
 /// string for a crosvm `--serial` flag. If `file` is none, creates a dummy sink device.
-fn format_serial_out_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+fn format_serial_out_arg(preserved_fds: &mut Vec<OwnedFd>, file: Option<File>) -> String {
     if let Some(file) = file {
         format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
     } else {
diff --git a/android/virtualizationservice/aidl/Android.bp b/android/virtualizationservice/aidl/Android.bp
index bca4512..c1bff5e 100644
--- a/android/virtualizationservice/aidl/Android.bp
+++ b/android/virtualizationservice/aidl/Android.bp
@@ -31,6 +31,7 @@
             apex_available: [
                 "com.android.virt",
                 "com.android.compos",
+                "com.android.microfuchsia",
             ],
         },
     },
@@ -150,6 +151,7 @@
             apex_available: [
                 "com.android.virt",
                 "com.android.compos",
+                "com.android.microfuchsia",
             ],
         },
     },
diff --git a/android/vm/src/run.rs b/android/vm/src/run.rs
index cb15802..b3743ae 100644
--- a/android/vm/src/run.rs
+++ b/android/vm/src/run.rs
@@ -36,7 +36,7 @@
 use std::fs::File;
 use std::io;
 use std::io::{Read, Write};
-use std::os::unix::io::{AsRawFd, FromRawFd};
+use std::os::fd::AsFd;
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
 use vmconfig::{get_debug_level, open_parcel_file, VmConfig};
@@ -365,16 +365,6 @@
 }
 
 /// Safely duplicate the file descriptor.
-fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
-    let fd = file.as_raw_fd();
-    // SAFETY: This just duplicates a file descriptor which we know to be valid, and we check for an
-    // an error.
-    let dup_fd = unsafe { libc::dup(fd) };
-    if dup_fd < 0 {
-        Err(io::Error::last_os_error())
-    } else {
-        // SAFETY: We have just duplicated the file descriptor so we own it, and `from_raw_fd` takes
-        // ownership of it.
-        Ok(unsafe { File::from_raw_fd(dup_fd) })
-    }
+fn duplicate_fd<T: AsFd>(file: T) -> io::Result<File> {
+    Ok(file.as_fd().try_clone_to_owned()?.into())
 }
diff --git a/docs/device_trees.md b/docs/device_trees.md
new file mode 100644
index 0000000..003e7be
--- /dev/null
+++ b/docs/device_trees.md
@@ -0,0 +1,211 @@
+# Device Trees in AVF
+
+This document aims to provide a centralized overview of the way the Android
+Virtualization Framework (AVF) composes and validates the device tree (DT)
+received by protected guest kernels, such as [Microdroid].
+
+[Microdroid]: ../guest/microdroid/README.md
+
+## Context
+
+As of Android 15, AVF only supports protected virtual machines (pVMs) on
+AArch64. On this architecture, the Linux kernel and many other embedded projects
+have adopted the [device tree format][dtspec] as the way to describe the
+platform to the software. This includes so-called "[platform devices]" (which are
+non-discoverable MMIO-based devices), CPUs (number, characteristics, ...),
+memory (address and size), and more.
+
+With virtualization, it is common for the virtual machine manager (VMM, e.g.
+crosvm or QEMU), typically a host userspace process, to generate the DT as it
+configures the virtual platform. In the case of AVF, the threat model prevents
+the guest from trusting the host and therefore the DT must be validated by a
+trusted entity. To avoid adding extra logic in the highly-privileged hypervisor,
+AVF relies on [pvmfw], a small piece of code that runs in the context of the
+guest (but before the guest kernel), loaded by the hypervisor, which validates
+the untrusted device tree. If any anomaly is detected, pvmfw aborts the boot of
+the guest. As a result, the guest kernel can trust the DT it receives.
+
+The DT sanitized by pvmfw is received by guests following the [Linux boot
+protocol][booting.txt] and includes both virtual and physical devices, which are
+hardly distinguishable from the guest's perspective (although the context could
+provide information helping to identify the nature of the device e.g. a
+virtio-blk device is likely to be virtual while a platform accelerator would be
+physical). The guest is not expected to treat physical devices differently from
+virtual devices and this distinction is therefore not relevant.
+
+```
+┌────────┐               ┌───────┐ valid              ┌───────┐
+│ crosvm ├──{input DT}──►│ pvmfw ├───────{guest DT}──►│ guest │
+└────────┘               └───┬───┘                    └───────┘
+                             │   invalid
+                             └───────────► SYSTEM RESET
+```
+
+[dtspec]: https://www.devicetree.org/specifications
+[platform devices]: https://docs.kernel.org/driver-api/driver-model/platform.html
+[pvmfw]: ../guest/pvmfw/README.md
+[booting.txt]: https://www.kernel.org/doc/Documentation/arm64/booting.txt
+
+## Device Tree Generation (Host-side)
+
+crosvm describes the virtual platform to the guest by generating a DT
+enumerating the memory region, virtual CPUs, virtual devices, and other
+properties (e.g. ramdisk, cmdline, ...). For physical devices (assigned using
+VFIO), it generates simple nodes describing the fundamental properties it
+configures for the devices i.e. `<reg>`, `<interrupts>`, `<iommus>`
+(respectively referring to IPA ranges, vIRQs, and pvIOMMUs).
+
+It is possible for the caller of crosvm to pass more DT properties or nodes to
+the guest by providing device tree overlays (DTBO) to crosvm. These overlays get
+applied after the DT describing the configured platform has been generated, the
+final result getting passed to the guest.
+
+For physical devices, crosvm supports applying a "filtered" subset of the DTBO
+received, where subnodes are only kept if they have a label corresponding to an
+assigned VFIO device. This allows the caller to always pass the same overlay,
+irrespective of which physical devices are being assigned, greatly simplifying
+the logic of the caller. This makes it possible for crosvm to support complex
+nodes for physical devices without including device-specific logic as any extra
+property (e.g. `<compatible>`) will be passed through the overlay and added to
+the final DT in a generic way. This _vm DTBO_ is read from an AVB-verified
+partition (see `ro.boot.hypervisor.vm_dtbo_idx`).
+
+Otherwise, if the `filter` option is not used, crosvm applies the overlay fully.
+This can be used to supplement the guest DT with nodes and properties which are
+not tied to particular assigned physical devices or emulated virtual devices. In
+particular, `virtualizationservice` currently makes use of it to pass
+AVF-specific properties.
+
+```
+            ┌─►{DTBO,filter}─┐
+┌─────────┐ │                │  ┌────────┐
+│ virtmgr ├─┼────►{DTBO}─────┼─►│ crosvm ├───►{guest DT}───► ...
+└─────────┘ │                │  └────────┘
+            └─►{VFIO sysfs}──┘
+```
+
+## Device Tree Sanitization
+
+pvmfw intercepts the boot sequence of the guest and locates the DT generated by
+the VMM through the VMM-guest ABI. A design goal of pvmfw is to have as little
+side-effect as possible on the guest so that the VMM can keep the illusion that
+it configured and booted the guest directly and the guest does not need to rely
+or expect pvmfw to have performed any noticeable work (a noteworthy exception
+being the memory region describing the [DICE chain]). As a result, both VMM and
+guest can mostly use the same logic between protected and non-protected VMs
+(where pvmfw does not run) and keep the simpler VMM-guest execution model they
+are used to. In the context of pvmfw and DT validation, the final DT passed by
+crosvm to the guest is typically referred to as the _input DT_.
+
+```
+┌────────┐                  ┌───────┐                  ┌───────┐
+│ crosvm ├───►{input DT}───►│ pvmfw │───►{guest DT}───►│ guest │
+└────────┘                  └───────┘                  └───────┘
+                              ▲   ▲
+   ┌─────┐  ┌─►{VM DTBO}──────┘   │
+   │ ABL ├──┤                     │
+   └─────┘  └─►{ref. DT}──────────┘
+```
+
+[DICE chain]: ../guest/pvmfw/README.md#virtual-platform-dice-chain-handover
+
+### Virtual Platform
+
+The DT sanitization policy in pvmfw matches the virtual platform defined by
+crosvm and its implementation is therefore tightly coupled with it (this is one
+reason why AVF expects pvmfw and the VMM to be updated in sync). It covers
+fundamental properties of the platform (e.g.  location of main memory,
+properties of CPUs, layout of the interrupt controller, ...) and the properties
+of (sometimes optional) virtual devices supported by crosvm and used by AVF
+guests.
+
+### Physical Devices
+
+To support device assignment, pvmfw needs to be able to validate physical
+platform-specific device properties. To achieve this in a platform-agnostic way,
+pvmfw receives a DT overlay (called the _VM DTBO_) from the Android Bootloader
+(ABL), containing a description of all the assignable devices. By detecting
+which devices have been assigned using platform-specific reserved DT labels, it
+can validate the properties of the physical devices through [generic logic].
+pvmfw also verifies with the hypervisor that the guest addresses from the DT
+have been properly mapped to the expected physical addresses of the devices; see
+[_Getting started with device assignment_][da.md].
+
+Note that, as pvmfw runs within the context of an individual pVM, it cannot
+detect abuses by the host of device assignment across guests (e.g.
+simultaneously assigning the same device to multiple guests), and it is the
+responsibility of the hypervisor to enforce this isolation. AVF also relies on
+the hypervisor to clear the state of the device on donation and (most
+importantly) on return to the host so that pvmfw does not need to access the
+assigned devices.
+
+[generic logic]: ../guest/pvmfw/src/device_assignment.rs
+[da.md]: ../docs/device_assignment.md
+
+### Extra Properties (Security-Sensitive)
+
+Some AVF use-cases require passing platform-specific inputs to protected guests.
+If these are security-sensitive, they must also be validated before being used
+by the guest. In most cases, the DT property is platform-agnostic (and supported
+by the generic guest) but its value is platform-specific. The _reference DT_ is
+an [input of pvmfw][pvmfw-config] (received from the loader) and used to
+validate DT entries which are:
+
+- security-sensitive: the host should not be able to tamper with these values
+- not confidential: the property is visible to the host (as it generates it)
+- Same across VMs: the property (if present) must be same across all instances
+- possibly optional: pvmfw does not abort the boot if the entry is missing
+
+[pvmfw-config]: ../guest/pvmfw/README.md#configuration-data-format
+
+### Extra Properties (Host-Generated)
+
+Finally, to allow the host to generate values that vary between guests (and
+which therefore can't be described using one the previous mechanisms), pvmfw
+treats the subtree of the input DT at path `/avf/untrusted` differently: it only
+performs minimal sanitization on it, allowing the host to pass arbitrary,
+unsanitized DT entries. Therefore, this subtree must be used with extra
+validation by guests e.g. only accessed by path (where the name, "`untrusted`",
+acts as a reminder), with no assumptions about the presence or correctness of
+nodes or properties, without expecting properties to be well-formed, ...
+
+In particular, pvmfw prevents other nodes from linking to this subtree
+(`<phandle>` is rejected) and limits the risk of guests unexpectedly parsing it
+other than by path (`<compatible>` is also rejected) but guests must not support
+non-standard ways of binding against nodes by property as they would then be
+vulnerable to attacks from a malicious host.
+
+### Implementation details
+
+DT sanitization is currently implemented in pvmfw by parsing the input DT into
+temporary data structures and pruning a built-in device tree (called the
+_platform DT_; see [platform.dts]) accordingly. For device assignment, it prunes
+the received VM DTBO to only keep the devices that have actually been assigned
+(as the overlay contains all assignable devices of the platform).
+
+[platform.dts]: ../guest/pvmfw/platform.dts
+
+## DT for guests
+
+### AVF-specific properties and nodes
+
+For Microdroid and other AVF guests, some special DT entries are defined:
+
+- the `/chosen/avf,new-instance` flag, set when pvmfw triggered the generation
+  of a new set of CDIs (see DICE) _i.e._ the pVM instance was booted for the
+  first time. This should be used by the next stages to synchronise the
+  generation of new CDIs and detect a malicious host attempting to force only
+  one stage to do so. This property becomes obsolete (and might not be set) when
+  [deferred rollback protection] is used by the guest kernel;
+
+- the `/chosen/avf,strict-boot` flag, always set for protected VMs and can be
+  used by guests to enable extra validation;
+
+- the `/avf/untrusted/defer-rollback-protection` flag controls [deferred
+  rollback protection] on devices and for guests which support it;
+
+- the host-allocated `/avf/untrusted/instance-id` is used to assign a unique
+  identifier to the VM instance & is used for differentiating VM secrets as well
+  as by guest OS to index external storage such as Secretkeeper.
+
+[deferred rollback protection]: ../docs/updatable_vm.md#deferring-rollback-protection
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index cc5ae71..4712d77 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -405,32 +405,25 @@
 ### Handover ABI
 
 After verifying the guest kernel, pvmfw boots it using the Linux ABI described
-above. It uses the device tree to pass the following:
+above. It uses the device tree to pass [AVF-specific properties][dt.md] and the
+DICE chain:
 
-- a reserved memory node containing the produced DICE chain:
-
-    ```
-    / {
-        reserved-memory {
-            #address-cells = <0x02>;
-            #size-cells = <0x02>;
-            ranges;
-            dice {
-                compatible = "google,open-dice";
-                no-map;
-                reg = <0x0 0x7fe0000>, <0x0 0x1000>;
-            };
+```
+/ {
+    reserved-memory {
+        #address-cells = <0x02>;
+        #size-cells = <0x02>;
+        ranges;
+        dice {
+            compatible = "google,open-dice";
+            no-map;
+            reg = <0x0 0x7fe0000>, <0x0 0x1000>;
         };
     };
-    ```
+};
+```
 
-- the `/chosen/avf,new-instance` flag, set when pvmfw generated a new secret
-  (_i.e._ the pVM instance was booted for the first time). This should be used
-  by the next stages to ensure that an attacker isn't trying to force new
-  secrets to be generated by one stage, in isolation;
-
-- the `/chosen/avf,strict-boot` flag, always set and can be used by guests to
-  enable extra validation
+[dt.md]: ../docs/device_trees.md#avf_specific-properties-and-nodes
 
 ### Guest Image Signing
 
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index a90adea..7c0d9dc 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -71,6 +71,7 @@
     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;
diff --git a/libs/libvmclient/Android.bp b/libs/libvmclient/Android.bp
index 96fe667..9fdeaf8 100644
--- a/libs/libvmclient/Android.bp
+++ b/libs/libvmclient/Android.bp
@@ -21,6 +21,7 @@
     ],
     apex_available: [
         "com.android.compos",
+        "com.android.microfuchsia",
         "com.android.virt",
     ],
 }
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index 88072a7..fe86504 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -45,6 +45,7 @@
 use shared_child::SharedChild;
 use std::io::{self, Read};
 use std::process::Command;
+use std::process::Stdio;
 use std::{
     fmt::{self, Debug, Formatter},
     fs::File,
@@ -90,16 +91,16 @@
         let (client_fd, server_fd) = posix_socketpair()?;
 
         let mut command = Command::new(VIRTMGR_PATH);
+        command.stdin(Stdio::null());
+        command.stdout(Stdio::null());
+        command.stderr(Stdio::null());
+        // Can't use BorrowedFd as it doesn't implement Display
         command.arg("--rpc-server-fd").arg(format!("{}", server_fd.as_raw_fd()));
         command.arg("--ready-fd").arg(format!("{}", ready_fd.as_raw_fd()));
-        command.preserved_fds(vec![server_fd.as_raw_fd(), ready_fd.as_raw_fd()]);
+        command.preserved_fds(vec![server_fd, ready_fd]);
 
         SharedChild::spawn(&mut command)?;
 
-        // Drop FDs that belong to virtmgr.
-        drop(server_fd);
-        drop(ready_fd);
-
         // Wait for the child to signal that the RpcBinder server is ready
         // by closing its end of the pipe.
         let _ignored = File::from(wait_fd).read(&mut [0]);
diff --git a/microfuchsia/README.md b/microfuchsia/README.md
new file mode 100644
index 0000000..82de725
--- /dev/null
+++ b/microfuchsia/README.md
@@ -0,0 +1,30 @@
+# Microfuchsia
+
+Microfuchsia is an experimental solution for running trusted applications on
+pkvm using the Android Virtualization Framework (AVF).
+
+# How to use
+
+Add the `com.android.microfuchsia` apex to your product.
+
+```
+PRODUCT_PACKAGES += com.android.microfuchsia
+```
+
+Define and add a `com.android.microfuchsia.images` apex to hold the images.
+
+```
+PRODUCT_PACKAGES += com.android.microfuchsia.images
+```
+
+This apex must have a prebuilt `fuchsia.zbi` in `/etc/fuchsia.zbi` and a boot
+shim in `/etc/linux-arm64-boot-shim.bin`.
+
+# Using the console
+
+This command will open the console for the first VM running in AVF, and can be
+used to connect to the microfuchsia console.
+
+```
+adb shell -t /apex/com.android.virt/bin/vm console
+```
diff --git a/microfuchsia/apex/Android.bp b/microfuchsia/apex/Android.bp
new file mode 100644
index 0000000..eddda9f
--- /dev/null
+++ b/microfuchsia/apex/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 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.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+apex {
+    name: "com.android.microfuchsia",
+    manifest: "manifest.json",
+    key: "com.android.microfuchsia.key",
+
+    // Allows us to specify a file_contexts in our own repository.
+    system_ext_specific: true,
+    file_contexts: "com.android.microfuchsia-file_contexts",
+
+    updatable: false,
+    future_updatable: false,
+    platform_apis: true,
+
+    binaries: [
+        // A daemon that starts on bootup that launches microfuchsia in AVF.
+        "microfuchsiad",
+    ],
+
+    prebuilts: [
+        // An init script to launch the microfuchsiad daemon on bootup which
+        // launches the microfuchsia VM in AVF.
+        "com.android.microfuchsia.init.rc",
+    ],
+}
+
+apex_key {
+    name: "com.android.microfuchsia.key",
+    public_key: "com.android.microfuchsia.avbpubkey",
+    private_key: "com.android.microfuchsia.pem",
+}
+
+prebuilt_etc {
+    name: "com.android.microfuchsia.init.rc",
+    src: "microfuchsia.rc",
+    filename: "init.rc",
+    installable: false,
+}
diff --git a/microfuchsia/apex/com.android.microfuchsia-file_contexts b/microfuchsia/apex/com.android.microfuchsia-file_contexts
new file mode 100644
index 0000000..13d7286
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia-file_contexts
@@ -0,0 +1,2 @@
+(/.*)?                   u:object_r:system_file:s0
+/bin/microfuchsiad       u:object_r:microfuchsiad_exec:s0
diff --git a/microfuchsia/apex/com.android.microfuchsia.avbpubkey b/microfuchsia/apex/com.android.microfuchsia.avbpubkey
new file mode 100644
index 0000000..10d4b88
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia.avbpubkey
Binary files differ
diff --git a/microfuchsia/apex/com.android.microfuchsia.pem b/microfuchsia/apex/com.android.microfuchsia.pem
new file mode 100644
index 0000000..541fa80
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+wa28hvmts5NyMy819GfPGqdRa14=
+-----END PRIVATE KEY-----
diff --git a/microfuchsia/apex/manifest.json b/microfuchsia/apex/manifest.json
new file mode 100644
index 0000000..b7ea23b
--- /dev/null
+++ b/microfuchsia/apex/manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.microfuchsia",
+  "version": 1
+}
diff --git a/microfuchsia/apex/microfuchsia.rc b/microfuchsia/apex/microfuchsia.rc
new file mode 100644
index 0000000..2b19ed3
--- /dev/null
+++ b/microfuchsia/apex/microfuchsia.rc
@@ -0,0 +1,22 @@
+# Copyright (C) 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.
+
+service microfuchsiad /apex/com.android.microfuchsia/bin/microfuchsiad
+    class main
+    user root
+    group system
+    # We need SYS_NICE in order to allow the crosvm child process to use it.
+    # (b/322197421). composd itself never uses it (and isn't allowed to by
+    # SELinux).
+    capabilities SYS_NICE
diff --git a/microfuchsia/microfuchsiad/Android.bp b/microfuchsia/microfuchsiad/Android.bp
new file mode 100644
index 0000000..ddf360d
--- /dev/null
+++ b/microfuchsia/microfuchsiad/Android.bp
@@ -0,0 +1,25 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// A daemon that launches microfuchsia in AVF.
+rust_binary {
+    name: "microfuchsiad",
+    srcs: ["src/main.rs"],
+    edition: "2021",
+    prefer_rlib: true,
+    defaults: ["avf_build_flags_rust"],
+    rustlibs: [
+        "android.system.microfuchsiad-rust",
+        "android.system.virtualizationservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libbinder_rs",
+        "liblog_rust",
+        "liblibc",
+        "libvmclient",
+    ],
+    apex_available: [
+        "com.android.microfuchsia",
+    ],
+}
diff --git a/microfuchsia/microfuchsiad/aidl/Android.bp b/microfuchsia/microfuchsiad/aidl/Android.bp
new file mode 100644
index 0000000..02bb7c6
--- /dev/null
+++ b/microfuchsia/microfuchsiad/aidl/Android.bp
@@ -0,0 +1,24 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "android.system.microfuchsiad",
+    srcs: ["android/system/microfuchsiad/*.aidl"],
+    // TODO: Make this stable when the APEX becomes updatable.
+    unstable: true,
+    backend: {
+        java: {
+            enabled: false,
+        },
+        ndk: {
+            enabled: false,
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.microfuchsia",
+            ],
+        },
+    },
+}
diff --git a/microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl b/microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl
new file mode 100644
index 0000000..a04ae2b
--- /dev/null
+++ b/microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+package android.system.microfuchsiad;
+
+// This service exists as a placeholder in case we want to communicate with the
+// daemon in the future.
+interface IMicrofuchsiaService {
+}
diff --git a/microfuchsia/microfuchsiad/src/instance_manager.rs b/microfuchsia/microfuchsiad/src/instance_manager.rs
new file mode 100644
index 0000000..5082e50
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/instance_manager.rs
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! Manages running instances of the Microfuchsia VM.
+//! At most one instance should be running at a time.
+
+use crate::instance_starter::{InstanceStarter, MicrofuchsiaInstance};
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
+use anyhow::{bail, Result};
+use binder::Strong;
+use virtualizationservice::IVirtualizationService::IVirtualizationService;
+
+pub struct InstanceManager {
+    service: Strong<dyn IVirtualizationService>,
+    started: bool,
+}
+
+impl InstanceManager {
+    pub fn new(service: Strong<dyn IVirtualizationService>) -> Self {
+        Self { service, started: false }
+    }
+
+    pub fn start_instance(&mut self) -> Result<MicrofuchsiaInstance> {
+        if self.started {
+            bail!("Cannot start multiple microfuchsia instances");
+        }
+
+        let instance_starter = InstanceStarter::new("Microfuchsia", 0);
+        let instance = instance_starter.start_new_instance(&*self.service);
+
+        if instance.is_ok() {
+            self.started = true;
+        }
+        instance
+    }
+}
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
new file mode 100644
index 0000000..15fcc06
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! Responsible for starting an instance of the Microfuchsia VM.
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
+    VirtualMachineConfig::VirtualMachineConfig, VirtualMachineRawConfig::VirtualMachineRawConfig,
+};
+use anyhow::{ensure, Context, Result};
+use binder::{LazyServiceGuard, ParcelFileDescriptor};
+use log::info;
+use std::ffi::CStr;
+use std::fs::File;
+use std::os::fd::FromRawFd;
+use vmclient::VmInstance;
+
+pub struct MicrofuchsiaInstance {
+    _vm_instance: VmInstance,
+    _lazy_service_guard: LazyServiceGuard,
+    _pty: Pty,
+}
+
+pub struct InstanceStarter {
+    instance_name: String,
+    instance_id: u8,
+}
+
+impl InstanceStarter {
+    pub fn new(instance_name: &str, instance_id: u8) -> Self {
+        Self { instance_name: instance_name.to_owned(), instance_id }
+    }
+
+    pub fn start_new_instance(
+        &self,
+        virtualization_service: &dyn IVirtualizationService,
+    ) -> Result<MicrofuchsiaInstance> {
+        info!("Creating {} instance", self.instance_name);
+
+        // Always use instance id 0, because we will only ever have one instance.
+        let mut instance_id = [0u8; 64];
+        instance_id[0] = self.instance_id;
+
+        // Open the kernel and initrd files from the microfuchsia.images apex.
+        let kernel_fd =
+            File::open("/apex/com.android.microfuchsia.images/etc/linux-arm64-boot-shim.bin")
+                .context("Failed to open the boot-shim")?;
+        let initrd_fd = File::open("/apex/com.android.microfuchsia.images/etc/fuchsia.zbi")
+            .context("Failed to open the fuchsia ZBI")?;
+        let kernel = Some(ParcelFileDescriptor::new(kernel_fd));
+        let initrd = Some(ParcelFileDescriptor::new(initrd_fd));
+
+        // Prepare a pty for console input/output.
+        let pty = openpty()?;
+        let console_in = Some(pty.leader.try_clone().context("cloning pty")?);
+        let console_out = Some(pty.leader.try_clone().context("cloning pty")?);
+
+        let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
+            name: "Microfuchsia".into(),
+            instanceId: instance_id,
+            kernel,
+            initrd,
+            params: None,
+            bootloader: None,
+            disks: vec![],
+            protectedVm: false,
+            memoryMib: 256,
+            cpuTopology: CpuTopology::ONE_CPU,
+            platformVersion: "1.0.0".into(),
+            // Fuchsia uses serial for console by default.
+            consoleInputDevice: Some("ttyS0".into()),
+            ..Default::default()
+        });
+        let vm_instance = VmInstance::create(
+            virtualization_service,
+            &config,
+            console_out,
+            console_in,
+            /* log= */ None,
+            None,
+        )
+        .context("Failed to create VM")?;
+        vm_instance
+            .vm
+            .setHostConsoleName(&pty.follower_name)
+            .context("Setting host console name")?;
+        vm_instance.start().context("Starting VM")?;
+
+        Ok(MicrofuchsiaInstance {
+            _vm_instance: vm_instance,
+            _lazy_service_guard: Default::default(),
+            _pty: pty,
+        })
+    }
+}
+
+struct Pty {
+    leader: File,
+    follower_name: String,
+}
+
+/// Opens a pseudoterminal (pty), configures it to be a raw terminal, and returns the file pair.
+fn openpty() -> Result<Pty> {
+    // Create a pty pair.
+    let mut leader: libc::c_int = -1;
+    let mut _follower: libc::c_int = -1;
+    let mut follower_name: Vec<libc::c_char> = vec![0; 32];
+
+    // SAFETY: calling openpty with valid+initialized variables is safe.
+    // The two null pointers are valid inputs for openpty.
+    unsafe {
+        ensure!(
+            libc::openpty(
+                &mut leader,
+                &mut _follower,
+                follower_name.as_mut_ptr(),
+                std::ptr::null_mut(),
+                std::ptr::null_mut(),
+            ) == 0,
+            "failed to openpty"
+        );
+    }
+
+    // SAFETY: calling these libc functions with valid+initialized variables is safe.
+    unsafe {
+        // Fetch the termios attributes.
+        let mut attr = libc::termios {
+            c_iflag: 0,
+            c_oflag: 0,
+            c_cflag: 0,
+            c_lflag: 0,
+            c_line: 0,
+            c_cc: [0u8; 19],
+        };
+        ensure!(libc::tcgetattr(leader, &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,
+            "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 })
+}
diff --git a/microfuchsia/microfuchsiad/src/main.rs b/microfuchsia/microfuchsiad/src/main.rs
new file mode 100644
index 0000000..ec290cc
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/main.rs
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! A daemon that can be launched on bootup that runs microfuchsia in AVF.
+//! An on-demand binder service is also prepared in case we want to communicate with the daemon in
+//! the future.
+
+mod instance_manager;
+mod instance_starter;
+mod service;
+
+use crate::instance_manager::InstanceManager;
+use anyhow::{Context, Result};
+use binder::{register_lazy_service, ProcessState};
+use log::{error, info};
+
+#[allow(clippy::eq_op)]
+fn try_main() -> Result<()> {
+    let debuggable = env!("TARGET_BUILD_VARIANT") != "user";
+    let log_level = if debuggable { log::LevelFilter::Debug } else { log::LevelFilter::Info };
+    android_logger::init_once(
+        android_logger::Config::default().with_tag("microfuchsiad").with_max_level(log_level),
+    );
+
+    ProcessState::start_thread_pool();
+
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    let virtualization_service =
+        virtmgr.connect().context("Failed to connect to VirtualizationService")?;
+
+    let instance_manager = InstanceManager::new(virtualization_service);
+    let service = service::new_binder(instance_manager);
+    register_lazy_service("android.system.microfuchsiad", service.as_binder())
+        .context("Registering microfuchsiad service")?;
+
+    info!("Registered services, joining threadpool");
+    ProcessState::join_thread_pool();
+
+    info!("Exiting");
+    Ok(())
+}
+
+fn main() {
+    if let Err(e) = try_main() {
+        error!("{:?}", e);
+        std::process::exit(1)
+    }
+}
diff --git a/microfuchsia/microfuchsiad/src/service.rs b/microfuchsia/microfuchsiad/src/service.rs
new file mode 100644
index 0000000..a2112b1
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/service.rs
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! Implementation of IMicrofuchsiaService that runs microfuchsia in AVF when
+//! created.
+
+use crate::instance_manager::InstanceManager;
+use crate::instance_starter::MicrofuchsiaInstance;
+use android_system_microfuchsiad::aidl::android::system::microfuchsiad::IMicrofuchsiaService::{
+    BnMicrofuchsiaService, IMicrofuchsiaService,
+};
+use anyhow::Context;
+use binder::{self, BinderFeatures, Interface, Strong};
+
+#[allow(unused)]
+pub struct MicrofuchsiaService {
+    instance_manager: InstanceManager,
+    microfuchsia: MicrofuchsiaInstance,
+}
+
+pub fn new_binder(mut instance_manager: InstanceManager) -> Strong<dyn IMicrofuchsiaService> {
+    let microfuchsia = instance_manager.start_instance().context("Starting Microfuchsia").unwrap();
+    let service = MicrofuchsiaService { instance_manager, microfuchsia };
+    BnMicrofuchsiaService::new_binder(service, BinderFeatures::default())
+}
+
+impl Interface for MicrofuchsiaService {}
+
+impl IMicrofuchsiaService for MicrofuchsiaService {}
diff --git a/tests/authfs/common/src/open_then_run.rs b/tests/authfs/common/src/open_then_run.rs
index e5e33eb..a9004b0 100644
--- a/tests/authfs/common/src/open_then_run.rs
+++ b/tests/authfs/common/src/open_then_run.rs
@@ -24,7 +24,7 @@
 use log::{debug, error};
 use std::fs::OpenOptions;
 use std::os::unix::fs::OpenOptionsExt;
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{OwnedFd, RawFd};
 use std::process::Command;
 
 // `PseudoRawFd` is just an integer and not necessarily backed by a real FD. It is used to denote
@@ -38,8 +38,8 @@
 }
 
 impl OwnedFdMapping {
-    fn as_fd_mapping(&self) -> FdMapping {
-        FdMapping { parent_fd: self.owned_fd.as_raw_fd(), child_fd: self.target_fd }
+    fn into_fd_mapping(self) -> FdMapping {
+        FdMapping { parent_fd: self.owned_fd, child_fd: self.target_fd }
     }
 }
 
@@ -148,9 +148,9 @@
 
     // Set up FD mappings in the child process.
     let mut fd_mappings = Vec::new();
-    fd_mappings.extend(args.ro_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
-    fd_mappings.extend(args.rw_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
-    fd_mappings.extend(args.dir_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+    fd_mappings.extend(args.ro_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+    fd_mappings.extend(args.rw_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+    fd_mappings.extend(args.dir_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
     command.fd_mappings(fd_mappings)?;
 
     debug!("Spawning {:?}", command);