Merge "Validate collecting guest_time in pkvm hypervisor only" into main
diff --git a/OWNERS b/OWNERS
index e560cec..40c709f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,7 +4,6 @@
 #
 # If you are not a member of the project please send review requests
 # to one of those listed below.
-dbrazdil@google.com
 jiyong@google.com
 smoreland@google.com
 willdeacon@google.com
@@ -13,6 +12,7 @@
 alanstokes@google.com
 aliceywang@google.com
 inseob@google.com
+ioffe@google.com
 jaewan@google.com
 jakobvukalovic@google.com
 jeffv@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index 99b2dee..43819dc 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -47,7 +47,7 @@
         "release_avf_enable_device_assignment",
         "release_avf_enable_llpvm_changes",
         "release_avf_enable_network",
-        "release_avf_enable_remote_attestation",
+        "avf_remote_attestation_enabled",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
         "release_avf_support_custom_vm_with_paravirtualized_devices",
@@ -204,7 +204,7 @@
                 },
             },
         },
-        release_avf_enable_remote_attestation: {
+        avf_remote_attestation_enabled: {
             vintf_fragments: [
                 "virtualizationservice.xml",
             ],
@@ -235,7 +235,7 @@
     config_namespace: "ANDROID",
     bool_variables: [
         "release_avf_enable_llpvm_changes",
-        "release_avf_enable_remote_attestation",
+        "avf_remote_attestation_enabled",
     ],
     properties: ["srcs"],
 }
@@ -247,7 +247,7 @@
         release_avf_enable_llpvm_changes: {
             srcs: ["virtualizationservice.rc.llpvm"],
         },
-        release_avf_enable_remote_attestation: {
+        avf_remote_attestation_enabled: {
             srcs: ["virtualizationservice.rc.ra"],
         },
     },
diff --git a/apex/vmnic.rc b/apex/vmnic.rc
index 486f387..f5dfd99 100644
--- a/apex/vmnic.rc
+++ b/apex/vmnic.rc
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 service vmnic /apex/com.android.virt/bin/vmnic
-    user system
-    group system
+    user root
+    group vpn
     interface aidl android.system.virtualizationservice_internal.IVmnic
     disabled
     oneshot
diff --git a/compos/apk/assets/vm_config.json b/compos/apk/assets/vm_config.json
index 1f5cdba..28e0f07 100644
--- a/compos/apk/assets/vm_config.json
+++ b/compos/apk/assets/vm_config.json
@@ -27,5 +27,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_staged.json b/compos/apk/assets/vm_config_staged.json
index 37b1d7a..afc3767 100644
--- a/compos/apk/assets/vm_config_staged.json
+++ b/compos/apk/assets/vm_config_staged.json
@@ -28,5 +28,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_system_ext.json b/compos/apk/assets/vm_config_system_ext.json
index 1ef43f0..730f592 100644
--- a/compos/apk/assets/vm_config_system_ext.json
+++ b/compos/apk/assets/vm_config_system_ext.json
@@ -30,5 +30,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_system_ext_staged.json b/compos/apk/assets/vm_config_system_ext_staged.json
index 9103a9e..6d91aa2 100644
--- a/compos/apk/assets/vm_config_system_ext_staged.json
+++ b/compos/apk/assets/vm_config_system_ext_staged.json
@@ -31,5 +31,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 0825f06..5511758 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -209,6 +209,14 @@
 $ adb unroot
 ```
 
+If virt apex is Google-signed, you need to enable the app and grant the
+permission to the app.
+```
+$ adb root
+$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivity
+$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+$ adb unroot
+```
 Then execute the below to set up the network. In the future, this step won't be necessary.
 
 ```
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 835dcac..3483351 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -106,3 +106,18 @@
     normal mode.
 -   The `vmComponents` field contains a list of all the APKs and apexes loaded
     by the pVM.
+
+## To Support It
+
+VM remote attestation is a strongly recommended feature from Android V. To support
+it, you only need to provide a valid VM DICE chain satisfying the following
+requirements:
+
+- The DICE chain must have a UDS-rooted public key registered at the RKP factory.
+- The DICE chain should have RKP VM markers that help identify RKP VM as required
+  by the [remote provisioning HAL][rkp-hal-markers].
+
+The feature is enabled by default. To disable it, you can set
+`PRODUCT_AVF_REMOTE_ATTESTATION_DISABLED` to true in your Makefile.
+
+[rkp-hal-markers]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md#hal
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 2f6e306..43f3db0 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -42,8 +42,8 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.FlaggedApi;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -63,8 +63,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+import android.system.ErrnoException;
+import android.system.OsConstants;
 import android.system.virtualizationcommon.DeathReason;
 import android.system.virtualizationcommon.ErrorCode;
 import android.system.virtualizationservice.IVirtualMachine;
@@ -78,13 +78,17 @@
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
 import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.system.virtualmachine.flags.Flags;
 
 import libcore.io.IoBridge;
+import libcore.io.IoUtils;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -97,17 +101,20 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.zip.ZipFile;
@@ -320,6 +327,10 @@
 
     private final boolean mVmConsoleInputSupported;
 
+    private final boolean mConnectVmConsole;
+
+    private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();
+
     /** The configuration that is currently associated with this VM. */
     @GuardedBy("mLock")
     @NonNull
@@ -348,6 +359,26 @@
 
     @GuardedBy("mLock")
     @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtyFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtsFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private String mPtsName;
+
+    @GuardedBy("mLock")
+    @Nullable
     private ParcelFileDescriptor mLogReader;
 
     @GuardedBy("mLock")
@@ -417,6 +448,7 @@
 
         mVmOutputCaptured = config.isVmOutputCaptured();
         mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+        mConnectVmConsole = config.isConnectVmConsole();
     }
 
     /**
@@ -1128,6 +1160,10 @@
             IVirtualizationService service = mVirtualizationService.getBinder();
 
             try {
+                if (mConnectVmConsole) {
+                    createPtyConsole();
+                }
+
                 if (mVmOutputCaptured) {
                     createVmOutputPipes();
                 }
@@ -1136,6 +1172,38 @@
                     createVmInputPipes();
                 }
 
+                ParcelFileDescriptor consoleOutFd = null;
+                if (mConnectVmConsole && mVmOutputCaptured) {
+                    // If we are enabling output pipes AND the host console, then we tee the console
+                    // output to both.
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mTeeConsoleOutReader = pipe[0];
+                    mTeeConsoleOutWriter = pipe[1];
+                    consoleOutFd = mTeeConsoleOutWriter;
+                    TeeWorker tee =
+                            new TeeWorker(
+                                    mName + " console",
+                                    new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
+                                    List.of(
+                                            new FileOutputStream(mPtyFd.getFileDescriptor()),
+                                            new FileOutputStream(
+                                                    mConsoleOutWriter.getFileDescriptor())));
+                    // If the VM is stopped then the tee worker thread would get an EOF or read()
+                    // error which would tear down itself.
+                    mConsoleExecutor.execute(tee);
+                } else if (mConnectVmConsole) {
+                    consoleOutFd = mPtyFd;
+                } else if (mVmOutputCaptured) {
+                    consoleOutFd = mConsoleOutWriter;
+                }
+
+                ParcelFileDescriptor consoleInFd = null;
+                if (mConnectVmConsole) {
+                    consoleInFd = mPtyFd;
+                } else if (mVmConsoleInputSupported) {
+                    consoleInFd = mConsoleInReader;
+                }
+
                 VirtualMachineConfig vmConfig = getConfig();
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
                         vmConfig.getCustomImageConfig() != null
@@ -1143,8 +1211,7 @@
                                 : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
-                        service.createVm(
-                                vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
+                        service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine.start();
@@ -1203,15 +1270,81 @@
     private void createVmInputPipes() throws VirtualMachineException {
         try {
             if (mConsoleInReader == null || mConsoleInWriter == null) {
-                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleInReader = pipe[0];
-                mConsoleInWriter = pipe[1];
+                if (mConnectVmConsole) {
+                    // If we are enabling input pipes AND the host console, then we should just use
+                    // the host pty peer end as the console write end.
+                    createPtyConsole();
+                    mConsoleInReader = mPtyFd.dup();
+                    mConsoleInWriter = mPtsFd.dup();
+                } else {
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mConsoleInReader = pipe[0];
+                    mConsoleInWriter = pipe[1];
+                }
             }
         } catch (IOException e) {
             throw new VirtualMachineException("Failed to create input stream for VM", e);
         }
     }
 
+    @FunctionalInterface
+    private static interface OpenPtyCallback {
+        public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
+    }
+
+    // Opens a pty and set the master end to raw mode and O_NONBLOCK.
+    private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
+            throws IOException;
+
+    @GuardedBy("mLock")
+    private void createPtyConsole() throws VirtualMachineException {
+        if (mPtyFd != null && mPtsFd != null) {
+            return;
+        }
+        List<FileDescriptor> fd = new ArrayList<>(2);
+        StringBuilder nameBuilder = new StringBuilder();
+        try {
+            try {
+                nativeOpenPtyRawNonblock(
+                        (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
+                            fd.add(mfd);
+                            fd.add(sfd);
+                            nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
+                        });
+            } catch (Exception e) {
+                fd.forEach(IoUtils::closeQuietly);
+                throw e;
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException(
+                    "Failed to create host console to connect to the VM console", e);
+        }
+        mPtyFd = new ParcelFileDescriptor(fd.get(0));
+        mPtsFd = new ParcelFileDescriptor(fd.get(1));
+        mPtsName = nameBuilder.toString();
+        Log.d(TAG, "Serial console device: " + mPtsName);
+    }
+
+    /**
+     * Returns the name of the peer end (ptsname) of the host console. The host console is only
+     * available if the {@link VirtualMachineConfig} specifies that a host console should
+     * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
+     *
+     * @throws VirtualMachineException if the host pseudoterminal could not be created, or
+     *     connecting to the VM console is not enabled.
+     * @hide
+     */
+    @NonNull
+    public String getHostConsoleName() throws VirtualMachineException {
+        if (!mConnectVmConsole) {
+            throw new VirtualMachineException("Host console is not enabled");
+        }
+        synchronized (mLock) {
+            createPtyConsole();
+            return mPtsName;
+        }
+    }
+
     /**
      * Returns the stream object representing the console output from the virtual machine. The
      * console output is only available if the {@link VirtualMachineConfig} specifies that it should
@@ -1811,4 +1944,61 @@
             }
         }
     }
+
+    /**
+     * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
+     *
+     * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
+     */
+    private static class TeeWorker implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final List<OutputStream> mOuts;
+
+        TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
+            mName = name;
+            mIn = in;
+            mOuts = new ArrayList<>(outs);
+        }
+
+        @Override
+        public void run() {
+            byte[] buffer = new byte[2048];
+            try {
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    for (OutputStream out : mOuts) {
+                        try {
+                            out.write(buffer, 0, len);
+                        } catch (IOException e) {
+                            // EAGAIN is expected because the file description has O_NONBLOCK flag.
+                            if (!isErrnoError(e, OsConstants.EAGAIN)) {
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Tee " + mName, e);
+            }
+        }
+
+        private static ErrnoException asErrnoException(Throwable e) {
+            if (e instanceof ErrnoException) {
+                return (ErrnoException) e;
+            } else if (e instanceof IOException) {
+                // Try to unwrap ErrnoException#rethrowAsIOException()
+                return asErrnoException(e.getCause());
+            }
+            return null;
+        }
+
+        private static boolean isErrnoError(Exception e, int expectedValue) {
+            ErrnoException errno = asErrnoException(e);
+            return errno != null && errno.errno == expectedValue;
+        }
+    }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 1b915cd..66d0f4b 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -48,7 +48,6 @@
 
 import com.android.system.virtualmachine.flags.Flags;
 
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -94,6 +93,7 @@
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole";
     private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
     private static final String KEY_OS = "os";
     private static final String KEY_EXTRA_APKS = "extraApks";
@@ -193,6 +193,9 @@
     /** Whether the app can write console input to the VM */
     private final boolean mVmConsoleInputSupported;
 
+    /** Whether to connect the VM console to a host console. */
+    private final boolean mConnectVmConsole;
+
     @Nullable private final File mVendorDiskImage;
 
     /** OS name of the VM using payload binaries. */
@@ -229,6 +232,7 @@
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
             boolean vmConsoleInputSupported,
+            boolean connectVmConsole,
             @Nullable File vendorDiskImage,
             @NonNull @OsName String os) {
         // This is only called from Builder.build(); the builder handles parameter validation.
@@ -249,6 +253,7 @@
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
+        mConnectVmConsole = connectVmConsole;
         mVendorDiskImage = vendorDiskImage;
         mOs = os;
     }
@@ -331,6 +336,7 @@
         }
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
         builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+        builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE));
 
         String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
         if (vendorDiskImagePath != null) {
@@ -384,6 +390,7 @@
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
         b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole);
         if (mVendorDiskImage != null) {
             b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
         }
@@ -544,6 +551,16 @@
     }
 
     /**
+     * Returns whether to connect the VM console to a host console.
+     *
+     * @see Builder#setConnectVmConsole
+     * @hide
+     */
+    public boolean isConnectVmConsole() {
+        return mConnectVmConsole;
+    }
+
+    /**
      * Returns the OS of the VM.
      *
      * @see Builder#setOs
@@ -577,6 +594,7 @@
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
                 && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+                && this.mConnectVmConsole == other.mConnectVmConsole
                 && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
@@ -789,6 +807,7 @@
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
+        private boolean mConnectVmConsole = false;
         @Nullable private File mVendorDiskImage;
         @NonNull @OsName private String mOs = DEFAULT_OS;
 
@@ -862,6 +881,11 @@
                 throw new IllegalStateException("debug level must be FULL to use console input");
             }
 
+            if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException(
+                        "debug level must be FULL to connect to the console");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -876,6 +900,7 @@
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
                     mVmConsoleInputSupported,
+                    mConnectVmConsole,
                     mVendorDiskImage,
                     mOs);
         }
@@ -1125,6 +1150,23 @@
         }
 
         /**
+         * Sets whether to connect the VM console to a host console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the shell to directly communicate with the VM
+         * console through the connected host console.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setConnectVmConsole(boolean supported) {
+            mConnectVmConsole = supported;
+            return this;
+        }
+
+        /**
          * Sets the path to the disk image with vendor-specific modules.
          *
          * @hide
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
index b3354cc..ed102bf 100644
--- a/java/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -17,16 +17,36 @@
 #define LOG_TAG "VirtualMachine"
 
 #include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android-base/scopeguard.h>
+#include <android-base/strings.h>
 #include <android/binder_auto_utils.h>
 #include <android/binder_ibinder_jni.h>
+#include <fcntl.h>
 #include <jni.h>
 #include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <pty.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
 
 #include <binder_rpc_unstable.hpp>
+#include <string>
 #include <tuple>
 
 #include "common.h"
 
+namespace {
+
+void throwIOException(JNIEnv *env, const std::string &msg) {
+    jniThrowException(env, "java/io/IOException", msg.c_str());
+}
+
+} // namespace
+
 extern "C" JNIEXPORT jobject JNICALL
 Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
         JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
@@ -65,3 +85,72 @@
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeOpenPtyRawNonblock(
+        JNIEnv *env, [[maybe_unused]] jclass clazz, jobject resultCallback) {
+    int pm, ps;
+    // man openpty says: "Nobody knows how much space should be reserved for name."
+    // but on modern Linux the format of the pts name is always `/dev/pts/[0-9]+`
+    // Realistically speaking, a buffer of 32 bytes leaves us with 22 digits for the pts number,
+    // which should be more than enough.
+    // NOTE: bionic implements openpty() with internal name buffer of size 32, musl 20.
+    char name[32];
+    if (openpty(&pm, &ps, name, nullptr, nullptr)) {
+        throwIOException(env, "openpty(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    fcntl(pm, F_SETFD, FD_CLOEXEC);
+    fcntl(ps, F_SETFD, FD_CLOEXEC);
+    name[sizeof(name) - 1] = '\0';
+    // Set world RW so adb shell can talk to the pts.
+    chmod(name, 0666);
+
+    if (int flags = fcntl(pm, F_GETFL, 0); flags < 0) {
+        throwIOException(env, "fcntl(F_GETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    } else if (fcntl(pm, F_SETFL, flags | O_NONBLOCK) < 0) {
+        throwIOException(env, "fcntl(F_SETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    android::base::ScopeGuard cleanup_handler([=] {
+        close(ps);
+        close(pm);
+    });
+
+    struct termios tio;
+    if (tcgetattr(pm, &tio)) {
+        throwIOException(env, "tcgetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    cfmakeraw(&tio);
+    if (tcsetattr(pm, TCSANOW, &tio)) {
+        throwIOException(env, "tcsetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    jobject mfd = jniCreateFileDescriptor(env, pm);
+    if (mfd == nullptr) {
+        return;
+    }
+    jobject sfd = jniCreateFileDescriptor(env, ps);
+    if (sfd == nullptr) {
+        return;
+    }
+    size_t len = strlen(name);
+    ScopedLocalRef<jbyteArray> ptsName(env, env->NewByteArray(len));
+    if (ptsName.get() != nullptr) {
+        env->SetByteArrayRegion(ptsName.get(), 0, len, (jbyte *)name);
+    }
+    ScopedLocalRef<jclass> callback_class(env, env->GetObjectClass(resultCallback));
+    jmethodID mid = env->GetMethodID(callback_class.get(), "apply",
+                                     "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;[B)V");
+    if (mid == nullptr) {
+        return;
+    }
+
+    env->CallVoidMethod(resultCallback, mid, mfd, sfd, ptsName.get());
+    // FD ownership is transferred to the callback, reset the auto-close hander.
+    cleanup_handler.Disable();
+}
diff --git a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
index c7bfc80..e42cdd1 100644
--- a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -16,6 +16,7 @@
 
 package android.crosvm;
 
+import android.os.ParcelFileDescriptor;
 import android.view.Surface;
 
 /**
@@ -23,7 +24,7 @@
  * display.
  */
 interface ICrosvmAndroidDisplayService {
-    void setSurface(inout Surface surface);
-
-    void removeSurface();
+    void setSurface(inout Surface surface, boolean forCursor);
+    void setCursorStream(in ParcelFileDescriptor stream);
+    void removeSurface(boolean forCursor);
 }
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 66320f3..0557127 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -35,34 +35,54 @@
     DisplayService() = default;
     virtual ~DisplayService() = default;
 
-    ndk::ScopedAStatus setSurface(Surface* surface) override {
+    ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
         {
             std::lock_guard lk(mSurfaceReadyMutex);
-            mSurface = std::make_unique<Surface>(surface->release());
+            if (forCursor) {
+                mCursorSurface = std::make_unique<Surface>(surface->release());
+            } else {
+                mSurface = std::make_unique<Surface>(surface->release());
+            }
         }
-        mSurfaceReady.notify_one();
+        mSurfaceReady.notify_all();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    ndk::ScopedAStatus removeSurface() override {
+    ndk::ScopedAStatus removeSurface(bool forCursor) override {
         {
             std::lock_guard lk(mSurfaceReadyMutex);
-            mSurface = nullptr;
+            if (forCursor) {
+                mCursorSurface = nullptr;
+            } else {
+                mSurface = nullptr;
+            }
         }
-        mSurfaceReady.notify_one();
+        mSurfaceReady.notify_all();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    Surface* getSurface() {
+    Surface* getSurface(bool forCursor) {
         std::unique_lock lk(mSurfaceReadyMutex);
-        mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
-        return mSurface.get();
+        if (forCursor) {
+            mSurfaceReady.wait(lk, [this] { return mCursorSurface != nullptr; });
+            return mCursorSurface.get();
+        } else {
+            mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
+            return mSurface.get();
+        }
+    }
+    ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
+    ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
+        mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
+        return ::ndk::ScopedAStatus::ok();
     }
 
 private:
     std::condition_variable mSurfaceReady;
     std::mutex mSurfaceReadyMutex;
     std::unique_ptr<Surface> mSurface;
+    std::unique_ptr<Surface> mCursorSurface;
+    ndk::ScopedFileDescriptor mCursorStream;
 };
 
 } // namespace
@@ -130,7 +150,7 @@
 }
 
 extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
-                                                 uint32_t height) {
+                                                 uint32_t height, bool for_cursor) {
     if (ctx->disp_service == nullptr) {
         ctx->errorf("Display service was not created");
         return nullptr;
@@ -139,7 +159,7 @@
     // where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope that we will
     // need alpha blending for the cursor surface.
     int format = HAL_PIXEL_FORMAT_BGRA_8888;
-    ANativeWindow* surface = ctx->disp_service->getSurface()->get(); // this can block
+    ANativeWindow* surface = ctx->disp_service->getSurface(for_cursor)->get(); // this can block
     if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
         ctx->errorf("Failed to set buffer gemoetry");
         return nullptr;
@@ -168,6 +188,21 @@
     return true;
 }
 
+extern "C" void set_android_surface_position(struct AndroidDisplayContext* ctx, uint32_t x,
+                                             uint32_t y) {
+    if (ctx->disp_service == nullptr) {
+        ctx->errorf("Display service was not created");
+        return;
+    }
+    auto fd = ctx->disp_service->getCursorStream().get();
+    if (fd == -1) {
+        ctx->errorf("Invalid fd");
+        return;
+    }
+    uint32_t pos[] = {x, y};
+    write(fd, pos, sizeof(pos));
+}
+
 extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
                                             ANativeWindow* surface) {
     if (ANativeWindow_unlockAndPost(surface) != 0) {
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 37a321d..144e81e 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -14,6 +14,7 @@
         "libaarch64_paging",
         "libbssl_avf_nostd",
         "libbssl_sys_nostd",
+        "libcbor_util_nostd",
         "libciborium_nostd",
         "libciborium_io_nostd",
         "libcstr",
@@ -116,9 +117,10 @@
     rustlibs: [
         "libcbor_util",
         "libciborium",
-        "libdiced_open_dice_nostd",
+        "libdiced_open_dice",
         "libpvmfw_avb_nostd",
         "libzerocopy_nostd",
+        "libhex",
     ],
 }
 
@@ -319,15 +321,22 @@
     installable: false,
 }
 
-prebuilt_etc {
+filegroup {
     name: "pvmfw_embedded_key",
-    src: ":avb_testkey_rsa4096_pub_bin",
-    installable: false,
+    srcs: [":avb_testkey_rsa4096"],
+}
+
+genrule {
+    name: "pvmfw_embedded_key_pub_bin",
+    tools: ["avbtool"],
+    srcs: [":pvmfw_embedded_key"],
+    out: ["pvmfw_embedded_key_pub.bin"],
+    cmd: "$(location avbtool) extract_public_key --key $(in) --output $(out)",
 }
 
 genrule {
     name: "pvmfw_embedded_key_rs",
-    srcs: [":pvmfw_embedded_key"],
+    srcs: [":pvmfw_embedded_key_pub_bin"],
     out: ["lib.rs"],
     cmd: "(" +
         "    echo '#![no_std]';" +
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 68acf13..99ecf8f 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -308,11 +308,11 @@
 			      GIC_PPI 0xa IRQ_TYPE_LEVEL_LOW>;
 	};
 
-	uart@2e8 {
+	uart@3f8 {
 		compatible = "ns16550a";
-		reg = <0x00 0x2e8 0x00 0x8>;
+		reg = <0x00 0x3f8 0x00 0x8>;
 		clock-frequency = <0x1c2000>;
-		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
 	};
 
 	uart@2f8 {
@@ -329,11 +329,11 @@
 		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
 	};
 
-	uart@3f8 {
+	uart@2e8 {
 		compatible = "ns16550a";
-		reg = <0x00 0x3f8 0x00 0x8>;
+		reg = <0x00 0x2e8 0x00 0x8>;
 		clock-frequency = <0x1c2000>;
-		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
 	};
 
 	psci {
diff --git a/pvmfw/src/bcc.rs b/pvmfw/src/bcc.rs
index f56e62b..7a13da7 100644
--- a/pvmfw/src/bcc.rs
+++ b/pvmfw/src/bcc.rs
@@ -27,10 +27,9 @@
 type Result<T> = core::result::Result<T, BccError>;
 
 pub enum BccError {
-    CborDecodeError(ciborium::de::Error<ciborium_io::EndOfFile>),
-    CborEncodeError(ciborium::ser::Error<core::convert::Infallible>),
+    CborDecodeError,
+    CborEncodeError,
     DiceError(diced_open_dice::DiceError),
-    ExtraneousBytes,
     MalformedBcc(&'static str),
     MissingBcc,
 }
@@ -38,10 +37,9 @@
 impl fmt::Display for BccError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Self::CborDecodeError(e) => write!(f, "Error parsing BCC CBOR: {e:?}"),
-            Self::CborEncodeError(e) => write!(f, "Error encoding BCC CBOR: {e:?}"),
+            Self::CborDecodeError => write!(f, "Error parsing BCC CBOR"),
+            Self::CborEncodeError => write!(f, "Error encoding BCC CBOR"),
             Self::DiceError(e) => write!(f, "Dice error: {e:?}"),
-            Self::ExtraneousBytes => write!(f, "Unexpected trailing data in BCC"),
             Self::MalformedBcc(s) => {
                 write!(f, "BCC does not have the expected CBOR structure: {s}")
             }
@@ -65,7 +63,7 @@
     // }
     let bcc_handover: Vec<(Value, Value)> =
         vec![(1.into(), cdi_attest.as_slice().into()), (2.into(), cdi_seal.as_slice().into())];
-    value_to_bytes(&bcc_handover.into())
+    cbor_util::serialize(&bcc_handover).map_err(|_| BccError::CborEncodeError)
 }
 
 fn taint_cdi(cdi: &Cdi, info: &str) -> Result<Cdi> {
@@ -100,7 +98,8 @@
         // We don't attempt to fully validate the BCC (e.g. we don't check the signatures) - we
         // have to trust our loader. But if it's invalid CBOR or otherwise clearly ill-formed,
         // something is very wrong, so we fail.
-        let bcc_cbor = value_from_bytes(received_bcc)?;
+        let bcc_cbor =
+            cbor_util::deserialize(received_bcc).map_err(|_| BccError::CborDecodeError)?;
 
         // Bcc = [
         //   PubKeyEd25519 / PubKeyECDSA256, // DK_pub
@@ -159,7 +158,7 @@
         // ]
         let payload =
             self.payload_bytes().ok_or(BccError::MalformedBcc("Invalid payload in BccEntry"))?;
-        let payload = value_from_bytes(payload)?;
+        let payload = cbor_util::deserialize(payload).map_err(|_| BccError::CborDecodeError)?;
         trace!("Bcc payload: {payload:?}");
         Ok(BccPayload(payload))
     }
@@ -215,21 +214,3 @@
         None
     }
 }
-
-/// Decodes the provided binary CBOR-encoded value and returns a
-/// ciborium::Value struct wrapped in Result.
-fn value_from_bytes(mut bytes: &[u8]) -> Result<Value> {
-    let value = ciborium::de::from_reader(&mut bytes).map_err(BccError::CborDecodeError)?;
-    // Ciborium tries to read one Value, but doesn't care if there is trailing data after it. We do.
-    if !bytes.is_empty() {
-        return Err(BccError::ExtraneousBytes);
-    }
-    Ok(value)
-}
-
-/// Encodes a ciborium::Value into bytes.
-fn value_to_bytes(value: &Value) -> Result<Vec<u8>> {
-    let mut bytes: Vec<u8> = Vec::new();
-    ciborium::ser::into_writer(&value, &mut bytes).map_err(BccError::CborEncodeError)?;
-    Ok(bytes)
-}
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 67865e5..da19931 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -13,16 +13,48 @@
 // limitations under the License.
 
 //! Support for DICE derivation and BCC generation.
+extern crate alloc;
 
+use alloc::format;
+use alloc::vec::Vec;
+use ciborium::cbor;
+use ciborium::Value;
 use core::mem::size_of;
-use cstr::cstr;
 use diced_open_dice::{
-    bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceConfigValues, DiceMode,
-    Hash, InputValues, HIDDEN_SIZE,
+    bcc_handover_main_flow, hash, Config, DiceMode, Hash, InputValues, HIDDEN_SIZE,
 };
 use pvmfw_avb::{Capability, DebugLevel, Digest, VerifiedBootData};
 use zerocopy::AsBytes;
 
+const COMPONENT_NAME_KEY: i64 = -70002;
+const SECURITY_VERSION_KEY: i64 = -70005;
+const RKP_VM_MARKER_KEY: i64 = -70006;
+// TODO(b/291245237): Document this key along with others used in ConfigDescriptor in AVF based VM.
+const INSTANCE_HASH_KEY: i64 = -71003;
+
+#[derive(Debug)]
+pub enum Error {
+    /// Error in CBOR operations
+    CborError(ciborium::value::Error),
+    /// Error in DICE operations
+    DiceError(diced_open_dice::DiceError),
+}
+
+impl From<ciborium::value::Error> for Error {
+    fn from(e: ciborium::value::Error) -> Self {
+        Self::CborError(e)
+    }
+}
+
+impl From<diced_open_dice::DiceError> for Error {
+    fn from(e: diced_open_dice::DiceError) -> Self {
+        Self::DiceError(e)
+    }
+}
+
+// DICE in pvmfw result type.
+type Result<T> = core::result::Result<T, Error>;
+
 fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
     match debug_level {
         DebugLevel::None => DiceMode::kDiceModeNormal,
@@ -30,15 +62,16 @@
     }
 }
 
-fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> diced_open_dice::Result<Hash> {
+fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> Result<Hash> {
     let mut digests = [0u8; size_of::<Digest>() * 2];
     digests[..size_of::<Digest>()].copy_from_slice(&verified_boot_data.kernel_digest);
     if let Some(initrd_digest) = verified_boot_data.initrd_digest {
         digests[size_of::<Digest>()..].copy_from_slice(&initrd_digest);
     }
-    hash(&digests)
+    Ok(hash(&digests)?)
 }
 
+#[derive(Clone)]
 pub struct PartialInputs {
     pub code_hash: Hash,
     pub auth_hash: Hash,
@@ -48,7 +81,7 @@
 }
 
 impl PartialInputs {
-    pub fn new(data: &VerifiedBootData) -> diced_open_dice::Result<Self> {
+    pub fn new(data: &VerifiedBootData) -> Result<Self> {
         let code_hash = to_dice_hash(data)?;
         let auth_hash = hash(data.public_key)?;
         let mode = to_dice_mode(data.debug_level);
@@ -63,26 +96,36 @@
         self,
         current_bcc_handover: &[u8],
         salt: &[u8; HIDDEN_SIZE],
+        instance_hash: Option<Hash>,
+        deferred_rollback_protection: bool,
         next_bcc: &mut [u8],
-    ) -> diced_open_dice::Result<()> {
-        let mut config_descriptor_buffer = [0; 128];
-        let config = self.generate_config_descriptor(&mut config_descriptor_buffer)?;
+    ) -> Result<()> {
+        let config = self
+            .generate_config_descriptor(instance_hash)
+            .map_err(|_| diced_open_dice::DiceError::InvalidInput)?;
 
         let dice_inputs = InputValues::new(
             self.code_hash,
-            Config::Descriptor(config),
+            Config::Descriptor(&config),
             self.auth_hash,
             self.mode,
-            self.make_hidden(salt)?,
+            self.make_hidden(salt, deferred_rollback_protection)?,
         );
         let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc)?;
         Ok(())
     }
 
-    fn make_hidden(&self, salt: &[u8; HIDDEN_SIZE]) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
+    fn make_hidden(
+        &self,
+        salt: &[u8; HIDDEN_SIZE],
+        deferred_rollback_protection: bool,
+    ) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
         // We want to make sure we get a different sealing CDI for:
         // - VMs with different salt values
         // - An RKP VM and any other VM (regardless of salt)
+        // - depending on whether rollback protection has been deferred to payload. This ensures the
+        //   adversary cannot leak the secrets by using old images & setting
+        //   `deferred_rollback_protection` to true.
         // The hidden input for DICE affects the sealing CDI (but the values in the config
         // descriptor do not).
         // Since the hidden input has to be a fixed size, create it as a hash of the values we
@@ -92,26 +135,34 @@
         struct HiddenInput {
             rkp_vm_marker: bool,
             salt: [u8; HIDDEN_SIZE],
+            deferred_rollback_protection: bool,
         }
-        // TODO(b/291213394): Include `defer_rollback_protection` flag in the Hidden Input to
-        // differentiate the secrets in both cases.
-        hash(HiddenInput { rkp_vm_marker: self.rkp_vm_marker, salt: *salt }.as_bytes())
+        hash(
+            HiddenInput {
+                rkp_vm_marker: self.rkp_vm_marker,
+                salt: *salt,
+                deferred_rollback_protection,
+            }
+            .as_bytes(),
+        )
     }
 
-    fn generate_config_descriptor<'a>(
-        &self,
-        config_descriptor_buffer: &'a mut [u8],
-    ) -> diced_open_dice::Result<&'a [u8]> {
-        let config_values = DiceConfigValues {
-            component_name: Some(cstr!("vm_entry")),
-            security_version: if cfg!(dice_changes) { Some(self.security_version) } else { None },
-            rkp_vm_marker: self.rkp_vm_marker,
-            ..Default::default()
-        };
-        let config_descriptor_size =
-            bcc_format_config_descriptor(&config_values, config_descriptor_buffer)?;
-        let config = &config_descriptor_buffer[..config_descriptor_size];
-        Ok(config)
+    fn generate_config_descriptor(&self, instance_hash: Option<Hash>) -> Result<Vec<u8>> {
+        let mut config = Vec::with_capacity(4);
+        config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!("vm_entry")?));
+        if cfg!(dice_changes) {
+            config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
+        }
+        if self.rkp_vm_marker {
+            config.push((cbor!(RKP_VM_MARKER_KEY)?, Value::Null))
+        }
+        if let Some(instance_hash) = instance_hash {
+            config.push((cbor!(INSTANCE_HASH_KEY)?, Value::from(instance_hash.as_slice())));
+        }
+        let config = Value::Map(config);
+        Ok(cbor_util::serialize(&config).map_err(|e| {
+            ciborium::value::Error::Custom(format!("Error in serialization: {e:?}"))
+        })?)
     }
 }
 
@@ -140,17 +191,24 @@
 
 #[cfg(test)]
 mod tests {
-    use super::*;
+    use crate::{
+        Hash, PartialInputs, COMPONENT_NAME_KEY, INSTANCE_HASH_KEY, RKP_VM_MARKER_KEY,
+        SECURITY_VERSION_KEY,
+    };
     use ciborium::Value;
+    use diced_open_dice::DiceArtifacts;
+    use diced_open_dice::DiceMode;
+    use diced_open_dice::HIDDEN_SIZE;
+    use pvmfw_avb::Capability;
+    use pvmfw_avb::DebugLevel;
+    use pvmfw_avb::Digest;
+    use pvmfw_avb::VerifiedBootData;
     use std::collections::HashMap;
+    use std::mem::size_of;
     use std::vec;
 
-    const COMPONENT_NAME_KEY: i64 = -70002;
     const COMPONENT_VERSION_KEY: i64 = -70003;
     const RESETTABLE_KEY: i64 = -70004;
-    const SECURITY_VERSION_KEY: i64 = -70005;
-    const RKP_VM_MARKER_KEY: i64 = -70006;
-
     const BASE_VB_DATA: VerifiedBootData = VerifiedBootData {
         debug_level: DebugLevel::None,
         kernel_digest: [1u8; size_of::<Digest>()],
@@ -159,6 +217,7 @@
         capabilities: vec![],
         rollback_index: 42,
     };
+    const HASH: Hash = *b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard";
 
     #[test]
     fn base_data_conversion() {
@@ -193,7 +252,7 @@
     fn base_config_descriptor() {
         let vb_data = BASE_VB_DATA;
         let inputs = PartialInputs::new(&vb_data).unwrap();
-        let config_map = decode_config_descriptor(&inputs);
+        let config_map = decode_config_descriptor(&inputs, None);
 
         assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
         assert_eq!(config_map.get(&COMPONENT_VERSION_KEY), None);
@@ -214,21 +273,104 @@
         let vb_data =
             VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
         let inputs = PartialInputs::new(&vb_data).unwrap();
-        let config_map = decode_config_descriptor(&inputs);
+        let config_map = decode_config_descriptor(&inputs, Some(HASH));
 
         assert!(config_map.get(&RKP_VM_MARKER_KEY).unwrap().is_null());
     }
 
-    fn decode_config_descriptor(inputs: &PartialInputs) -> HashMap<i64, Value> {
-        let mut buffer = [0; 128];
-        let config_descriptor = inputs.generate_config_descriptor(&mut buffer).unwrap();
+    #[test]
+    fn config_descriptor_with_instance_hash() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs, Some(HASH));
+        assert_eq!(*config_map.get(&INSTANCE_HASH_KEY).unwrap(), Value::from(HASH.as_slice()));
+    }
+
+    #[test]
+    fn config_descriptor_without_instance_hash() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs, None);
+        assert!(!config_map.contains_key(&INSTANCE_HASH_KEY));
+    }
+
+    fn decode_config_descriptor(
+        inputs: &PartialInputs,
+        instance_hash: Option<Hash>,
+    ) -> HashMap<i64, Value> {
+        let config_descriptor = inputs.generate_config_descriptor(instance_hash).unwrap();
 
         let cbor_map =
-            cbor_util::deserialize::<Value>(config_descriptor).unwrap().into_map().unwrap();
+            cbor_util::deserialize::<Value>(&config_descriptor).unwrap().into_map().unwrap();
 
         cbor_map
             .into_iter()
             .map(|(k, v)| ((k.into_integer().unwrap().try_into().unwrap()), v))
             .collect()
     }
+
+    #[test]
+    fn changing_deferred_rpb_changes_secrets() {
+        let vb_data = VerifiedBootData { debug_level: DebugLevel::Full, ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let mut buffer_without_defer = [0; 4096];
+        let mut buffer_with_defer = [0; 4096];
+        let mut buffer_without_defer_retry = [0; 4096];
+
+        let sample_dice_input: &[u8] = &[
+            0xa3, // CDI attest
+            0x01, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CDI seal
+            0x02, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DICE chain
+            0x03, 0x82, 0xa6, 0x01, 0x02, 0x03, 0x27, 0x04, 0x02, 0x20, 0x01, 0x21, 0x40, 0x22,
+            0x40, 0x84, 0x40, 0xa0, 0x40, 0x40,
+            // 8-bytes of trailing data that aren't part of the DICE chain.
+            0x84, 0x41, 0x55, 0xa0, 0x42, 0x11, 0x22, 0x40,
+        ];
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                false,
+                &mut buffer_without_defer,
+            )
+            .unwrap();
+        let bcc_handover1 = diced_open_dice::bcc_handover_parse(&buffer_without_defer).unwrap();
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                true,
+                &mut buffer_with_defer,
+            )
+            .unwrap();
+        let bcc_handover2 = diced_open_dice::bcc_handover_parse(&buffer_with_defer).unwrap();
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                false,
+                &mut buffer_without_defer_retry,
+            )
+            .unwrap();
+        let bcc_handover3 =
+            diced_open_dice::bcc_handover_parse(&buffer_without_defer_retry).unwrap();
+
+        assert_ne!(bcc_handover1.cdi_seal(), bcc_handover2.cdi_seal());
+        assert_eq!(bcc_handover1.cdi_seal(), bcc_handover3.cdi_seal());
+    }
 }
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 2af19c4..299d1c0 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -143,10 +143,10 @@
         RebootReason::InternalError
     })?;
 
-    let (new_instance, salt) = if cfg!(llpvm_changes)
-        && should_defer_rollback_protection(fdt)?
-        && verified_boot_data.has_capability(Capability::SecretkeeperProtection)
-    {
+    let instance_hash = if cfg!(llpvm_changes) { Some(salt_from_instance_id(fdt)?) } else { None };
+    let defer_rollback_protection = should_defer_rollback_protection(fdt)?
+        && verified_boot_data.has_capability(Capability::SecretkeeperProtection);
+    let (new_instance, salt) = if defer_rollback_protection {
         info!("Guest OS is capable of Secretkeeper protection, deferring rollback protection");
         // rollback_index of the image is used as security_version and is expected to be > 0 to
         // discourage implicit allocation.
@@ -155,7 +155,7 @@
             return Err(RebootReason::InvalidPayload);
         };
         // `new_instance` cannot be known to pvmfw
-        (false, salt_from_instance_id(fdt)?)
+        (false, instance_hash.unwrap())
     } else {
         let (recorded_entry, mut instance_img, header_index) =
             get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
@@ -164,18 +164,15 @@
             })?;
         let (new_instance, salt) = if let Some(entry) = recorded_entry {
             maybe_check_dice_measurements_match_entry(&dice_inputs, &entry)?;
-            let salt = if cfg!(llpvm_changes) { salt_from_instance_id(fdt)? } else { entry.salt };
+            let salt = instance_hash.unwrap_or(entry.salt);
             (false, salt)
         } else {
             // New instance!
-            let salt = if cfg!(llpvm_changes) {
-                salt_from_instance_id(fdt)?
-            } else {
-                rand::random_array().map_err(|e| {
-                    error!("Failed to generated instance.img salt: {e}");
-                    RebootReason::InternalError
-                })?
-            };
+            let salt = instance_hash.map_or_else(rand::random_array, Ok).map_err(|e| {
+                error!("Failed to generated instance.img salt: {e}");
+                RebootReason::InternalError
+            })?;
+
             let entry = EntryBody::new(&dice_inputs, &salt);
             record_instance_entry(&entry, cdi_seal, &mut instance_img, header_index).map_err(
                 |e| {
@@ -204,10 +201,18 @@
         Cow::Owned(truncated_bcc_handover)
     };
 
-    dice_inputs.write_next_bcc(new_bcc_handover.as_ref(), &salt, next_bcc).map_err(|e| {
-        error!("Failed to derive next-stage DICE secrets: {e:?}");
-        RebootReason::SecretDerivationError
-    })?;
+    dice_inputs
+        .write_next_bcc(
+            new_bcc_handover.as_ref(),
+            &salt,
+            instance_hash,
+            defer_rollback_protection,
+            next_bcc,
+        )
+        .map_err(|e| {
+            error!("Failed to derive next-stage DICE secrets: {e:?}");
+            RebootReason::SecretDerivationError
+        })?;
     flush(next_bcc);
 
     let kaslr_seed = u64::from_ne_bytes(rand::random_array().map_err(|e| {
diff --git a/service_vm/requests/src/rkp.rs b/service_vm/requests/src/rkp.rs
index aa363e5..c62a36b 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/service_vm/requests/src/rkp.rs
@@ -127,7 +127,7 @@
         "product" => "avf",
         "vb_state" => "avf",
         "manufacturer" => "aosp-avf",
-        "vbmeta_digest" => Value::Bytes(vec![1u8; 0]),
+        "vbmeta_digest" => Value::Bytes(vec![1u8; 1]),
         "security_level" => "avf",
         "boot_patch_level" => 20240202,
         "bootloader_state" => "avf",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index b6003f6..46df011 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -55,6 +55,7 @@
     protected static final String LOG_PATH = TEST_ROOT + "log.txt";
     protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt";
     protected static final String TRADEFED_CONSOLE_PATH = TRADEFED_TEST_ROOT + "console.txt";
+    protected static final String TRADEFED_LOG_PATH = TRADEFED_TEST_ROOT + "log.txt";
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index e676841..9d0b04b 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -877,10 +877,13 @@
         assertWithMessage("Incorrect ABI list").that(abis).hasLength(1);
 
         // Check that no denials have happened so far
-        String logText =
-                getDevice().pullFileContents(CONSOLE_PATH) + getDevice().pullFileContents(LOG_PATH);
+        String consoleText = getDevice().pullFileContents(TRADEFED_CONSOLE_PATH);
+        assertWithMessage("Console output shouldn't be empty").that(consoleText).isNotEmpty();
+        String logText = getDevice().pullFileContents(TRADEFED_LOG_PATH);
+        assertWithMessage("Log output shouldn't be empty").that(logText).isNotEmpty();
+
         assertWithMessage("Unexpected denials during VM boot")
-                .that(logText)
+                .that(consoleText + logText)
                 .doesNotContainMatch("avc:\\s+denied");
 
         assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
@@ -1177,6 +1180,40 @@
         }
     }
 
+    @Test
+    public void testHugePages() throws Exception {
+        ITestDevice device = getDevice();
+        boolean disableRoot = !device.isAdbRoot();
+        CommandRunner android = new CommandRunner(device);
+
+        final String SHMEM_ENABLED_PATH = "/sys/kernel/mm/transparent_hugepage/shmem_enabled";
+        String thpShmemStr = android.run("cat", SHMEM_ENABLED_PATH);
+
+        assumeFalse("shmem already enabled, skip", thpShmemStr.contains("[advise]"));
+        assumeTrue("Unsupported shmem, skip", thpShmemStr.contains("[never]"));
+
+        device.enableAdbRoot();
+        assumeTrue("adb root is not enabled", device.isAdbRoot());
+        android.run("echo advise > " + SHMEM_ENABLED_PATH);
+
+        final String configPath = "assets/vm_config.json";
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .cpuTopology("match_host")
+                        .protectedVm(mProtectedVm)
+                        .gki(mGki)
+                        .hugePages(true)
+                        .build(getAndroidDevice());
+        mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
+
+        android.run("echo never >" + SHMEM_ENABLED_PATH);
+        if (disableRoot) {
+            device.disableAdbRoot();
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         assumeDeviceIsCapable(getDevice());
diff --git a/tests/pvmfw/helper/Android.bp b/tests/pvmfw/helper/Android.bp
index 90ca03e..a75f034 100644
--- a/tests/pvmfw/helper/Android.bp
+++ b/tests/pvmfw/helper/Android.bp
@@ -5,7 +5,7 @@
 java_library_host {
     name: "PvmfwHostTestHelper",
     srcs: ["java/**/*.java"],
-    libs: [
+    static_libs: [
         "androidx.annotation_annotation",
         "truth",
     ],
diff --git a/tests/pvmfw/tools/PvmfwTool.java b/tests/pvmfw/tools/PvmfwTool.java
index e150ec4..9f0cb42 100644
--- a/tests/pvmfw/tools/PvmfwTool.java
+++ b/tests/pvmfw/tools/PvmfwTool.java
@@ -25,10 +25,10 @@
 public class PvmfwTool {
     public static void printUsage() {
         System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
-        System.out.println("            Requires BCC and VM reference DT.");
-        System.out.println("            VM DTBO and Debug policy can optionally be specified");
+        System.out.println("            Requires BCC. VM Reference DT, VM DTBO, and Debug policy");
+        System.out.println("            can optionally be specified");
         System.out.println(
-                "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> <VM reference DT> [VM DTBO] [debug"
+                "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> [VM reference DT] [VM DTBO] [debug"
                         + " policy]");
     }
 
@@ -41,10 +41,13 @@
         File out = new File(args[0]);
         File pvmfwBin = new File(args[1]);
         File bccData = new File(args[2]);
-        File vmReferenceDt = new File(args[3]);
 
+        File vmReferenceDt = null;
         File vmDtbo = null;
         File dp = null;
+        if (args.length > 3) {
+            vmReferenceDt = new File(args[3]);
+        }
         if (args.length > 4) {
             vmDtbo = new File(args[4]);
         }
@@ -53,12 +56,18 @@
         }
 
         try {
-            Pvmfw pvmfw =
+            Pvmfw.Builder builder =
                     new Pvmfw.Builder(pvmfwBin, bccData)
                             .setVmReferenceDt(vmReferenceDt)
                             .setDebugPolicyOverlay(dp)
-                            .setVmDtbo(vmDtbo)
-                            .build();
+                            .setVmDtbo(vmDtbo);
+            if (vmReferenceDt == null) {
+                builder.setVersion(1, 1);
+            } else {
+                builder.setVersion(1, 2);
+            }
+
+            Pvmfw pvmfw = builder.build();
             pvmfw.serialize(out);
         } catch (IOException e) {
             e.printStackTrace();
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index d8f8209..d1ef4de 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -100,3 +100,21 @@
     ],
     test_suites: ["general-tests"],
 }
+
+cc_fuzz {
+    name: "virtualizationmanager_fuzzer",
+    defaults: ["service_fuzzer_defaults"],
+    srcs: ["fuzzer.cpp"],
+    static_libs: [
+        "android.system.virtualizationservice-ndk",
+        "libbase",
+    ],
+    shared_libs: [
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+    ],
+    fuzz_config: {
+        cc: ["android-kvm@google.com"],
+    },
+}
diff --git a/virtualizationmanager/fuzzer.cpp b/virtualizationmanager/fuzzer.cpp
new file mode 100644
index 0000000..6afea46
--- /dev/null
+++ b/virtualizationmanager/fuzzer.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+#include <aidl/android/system/virtualizationservice/IVirtualizationService.h>
+#include <android-base/file.h>
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <fuzzbinder/libbinder_ndk_driver.h>
+#include <fuzzer/FuzzedDataProvider.h>
+#include <unistd.h>
+
+#include <binder_rpc_unstable.hpp>
+#include <cstdlib>
+#include <iostream>
+
+using aidl::android::system::virtualizationservice::IVirtualizationService;
+using android::fuzzService;
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Pipe;
+using android::base::Result;
+using android::base::Socketpair;
+using android::base::unique_fd;
+using ndk::SpAIBinder;
+
+static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
+static constexpr size_t VIRTMGR_THREADS = 2;
+
+Result<unique_fd> get_service_fd() {
+    unique_fd server_fd, client_fd;
+    if (!Socketpair(SOCK_STREAM, &server_fd, &client_fd)) {
+        return ErrnoError() << "Failed to create socketpair";
+    }
+
+    unique_fd wait_fd, ready_fd;
+    if (!Pipe(&wait_fd, &ready_fd, 0)) {
+        return ErrnoError() << "Failed to create pipe";
+    }
+
+    if (int pid = fork(); pid == 0) {
+        client_fd.reset();
+        wait_fd.reset();
+
+        auto server_fd_str = std::to_string(server_fd.get());
+        auto ready_fd_str = std::to_string(ready_fd.get());
+
+        if (execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", server_fd_str.c_str(),
+                  "--ready-fd", ready_fd_str.c_str(), nullptr) == -1) {
+            return ErrnoError() << "Failed to execute virtmgr";
+        }
+    } else if (pid < 0) {
+        return ErrnoError() << "Failed to fork";
+    }
+
+    server_fd.reset();
+    ready_fd.reset();
+
+    char buf;
+    if (read(wait_fd.get(), &buf, sizeof(buf)) < 0) {
+        return ErrnoError() << "Failed to wait for VirtualizationService to be ready";
+    }
+
+    return client_fd;
+}
+
+Result<std::shared_ptr<IVirtualizationService>> connect_service(int fd) {
+    std::unique_ptr<ARpcSession, decltype(&ARpcSession_free)> session(ARpcSession_new(),
+                                                                      &ARpcSession_free);
+    ARpcSession_setFileDescriptorTransportMode(session.get(),
+                                               ARpcSession_FileDescriptorTransportMode::Unix);
+    ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
+    ARpcSession_setMaxOutgoingConnections(session.get(), VIRTMGR_THREADS);
+    AIBinder* binder = ARpcSession_setupUnixDomainBootstrapClient(session.get(), fd);
+    if (binder == nullptr) {
+        return Error() << "Failed to connect to VirtualizationService";
+    }
+    return IVirtualizationService::fromBinder(SpAIBinder{binder});
+}
+
+Result<void> inner_fuzz(const uint8_t* data, size_t size) {
+    unique_fd fd = OR_RETURN(get_service_fd());
+    std::shared_ptr<IVirtualizationService> service = OR_RETURN(connect_service(fd.get()));
+    fuzzService(service->asBinder().get(), FuzzedDataProvider(data, size));
+
+    return {};
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    if (auto ret = inner_fuzz(data, size); !ret.ok()) {
+        std::cerr << "connecting to service failed: " << ret.error() << std::endl;
+        abort();
+    }
+    return 0;
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index aeee6f7..dd17b46 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -607,16 +607,25 @@
         };
 
         // Create TAP network interface if the VM supports network.
-        let _tap_fd = if cfg!(network) && config.networkSupported {
+        let tap = if cfg!(network) && config.networkSupported {
             if *is_protected {
                 return Err(anyhow!("Network feature is not supported for pVM yet"))
                     .with_log()
                     .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)?;
             }
-            Some(GLOBAL_SERVICE.createTapInterface(&get_this_pid().to_string())?)
+            Some(File::from(
+                GLOBAL_SERVICE
+                    .createTapInterface(&get_this_pid().to_string())?
+                    .as_ref()
+                    .try_clone()
+                    .context("Failed to get TAP interface from ParcelFileDescriptor")
+                    .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+            ))
         } else {
             None
         };
+        let virtio_snd_backend =
+            if cfg!(paravirtualized_devices) { Some(String::from("aaudio")) } else { None };
 
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
@@ -646,6 +655,8 @@
             display_config,
             input_device_options,
             hugepages: config.hugePages,
+            tap,
+            virtio_snd_backend,
         };
         let instance = Arc::new(
             VmInstance::new(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index d48ef7b..371a908 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -122,6 +122,8 @@
     pub display_config: Option<DisplayConfig>,
     pub input_device_options: Vec<InputDeviceOption>,
     pub hugepages: bool,
+    pub tap: Option<File>,
+    pub virtio_snd_backend: Option<String>,
 }
 
 #[derive(Debug)]
@@ -979,7 +981,7 @@
     }
 
     if cfg!(paravirtualized_devices) {
-        // TODO(b/325929096): Need to set up network from the config
+        // TODO(b/340376951): Remove this after tap in CrosvmConfig is connected to tethering.
         if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
             .unwrap_or(false)
         {
@@ -987,6 +989,14 @@
         }
     }
 
+    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 cfg!(paravirtualized_devices) {
         for input_device_option in config.input_device_options.iter() {
             command.arg("--input");
@@ -1020,6 +1030,12 @@
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
+    if cfg!(paravirtualized_devices) {
+        if let Some(virtio_snd_backend) = &config.virtio_snd_backend {
+            command.arg("--virtio-snd").arg(format!("backend={}", virtio_snd_backend));
+        }
+    }
+
     print_crosvm_args(&command);
 
     let result = SharedChild::spawn(&mut command)?;
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5e71245..8fe4167 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -346,7 +346,7 @@
             ))
             .with_log();
         }
-        if !remotely_provisioned_component_service_exists()? {
+        if !is_remote_provisioning_hal_declared()? {
             return Err(Status::new_exception_str(
                 ExceptionCode::UNSUPPORTED_OPERATION,
                 Some("AVF remotely provisioned component service is not declared"),
@@ -403,7 +403,7 @@
     }
 
     fn isRemoteAttestationSupported(&self) -> binder::Result<bool> {
-        remotely_provisioned_component_service_exists()
+        is_remote_provisioning_hal_declared()
     }
 
     fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
@@ -862,7 +862,9 @@
     Ok(())
 }
 
-fn remotely_provisioned_component_service_exists() -> binder::Result<bool> {
+/// Returns true if the AVF remotely provisioned component service is declared in the
+/// VINTF manifest.
+pub(crate) fn is_remote_provisioning_hal_declared() -> binder::Result<bool> {
     Ok(binder::is_declared(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME)?)
 }
 
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 8acfdd3..55245f6 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -20,7 +20,10 @@
 mod remote_provisioning;
 mod rkpvm;
 
-use crate::aidl::{remove_temporary_dir, VirtualizationServiceInternal, TEMPORARY_DIRECTORY};
+use crate::aidl::{
+    is_remote_provisioning_hal_declared, remove_temporary_dir, VirtualizationServiceInternal,
+    TEMPORARY_DIRECTORY,
+};
 use android_logger::{Config, FilterBuilder};
 use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal;
@@ -81,7 +84,7 @@
         BnVirtualizationServiceInternal::new_binder(service.clone(), BinderFeatures::default());
     register(INTERNAL_SERVICE_NAME, internal_service)?;
 
-    if cfg!(remote_attestation) {
+    if is_remote_provisioning_hal_declared().unwrap_or(false) {
         // The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
         // RKP VM attestation.
         let remote_provisioning_service = remote_provisioning::new_binder();
diff --git a/virtualizationservice/vmnic/Android.bp b/virtualizationservice/vmnic/Android.bp
index 784c648..247be85 100644
--- a/virtualizationservice/vmnic/Android.bp
+++ b/virtualizationservice/vmnic/Android.bp
@@ -14,7 +14,9 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
+        "liblibc",
         "liblog_rust",
+        "libnix",
     ],
     apex_available: ["com.android.virt"],
 }
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index 6443258..a206c25 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -14,10 +14,64 @@
 
 //! Implementation of the AIDL interface of Vmnic.
 
-use anyhow::anyhow;
+use anyhow::{anyhow, Context, Result};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVmnic::IVmnic;
-use binder::{self, ExceptionCode, Interface, IntoBinderResult, ParcelFileDescriptor};
+use binder::{self, Interface, IntoBinderResult, ParcelFileDescriptor};
+use libc::{c_char, c_int, c_short, ifreq, IFF_NO_PI, IFF_TAP, IFF_UP, IFF_VNET_HDR, IFNAMSIZ};
 use log::info;
+use nix::{ioctl_write_int_bad, ioctl_write_ptr_bad};
+use nix::sys::ioctl::ioctl_num_type;
+use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
+use std::ffi::CString;
+use std::fs::File;
+use std::os::fd::{AsRawFd, RawFd};
+use std::slice::from_raw_parts;
+
+const TUNSETIFF: ioctl_num_type = 0x400454ca;
+const TUNSETPERSIST: ioctl_num_type = 0x400454cb;
+const SIOCGIFFLAGS: ioctl_num_type = 0x00008913;
+const SIOCSIFFLAGS: ioctl_num_type = 0x00008914;
+
+ioctl_write_ptr_bad!(ioctl_tunsetiff, TUNSETIFF, ifreq);
+ioctl_write_int_bad!(ioctl_tunsetpersist, TUNSETPERSIST);
+ioctl_write_ptr_bad!(ioctl_siocgifflags, SIOCGIFFLAGS, ifreq);
+ioctl_write_ptr_bad!(ioctl_siocsifflags, SIOCSIFFLAGS, ifreq);
+
+fn validate_ifname(ifname: &[c_char]) -> Result<()> {
+    if ifname.len() >= IFNAMSIZ {
+        return Err(anyhow!(format!("Interface name is too long")));
+    }
+    Ok(())
+}
+
+fn create_tap_interface(fd: RawFd, ifname: &[c_char]) -> Result<()> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let mut ifr: ifreq = unsafe { std::mem::zeroed() };
+    ifr.ifr_ifru.ifru_flags = (IFF_TAP | IFF_NO_PI | IFF_VNET_HDR) as c_short;
+    ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_tunsetiff(fd, &ifr) }.context("Failed to ioctl TUNSETIFF")?;
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_tunsetpersist(fd, 1) }.context("Failed to ioctl TUNSETPERSIST")?;
+    Ok(())
+}
+
+fn bring_up_interface(sockfd: c_int, ifname: &[c_char]) -> Result<()> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let mut ifr: ifreq = unsafe { std::mem::zeroed() };
+    ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_siocgifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
+    // SAFETY: After calling SIOCGIFFLAGS, ifr_ifru holds ifru_flags in its union field.
+    unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_siocsifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
+    Ok(())
+}
 
 #[derive(Debug, Default)]
 pub struct Vmnic {}
@@ -32,10 +86,34 @@
 
 impl IVmnic for Vmnic {
     fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
-        let ifname = format!("avf_tap_{iface_name_suffix}");
-        info!("Creating TAP interface {}", ifname);
+        let ifname = CString::new(format!("avf_tap_{iface_name_suffix}"))
+            .context(format!(
+                "Failed to construct TAP interface name as CString: avf_tap_{iface_name_suffix}"
+            ))
+            .or_service_specific_exception(-1)?;
+        let ifname_bytes = ifname.as_bytes_with_nul();
+        // SAFETY: Converting from &[u8] into &[c_char].
+        let ifname_bytes =
+            unsafe { from_raw_parts(ifname_bytes.as_ptr().cast::<c_char>(), ifname_bytes.len()) };
+        validate_ifname(ifname_bytes)
+            .context(format!("Invalid interface name: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
 
-        Err(anyhow!("Creating TAP network interface is not supported yet"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+        let tunfd = File::open("/dev/tun")
+            .context("Failed to open /dev/tun")
+            .or_service_specific_exception(-1)?;
+        create_tap_interface(tunfd.as_raw_fd(), ifname_bytes)
+            .context(format!("Failed to create TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
+            .context("Failed to create socket")
+            .or_service_specific_exception(-1)?;
+        bring_up_interface(sock.as_raw_fd(), ifname_bytes)
+            .context(format!("Failed to bring up TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        info!("Created TAP network interface: {ifname:#?}");
+        Ok(ParcelFileDescriptor::new(tunfd))
     }
 }
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 10f8bf6..4d79235 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,40 +19,49 @@
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.app.Activity;
+import android.crosvm.ICrosvmAndroidDisplayService;
+import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.crosvm.ICrosvmAndroidDisplayService;
 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.util.DisplayMetrics;
-import android.util.Log;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
-import android.view.KeyEvent;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
+import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import libcore.io.IoBridge;
+
+import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Arrays;
@@ -63,8 +72,9 @@
     private static final String TAG = "VmLauncherApp";
     private static final String VM_NAME = "my_custom_vm";
     private static final boolean DEBUG = true;
-    private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+    private ExecutorService mExecutorService;
     private VirtualMachine mVirtualMachine;
+    private ParcelFileDescriptor mCursorStream;
 
     private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
         VirtualMachineConfig.Builder configBuilder =
@@ -75,6 +85,7 @@
         if (DEBUG) {
             configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
             configBuilder.setVmOutputCaptured(true);
+            configBuilder.setConnectVmConsole(true);
         }
         VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
                 new VirtualMachineCustomImageConfig.Builder();
@@ -160,6 +171,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mExecutorService = Executors.newCachedThreadPool();
         try {
             // To ensure that the previous display service is removed.
             IVirtualizationServiceInternal.Stub.asInterface(
@@ -239,14 +251,19 @@
             if (DEBUG) {
                 InputStream console = mVirtualMachine.getConsoleOutput();
                 InputStream log = mVirtualMachine.getLogOutput();
-                mExecutorService.execute(new Reader("console", console));
+                OutputStream consoleLogFile =
+                        new LineBufferedOutputStream(
+                                getApplicationContext().openFileOutput("console.log", 0));
+                mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
                 mExecutorService.execute(new Reader("log", log));
             }
-        } catch (VirtualMachineException e) {
+        } catch (VirtualMachineException | IOException e) {
             throw new RuntimeException(e);
         }
 
         SurfaceView surfaceView = findViewById(R.id.surface_view);
+        SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view);
+        cursorSurfaceView.setZOrderMediaOverlay(true);
         View backgroundTouchView = findViewById(R.id.background_touch_view);
         backgroundTouchView.setOnTouchListener(
                 (v, event) -> {
@@ -280,7 +297,10 @@
                                                 + holder.getSurface()
                                                 + ")");
                                 runWithDisplayService(
-                                        (service) -> service.setSurface(holder.getSurface()));
+                                        (service) ->
+                                                service.setSurface(
+                                                        holder.getSurface(),
+                                                        false /* forCursor */));
                             }
 
                             @Override
@@ -292,7 +312,52 @@
                             @Override
                             public void surfaceDestroyed(SurfaceHolder holder) {
                                 Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
-                                runWithDisplayService((service) -> service.removeSurface());
+                                runWithDisplayService(
+                                        (service) -> service.removeSurface(false /* forCursor */));
+                            }
+                        });
+        cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
+        cursorSurfaceView
+                .getHolder()
+                .addCallback(
+                        new SurfaceHolder.Callback() {
+                            @Override
+                            public void surfaceCreated(SurfaceHolder holder) {
+                                try {
+                                    ParcelFileDescriptor[] pfds =
+                                            ParcelFileDescriptor.createSocketPair();
+                                    mExecutorService.execute(
+                                            new CursorHandler(cursorSurfaceView, pfds[0]));
+                                    mCursorStream = pfds[0];
+                                    runWithDisplayService(
+                                            (service) -> service.setCursorStream(pfds[1]));
+                                } catch (Exception e) {
+                                    Log.d("TAG", "failed to run cursor stream handler", e);
+                                }
+                                runWithDisplayService(
+                                        (service) ->
+                                                service.setSurface(
+                                                        holder.getSurface(), true /* forCursor */));
+                            }
+
+                            @Override
+                            public void surfaceChanged(
+                                    SurfaceHolder holder, int format, int width, int height) {
+                                Log.d(TAG, "width: " + width + ", height: " + height);
+                            }
+
+                            @Override
+                            public void surfaceDestroyed(SurfaceHolder holder) {
+                                Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
+                                runWithDisplayService(
+                                        (service) -> service.removeSurface(true /* forCursor */));
+                                if (mCursorStream != null) {
+                                    try {
+                                        mCursorStream.close();
+                                    } catch (IOException e) {
+                                        Log.d(TAG, "failed to close fd", e);
+                                    }
+                                }
                             }
                         });
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -305,6 +370,15 @@
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mExecutorService != null) {
+            mExecutorService.shutdownNow();
+        }
+        Log.d(TAG, "destroyed");
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
         if (hasFocus) {
@@ -336,6 +410,43 @@
         }
     }
 
+    static class CursorHandler implements Runnable {
+        private final SurfaceView mSurfaceView;
+        private final ParcelFileDescriptor mStream;
+
+        CursorHandler(SurfaceView s, ParcelFileDescriptor stream) {
+            mSurfaceView = s;
+            mStream = stream;
+        }
+
+        @Override
+        public void run() {
+            Log.d(TAG, "CursorHandler");
+            try {
+                ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
+                byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+                while (true) {
+                    byteBuffer.clear();
+                    int bytes =
+                            IoBridge.read(
+                                    mStream.getFileDescriptor(),
+                                    byteBuffer.array(),
+                                    0,
+                                    byteBuffer.array().length);
+                    float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    mSurfaceView.post(
+                            () -> {
+                                mSurfaceView.setTranslationX(x);
+                                mSurfaceView.setTranslationY(y);
+                            });
+                }
+            } catch (IOException e) {
+                Log.e(TAG, e.getMessage());
+            }
+        }
+    }
+
     /** Reads data from an input stream and posts it to the output data */
     static class Reader implements Runnable {
         private final String mName;
@@ -359,4 +470,49 @@
             }
         }
     }
+
+    private static class CopyStreamTask implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final OutputStream mOut;
+
+        CopyStreamTask(String name, InputStream in, OutputStream out) {
+            mName = name;
+            mIn = in;
+            mOut = out;
+        }
+
+        @Override
+        public void run() {
+            try {
+                byte[] buffer = new byte[2048];
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    mOut.write(buffer, 0, len);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Exception while posting " + mName, e);
+            }
+        }
+    }
+
+    private static class LineBufferedOutputStream extends BufferedOutputStream {
+        LineBufferedOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) throws IOException {
+            super.write(buf, off, len);
+            for (int i = 0; i < len; ++i) {
+                if (buf[off + i] == '\n') {
+                    flush();
+                    break;
+                }
+            }
+        }
+    }
 }
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
index e52dfcd..a80ece0 100644
--- a/vmlauncher_app/res/layout/activity_main.xml
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
       android:layout_height="match_parent"
     />
   <SurfaceView
-        android:id="@+id/surface_view"
+      android:id="@+id/surface_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:focusable="true"
@@ -20,5 +20,11 @@
       android:defaultFocusHighlightEnabled="true">
     <requestFocus />
   </SurfaceView>
+  <!-- A cursor size in virtio-gpu spec is always 64x64 -->
+  <SurfaceView
+      android:id="@+id/cursor_surface_view"
+      android:layout_width="64px"
+      android:layout_height="64px">
+  </SurfaceView>
 
 </merge>