Merge changes I8f4166ff,I51f1c1ab into main

* changes:
  Add tests for Rust VM Payload
  Create a Rust wrapper for vm_payload
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index d52aa95..1e15d16 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -190,6 +190,10 @@
             "writable": true
         }
     ],
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
     "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
     "protected": false,
     "cpu_topology": "match_host",
@@ -285,6 +289,8 @@
 
 ```
 $ adb shell pm clear com.android.virtualization.vmlauncher
+# or
+$ adb shell pm clear com.google.android.virtualization.vmlauncher
 ```
 
 ### Inside guest OS (for ChromiumOS only)
@@ -301,8 +307,19 @@
 
 ### Debugging
 
-To see console log, check
+To open the serial console (interactive terminal):
+```shell
+$ adb shell -t /apex/com.android.virt/bin/vm console
+```
+
+To see console logs only, check
 `/data/data/com.android.virtualization.vmlauncher/files/console.log`
+Or
+`/data/data/com.google.android.virtualization.vmlauncher/files/console.log`
+
+```shell
+$ adb shell su root tail +0 -F /data/data/com{,.google}.android.virtualization.vmlauncher/files/console.log
+```
 
 For ChromiumOS, you can ssh-in. Use following commands after network setup.
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 43f3db0..b6f811e 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -1214,6 +1214,9 @@
                         service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                if (mConnectVmConsole) {
+                    mVirtualMachine.setHostConsoleName(getHostConsoleName());
+                }
                 mVirtualMachine.start();
             } catch (IOException e) {
                 throw new VirtualMachineException("failed to persist files", e);
@@ -1335,7 +1338,7 @@
      * @hide
      */
     @NonNull
-    public String getHostConsoleName() throws VirtualMachineException {
+    private String getHostConsoleName() throws VirtualMachineException {
         if (!mConnectVmConsole) {
             throw new VirtualMachineException("Host console is not enabled");
         }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 1e0f6c6..8b444fc 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -711,6 +711,10 @@
                 Optional.ofNullable(customImageConfig.getDisplayConfig())
                         .map(dc -> dc.toParcelable())
                         .orElse(null);
+        config.gpuConfig =
+                Optional.ofNullable(customImageConfig.getGpuConfig())
+                        .map(dc -> dc.toParcelable())
+                        .orElse(null);
         config.protectedVm = this.mProtectedVm;
         config.memoryMib = bytesToMebiBytes(mMemoryBytes);
         config.cpuTopology = (byte) this.mCpuTopology;
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 2fcad20..c0ff11a 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -36,6 +36,7 @@
     private static final String KEY_TOUCH = "touch";
     private static final String KEY_KEYBOARD = "keyboard";
     private static final String KEY_MOUSE = "mouse";
+    private static final String KEY_GPU = "gpu";
 
     @Nullable private final String name;
     @Nullable private final String kernelPath;
@@ -47,6 +48,7 @@
     private final boolean touch;
     private final boolean keyboard;
     private final boolean mouse;
+    @Nullable private final GpuConfig gpuConfig;
 
     @Nullable
     public Disk[] getDisks() {
@@ -101,7 +103,8 @@
             DisplayConfig displayConfig,
             boolean touch,
             boolean keyboard,
-            boolean mouse) {
+            boolean mouse,
+            GpuConfig gpuConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -112,6 +115,7 @@
         this.touch = touch;
         this.keyboard = keyboard;
         this.mouse = mouse;
+        this.gpuConfig = gpuConfig;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -142,6 +146,7 @@
         builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
         builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
         builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
+        builder.setGpuConfig(GpuConfig.from(customImageConfigBundle.getPersistableBundle(KEY_GPU)));
         return builder.build();
     }
 
@@ -173,6 +178,9 @@
         pb.putBoolean(KEY_TOUCH, touch);
         pb.putBoolean(KEY_KEYBOARD, keyboard);
         pb.putBoolean(KEY_MOUSE, mouse);
+        pb.putPersistableBundle(
+                KEY_GPU,
+                Optional.ofNullable(gpuConfig).map(gc -> gc.toPersistableBundle()).orElse(null));
         return pb;
     }
 
@@ -181,6 +189,11 @@
         return displayConfig;
     }
 
+    @Nullable
+    public GpuConfig getGpuConfig() {
+        return gpuConfig;
+    }
+
     /** @hide */
     public static final class Disk {
         private final boolean writable;
@@ -224,6 +237,7 @@
         private boolean touch;
         private boolean keyboard;
         private boolean mouse;
+        private GpuConfig gpuConfig;
 
         /** @hide */
         public Builder() {}
@@ -271,6 +285,12 @@
         }
 
         /** @hide */
+        public Builder setGpuConfig(GpuConfig gpuConfig) {
+            this.gpuConfig = gpuConfig;
+            return this;
+        }
+
+        /** @hide */
         public Builder useTouch(boolean touch) {
             this.touch = touch;
             return this;
@@ -300,7 +320,8 @@
                     displayConfig,
                     touch,
                     keyboard,
-                    mouse);
+                    mouse,
+                    gpuConfig);
         }
     }
 
@@ -437,4 +458,223 @@
             }
         }
     }
+
+    /** @hide */
+    public static final class GpuConfig {
+        private static final String KEY_BACKEND = "backend";
+        private static final String KEY_CONTEXT_TYPES = "context_types";
+        private static final String KEY_PCI_ADDRESS = "pci_address";
+        private static final String KEY_RENDERER_FEATURES = "renderer_features";
+        private static final String KEY_RENDERER_USE_EGL = "renderer_use_egl";
+        private static final String KEY_RENDERER_USE_GLES = "renderer_use_gles";
+        private static final String KEY_RENDERER_USE_GLX = "renderer_use_glx";
+        private static final String KEY_RENDERER_USE_SURFACELESS = "renderer_use_surfaceless";
+        private static final String KEY_RENDERER_USE_VULKAN = "renderer_use_vulkan";
+
+        private final String backend;
+        private final String[] contextTypes;
+        private final String pciAddress;
+        private final String rendererFeatures;
+        private final boolean rendererUseEgl;
+        private final boolean rendererUseGles;
+        private final boolean rendererUseGlx;
+        private final boolean rendererUseSurfaceless;
+        private final boolean rendererUseVulkan;
+
+        private GpuConfig(
+                String backend,
+                String[] contextTypes,
+                String pciAddress,
+                String rendererFeatures,
+                boolean rendererUseEgl,
+                boolean rendererUseGles,
+                boolean rendererUseGlx,
+                boolean rendererUseSurfaceless,
+                boolean rendererUseVulkan) {
+            this.backend = backend;
+            this.contextTypes = contextTypes;
+            this.pciAddress = pciAddress;
+            this.rendererFeatures = rendererFeatures;
+            this.rendererUseEgl = rendererUseEgl;
+            this.rendererUseGles = rendererUseGles;
+            this.rendererUseGlx = rendererUseGlx;
+            this.rendererUseSurfaceless = rendererUseSurfaceless;
+            this.rendererUseVulkan = rendererUseVulkan;
+        }
+
+        /** @hide */
+        public String getBackend() {
+            return backend;
+        }
+
+        /** @hide */
+        public String[] getContextTypes() {
+            return contextTypes;
+        }
+
+        /** @hide */
+        public String getPciAddress() {
+            return pciAddress;
+        }
+
+        /** @hide */
+        public String getRendererFeatures() {
+            return rendererFeatures;
+        }
+
+        /** @hide */
+        public boolean getRendererUseEgl() {
+            return rendererUseEgl;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGles() {
+            return rendererUseGles;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGlx() {
+            return rendererUseGlx;
+        }
+
+        /** @hide */
+        public boolean getRendererUseSurfaceless() {
+            return rendererUseSurfaceless;
+        }
+
+        /** @hide */
+        public boolean getRendererUseVulkan() {
+            return rendererUseVulkan;
+        }
+
+        android.system.virtualizationservice.GpuConfig toParcelable() {
+            android.system.virtualizationservice.GpuConfig parcelable =
+                    new android.system.virtualizationservice.GpuConfig();
+            parcelable.backend = this.backend;
+            parcelable.contextTypes = this.contextTypes;
+            parcelable.pciAddress = this.pciAddress;
+            parcelable.rendererFeatures = this.rendererFeatures;
+            parcelable.rendererUseEgl = this.rendererUseEgl;
+            parcelable.rendererUseGles = this.rendererUseGles;
+            parcelable.rendererUseGlx = this.rendererUseGlx;
+            parcelable.rendererUseSurfaceless = this.rendererUseSurfaceless;
+            parcelable.rendererUseVulkan = this.rendererUseVulkan;
+            return parcelable;
+        }
+
+        private static GpuConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setBackend(pb.getString(KEY_BACKEND));
+            builder.setContextTypes(pb.getStringArray(KEY_CONTEXT_TYPES));
+            builder.setPciAddress(pb.getString(KEY_PCI_ADDRESS));
+            builder.setRendererFeatures(pb.getString(KEY_RENDERER_FEATURES));
+            builder.setRendererUseEgl(pb.getBoolean(KEY_RENDERER_USE_EGL));
+            builder.setRendererUseGles(pb.getBoolean(KEY_RENDERER_USE_GLES));
+            builder.setRendererUseGlx(pb.getBoolean(KEY_RENDERER_USE_GLX));
+            builder.setRendererUseSurfaceless(pb.getBoolean(KEY_RENDERER_USE_SURFACELESS));
+            builder.setRendererUseVulkan(pb.getBoolean(KEY_RENDERER_USE_VULKAN));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putString(KEY_BACKEND, this.backend);
+            pb.putStringArray(KEY_CONTEXT_TYPES, this.contextTypes);
+            pb.putString(KEY_PCI_ADDRESS, this.pciAddress);
+            pb.putString(KEY_RENDERER_FEATURES, this.rendererFeatures);
+            pb.putBoolean(KEY_RENDERER_USE_EGL, this.rendererUseEgl);
+            pb.putBoolean(KEY_RENDERER_USE_GLES, this.rendererUseGles);
+            pb.putBoolean(KEY_RENDERER_USE_GLX, this.rendererUseGlx);
+            pb.putBoolean(KEY_RENDERER_USE_SURFACELESS, this.rendererUseSurfaceless);
+            pb.putBoolean(KEY_RENDERER_USE_VULKAN, this.rendererUseVulkan);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            private String backend;
+            private String[] contextTypes;
+            private String pciAddress;
+            private String rendererFeatures;
+            private boolean rendererUseEgl = true;
+            private boolean rendererUseGles = true;
+            private boolean rendererUseGlx = false;
+            private boolean rendererUseSurfaceless = true;
+            private boolean rendererUseVulkan = false;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setBackend(String backend) {
+                this.backend = backend;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setContextTypes(String[] contextTypes) {
+                this.contextTypes = contextTypes;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setPciAddress(String pciAddress) {
+                this.pciAddress = pciAddress;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererFeatures(String rendererFeatures) {
+                this.rendererFeatures = rendererFeatures;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseEgl(Boolean rendererUseEgl) {
+                this.rendererUseEgl = rendererUseEgl;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGles(Boolean rendererUseGles) {
+                this.rendererUseGles = rendererUseGles;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGlx(Boolean rendererUseGlx) {
+                this.rendererUseGlx = rendererUseGlx;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseSurfaceless(Boolean rendererUseSurfaceless) {
+                this.rendererUseSurfaceless = rendererUseSurfaceless;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseVulkan(Boolean rendererUseVulkan) {
+                this.rendererUseVulkan = rendererUseVulkan;
+                return this;
+            }
+
+            /** @hide */
+            public GpuConfig build() {
+                return new GpuConfig(
+                        backend,
+                        contextTypes,
+                        pciAddress,
+                        rendererFeatures,
+                        rendererUseEgl,
+                        rendererUseGles,
+                        rendererUseGlx,
+                        rendererUseSurfaceless,
+                        rendererUseVulkan);
+            }
+        }
+    }
 }
diff --git a/java/service/Android.bp b/java/service/Android.bp
index 814445c..8bac7be 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -31,7 +31,6 @@
     ],
     static_libs: [
         "android.system.virtualizationmaintenance-java",
-        "android.system.vmtethering-java",
     ],
     sdk_version: "core_platform",
     apex_available: ["com.android.virt"],
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 4903b1c..2461755 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -26,7 +26,6 @@
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
-import android.system.vmtethering.IVmTethering;
 import android.util.Log;
 
 import com.android.internal.os.BackgroundThread;
@@ -41,19 +40,17 @@
  */
 public class VirtualizationSystemService extends SystemService {
     private static final String TAG = VirtualizationSystemService.class.getName();
-    private static final String MAINTENANCE_SERVICE_NAME =
-            "android.system.virtualizationmaintenance";
+    private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
     private Handler mHandler;
-    private final TetheringService mTetheringService;
 
     public VirtualizationSystemService(Context context) {
         super(context);
-        mTetheringService = new TetheringService();
     }
 
     @Override
     public void onStart() {
-        publishBinderService(IVmTethering.DESCRIPTOR, mTetheringService);
+        // Nothing needed here - we don't expose any binder service. The binder service we use is
+        // exposed as a lazy service by the virtualizationservice native binary.
     }
 
     @Override
@@ -85,11 +82,11 @@
     }
 
     static IVirtualizationMaintenance connectToMaintenanceService() {
-        IBinder binder = ServiceManager.waitForService(MAINTENANCE_SERVICE_NAME);
+        IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
         IVirtualizationMaintenance maintenance =
                 IVirtualizationMaintenance.Stub.asInterface(binder);
         if (maintenance == null) {
-            throw new IllegalStateException("Failed to connect to " + MAINTENANCE_SERVICE_NAME);
+            throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
         }
         return maintenance;
     }
@@ -139,11 +136,4 @@
             }
         }
     }
-
-    private static final class TetheringService extends IVmTethering.Stub {
-        @Override
-        public void enableVmTethering() throws UnsupportedOperationException {
-            throw new UnsupportedOperationException("VM tethering is not supported yet");
-        }
-    }
 }
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 0557127..6e4a793 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -30,47 +30,179 @@
 
 namespace {
 
+class SinkANativeWindow_Buffer {
+public:
+    SinkANativeWindow_Buffer() = default;
+    virtual ~SinkANativeWindow_Buffer() = default;
+
+    bool configure(uint32_t width, uint32_t height, int format) {
+        if (format != HAL_PIXEL_FORMAT_BGRA_8888) {
+            return false;
+        }
+
+        mBufferBits.resize(width * height * 4);
+        mBuffer = ANativeWindow_Buffer{
+                .width = static_cast<int32_t>(width),
+                .height = static_cast<int32_t>(height),
+                .stride = static_cast<int32_t>(width),
+                .format = format,
+                .bits = mBufferBits.data(),
+        };
+        return true;
+    }
+
+    operator ANativeWindow_Buffer&() { return mBuffer; }
+
+private:
+    ANativeWindow_Buffer mBuffer;
+    std::vector<uint8_t> mBufferBits;
+};
+
+// Wrapper which contains the latest available Surface/ANativeWindow
+// from the DisplayService, if available. A Surface/ANativeWindow may
+// not always be available if, for example, the VmLauncherApp on the
+// other end of the DisplayService is not in the foreground / is paused.
+class AndroidDisplaySurface {
+public:
+    AndroidDisplaySurface() = default;
+    virtual ~AndroidDisplaySurface() = default;
+
+    void setSurface(Surface* surface) {
+        {
+            std::lock_guard lk(mSurfaceMutex);
+            mNativeSurface = std::make_unique<Surface>(surface->release());
+            mNativeSurfaceNeedsConfiguring = true;
+        }
+
+        mNativeSurfaceReady.notify_one();
+    }
+
+    void removeSurface() {
+        {
+            std::lock_guard lk(mSurfaceMutex);
+            mNativeSurface = nullptr;
+        }
+        mNativeSurfaceReady.notify_one();
+    }
+
+    Surface* getSurface() {
+        std::unique_lock lk(mSurfaceMutex);
+        return mNativeSurface.get();
+    }
+
+    void configure(uint32_t width, uint32_t height) {
+        std::unique_lock lk(mSurfaceMutex);
+
+        mRequestedSurfaceDimensions = Rect{
+                .width = width,
+                .height = height,
+        };
+
+        mSinkBuffer.configure(width, height, kFormat);
+    }
+
+    void waitForNativeSurface() {
+        std::unique_lock lk(mSurfaceMutex);
+        mNativeSurfaceReady.wait(lk, [this] { return mNativeSurface != nullptr; });
+    }
+
+    int lock(ANativeWindow_Buffer* out_buffer) {
+        std::unique_lock lk(mSurfaceMutex);
+
+        Surface* surface = mNativeSurface.get();
+        if (surface == nullptr) {
+            // Surface not currently available but not necessarily an error
+            // if, for example, the VmLauncherApp is not in the foreground.
+            *out_buffer = mSinkBuffer;
+            return 0;
+        }
+
+        ANativeWindow* anw = surface->get();
+        if (anw == nullptr) {
+            return -1;
+        }
+
+        if (mNativeSurfaceNeedsConfiguring) {
+            if (!mRequestedSurfaceDimensions) {
+                return -1;
+            }
+            const auto& dims = *mRequestedSurfaceDimensions;
+
+            // Ensure locked buffers have our desired format.
+            if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
+                return -1;
+            }
+
+            mNativeSurfaceNeedsConfiguring = false;
+        }
+
+        return ANativeWindow_lock(anw, out_buffer, nullptr);
+    }
+
+    int unlockAndPost() {
+        std::unique_lock lk(mSurfaceMutex);
+
+        Surface* surface = mNativeSurface.get();
+        if (surface == nullptr) {
+            // Surface not currently available but not necessarily an error
+            // if, for example, the VmLauncherApp is not in the foreground.
+            return 0;
+        }
+
+        ANativeWindow* anw = surface->get();
+        if (anw == nullptr) {
+            return -1;
+        }
+
+        return ANativeWindow_unlockAndPost(anw);
+    }
+
+private:
+    // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in
+    // crosvm 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.
+    static constexpr const int kFormat = HAL_PIXEL_FORMAT_BGRA_8888;
+
+    std::mutex mSurfaceMutex;
+    std::unique_ptr<Surface> mNativeSurface;
+    std::condition_variable mNativeSurfaceReady;
+    bool mNativeSurfaceNeedsConfiguring = true;
+
+    SinkANativeWindow_Buffer mSinkBuffer;
+
+    struct Rect {
+        uint32_t width = 0;
+        uint32_t height = 0;
+    };
+    std::optional<Rect> mRequestedSurfaceDimensions;
+};
+
 class DisplayService : public BnCrosvmAndroidDisplayService {
 public:
     DisplayService() = default;
     virtual ~DisplayService() = default;
 
     ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
-        {
-            std::lock_guard lk(mSurfaceReadyMutex);
-            if (forCursor) {
-                mCursorSurface = std::make_unique<Surface>(surface->release());
-            } else {
-                mSurface = std::make_unique<Surface>(surface->release());
-            }
+        if (forCursor) {
+            mCursor.setSurface(surface);
+        } else {
+            mScanout.setSurface(surface);
         }
-        mSurfaceReady.notify_all();
         return ::ndk::ScopedAStatus::ok();
     }
 
     ndk::ScopedAStatus removeSurface(bool forCursor) override {
-        {
-            std::lock_guard lk(mSurfaceReadyMutex);
-            if (forCursor) {
-                mCursorSurface = nullptr;
-            } else {
-                mSurface = nullptr;
-            }
+        if (forCursor) {
+            mCursor.removeSurface();
+        } else {
+            mScanout.removeSurface();
         }
-        mSurfaceReady.notify_all();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    Surface* getSurface(bool forCursor) {
-        std::unique_lock lk(mSurfaceReadyMutex);
-        if (forCursor) {
-            mSurfaceReady.wait(lk, [this] { return mCursorSurface != nullptr; });
-            return mCursorSurface.get();
-        } else {
-            mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
-            return mSurface.get();
-        }
-    }
+    AndroidDisplaySurface* getCursorSurface() { return &mCursor; }
+    AndroidDisplaySurface* getScanoutSurface() { return &mScanout; }
+
     ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
     ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
         mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
@@ -78,10 +210,8 @@
     }
 
 private:
-    std::condition_variable mSurfaceReady;
-    std::mutex mSurfaceReadyMutex;
-    std::unique_ptr<Surface> mSurface;
-    std::unique_ptr<Surface> mCursorSurface;
+    AndroidDisplaySurface mScanout;
+    AndroidDisplaySurface mCursor;
     ndk::ScopedFileDescriptor mCursorStream;
 };
 
@@ -149,25 +279,29 @@
     delete ctx;
 }
 
-extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
-                                                 uint32_t height, bool for_cursor) {
+extern "C" AndroidDisplaySurface* create_android_surface(struct AndroidDisplayContext* ctx,
+                                                         uint32_t width, uint32_t height,
+                                                         bool forCursor) {
     if (ctx->disp_service == nullptr) {
         ctx->errorf("Display service was not created");
         return nullptr;
     }
-    // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in crosvm
-    // 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(for_cursor)->get(); // this can block
-    if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
-        ctx->errorf("Failed to set buffer gemoetry");
+
+    AndroidDisplaySurface* displaySurface = forCursor ? ctx->disp_service->getCursorSurface()
+                                                      : ctx->disp_service->getScanoutSurface();
+    if (displaySurface == nullptr) {
+        ctx->errorf("AndroidDisplaySurface was not created");
         return nullptr;
     }
+
+    displaySurface->configure(width, height);
+
+    displaySurface->waitForNativeSurface(); // this can block
+
     // TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
     // consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
     // crosvm doesn't hold a dangling pointer.
-    return surface;
+    return displaySurface;
 }
 
 extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
@@ -175,16 +309,23 @@
 }
 
 extern "C" bool get_android_surface_buffer(struct AndroidDisplayContext* ctx,
-                                           ANativeWindow* surface,
+                                           AndroidDisplaySurface* surface,
                                            ANativeWindow_Buffer* out_buffer) {
     if (out_buffer == nullptr) {
         ctx->errorf("out_buffer is null");
         return false;
     }
-    if (ANativeWindow_lock(surface, out_buffer, nullptr) != 0) {
+
+    if (surface == nullptr) {
+        ctx->errorf("Invalid AndroidDisplaySurface provided");
+        return false;
+    }
+
+    if (surface->lock(out_buffer) != 0) {
         ctx->errorf("Failed to lock buffer");
         return false;
     }
+
     return true;
 }
 
@@ -204,9 +345,14 @@
 }
 
 extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
-                                            ANativeWindow* surface) {
-    if (ANativeWindow_unlockAndPost(surface) != 0) {
-        ctx->errorf("Failed to unlock and post surface.");
+                                            AndroidDisplaySurface* surface) {
+    if (surface == nullptr) {
+        ctx->errorf("Invalid AndroidDisplaySurface provided");
+        return;
+    }
+
+    if (surface->unlockAndPost() != 0) {
+        ctx->errorf("Failed to unlock and post AndroidDisplaySurface.");
         return;
     }
 }
diff --git a/microdroid_manager/src/verify.rs b/microdroid_manager/src/verify.rs
index 65c32b0..84feb68 100644
--- a/microdroid_manager/src/verify.rs
+++ b/microdroid_manager/src/verify.rs
@@ -14,7 +14,7 @@
 
 use crate::instance::{ApexData, ApkData, MicrodroidData};
 use crate::payload::{get_apex_data_from_payload, to_metadata};
-use crate::{is_strict_boot, is_verified_boot, MicrodroidError};
+use crate::{is_strict_boot, MicrodroidError};
 use anyhow::{anyhow, ensure, Context, Result};
 use apkmanifest::get_manifest_info;
 use apkverify::{extract_signed_data, verify, V4Signature};
@@ -130,11 +130,10 @@
     // APEX payload.
     let apex_data_from_payload = get_apex_data_from_payload(metadata)?;
 
-    // Writing /apex/vm-payload-metadata is to verify that the payload isn't changed.
-    // Skip writing it if the debug policy ignoring identity is on
-    if is_verified_boot() {
-        write_apex_payload_data(saved_data, &apex_data_from_payload)?;
-    }
+    // To prevent a TOCTOU attack, we need to make sure that when apexd verifies & mounts the
+    // APEXes it sees the same ones that we just read - so we write the metadata we just collected
+    // to a file (that the host can't access) that apexd will then verify against. See b/199371341.
+    write_apex_payload_data(saved_data, &apex_data_from_payload)?;
 
     if cfg!(not(dice_changes)) {
         // Start apexd to activate APEXes
@@ -222,16 +221,17 @@
             saved_apex_data == apex_data_from_payload,
             MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
         );
-        let apex_metadata = to_metadata(apex_data_from_payload);
-        // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
-        // metadata instead of the default one (/dev/block/by-name/payload-metadata)
-        OpenOptions::new()
-            .create_new(true)
-            .write(true)
-            .open("/apex/vm-payload-metadata")
-            .context("Failed to open /apex/vm-payload-metadata")
-            .and_then(|f| write_metadata(&apex_metadata, f))?;
     }
+    let apex_metadata = to_metadata(apex_data_from_payload);
+    // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
+    // metadata instead of the default one (/dev/block/by-name/payload-metadata)
+    OpenOptions::new()
+        .create_new(true)
+        .write(true)
+        .open("/apex/vm-payload-metadata")
+        .context("Failed to open /apex/vm-payload-metadata")
+        .and_then(|f| write_metadata(&apex_metadata, f))?;
+
     Ok(())
 }
 
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index ec40b45..c16a45e 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -20,7 +20,7 @@
 use secretkeeper_comm::data_types::request::Request;
 use binder::{Strong};
 use coset::{CoseKey, CborSerializable, CborOrdering};
-use dice_policy_builder::{CertIndex, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
+use dice_policy_builder::{TargetEntry, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
 use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
 use keystore2_crypto::ZVec;
 use openssl::hkdf::hkdf;
@@ -45,9 +45,10 @@
 const SUBCOMPONENT_DESCRIPTORS: i64 = -71002;
 const SUBCOMPONENT_SECURITY_VERSION: i64 = 2;
 const SUBCOMPONENT_AUTHORITY_HASH: i64 = 4;
-// Index of DiceChainEntry corresponding to Payload (relative to the end considering DICE Chain
-// as an array)
-const PAYLOAD_INDEX_FROM_END: usize = 0;
+// See dice_for_avf_guest.cddl for the `component_name` used by different boot stages in guest VM.
+const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
+const GUEST_OS_COMPONENT_NAME: &str = "vm_entry";
+const INSTANCE_HASH_KEY: i64 = -71003;
 
 // Generated using hexdump -vn32 -e'14/1 "0x%02X, " 1 "\n"' /dev/urandom
 const SALT_ENCRYPTED_STORE: &[u8] = &[
@@ -173,25 +174,27 @@
 //     microdroid_manager/src/vm_config.cddl):
 //       - GreaterOrEqual on SECURITY_VERSION (Required)
 //       - ExactMatch on AUTHORITY_HASH (Required).
+//  5. ExactMatch on Instance Hash (Required) - This uniquely identifies one VM instance from
+//     another even if they are running the exact same images.
 fn sealing_policy(dice: &[u8]) -> Result<Vec<u8>, String> {
-    let constraint_spec = [
+    let constraint_spec = vec![
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
             vec![AUTHORITY_HASH],
             MissingAction::Fail,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
             vec![MODE],
             MissingAction::Fail,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::GreaterOrEqual,
             vec![CONFIG_DESC, SECURITY_VERSION],
             MissingAction::Ignore,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::GreaterOrEqual,
@@ -202,7 +205,7 @@
                 SUBCOMPONENT_SECURITY_VERSION,
             ],
             MissingAction::Fail,
-            CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+            TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
         ),
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
@@ -213,11 +216,17 @@
                 SUBCOMPONENT_AUTHORITY_HASH,
             ],
             MissingAction::Fail,
-            CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+            TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
+        ),
+        ConstraintSpec::new(
+            ConstraintType::ExactMatch,
+            vec![CONFIG_DESC, INSTANCE_HASH_KEY],
+            MissingAction::Fail,
+            TargetEntry::ByName(GUEST_OS_COMPONENT_NAME.to_string()),
         ),
     ];
 
-    policy_for_dice_chain(dice, &constraint_spec)?
+    policy_for_dice_chain(dice, constraint_spec)?
         .to_vec()
         .map_err(|e| format!("DicePolicy construction failed {e:?}"))
 }
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index d8b17f1..639de06 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -346,6 +346,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldBoostUclamp(true)
                         .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -728,6 +729,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldBoostUclamp(true)
                         .build();
 
         List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
diff --git a/tests/ferrochrome/assets/vm_config.json b/tests/ferrochrome/assets/vm_config.json
index f8a3099..1d32463 100644
--- a/tests/ferrochrome/assets/vm_config.json
+++ b/tests/ferrochrome/assets/vm_config.json
@@ -1,6 +1,5 @@
 {
     "name": "cros",
-    "kernel": "/data/local/tmp/ferrochrome/vmlinuz",
     "disks": [
         {
             "image": "/data/local/tmp/ferrochrome/chromiumos_test_image.bin",
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 4dde401..5638b34 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -102,16 +102,16 @@
 
     echo "Downloading ferrochrome image to ${fecr_dir}"
     fecr_version=${fecr_version:-${FECR_DEFAULT_VERSION}}
-    curl --output-dir ${fecr_dir} -O ${FECR_GS_URL}/${fecr_version}/image.zip
+    curl --output-dir ${fecr_dir} -O ${FECR_GS_URL}/${fecr_version}/chromiumos_test_image.tar.xz
   fi
   if [[ ! -f "${fecr_dir}/chromiumos_test_image.bin" ]]; then
-    unzip ${fecr_dir}/image.zip chromiumos_test_image.bin boot_images/vmlinuz* -d ${fecr_dir} > /dev/null
+    echo "Extrating ferrochrome image"
+    tar xvf ${fecr_dir}/chromiumos_test_image.tar.xz -C ${fecr_dir} > /dev/null
   fi
 
   echo "Pushing ferrochrome image to ${FECR_DEVICE_DIR}"
   adb shell mkdir -p ${FECR_DEVICE_DIR} > /dev/null || true
   adb push ${fecr_dir}/chromiumos_test_image.bin ${FECR_DEVICE_DIR}
-  adb push ${fecr_dir}/boot_images/vmlinuz ${FECR_DEVICE_DIR}
   adb push ${fecr_script_path}/assets/vm_config.json ${FECR_CONFIG_PATH}
 fi
 
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 98ef092..07f51b3 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -213,6 +213,7 @@
                 .isTrue();
         int vendorApiLevel = getVendorApiLevel();
         boolean isGsi = new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+        Log.i(TAG, "isGsi = " + isGsi + ", vendor api level = " + vendorApiLevel);
         assume().withMessage("GSI with vendor API level < 202404 may not support AVF")
                 .that(isGsi && vendorApiLevel < 202404)
                 .isFalse();
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 8e1b6bb..9df376a 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -535,6 +535,16 @@
         } else {
             None
         };
+        let gpu_config = if cfg!(paravirtualized_devices) {
+            config
+                .gpuConfig
+                .as_ref()
+                .map(GpuConfig::new)
+                .transpose()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            None
+        };
 
         let input_device_options = if cfg!(paravirtualized_devices) {
             config
@@ -600,6 +610,7 @@
             virtio_snd_backend,
             console_input_device: config.consoleInputDevice.clone(),
             boost_uclamp: config.boostUclamp,
+            gpu_config,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -1229,6 +1240,10 @@
             .or_service_specific_exception(-1)?;
         Ok(vsock_stream_to_pfd(stream))
     }
+
+    fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
+        self.instance.vm_context.global_context.setHostConsoleName(ptsname)
+    }
 }
 
 impl Drop for VirtualMachine {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 7769f61..ee5f5cd 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -47,6 +47,7 @@
     MemoryTrimLevel::MemoryTrimLevel,
     VirtualMachineAppConfig::DebugLevel::DebugLevel,
     DisplayConfig::DisplayConfig as DisplayConfigParcelable,
+    GpuConfig::GpuConfig as GpuConfigParcelable,
 };
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -133,6 +134,7 @@
     pub virtio_snd_backend: Option<String>,
     pub console_input_device: Option<String>,
     pub boost_uclamp: bool,
+    pub gpu_config: Option<GpuConfig>,
 }
 
 #[derive(Debug)]
@@ -155,6 +157,37 @@
     }
 }
 
+#[derive(Debug)]
+pub struct GpuConfig {
+    pub backend: Option<String>,
+    pub context_types: Option<Vec<String>>,
+    pub pci_address: Option<String>,
+    pub renderer_features: Option<String>,
+    pub renderer_use_egl: Option<bool>,
+    pub renderer_use_gles: Option<bool>,
+    pub renderer_use_glx: Option<bool>,
+    pub renderer_use_surfaceless: Option<bool>,
+    pub renderer_use_vulkan: Option<bool>,
+}
+
+impl GpuConfig {
+    pub fn new(raw_config: &GpuConfigParcelable) -> Result<GpuConfig> {
+        Ok(GpuConfig {
+            backend: raw_config.backend.clone(),
+            context_types: raw_config.contextTypes.clone().map(|context_types| {
+                context_types.iter().filter_map(|context_type| context_type.clone()).collect()
+            }),
+            pci_address: raw_config.pciAddress.clone(),
+            renderer_features: raw_config.rendererFeatures.clone(),
+            renderer_use_egl: Some(raw_config.rendererUseEgl),
+            renderer_use_gles: Some(raw_config.rendererUseGles),
+            renderer_use_glx: Some(raw_config.rendererUseGlx),
+            renderer_use_surfaceless: Some(raw_config.rendererUseSurfaceless),
+            renderer_use_vulkan: Some(raw_config.rendererUseVulkan),
+        })
+    }
+}
+
 fn try_into_non_zero_u32(value: i32) -> Result<NonZeroU32> {
     let u32_value = value.try_into()?;
     NonZeroU32::new(u32_value).ok_or(anyhow!("value should be greater than 0"))
@@ -283,7 +316,7 @@
 #[derive(Debug)]
 pub struct VmContext {
     #[allow(dead_code)] // Keeps the global context alive
-    global_context: Strong<dyn IGlobalVmContext>,
+    pub(crate) global_context: Strong<dyn IGlobalVmContext>,
     #[allow(dead_code)] // Keeps the server alive
     vm_server: RpcServer,
 }
@@ -302,7 +335,7 @@
     pub vm_state: Mutex<VmState>,
     /// Global resources allocated for this VM.
     #[allow(dead_code)] // Keeps the context alive
-    vm_context: VmContext,
+    pub(crate) vm_context: VmContext,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// Path to crosvm control socket
@@ -1009,12 +1042,48 @@
     }
 
     if cfg!(paravirtualized_devices) {
+        if let Some(gpu_config) = &config.gpu_config {
+            let mut gpu_args = Vec::new();
+            if let Some(backend) = &gpu_config.backend {
+                gpu_args.push(format!("backend={}", backend));
+            }
+            if let Some(context_types) = &gpu_config.context_types {
+                gpu_args.push(format!("context-types={}", context_types.join(":")));
+            }
+            if let Some(pci_address) = &gpu_config.pci_address {
+                gpu_args.push(format!("pci-address={}", pci_address));
+            }
+            if let Some(renderer_features) = &gpu_config.renderer_features {
+                gpu_args.push(format!("renderer-features={}", renderer_features));
+            }
+            if gpu_config.renderer_use_egl.unwrap_or(false) {
+                gpu_args.push("egl=true".to_string());
+            }
+            if gpu_config.renderer_use_gles.unwrap_or(false) {
+                gpu_args.push("gles=true".to_string());
+            }
+            if gpu_config.renderer_use_glx.unwrap_or(false) {
+                gpu_args.push("glx=true".to_string());
+            }
+            if gpu_config.renderer_use_surfaceless.unwrap_or(false) {
+                gpu_args.push("surfaceless=true".to_string());
+            }
+            if gpu_config.renderer_use_vulkan.unwrap_or(false) {
+                gpu_args.push("vulkan=true".to_string());
+            }
+            command.arg(format!("--gpu={}", gpu_args.join(",")));
+        }
         if let Some(display_config) = &config.display_config {
-            command.arg("--gpu")
-            // TODO(b/331708504): support backend config as well
-            .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
-            .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
-            .arg(format!("--android-display-service={}", config.name));
+            command
+                .arg(format!(
+                    "--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}",
+                    display_config.width,
+                    display_config.height,
+                    display_config.horizontal_dpi,
+                    display_config.vertical_dpi,
+                    display_config.refresh_rate
+                ))
+                .arg(format!("--android-display-service={}", config.name));
         }
     }
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index f9034af..0c39501 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -30,7 +30,6 @@
         "android.system.virtualizationservice-rust",
         "android.system.virtualizationservice_internal-rust",
         "android.system.virtualmachineservice-rust",
-        "android.system.vmtethering-rust",
         "android.os.permissions_aidl-rust",
         "libandroid_logger",
         "libanyhow",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index bca4512..fb89772 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -86,26 +86,6 @@
 }
 
 aidl_interface {
-    name: "android.system.vmtethering",
-    srcs: ["android/system/vmtethering/**/*.aidl"],
-    unstable: true,
-    backend: {
-        java: {
-            sdk_version: "module_current",
-            apex_available: [
-                "com.android.virt",
-            ],
-        },
-        rust: {
-            enabled: true,
-            apex_available: [
-                "com.android.virt",
-            ],
-        },
-    },
-}
-
-aidl_interface {
     name: "android.system.virtualmachineservice",
     srcs: ["android/system/virtualmachineservice/**/*.aidl"],
     imports: [
diff --git a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
similarity index 60%
rename from virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
rename to virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
index 732a515..1cd4dc6 100644
--- a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
@@ -13,12 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.system.virtualizationservice;
 
-package android.system.vmtethering;
-
-interface IVmTethering {
-    /**
-     * Start VM tethering to provide external network to VM.
-     */
-    void enableVmTethering();
+parcelable GpuConfig {
+    @nullable String backend;
+    @nullable String[] contextTypes;
+    @nullable String pciAddress;
+    @nullable String rendererFeatures;
+    boolean rendererUseEgl = false;
+    boolean rendererUseGles = false;
+    boolean rendererUseGlx = false;
+    boolean rendererUseSurfaceless = false;
+    boolean rendererUseVulkan = false;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index d76b586..d4001c8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -47,4 +47,7 @@
 
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(in @utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index 870a342..9f033b1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -33,4 +33,7 @@
      * the PID may have been reused for a different process, so this should not be trusted.
      */
     int requesterPid;
+
+    /** The peer end (ptsname) of the host console. */
+    @nullable @utf8InCpp String hostConsoleName;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 0d175dd..69664b4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -18,6 +18,7 @@
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.DisplayConfig;
+import android.system.virtualizationservice.GpuConfig;
 import android.system.virtualizationservice.InputDevice;
 
 /** Raw configuration for running a VM. */
@@ -94,4 +95,6 @@
 
     /** Enable boost UClamp for less variance during testing/benchmarking */
     boolean boostUclamp;
+
+    @nullable GpuConfig gpuConfig;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index a4d5d19..ea52591 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -21,4 +21,7 @@
 
     /** Get the path to the temporary folder of the VM. */
     String getTemporaryDirectory();
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(@utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index f69cad4..70da37b 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -25,7 +25,6 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use android_system_virtualizationservice_internal as android_vs_internal;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
-use android_system_vmtethering::aidl::android::system::vmtethering;
 use android_vs_internal::aidl::android::system::virtualizationservice_internal;
 use anyhow::{anyhow, ensure, Context, Result};
 use avflog::LogResult;
@@ -74,7 +73,6 @@
     IVmnic::{BpVmnic, IVmnic},
 };
 use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
-use vmtethering::IVmTethering::{BpVmTethering, IVmTethering};
 use vsock::{VsockListener, VsockStream};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
@@ -165,9 +163,6 @@
     static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
         wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
             .expect("Could not connect to Vmnic");
-    static ref TETHERING_SERVICE: Strong<dyn IVmTethering> =
-        wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
-            .expect("Could not connect to VmTethering");
 }
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -287,11 +282,15 @@
             .held_contexts
             .iter()
             .filter_map(|(_, inst)| Weak::upgrade(inst))
-            .map(|vm| VirtualMachineDebugInfo {
-                cid: vm.cid as i32,
-                temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
-                requesterUid: vm.requester_uid as i32,
-                requesterPid: vm.requester_debug_pid,
+            .map(|vm| {
+                let vm = vm.lock().unwrap();
+                VirtualMachineDebugInfo {
+                    cid: vm.cid as i32,
+                    temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
+                    requesterUid: vm.requester_uid as i32,
+                    requesterPid: vm.requester_debug_pid,
+                    hostConsoleName: vm.host_console_name.clone(),
+                }
             })
             .collect();
         Ok(cids)
@@ -524,19 +523,7 @@
             ))
             .with_log();
         }
-        let tap_fd = NETWORK_SERVICE.createTapInterface(iface_name_suffix)?;
-
-        // TODO(340377643): Enabling tethering should be for bridge interface, not TAP interface.
-        let ret = TETHERING_SERVICE.enableVmTethering();
-        if let Err(e) = ret {
-            if e.exception_code() == ExceptionCode::UNSUPPORTED_OPERATION {
-                warn!("{}", e.get_description());
-            } else {
-                return Err(e);
-            }
-        }
-
-        Ok(tap_fd)
+        NETWORK_SERVICE.createTapInterface(iface_name_suffix)
     }
 
     fn deleteTapInterface(&self, tap_fd: &ParcelFileDescriptor) -> binder::Result<()> {
@@ -660,6 +647,8 @@
     requester_uid: uid_t,
     /// PID of the client who requested this VM instance.
     requester_debug_pid: pid_t,
+    /// Name of the host console.
+    host_console_name: Option<String>,
 }
 
 impl GlobalVmInstance {
@@ -674,7 +663,7 @@
 struct GlobalState {
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
-    held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+    held_contexts: HashMap<Cid, Weak<Mutex<GlobalVmInstance>>>,
 
     /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
     dtbo_file: Mutex<Option<File>>,
@@ -754,8 +743,13 @@
         self.held_contexts.retain(|_, instance| instance.strong_count() > 0);
 
         let cid = self.get_next_available_cid()?;
-        let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
-        create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
+        let instance = Arc::new(Mutex::new(GlobalVmInstance {
+            cid,
+            requester_uid,
+            requester_debug_pid,
+            ..Default::default()
+        }));
+        create_temporary_directory(&instance.lock().unwrap().get_temp_dir(), Some(requester_uid))?;
 
         self.held_contexts.insert(cid, Arc::downgrade(&instance));
         let binder = GlobalVmContext { instance, ..Default::default() };
@@ -835,7 +829,7 @@
 #[derive(Debug, Default)]
 struct GlobalVmContext {
     /// Strong reference to the context's instance data structure.
-    instance: Arc<GlobalVmInstance>,
+    instance: Arc<Mutex<GlobalVmInstance>>,
     /// Keeps our service process running as long as this VM context exists.
     #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
@@ -845,11 +839,16 @@
 
 impl IGlobalVmContext for GlobalVmContext {
     fn getCid(&self) -> binder::Result<i32> {
-        Ok(self.instance.cid as i32)
+        Ok(self.instance.lock().unwrap().cid as i32)
     }
 
     fn getTemporaryDirectory(&self) -> binder::Result<String> {
-        Ok(self.instance.get_temp_dir().to_string_lossy().to_string())
+        Ok(self.instance.lock().unwrap().get_temp_dir().to_string_lossy().to_string())
+    }
+
+    fn setHostConsoleName(&self, pathname: &str) -> binder::Result<()> {
+        self.instance.lock().unwrap().host_console_name = Some(pathname.to_string());
+        Ok(())
     }
 }
 
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index 69c37b8..03819b8 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -19,22 +19,20 @@
 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::ioctl_write_ptr_bad;
 use nix::sys::ioctl::ioctl_num_type;
 use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
 use std::ffi::{CStr, CString};
-use std::fs::File;
+use std::fs::OpenOptions;
 use std::os::fd::{AsRawFd, RawFd};
 use std::slice::from_raw_parts;
 
-const TUNGETIFF: ioctl_num_type = 0x800454d2u32 as c_int;
+const TUNGETIFF: ioctl_num_type = 0x800454d2u32 as ioctl_num_type;
 const TUNSETIFF: ioctl_num_type = 0x400454ca;
-const TUNSETPERSIST: ioctl_num_type = 0x400454cb;
 const SIOCSIFFLAGS: ioctl_num_type = 0x00008914;
 
 ioctl_write_ptr_bad!(ioctl_tungetiff, TUNGETIFF, ifreq);
 ioctl_write_ptr_bad!(ioctl_tunsetiff, TUNSETIFF, ifreq);
-ioctl_write_int_bad!(ioctl_tunsetpersist, TUNSETPERSIST);
 ioctl_write_ptr_bad!(ioctl_siocsifflags, SIOCSIFFLAGS, ifreq);
 
 fn validate_ifname(ifname: &[c_char]) -> Result<()> {
@@ -51,8 +49,6 @@
     ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
     // SAFETY: 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: 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")?;
     // SAFETY: ifr_ifru holds ifru_flags in its union field.
     unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
     // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
@@ -69,13 +65,11 @@
     Ok(ifr)
 }
 
-fn delete_tap_interface(fd: RawFd, sockfd: c_int, ifr: &mut ifreq) -> Result<()> {
+fn delete_tap_interface(sockfd: c_int, ifr: &mut ifreq) -> Result<()> {
     // SAFETY: After calling TUNGETIFF, ifr_ifru holds ifru_flags in its union field.
     unsafe { ifr.ifr_ifru.ifru_flags &= !IFF_UP as c_short };
     // SAFETY: 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 SIOCSIFFLAGS")?;
-    // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
-    unsafe { ioctl_tunsetpersist(fd, 0) }.context("Failed to ioctl TUNSETPERSIST")?;
     Ok(())
 }
 
@@ -105,7 +99,10 @@
             .context(format!("Invalid interface name: {ifname:#?}"))
             .or_service_specific_exception(-1)?;
 
-        let tunfd = File::open("/dev/tun")
+        let tunfd = OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open("/dev/tun")
             .context("Failed to open /dev/tun")
             .or_service_specific_exception(-1)?;
         let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
@@ -120,8 +117,7 @@
     }
 
     fn deleteTapInterface(&self, tapfd: &ParcelFileDescriptor) -> binder::Result<()> {
-        let tap = tapfd.as_raw_fd();
-        let mut tap_ifreq = get_tap_ifreq(tap)
+        let mut tap_ifreq = get_tap_ifreq(tapfd.as_raw_fd())
             .context("Failed to get ifreq of TAP interface")
             .or_service_specific_exception(-1)?;
         // SAFETY: tap_ifreq.ifr_name is null-terminated within IFNAMSIZ, validated when creating
@@ -131,7 +127,7 @@
         let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
             .context("Failed to create socket")
             .or_service_specific_exception(-1)?;
-        delete_tap_interface(tap, sock.as_raw_fd(), &mut tap_ifreq)
+        delete_tap_interface(sock.as_raw_fd(), &mut tap_ifreq)
             .context(format!("Failed to create TAP interface: {ifname:#?}"))
             .or_service_specific_exception(-1)?;
 
diff --git a/vm/src/main.rs b/vm/src/main.rs
index a250c35..3c0887c 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -24,15 +24,18 @@
 };
 #[cfg(not(llpvm_changes))]
 use anyhow::anyhow;
-use anyhow::{Context, Error};
+use anyhow::{bail, Context, Error};
 use binder::{ProcessState, Strong};
 use clap::{Args, Parser};
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
 use serde::Serialize;
+use std::io::{self, IsTerminal};
 use std::num::NonZeroU16;
+use std::os::unix::process::CommandExt;
 use std::path::{Path, PathBuf};
+use std::process::Command;
 
 #[derive(Args, Default)]
 /// Collection of flags that are at VM level and therefore applicable to all subcommands
@@ -324,6 +327,11 @@
         /// Path to idsig of the APK
         path: PathBuf,
     },
+    /// Connect to the serial console of a VM
+    Console {
+        /// CID of the VM
+        cid: Option<i32>,
+    },
 }
 
 fn parse_debug_level(s: &str) -> Result<DebugLevel, String> {
@@ -386,6 +394,7 @@
         Opt::CreateIdsig { apk, path } => {
             command_create_idsig(get_service()?.as_ref(), &apk, &path)
         }
+        Opt::Console { cid } => command_console(cid),
     }
 }
 
@@ -450,6 +459,21 @@
     Ok(())
 }
 
+fn command_console(cid: Option<i32>) -> Result<(), Error> {
+    if !io::stdin().is_terminal() {
+        bail!("Stdin must be a terminal (tty). Use 'adb shell -t' to force allocate tty.");
+    }
+    let mut vms = get_service()?.debugListVms().context("Failed to get list of VMs")?;
+    if let Some(cid) = cid {
+        vms.retain(|vm_info| vm_info.cid == cid);
+    }
+    let host_console_name = vms
+        .into_iter()
+        .find_map(|vm_info| vm_info.hostConsoleName)
+        .context("Failed to get VM with console")?;
+    Err(Command::new("microcom").arg(host_console_name).exec().into())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index f01e8aa..ee12e85 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -57,6 +57,8 @@
         hwaddress: false,
     },
     native_coverage: false,
+    // TODO(b/346974429): Workaround pvmfw failure when enabling full LTO
+    lto_O0: true,
 }
 
 // Used by cc_binary when producing the ELF of a vmbase-based binary.
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 5355313..33e4755 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -32,6 +32,7 @@
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.DisplayMetrics;
@@ -63,6 +64,7 @@
 import java.nio.ByteOrder;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -125,6 +127,47 @@
             if (json.has("console_input_device")) {
                 configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
             }
+            if (json.has("gpu")) {
+                JSONObject gpuJson = json.getJSONObject("gpu");
+
+                GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder();
+
+                if (gpuJson.has("backend")) {
+                    gpuConfigBuilder.setBackend(gpuJson.getString("backend"));
+                }
+                if (gpuJson.has("context_types")) {
+                    ArrayList<String> contextTypes = new ArrayList<String>();
+                    JSONArray contextTypesJson = gpuJson.getJSONArray("context_types");
+                    for (int i = 0; i < contextTypesJson.length(); i++) {
+                        contextTypes.add(contextTypesJson.getString(i));
+                    }
+                    gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0]));
+                }
+                if (gpuJson.has("pci_address")) {
+                    gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address"));
+                }
+                if (gpuJson.has("renderer_features")) {
+                    gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features"));
+                }
+                if (gpuJson.has("renderer_use_egl")) {
+                    gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl"));
+                }
+                if (gpuJson.has("renderer_use_gles")) {
+                    gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles"));
+                }
+                if (gpuJson.has("renderer_use_glx")) {
+                    gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx"));
+                }
+                if (gpuJson.has("renderer_use_surfaceless")) {
+                    gpuConfigBuilder.setRendererUseSurfaceless(
+                            gpuJson.getBoolean("renderer_use_surfaceless"));
+                }
+                if (gpuJson.has("renderer_use_vulkan")) {
+                    gpuConfigBuilder.setRendererUseVulkan(
+                            gpuJson.getBoolean("renderer_use_vulkan"));
+                }
+                customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
+            }
 
             configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
             WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();