Merge "[attestation] Document pVM remote attestation" into main
diff --git a/java/framework/README.md b/java/framework/README.md
index bbcd0ef..61ba096 100644
--- a/java/framework/README.md
+++ b/java/framework/README.md
@@ -11,13 +11,17 @@
 The API classes are all in the
 [`android.system.virtualmachine`](src/android/system/virtualmachine) package.
 
-Note that these APIs are all `@SystemApi` and require the restricted
-`android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not
-available to third party apps.
-
 All of these APIs were introduced in API level 34 (Android 14). The classes may
 not exist in devices running an earlier version.
 
+Note that they are all `@SystemApi` and require the restricted
+`android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not
+available to third party apps. In Android 14 the permission was available only to
+privileged apps; in Android 15 it is available to all preinstalled apps. On both
+versions it can also be granted to other apps via `adb shell pm grant` for
+development purposes.
+
+
 ## Detecting AVF Support
 
 The simplest way to detect whether a device has support for AVF is to retrieve
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index dd2719c..d746c7c 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -433,6 +433,7 @@
                 config.serialize(vm.mConfigFilePath);
                 if (vm.mInstanceIdPath != null) {
                     vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
+                    vm.claimInstance();
                 }
 
                 try {
@@ -456,8 +457,8 @@
         } catch (VirtualMachineException | RuntimeException e) {
             // If anything goes wrong, delete any files created so far and the VM's directory
             try {
-                deleteRecursively(vmDir);
-            } catch (IOException innerException) {
+                vmInstanceCleanup(context, name);
+            } catch (Exception innerException) {
                 e.addSuppressed(innerException);
             }
             throw e;
@@ -543,8 +544,8 @@
         } catch (VirtualMachineException | RuntimeException e) {
             // If anything goes wrong, delete any files created so far and the VM's directory
             try {
-                deleteRecursively(vmDir);
-            } catch (IOException innerException) {
+                vmInstanceCleanup(context, name);
+            } catch (Exception innerException) {
                 e.addSuppressed(innerException);
             }
             throw e;
@@ -586,18 +587,52 @@
             // if a new VM is created with the same name (and files) that's unrelated.
             mWasDeleted = true;
         }
-        // TODO(b/294177871): Request deletion of VM secrets.
-        deleteVmDirectory(context, name);
+        vmInstanceCleanup(context, name);
     }
 
-    static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
+    // Delete the full VM directory and notify VirtualizationService to remove this
+    // VM instance for housekeeping.
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    static void vmInstanceCleanup(Context context, String name) throws VirtualMachineException {
+        File vmDir = getVmDir(context, name);
+        notifyInstanceRemoval(vmDir, VirtualizationService.getInstance());
         try {
-            deleteRecursively(getVmDir(context, name));
+            deleteRecursively(vmDir);
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
     }
 
+    private static void notifyInstanceRemoval(
+            File vmDirectory, @NonNull VirtualizationService service) {
+        File instanceIdFile = new File(vmDirectory, INSTANCE_ID_FILE);
+        try {
+            byte[] instanceId = Files.readAllBytes(instanceIdFile.toPath());
+            service.getBinder().removeVmInstance(instanceId);
+        } catch (Exception e) {
+            // Deliberately ignoring error in removing VM instance. This potentially leads to
+            // unaccounted instances in the VS' database. But, nothing much can be done by caller.
+            Log.w(TAG, "Failed to notify VS to remove the VM instance", e);
+        }
+    }
+
+    // Claim the instance. This notifies the global VS about the ownership of this
+    // instance_id for housekeeping purpose.
+    void claimInstance() throws VirtualMachineException {
+        if (mInstanceIdPath != null) {
+            IVirtualizationService service = mVirtualizationService.getBinder();
+            try {
+                byte[] instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+                service.claimVmInstance(instanceId);
+            }
+            catch (IOException e) {
+                throw new VirtualMachineException("failed to read instance_id", e);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
     @GuardedBy("VirtualMachineManager.sCreateLock")
     @NonNull
     private static File createVmDir(@NonNull Context context, @NonNull String name)
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index be80db8..a8f318c 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -641,6 +641,10 @@
             config.disks[i].partitions = new Partition[0];
         }
 
+        config.displayConfig =
+                Optional.ofNullable(customImageConfig.getDisplayConfig())
+                        .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 7a4f564..89df1f2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -22,6 +22,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 /** @hide */
 public class VirtualMachineCustomImageConfig {
@@ -32,12 +33,15 @@
     private static final String KEY_PARAMS = "params";
     private static final String KEY_DISK_WRITABLES = "disk_writables";
     private static final String KEY_DISK_IMAGES = "disk_images";
+    private static final String KEY_DISPLAY_CONFIG = "display_config";
+
     @Nullable private final String name;
     @NonNull private final String kernelPath;
     @Nullable private final String initrdPath;
     @Nullable private final String bootloaderPath;
     @Nullable private final String[] params;
     @Nullable private final Disk[] disks;
+    @Nullable private final DisplayConfig displayConfig;
 
     @Nullable
     public Disk[] getDisks() {
@@ -76,13 +80,15 @@
             String initrdPath,
             String bootloaderPath,
             String[] params,
-            Disk[] disks) {
+            Disk[] disks,
+            DisplayConfig displayConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
         this.bootloaderPath = bootloaderPath;
         this.params = params;
         this.disks = disks;
+        this.displayConfig = displayConfig;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -107,9 +113,15 @@
                 }
             }
         }
+        PersistableBundle displayConfigPb =
+                customImageConfigBundle.getPersistableBundle(KEY_DISPLAY_CONFIG);
+        builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
+
         return builder.build();
     }
 
+
+
     PersistableBundle toPersistableBundle() {
         PersistableBundle pb = new PersistableBundle();
         pb.putString(KEY_NAME, this.name);
@@ -128,9 +140,19 @@
             pb.putBooleanArray(KEY_DISK_WRITABLES, writables);
             pb.putStringArray(KEY_DISK_IMAGES, images);
         }
+        pb.putPersistableBundle(
+                KEY_DISPLAY_CONFIG,
+                Optional.ofNullable(displayConfig)
+                        .map(dc -> dc.toPersistableBundle())
+                        .orElse(null));
         return pb;
     }
 
+    @Nullable
+    public DisplayConfig getDisplayConfig() {
+        return displayConfig;
+    }
+
     /** @hide */
     public static final class Disk {
         private final boolean writable;
@@ -170,6 +192,7 @@
         private String bootloaderPath;
         private List<String> params = new ArrayList<>();
         private List<Disk> disks = new ArrayList<>();
+        private DisplayConfig displayConfig;
 
         /** @hide */
         public Builder() {}
@@ -211,6 +234,12 @@
         }
 
         /** @hide */
+        public Builder setDisplayConfig(DisplayConfig displayConfig) {
+            this.displayConfig = displayConfig;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -218,7 +247,142 @@
                     this.initrdPath,
                     this.bootloaderPath,
                     this.params.toArray(new String[0]),
-                    this.disks.toArray(new Disk[0]));
+                    this.disks.toArray(new Disk[0]),
+                    displayConfig);
+        }
+    }
+
+    /** @hide */
+    public static final class DisplayConfig {
+        private static final String KEY_WIDTH = "width";
+        private static final String KEY_HEIGHT = "height";
+        private static final String KEY_HORIZONTAL_DPI = "horizontal_dpi";
+        private static final String KEY_VERTICAL_DPI = "vertical_dpi";
+        private static final String KEY_REFRESH_RATE = "refresh_rate";
+        private final int width;
+        private final int height;
+        private final int horizontalDpi;
+        private final int verticalDpi;
+        private final int refreshRate;
+
+        private DisplayConfig(
+                int width, int height, int horizontalDpi, int verticalDpi, int refreshRate) {
+            this.width = width;
+            this.height = height;
+            this.horizontalDpi = horizontalDpi;
+            this.verticalDpi = verticalDpi;
+            this.refreshRate = refreshRate;
+        }
+
+        /** @hide */
+        public int getWidth() {
+            return width;
+        }
+
+        /** @hide */
+        public int getHeight() {
+            return height;
+        }
+
+        /** @hide */
+        public int getHorizontalDpi() {
+            return horizontalDpi;
+        }
+
+        /** @hide */
+        public int getVerticalDpi() {
+            return verticalDpi;
+        }
+
+        /** @hide */
+        public int getRefreshRate() {
+            return refreshRate;
+        }
+
+        android.system.virtualizationservice.DisplayConfig toParcelable() {
+            android.system.virtualizationservice.DisplayConfig parcelable =
+                    new android.system.virtualizationservice.DisplayConfig();
+            parcelable.width = this.width;
+            parcelable.height = this.height;
+            parcelable.horizontalDpi = this.horizontalDpi;
+            parcelable.verticalDpi = this.verticalDpi;
+            parcelable.refreshRate = this.refreshRate;
+
+            return parcelable;
+        }
+
+        private static DisplayConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setWidth(pb.getInt(KEY_WIDTH));
+            builder.setHeight(pb.getInt(KEY_HEIGHT));
+            builder.setHorizontalDpi(pb.getInt(KEY_HORIZONTAL_DPI));
+            builder.setVerticalDpi(pb.getInt(KEY_VERTICAL_DPI));
+            builder.setRefreshRate(pb.getInt(KEY_REFRESH_RATE));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putInt(KEY_WIDTH, this.width);
+            pb.putInt(KEY_HEIGHT, this.height);
+            pb.putInt(KEY_HORIZONTAL_DPI, this.horizontalDpi);
+            pb.putInt(KEY_VERTICAL_DPI, this.verticalDpi);
+            pb.putInt(KEY_REFRESH_RATE, this.refreshRate);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            // Default values come from external/crosvm/vm_control/src/gpu.rs
+            private int width;
+            private int height;
+            private int horizontalDpi = 320;
+            private int verticalDpi = 320;
+            private int refreshRate = 60;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setWidth(int width) {
+                this.width = width;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHeight(int height) {
+                this.height = height;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setHorizontalDpi(int horizontalDpi) {
+                this.horizontalDpi = horizontalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setVerticalDpi(int verticalDpi) {
+                this.verticalDpi = verticalDpi;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRefreshRate(int refreshRate) {
+                this.refreshRate = refreshRate;
+                return this;
+            }
+
+            /** @hide */
+            public DisplayConfig build() {
+                if (this.width == 0 || this.height == 0) {
+                    throw new IllegalStateException("width and height must be specified");
+                }
+                return new DisplayConfig(width, height, horizontalDpi, verticalDpi, refreshRate);
+            }
         }
     }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 091d317..8c0c20e 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -284,11 +284,12 @@
     public VirtualMachine importFromDescriptor(
             @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
             throws VirtualMachineException {
+        VirtualMachine vm;
         synchronized (sCreateLock) {
-            VirtualMachine vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+            vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
             mVmsByName.put(name, new WeakReference<>(vm));
-            return vm;
         }
+        return vm;
     }
 
     /**
@@ -334,7 +335,7 @@
         synchronized (sCreateLock) {
             VirtualMachine vm = getVmByName(name);
             if (vm == null) {
-                VirtualMachine.deleteVmDirectory(mContext, name);
+                VirtualMachine.vmInstanceCleanup(mContext, name);
             } else {
                 vm.delete(mContext, name);
             }
diff --git a/libs/android_display_backend/Android.bp b/libs/android_display_backend/Android.bp
new file mode 100644
index 0000000..6ad5fab
--- /dev/null
+++ b/libs/android_display_backend/Android.bp
@@ -0,0 +1,55 @@
+aidl_interface {
+    name: "libcrosvm_android_display_service",
+    srcs: [
+        "aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl",
+    ],
+    include_dirs: [
+        "frameworks/native/aidl/gui",
+    ],
+    local_include_dir: "aidl",
+    unstable: true,
+    backend: {
+        java: {
+            enabled: true,
+        },
+        cpp: {
+            enabled: false,
+        },
+        rust: {
+            enabled: false,
+        },
+        ndk: {
+            enabled: true,
+            // To use Surface
+            additional_shared_libraries: [
+                "libnativewindow",
+            ],
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.virt",
+            ],
+        },
+    },
+}
+
+cc_library_static {
+    name: "libcrosvm_android_display_client",
+    srcs: [
+        "crosvm_android_display_client.cpp",
+    ],
+    whole_static_libs: [
+        "libcrosvm_android_display_service-ndk",
+        "android.system.virtualizationservice_internal-ndk",
+        "android.system.virtualizationcommon-ndk",
+        "android.system.virtualizationservice-ndk",
+        "libyuv",
+    ],
+    shared_libs: [
+        "libbinder_ndk",
+        "libnativewindow",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+}
diff --git a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
new file mode 100644
index 0000000..c7bfc80
--- /dev/null
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.crosvm;
+
+import android.view.Surface;
+
+/**
+ * Service to provide Crosvm with an Android Surface for showing a guest's
+ * display.
+ */
+interface ICrosvmAndroidDisplayService {
+    void setSurface(inout Surface surface);
+
+    void removeSurface();
+}
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
new file mode 100644
index 0000000..a16b7f2
--- /dev/null
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -0,0 +1,286 @@
+/*
+ * 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/crosvm/BnCrosvmAndroidDisplayService.h>
+#include <aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+#include <android/native_window.h>
+#include <android/native_window_aidl.h>
+#include <libyuv.h>
+#include <stdint.h>
+#include <utils/Errors.h>
+
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <vector>
+
+using aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal;
+
+#define LIBEXPORT __attribute__((visibility("default"))) extern "C"
+
+typedef void (*android_display_log_callback_type)(const char* message);
+
+static void android_display_log_callback_stub(const char* message) {
+    (void)message;
+}
+
+namespace {
+
+class DisplayService : public aidl::android::crosvm::BnCrosvmAndroidDisplayService {
+public:
+    DisplayService() = default;
+    virtual ~DisplayService() = default;
+
+    ndk::ScopedAStatus setSurface(aidl::android::view::Surface* surface) override {
+        {
+            std::lock_guard lk(mSurfaceReadyMutex);
+            mSurface = std::make_unique<aidl::android::view::Surface>(surface->release());
+        }
+        mSurfaceReady.notify_one();
+        return ::ndk::ScopedAStatus::ok();
+    }
+
+    ndk::ScopedAStatus removeSurface() override {
+        {
+            std::lock_guard lk(mSurfaceReadyMutex);
+            mSurface = nullptr;
+        }
+        mSurfaceReady.notify_one();
+        return ::ndk::ScopedAStatus::ok();
+    }
+
+    aidl::android::view::Surface* getSurface() {
+        std::unique_lock lk(mSurfaceReadyMutex);
+        mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
+        return mSurface.get();
+    }
+
+private:
+    std::condition_variable mSurfaceReady;
+    std::mutex mSurfaceReadyMutex;
+    std::unique_ptr<aidl::android::view::Surface> mSurface;
+};
+
+void ErrorF(android_display_log_callback_type error_callback, const char* format, ...) {
+    char buffer[1024];
+
+    va_list vararg;
+    va_start(vararg, format);
+    vsnprintf(buffer, sizeof(buffer), format, vararg);
+    va_end(vararg);
+
+    error_callback(buffer);
+}
+
+} // namespace
+
+struct android_display_context {
+    uint32_t width;
+    uint32_t height;
+    std::shared_ptr<DisplayService> displayService;
+};
+
+LIBEXPORT
+struct android_display_context* create_android_display_context(
+        const char* name, size_t name_len, android_display_log_callback_type error_callback) {
+    auto ctx = new android_display_context();
+
+    auto service = ::ndk::SharedRefBase::make<DisplayService>();
+
+    if (strlen(name) != name_len) {
+        ErrorF(error_callback, "Invalid service name length. Expected %u, actual %u", name_len,
+               strlen(name));
+        return nullptr;
+    }
+    ::ndk::SpAIBinder binder(
+            AServiceManager_waitForService("android.system.virtualizationservice"));
+
+    auto virt_service = IVirtualizationServiceInternal::fromBinder(binder);
+    if (virt_service == nullptr) {
+        ErrorF(error_callback, "Failed to find android.system.virtualizationservice");
+        return nullptr;
+    }
+    auto status = virt_service->setDisplayService(service->asBinder());
+    if (!status.isOk()) {
+        ErrorF(error_callback, "Failed to register %s",
+               aidl::android::crosvm::ICrosvmAndroidDisplayService::descriptor);
+        return nullptr;
+    }
+
+    ABinderProcess_startThreadPool();
+
+    auto surface = service->getSurface();
+    ctx->width = static_cast<uint32_t>(ANativeWindow_getWidth(surface->get()));
+    ctx->height = static_cast<uint32_t>(ANativeWindow_getHeight(surface->get()));
+    ctx->displayService = service;
+    return ctx;
+}
+
+LIBEXPORT
+void destroy_android_display_context(android_display_log_callback_type error_callback,
+                                     struct android_display_context* ctx) {
+    auto service = ::ndk::SharedRefBase::make<DisplayService>();
+    ::ndk::SpAIBinder binder(
+            AServiceManager_waitForService("android.system.virtualizationservice"));
+    auto virt_service = IVirtualizationServiceInternal::fromBinder(binder);
+    if (virt_service != nullptr) {
+        auto status = virt_service->clearDisplayService();
+    } else {
+        ErrorF(error_callback, "Failed to find android.system.virtualizationservice");
+    }
+
+    if (!ctx) {
+        ErrorF(error_callback, "Invalid context.");
+        return;
+    }
+
+    delete ctx;
+}
+
+LIBEXPORT
+uint32_t get_android_display_width(android_display_log_callback_type error_callback,
+                                   struct android_display_context* ctx) {
+    if (!ctx) {
+        ErrorF(error_callback, "Invalid context.");
+        return -1;
+    }
+    if (!ctx->displayService->getSurface()) {
+        ErrorF(error_callback, "Invalid context surface for ctx:%p.", ctx);
+        return -1;
+    }
+    return ctx->width;
+}
+
+LIBEXPORT
+uint32_t get_android_display_height(android_display_log_callback_type error_callback,
+                                    struct android_display_context* ctx) {
+    if (!ctx) {
+        ErrorF(error_callback, "Invalid context.");
+        return -1;
+    }
+    if (!ctx->displayService->getSurface()) {
+        ErrorF(error_callback, "Invalid context surface for ctx:%p.", ctx);
+        return -1;
+    }
+    return ctx->height;
+}
+
+uint16_t RGBA8888ToRGB565(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+    (void)a;
+    return (static_cast<uint16_t>(r >> 3) << 11) | (static_cast<uint16_t>(g >> 2) << 5) |
+            (static_cast<uint16_t>(b >> 3) << 0);
+}
+
+LIBEXPORT
+void blit_android_display(android_display_log_callback_type error_callback,
+                          struct android_display_context* ctx, uint32_t width, uint32_t height,
+                          uint8_t* pixels, size_t pixels_num_bytes) {
+    if (!ctx) {
+        ErrorF(error_callback, "Invalid context.");
+        return;
+    }
+    if (!ctx->displayService->getSurface()) {
+        ErrorF(error_callback, "Invalid context surface.");
+        return;
+    }
+    if (pixels_num_bytes != width * height * 4) {
+        ErrorF(error_callback, "Invalid buffer size.");
+        return;
+    }
+    ANativeWindow* anw = ctx->displayService->getSurface()->get();
+    if (!anw) {
+        ErrorF(error_callback, "Invalid context surface.");
+        return;
+    }
+
+    ANativeWindow_Buffer anwBuffer = {};
+    if (ANativeWindow_lock(anw, &anwBuffer, nullptr) != android::OK) {
+        ErrorF(error_callback, "Failed to lock ANativeWindow.");
+        return;
+    }
+
+    // Source is always BGRA8888.
+    auto* src = reinterpret_cast<uint32_t*>(pixels);
+    auto srcWidth = static_cast<uint32_t>(width);
+    auto srcHeight = static_cast<uint32_t>(height);
+    auto srcStrideBytes = srcWidth * 4;
+    auto srcStridePixels = srcWidth;
+
+    auto dstWidth = static_cast<uint32_t>(anwBuffer.width);
+    auto dstHeight = static_cast<uint32_t>(anwBuffer.height);
+
+    // Scale to fit if needed.
+    std::vector<uint32_t> scaledSrc;
+    if (srcWidth != dstWidth || srcHeight != dstHeight) {
+        const float ratioWidth = static_cast<float>(dstWidth) / static_cast<float>(srcWidth);
+        const float ratioHeight = static_cast<float>(dstHeight) / static_cast<float>(srcHeight);
+        const float ratioUsed = std::min(ratioWidth, ratioHeight);
+
+        uint32_t scaledSrcWidth = static_cast<uint32_t>(static_cast<float>(srcWidth) * ratioUsed);
+        uint32_t scaledSrcHeight = static_cast<uint32_t>(static_cast<float>(srcHeight) * ratioUsed);
+        uint32_t scaledSrcStrideBytes = scaledSrcWidth * 4;
+        uint32_t scaledSrcStridePixels = scaledSrcWidth;
+
+        scaledSrc.resize(scaledSrcHeight * scaledSrcStridePixels);
+
+        libyuv::ARGBScale(reinterpret_cast<uint8_t*>(src), srcStrideBytes, srcWidth, srcHeight,
+                          reinterpret_cast<uint8_t*>(scaledSrc.data()), scaledSrcStrideBytes,
+                          scaledSrcWidth, scaledSrcHeight, libyuv::kFilterBilinear);
+
+        src = scaledSrc.data();
+        srcWidth = scaledSrcWidth;
+        srcHeight = scaledSrcHeight;
+        srcStrideBytes = scaledSrcStrideBytes;
+        srcStridePixels = scaledSrcStridePixels;
+    }
+
+    if (anwBuffer.format == AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) {
+        auto* dst = reinterpret_cast<uint32_t*>(anwBuffer.bits);
+        auto dstStridePixels = static_cast<uint32_t>(anwBuffer.stride);
+
+        for (uint32_t h = 0; h < std::min(srcHeight, dstHeight); h++) {
+            for (uint32_t w = 0; w < std::min(srcWidth, dstWidth); w++) {
+                dst[(h * dstStridePixels) + w] = src[(h * srcStridePixels) + w];
+            }
+        }
+    } else if (anwBuffer.format == AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM) {
+        auto* dst = reinterpret_cast<uint16_t*>(anwBuffer.bits);
+        auto dstWidth = static_cast<uint32_t>(anwBuffer.width);
+        auto dstHeight = static_cast<uint32_t>(anwBuffer.height);
+        auto dstStridePixels = static_cast<uint32_t>(anwBuffer.stride);
+
+        for (uint32_t h = 0; h < std::min(srcHeight, dstHeight); h++) {
+            for (uint32_t w = 0; w < std::min(srcWidth, dstWidth); w++) {
+                uint32_t srcPixel = src[(h * srcStridePixels) + w];
+                uint8_t* srcPixelBytes = reinterpret_cast<uint8_t*>(&srcPixel);
+                uint8_t r = srcPixelBytes[2];
+                uint8_t g = srcPixelBytes[1];
+                uint8_t b = srcPixelBytes[0];
+                uint8_t a = srcPixelBytes[3];
+                dst[(h * dstStridePixels) + w] = RGBA8888ToRGB565(r, g, b, a);
+            }
+        }
+    } else {
+        ErrorF(error_callback, "Unhandled format: %d", anwBuffer.format);
+    }
+
+    if (ANativeWindow_unlockAndPost(anw) != android::OK) {
+        ErrorF(error_callback, "Failed to unlock and post ANativeWindow.");
+        return;
+    }
+}
diff --git a/libs/android_display_backend/crosvm_android_display_client.h b/libs/android_display_backend/crosvm_android_display_client.h
new file mode 100644
index 0000000..75b845b
--- /dev/null
+++ b/libs/android_display_backend/crosvm_android_display_client.h
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+extern "C" {
+
+typedef void (*android_display_log_callback_type)(const char* message);
+
+static void android_display_log_callback_stub(const char* message) {
+    (void)message;
+}
+
+struct android_display_context {
+    uint32_t test;
+};
+
+__attribute__((visibility("default"))) struct android_display_context*
+create_android_display_context(const char* name, size_t name_len,
+                               android_display_log_callback_type error_callback);
+
+__attribute__((visibility("default"))) void destroy_android_display_context(
+        android_display_log_callback_type error_callback, struct android_display_context* ctx);
+
+} // extern C
diff --git a/libs/dice/driver/src/lib.rs b/libs/dice/driver/src/lib.rs
index 79edb51..b5c1f12 100644
--- a/libs/dice/driver/src/lib.rs
+++ b/libs/dice/driver/src/lib.rs
@@ -65,6 +65,7 @@
 
     /// Creates a new dice driver from the given driver_path.
     pub fn new(driver_path: &Path, is_strict_boot: bool) -> Result<Self> {
+        log::info!("Creating DiceDriver backed by {driver_path:?} driver");
         if driver_path.exists() {
             log::info!("Using DICE values from driver");
         } else if is_strict_boot {
@@ -107,6 +108,7 @@
 
     /// Create a new dice driver that reads dice_artifacts from the given file.
     pub fn from_file(file_path: &Path) -> Result<Self> {
+        log::info!("Creating DiceDriver backed by {file_path:?} file");
         let file =
             fs::File::open(file_path).map_err(|error| Error::new(error).context("open file"))?;
         let dice_artifacts = serde_cbor::from_reader(file)
@@ -149,11 +151,18 @@
             &input_values,
         )
         .context("DICE derive from driver")?;
-        if let Self::Real { driver_path, .. } = &self {
-            // Writing to the device wipes the artifacts. The string is ignored by the driver but
-            // included for documentation.
-            fs::write(driver_path, "wipe")
-                .map_err(|err| Error::new(err).context("Wiping driver"))?;
+        match &self {
+            Self::Real { driver_path, .. } => {
+                // Writing to the device wipes the artifacts. The string is ignored by the driver
+                // but included for documentation.
+                fs::write(driver_path, "wipe")
+                    .map_err(|err| Error::new(err).context("Wiping driver"))?;
+            }
+            Self::FromFile { file_path, .. } => {
+                fs::remove_file(file_path)
+                    .map_err(|err| Error::new(err).context("Deleting file"))?;
+            }
+            Self::Fake { .. } => (),
         }
         Ok(next_dice_artifacts)
     }
@@ -176,6 +185,11 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use core::ffi::CStr;
+    use diced_open_dice::{
+        hash, retry_bcc_format_config_descriptor, DiceConfigValues, HIDDEN_SIZE,
+    };
+    use std::fs::File;
 
     fn assert_eq_bytes(expected: &[u8], actual: &[u8]) {
         assert_eq!(
@@ -204,4 +218,34 @@
 
         Ok(())
     }
+
+    #[test]
+    fn test_dice_driver_from_file_deletes_file_after_derive() -> Result<()> {
+        let tmp_dir = tempfile::tempdir()?;
+
+        let file_path = tmp_dir.path().join("test-dice-chain.raw");
+
+        {
+            let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()?;
+            let file = File::create(&file_path)?;
+            serde_cbor::to_writer(file, &dice_artifacts)?;
+        }
+
+        let dice = DiceDriver::from_file(&file_path)?;
+
+        let values = DiceConfigValues {
+            component_name: Some(CStr::from_bytes_with_nul(b"test\0")?),
+            ..Default::default()
+        };
+        let desc = retry_bcc_format_config_descriptor(&values)?;
+        let code_hash = hash(&String::from("test code hash").into_bytes())?;
+        let authority_hash = hash(&String::from("test authority hash").into_bytes())?;
+        let hidden = [0; HIDDEN_SIZE];
+
+        let _ = dice.derive(code_hash, &desc, authority_hash, false, hidden)?;
+
+        assert!(!file_path.exists());
+
+        Ok(())
+    }
 }
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
index 4fb6ae4..9a7375d 100644
--- a/libs/vbmeta/Android.bp
+++ b/libs/vbmeta/Android.bp
@@ -35,6 +35,8 @@
         ":avb_testkey_rsa2048",
         ":avb_testkey_rsa4096",
         ":avb_testkey_rsa8192",
+        ":test_microdroid_vendor_image",
+        ":test_microdroid_vendor_image_no_rollback_index",
     ],
     required: ["avbtool"],
     test_suites: ["general-tests"],
diff --git a/libs/vbmeta/src/lib.rs b/libs/vbmeta/src/lib.rs
index 1a40e45..a15f699 100644
--- a/libs/vbmeta/src/lib.rs
+++ b/libs/vbmeta/src/lib.rs
@@ -148,6 +148,11 @@
         Descriptors::from_image(&self.data)
     }
 
+    /// Returns the rollback_index of the VBMeta image.
+    pub fn rollback_index(&self) -> u64 {
+        self.header.rollback_index
+    }
+
     /// Get the raw VBMeta image.
     pub fn data(&self) -> &[u8] {
         &self.data
@@ -283,4 +288,19 @@
     fn test_rsa8192_signed_image() -> Result<()> {
         signed_image_has_valid_vbmeta("SHA256_RSA8192", "data/testkey_rsa8192.pem")
     }
+
+    #[test]
+    fn test_rollback_index() -> Result<()> {
+        let vbmeta = VbMetaImage::verify_path("test_microdroid_vendor_image.img")?;
+        assert_eq!(5, vbmeta.rollback_index());
+        Ok(())
+    }
+
+    #[test]
+    fn test_rollback_index_default_zero() -> Result<()> {
+        let vbmeta =
+            VbMetaImage::verify_path("test_microdroid_vendor_image_no_rollback_index.img")?;
+        assert_eq!(0, vbmeta.rollback_index());
+        Ok(())
+    }
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 169ecae..33d98dc 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -55,6 +55,7 @@
     properties: [
         "deps",
         "dirs",
+        "multilib",
     ],
 }
 
@@ -157,9 +158,11 @@
     // Below are dependencies that are conditionally enabled depending on value of build flags.
     soong_config_variables: {
         release_avf_enable_dice_changes: {
-            deps: [
-                "derive_microdroid_vendor_dice_node",
-            ],
+            multilib: {
+                lib64: {
+                    deps: ["derive_microdroid_vendor_dice_node"],
+                },
+            },
             dirs: [
                 "microdroid_resources",
             ],
diff --git a/microdroid/README.md b/microdroid/README.md
index 6e7b20c..baf41b0 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -133,8 +133,8 @@
 
 Use the [Android Virtualization Framework Java
 APIs](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/javalib/api/system-current.txt)
-in your app to create a microdroid VM and run payload in it. The APIs currently
-are @SystemApi, thus available only to privileged apps.
+in your app to create a microdroid VM and run payload in it. The APIs are currently
+@SystemApi, and only available to preinstalled apps.
 
 If you are looking for an example usage of the APIs, you may refer to the [demo
 app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/demo/).
diff --git a/microdroid/derive_microdroid_vendor_dice_node/Android.bp b/microdroid/derive_microdroid_vendor_dice_node/Android.bp
index de1bef7..8b79aad 100644
--- a/microdroid/derive_microdroid_vendor_dice_node/Android.bp
+++ b/microdroid/derive_microdroid_vendor_dice_node/Android.bp
@@ -10,9 +10,20 @@
     rustlibs: [
         "libanyhow",
         "libclap",
+        "libcstr",
+        "libdice_driver",
+        "libdiced_open_dice",
+        "libdm_rust",
+        "libserde_cbor",
+        "libvbmeta_rust",
     ],
     bootstrap: true,
     prefer_rlib: true,
+    multilib: {
+        lib32: {
+            enabled: false,
+        },
+    },
 }
 
 rust_binary {
diff --git a/microdroid/derive_microdroid_vendor_dice_node/src/main.rs b/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
index 1d5db0d..c7bc3f5 100644
--- a/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
+++ b/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
@@ -14,9 +14,19 @@
 
 //! Derives microdroid vendor dice node.
 
-use anyhow::Error;
+use anyhow::{bail, Context, Result};
 use clap::Parser;
-use std::path::PathBuf;
+use cstr::cstr;
+use dice_driver::DiceDriver;
+use diced_open_dice::{
+    hash, retry_bcc_format_config_descriptor, DiceConfigValues, OwnedDiceArtifacts, HIDDEN_SIZE,
+};
+use dm::util::blkgetsize64;
+use std::fs::{read_link, File};
+use std::path::{Path, PathBuf};
+use vbmeta::VbMetaImage;
+
+const AVF_STRICT_BOOT: &str = "/proc/device-tree/chosen/avf,strict-boot";
 
 #[derive(Parser)]
 struct Args {
@@ -31,8 +41,74 @@
     output: PathBuf,
 }
 
-fn main() -> Result<(), Error> {
+// TODO(ioffe): move to a library to reuse same code here, in microdroid_manager and in
+// first_stage_init.
+fn is_strict_boot() -> bool {
+    Path::new(AVF_STRICT_BOOT).exists()
+}
+
+fn build_descriptor(vbmeta: &VbMetaImage) -> Result<Vec<u8>> {
+    let values = DiceConfigValues {
+        component_name: Some(cstr!("Microdroid vendor")),
+        security_version: Some(vbmeta.rollback_index()),
+        ..Default::default()
+    };
+    Ok(retry_bcc_format_config_descriptor(&values)?)
+}
+
+// TODO(ioffe): move to libvbmeta.
+fn find_root_digest(vbmeta: &VbMetaImage) -> Result<Option<Vec<u8>>> {
+    for descriptor in vbmeta.descriptors()?.iter() {
+        if let vbmeta::Descriptor::Hashtree(_) = descriptor {
+            return Ok(Some(descriptor.to_hashtree()?.root_digest().to_vec()));
+        }
+    }
+    Ok(None)
+}
+
+fn dice_derivation(dice: DiceDriver, vbmeta: &VbMetaImage) -> Result<OwnedDiceArtifacts> {
+    let authority_hash = if let Some(pubkey) = vbmeta.public_key() {
+        hash(pubkey).context("hash pubkey")?
+    } else {
+        bail!("no public key")
+    };
+    let code_hash = if let Some(root_digest) = find_root_digest(vbmeta)? {
+        hash(root_digest.as_ref()).context("hash root_digest")?
+    } else {
+        bail!("no hashtree")
+    };
+    let desc = build_descriptor(vbmeta).context("build descriptor")?;
+    let hidden = [0; HIDDEN_SIZE];
+    // The microdroid vendor partition doesn't contribute to the debuggability of the VM, and it is
+    // a bit tricky to propagate the info on whether the VM is debuggable to
+    // derive_microdroid_dice_node binary. Given these, we just always set `is_debuggable: false`
+    // for the "Microdroid vendor" dice node. The adjacent dice nodes (pvmfw & payload) provide the
+    // accurate information on whether VM is debuggable.
+    dice.derive(code_hash, &desc, authority_hash, /* debug= */ false, hidden)
+}
+
+fn extract_vbmeta(block_dev: &Path) -> Result<VbMetaImage> {
+    let size = blkgetsize64(block_dev).context("blkgetsize64  failed")?;
+    let file = File::open(block_dev).context("open failed")?;
+    let vbmeta = VbMetaImage::verify_reader_region(file, 0, size)?;
+    Ok(vbmeta)
+}
+
+fn try_main() -> Result<()> {
     let args = Args::parse();
-    eprintln!("{:?} {:?} {:?}", args.dice_driver, args.microdroid_vendor_disk_image, args.output);
+    let dice =
+        DiceDriver::new(&args.dice_driver, is_strict_boot()).context("Failed to load DICE")?;
+    let path = read_link(args.microdroid_vendor_disk_image).context("failed to read symlink")?;
+    let vbmeta = extract_vbmeta(&path).context("failed to extract vbmeta")?;
+    let dice_artifacts = dice_derivation(dice, &vbmeta).context("failed to derive dice chain")?;
+    let file = File::create(&args.output).context("failed to create output")?;
+    serde_cbor::to_writer(file, &dice_artifacts).context("failed to write dice artifacts")?;
     Ok(())
 }
+
+fn main() {
+    if let Err(e) = try_main() {
+        eprintln!("failed with {:?}", e);
+        std::process::exit(1);
+    }
+}
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 2386bd4..7da9ea4 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -43,6 +43,7 @@
 use log::{error, info};
 use microdroid_metadata::{Metadata, PayloadMetadata};
 use microdroid_payload_config::{ApkConfig, OsConfig, Task, TaskType, VmPayloadConfig};
+use nix::mount::{umount2, MntFlags};
 use nix::sys::signal::Signal;
 use payload::load_metadata;
 use rpcbinder::RpcSession;
@@ -86,6 +87,8 @@
 const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
 const ENCRYPTEDSTORE_KEYSIZE: usize = 32;
 
+const DICE_CHAIN_FILE: &str = "/microdroid_resources/dice_chain.raw";
+
 #[derive(thiserror::Error, Debug)]
 enum MicrodroidError {
     #[error("Cannot connect to virtualization service: {0}")]
@@ -301,8 +304,13 @@
     vm_payload_service_fd: OwnedFd,
 ) -> Result<i32> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
-    let dice = DiceDriver::new(Path::new("/dev/open-dice0"), is_strict_boot())
-        .context("Failed to load DICE")?;
+    let dice = if Path::new(DICE_CHAIN_FILE).exists() {
+        DiceDriver::from_file(Path::new(DICE_CHAIN_FILE))
+            .context("Failed to load DICE from file")?
+    } else {
+        DiceDriver::new(Path::new("/dev/open-dice0"), is_strict_boot())
+            .context("Failed to load DICE from driver")?
+    };
 
     // Microdroid skips checking payload against instance image iff the device supports
     // secretkeeper. In that case Microdroid use VmSecret::V2, which provide protection against
@@ -328,6 +336,10 @@
 
         // Start apexd to activate APEXes. This may allow code within them to run.
         system_properties::write("ctl.start", "apexd-vm")?;
+
+        // Unmounting /microdroid_resources is a defence-in-depth effort to ensure that payload
+        // can't get hold of dice chain stored there.
+        umount2("/microdroid_resources", MntFlags::MNT_DETACH)?;
     }
 
     // Run encryptedstore binary to prepare the storage
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
index 26dbc01..66f0219 100644
--- a/tests/vendor_images/Android.bp
+++ b/tests/vendor_images/Android.bp
@@ -15,6 +15,16 @@
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
     avb_private_key: ":vendor_sign_key",
+    rollback_index: 5,
+}
+
+android_filesystem {
+    name: "test_microdroid_vendor_image_no_rollback_index",
+    partition_name: "microdroid-vendor",
+    type: "ext4",
+    file_contexts: ":microdroid_vendor_file_contexts.gen",
+    use_avb: true,
+    avb_private_key: ":vendor_sign_key",
 }
 
 android_filesystem {
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index b8c6315..279b4ec 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, 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};
@@ -231,6 +231,7 @@
 
     /// Allocate a new instance_id to the VM
     fn allocateInstanceId(&self) -> binder::Result<[u8; 64]> {
+        check_manage_access()?;
         GLOBAL_SERVICE.allocateInstanceId()
     }
 
@@ -326,6 +327,16 @@
         check_manage_access()?;
         Ok(is_secretkeeper_supported())
     }
+
+    fn removeVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
+        check_manage_access()?;
+        GLOBAL_SERVICE.removeVmInstance(instance_id)
+    }
+
+    fn claimVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
+        check_manage_access()?;
+        GLOBAL_SERVICE.claimVmInstance(instance_id)
+    }
 }
 
 impl VirtualizationService {
@@ -571,6 +582,13 @@
             (vec![], None)
         };
 
+        let display_config = config
+            .displayConfig
+            .as_ref()
+            .map(DisplayConfig::new)
+            .transpose()
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -596,6 +614,7 @@
             vfio_devices,
             dtbo,
             device_tree_overlay,
+            display_config,
         };
         let instance = Arc::new(
             VmInstance::new(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 97a27e0..86c9af3 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -44,7 +44,8 @@
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     MemoryTrimLevel::MemoryTrimLevel,
-    VirtualMachineAppConfig::DebugLevel::DebugLevel
+    VirtualMachineAppConfig::DebugLevel::DebugLevel,
+    DisplayConfig::DisplayConfig as DisplayConfigParcelable,
 };
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -118,6 +119,32 @@
     pub vfio_devices: Vec<VfioDevice>,
     pub dtbo: Option<File>,
     pub device_tree_overlay: Option<File>,
+    pub display_config: Option<DisplayConfig>,
+}
+
+#[derive(Debug)]
+pub struct DisplayConfig {
+    pub width: NonZeroU32,
+    pub height: NonZeroU32,
+    pub horizontal_dpi: NonZeroU32,
+    pub vertical_dpi: NonZeroU32,
+    pub refresh_rate: NonZeroU32,
+}
+
+impl DisplayConfig {
+    pub fn new(raw_config: &DisplayConfigParcelable) -> Result<DisplayConfig> {
+        let width = try_into_non_zero_u32(raw_config.width)?;
+        let height = try_into_non_zero_u32(raw_config.height)?;
+        let horizontal_dpi = try_into_non_zero_u32(raw_config.horizontalDpi)?;
+        let vertical_dpi = try_into_non_zero_u32(raw_config.verticalDpi)?;
+        let refresh_rate = try_into_non_zero_u32(raw_config.refreshRate)?;
+        Ok(DisplayConfig { width, height, horizontal_dpi, vertical_dpi, refresh_rate })
+    }
+}
+
+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"))
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -415,12 +442,13 @@
     /// the VM to prevent indefinite hangup and update the payload_state accordingly.
     fn monitor_payload_hangup(&self, child: Arc<SharedChild>) {
         debug!("Starting to monitor hangup for Microdroid({})", child.id());
-        let (_, result) = self
+        let (state, result) = self
             .payload_state_updated
             .wait_timeout_while(self.payload_state.lock().unwrap(), *BOOT_HANGUP_TIMEOUT, |s| {
                 *s < PayloadState::Started
             })
             .unwrap();
+        drop(state); // we are not interested in state
         let child_still_running = child.try_wait().ok() == Some(None);
         if result.timed_out() && child_still_running {
             error!(
@@ -927,6 +955,13 @@
     if let Some(dt_overlay) = &config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
+    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));
+    }
 
     append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 112e1cc..c479691 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -12,7 +12,10 @@
     backend: {
         java: {
             sdk_version: "module_current",
-            apex_available: ["com.android.virt"],
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.virt",
+            ],
         },
         cpp: {
             enabled: true,
@@ -44,6 +47,9 @@
     backend: {
         java: {
             sdk_version: "module_current",
+            apex_available: [
+                "//apex_available:platform",
+            ],
         },
         rust: {
             enabled: true,
@@ -51,6 +57,11 @@
                 "com.android.virt",
             ],
         },
+        ndk: {
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
     },
 }
 
@@ -103,7 +114,10 @@
     backend: {
         java: {
             sdk_version: "module_current",
-            apex_available: ["com.android.virt"],
+            apex_available: [
+                "com.android.virt",
+                "//apex_available:platform",
+            ],
         },
         ndk: {
             apex_available: [
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
new file mode 100644
index 0000000..1fd392b
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.virtualizationservice;
+
+parcelable DisplayConfig {
+    int width;
+    int height;
+    int horizontalDpi;
+    int verticalDpi;
+    int refreshRate;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index 462932c..f8b5087 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -98,4 +98,19 @@
      * For more info see packages/modules/Virtualization/docs/updatable_vm.md
      */
     boolean isUpdatableVmSupported();
+
+    /**
+     * Notification that state associated with a VM should be removed.
+     *
+     * @param instanceId The ID for the VM.
+     */
+    void removeVmInstance(in byte[64] instanceId);
+
+    /**
+     * Notification that ownership of a VM has been claimed by the caller.  Note that no permission
+     * checks (with respect to the previous owner) are performed.
+     *
+     * @param instanceId The ID for the VM.
+     */
+    void claimVmInstance(in byte[64] instanceId);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index b2116c4..1a18bf8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -17,6 +17,7 @@
 
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.DisplayConfig;
 
 /** Raw configuration for running a VM. */
 parcelable VirtualMachineRawConfig {
@@ -70,4 +71,6 @@
 
     /** List of SysFS nodes of devices to be assigned */
     String[] devices;
+
+    @nullable DisplayConfig displayConfig;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 16975ee..84f8734 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -116,4 +116,9 @@
      * @param instanceId The ID for the VM.
      */
     void claimVmInstance(in byte[64] instanceId);
+
+    // TODO(b/330257000): Remove these functions when a display service is running with binder RPC.
+    void setDisplayService(IBinder ibinder);
+    void clearDisplayService();
+    IBinder waitDisplayService();
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c6150b2..1fa634d 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -51,7 +51,7 @@
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
 use std::path::{Path, PathBuf};
-use std::sync::{Arc, Mutex, Weak};
+use std::sync::{Arc, Condvar, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use virtualizationcommon::Certificate::Certificate;
 use virtualizationmaintenance::{
@@ -170,12 +170,15 @@
 #[derive(Clone)]
 pub struct VirtualizationServiceInternal {
     state: Arc<Mutex<GlobalState>>,
+    display_service_set: Arc<Condvar>,
 }
 
 impl VirtualizationServiceInternal {
     pub fn init() -> VirtualizationServiceInternal {
-        let service =
-            VirtualizationServiceInternal { state: Arc::new(Mutex::new(GlobalState::new())) };
+        let service = VirtualizationServiceInternal {
+            state: Arc::new(Mutex::new(GlobalState::new())),
+            display_service_set: Arc::new(Condvar::new()),
+        };
 
         std::thread::spawn(|| {
             if let Err(e) = handle_stream_connection_tombstoned() {
@@ -190,6 +193,39 @@
 impl Interface for VirtualizationServiceInternal {}
 
 impl IVirtualizationServiceInternal for VirtualizationServiceInternal {
+    fn setDisplayService(
+        &self,
+        ibinder: &binder::SpIBinder,
+    ) -> std::result::Result<(), binder::Status> {
+        check_manage_access()?;
+        check_use_custom_virtual_machine()?;
+        let state = &mut *self.state.lock().unwrap();
+        state.display_service = Some(ibinder.clone());
+        self.display_service_set.notify_all();
+        Ok(())
+    }
+
+    fn clearDisplayService(&self) -> std::result::Result<(), binder::Status> {
+        check_manage_access()?;
+        check_use_custom_virtual_machine()?;
+        let state = &mut *self.state.lock().unwrap();
+        state.display_service = None;
+        self.display_service_set.notify_all();
+        Ok(())
+    }
+
+    fn waitDisplayService(&self) -> std::result::Result<binder::SpIBinder, binder::Status> {
+        check_manage_access()?;
+        check_use_custom_virtual_machine()?;
+        let state = self
+            .display_service_set
+            .wait_while(self.state.lock().unwrap(), |state| state.display_service.is_none())
+            .unwrap();
+        Ok((state.display_service)
+            .as_ref()
+            .cloned()
+            .expect("Display service cannot be None in this context"))
+    }
     fn removeMemlockRlimit(&self) -> binder::Result<()> {
         let pid = get_calling_pid();
         let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
@@ -588,6 +624,8 @@
 
     /// State relating to secrets held by (optional) Secretkeeper instance on behalf of VMs.
     sk_state: Option<maintenance::State>,
+
+    display_service: Option<binder::SpIBinder>,
 }
 
 impl GlobalState {
@@ -596,6 +634,7 @@
             held_contexts: HashMap::new(),
             dtbo_file: Mutex::new(None),
             sk_state: maintenance::State::new(),
+            display_service: None,
         }
     }
 
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index a049616..63a6894 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -14,17 +14,17 @@
 
 //! Low-level compatibility layer between baremetal Rust and Bionic C functions.
 
-use core::ffi::c_char;
-use core::ffi::c_int;
-use core::ffi::c_void;
-use core::ffi::CStr;
-use core::slice;
-use core::str;
-
 use crate::console;
 use crate::eprintln;
 use crate::rand::fill_with_entropy;
 use crate::read_sysreg;
+use core::ffi::c_char;
+use core::ffi::c_int;
+use core::ffi::c_void;
+use core::ffi::CStr;
+use core::ptr::addr_of_mut;
+use core::slice;
+use core::str;
 
 use cstr::cstr;
 
@@ -75,7 +75,7 @@
 unsafe extern "C" fn __errno() -> *mut c_int {
     // SAFETY: C functions which call this are only called from the main thread, not from exception
     // handlers.
-    unsafe { &mut ERRNO as *mut _ }
+    unsafe { addr_of_mut!(ERRNO) as *mut _ }
 }
 
 fn set_errno(value: c_int) {
diff --git a/vmlauncher_app/Android.bp b/vmlauncher_app/Android.bp
index cd40448..06dcf7a 100644
--- a/vmlauncher_app/Android.bp
+++ b/vmlauncher_app/Android.bp
@@ -10,6 +10,10 @@
         "androidx-constraintlayout_constraintlayout",
         "androidx.appcompat_appcompat",
         "com.google.android.material_material",
+        // TODO(b/330257000): will be removed when binder RPC is used
+        "android.system.virtualizationservice_internal-java",
+        // TODO(b/331708504): will be removed when AVF framework handles surface
+        "libcrosvm_android_display_service-java",
     ],
     libs: [
         "framework-virtualization.impl",
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index de9d094..860c03f 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -7,7 +7,11 @@
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
         android:label="VmLauncherApp">
-        <activity android:name=".MainActivity" android:exported="true">
+        <activity android:name=".MainActivity"
+                  android:screenOrientation="landscape"
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+                  android:theme="@style/MyTheme"
+                  android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 7c927c9..b5995b8 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,14 +19,28 @@
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.app.Activity;
+import android.graphics.Rect;
 import android.os.Bundle;
+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.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.view.Display;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowMetrics;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -97,6 +111,21 @@
             }
 
             configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
+            WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
+            Rect windowSize = windowMetrics.getBounds();
+            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity());
+            DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder();
+            displayConfigBuilder.setWidth(windowSize.right);
+            displayConfigBuilder.setHeight(windowSize.bottom);
+            displayConfigBuilder.setHorizontalDpi(dpi);
+            displayConfigBuilder.setVerticalDpi(dpi);
+
+            Display display = getDisplay();
+            if (display != null) {
+                displayConfigBuilder.setRefreshRate((int) display.getRefreshRate());
+            }
+
+            customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
         } catch (JSONException | IOException e) {
@@ -108,6 +137,15 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        try {
+            // To ensure that the previous display service is removed.
+            IVirtualizationServiceInternal.Stub.asInterface(
+                            ServiceManager.waitForService("android.system.virtualizationservice"))
+                    .clearDisplayService();
+        } catch (RemoteException e) {
+            Log.d(TAG, "failed to clearDisplayService");
+        }
+        getWindow().setDecorFitsSystemWindows(false);
         setContentView(R.layout.activity_main);
         VirtualMachineCallback callback =
                 new VirtualMachineCallback() {
@@ -184,6 +222,69 @@
         } catch (VirtualMachineException e) {
             throw new RuntimeException(e);
         }
+
+        SurfaceView surfaceView = findViewById(R.id.surface_view);
+        surfaceView
+                .getHolder()
+                .addCallback(
+                        // TODO(b/331708504): it should be handled in AVF framework.
+                        new SurfaceHolder.Callback() {
+                            @Override
+                            public void surfaceCreated(SurfaceHolder holder) {
+                                Log.d(
+                                        TAG,
+                                        "surface size: "
+                                                + holder.getSurfaceFrame().flattenToString());
+                                Log.d(
+                                        TAG,
+                                        "ICrosvmAndroidDisplayService.setSurface("
+                                                + holder.getSurface()
+                                                + ")");
+                                runWithDisplayService(
+                                        (service) -> service.setSurface(holder.getSurface()));
+                            }
+
+                            @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());
+                            }
+                        });
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+        // Fullscreen:
+        WindowInsetsController windowInsetsController = surfaceView.getWindowInsetsController();
+        windowInsetsController.setSystemBarsBehavior(
+                WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+        windowInsetsController.hide(WindowInsets.Type.systemBars());
+    }
+
+    @FunctionalInterface
+    public interface RemoteExceptionCheckedFunction<T> {
+        void apply(T t) throws RemoteException;
+    }
+
+    private void runWithDisplayService(
+            RemoteExceptionCheckedFunction<ICrosvmAndroidDisplayService> func) {
+        IVirtualizationServiceInternal vs =
+                IVirtualizationServiceInternal.Stub.asInterface(
+                        ServiceManager.waitForService("android.system.virtualizationservice"));
+        try {
+            Log.d(TAG, "wait for the display service");
+            ICrosvmAndroidDisplayService service =
+                    ICrosvmAndroidDisplayService.Stub.asInterface(vs.waitDisplayService());
+            assert service != null;
+            func.apply(service);
+            Log.d(TAG, "job done");
+        } catch (Exception e) {
+            Log.d(TAG, "error", e);
+        }
     }
 
     /** Reads data from an input stream and posts it to the output data */
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
index 5cbda78..6cc899f 100644
--- a/vmlauncher_app/res/layout/activity_main.xml
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -7,12 +7,10 @@
     android:scrollbars="horizontal|vertical"
     android:textAlignment="textStart"
     tools:context=".MainActivity">
-
-    <LinearLayout
+    <SurfaceView
+        android:id="@+id/surface_view"
+        android:focusable="true"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-
-    </LinearLayout>
+        android:layout_height="match_parent" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/vmlauncher_app/res/values/themes.xml b/vmlauncher_app/res/values/themes.xml
new file mode 100644
index 0000000..395f089
--- /dev/null
+++ b/vmlauncher_app/res/values/themes.xml
@@ -0,0 +1,14 @@
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <style name="MyTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+        <item name="android:navigationBarColor">
+            @android:color/transparent
+        </item>
+        <item name="android:statusBarColor">
+            @android:color/transparent
+        </item>
+        <item name="android:windowLayoutInDisplayCutoutMode">
+            shortEdges
+        </item>
+    </style>
+</resources>