Mark ab/11976889 as merged in aosp-main-future

Bug: 347831320
Merged-In: I84c9d0e0fa78694ccc89caf73c31d3203fc5d81b
Change-Id: I38d01424d1618aaba68a6117f6b607b04c717451
diff --git a/compos/Android.bp b/compos/Android.bp
index b840506..220533a 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -25,7 +25,7 @@
         "librpcbinder_rs",
         "librustutils",
         "libscopeguard",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
     prefer_rlib: true,
     shared_libs: [
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 06cc599..9bc522c 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -23,13 +23,9 @@
 mod fsverity;
 
 use anyhow::Result;
-use binder::unstable_api::AsNative;
 use compos_common::COMPOS_VSOCK_PORT;
 use log::{debug, error};
-use std::os::raw::c_void;
 use std::panic;
-use std::ptr;
-use vm_payload_bindgen::{AIBinder, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer};
 
 fn main() {
     if let Err(e) = try_main() {
@@ -50,17 +46,5 @@
     }));
 
     debug!("compsvc is starting as a rpc service.");
-    let param = ptr::null_mut();
-    let mut service = compsvc::new_binder()?.as_binder();
-    let service = service.as_native_mut() as *mut AIBinder;
-    // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
-    // is the same type as sys::AIBinder. It is safe for on_ready to be invoked at any time, with
-    // any parameter.
-    unsafe { AVmPayload_runVsockRpcServer(service, COMPOS_VSOCK_PORT, Some(on_ready), param) }
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
-    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
-    // call at any time.
-    unsafe { AVmPayload_notifyPayloadReady() };
+    vm_payload::run_single_vsock_service(compsvc::new_binder()?, COMPOS_VSOCK_PORT)
 }
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 840acc3..1e15d16 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -289,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)
@@ -305,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/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/service_vm/demo_apk/Android.bp b/service_vm/demo_apk/Android.bp
index 3750fe6..c64b70a 100644
--- a/service_vm/demo_apk/Android.bp
+++ b/service_vm/demo_apk/Android.bp
@@ -23,7 +23,7 @@
         "libandroid_logger",
         "libanyhow",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 8ea4e65..26df52c 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -14,25 +14,15 @@
 
 //! Main executable of Service VM client for manual testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{ensure, Context, Result};
 use log::{error, info};
-use std::{
-    ffi::{c_void, CStr},
-    panic,
-    ptr::{self, NonNull},
-    result,
-};
-use vm_payload_bindgen::{
-    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
-    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
-    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
-    AVmPayload_requestAttestation,
-};
+use std::panic;
+use vm_payload::AttestationError;
+
+vm_payload::main!(main);
 
 /// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -52,15 +42,11 @@
     info!("Welcome to Service VM Client!");
 
     let too_big_challenge = &[0u8; 66];
-    let res = AttestationResult::request_attestation(too_big_challenge);
+    let res = vm_payload::request_attestation(too_big_challenge);
     ensure!(res.is_err());
-    let status = res.unwrap_err();
-    ensure!(
-        status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
-        "Unexpected status: {:?}",
-        status
-    );
-    info!("Status: {:?}", status_to_cstr(status));
+    let error = res.unwrap_err();
+    ensure!(error == AttestationError::InvalidChallenge, "Unexpected error: {error:?}");
+    info!("Error: {error}");
 
     // The data below is only a placeholder generated randomly with urandom
     let challenge = &[
@@ -68,162 +54,18 @@
         0x67, 0xc3, 0x3e, 0x73, 0x9b, 0x30, 0xbd, 0x04, 0x20, 0x2e, 0xde, 0x3b, 0x1d, 0xc8, 0x07,
         0x11, 0x7b,
     ];
-    let res = AttestationResult::request_attestation(challenge)
-        .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))?;
+    let res = vm_payload::request_attestation(challenge).context("Unexpected attestation error")?;
 
-    let cert_chain = res.certificate_chain()?;
+    let cert_chain: Vec<_> = res.certificate_chain().collect();
     info!("Attestation result certificateChain = {:?}", cert_chain);
 
-    let private_key = res.private_key()?;
+    let private_key = res.private_key();
     info!("Attestation result privateKey = {:?}", private_key);
 
     let message = b"Hello from Service VM client";
     info!("Signing message: {:?}", message);
-    let signature = res.sign(message)?;
+    let signature = res.sign_message(message);
     info!("Signature: {:?}", signature);
 
     Ok(())
 }
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::with_capacity(num_certs);
-        for i in 0..num_certs {
-            certs.push(get_certificate_at(self.as_ref(), i)?);
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
-        sign_with_attested_key(self.as_ref(), message)
-    }
-}
-
-impl AsRef<AVmAttestationResult> for AttestationResult {
-    fn as_ref(&self) -> &AVmAttestationResult {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`.
-        unsafe { self.0.as_ref() }
-    }
-}
-
-impl Drop for AttestationResult {
-    fn drop(&mut self) {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`, and not freed elsewhere.
-        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
-    }
-}
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
-    let mut cert = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `cert`.
-    // And `cert` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getCertificateAt(
-            res,
-            index,
-            cert.as_mut_ptr() as *mut c_void,
-            cert.len(),
-        )
-    };
-    ensure!(size == cert.len());
-    Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
-    let mut private_key = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `private_key`.
-    // And `private_key` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getPrivateKey(
-            res,
-            private_key.as_mut_ptr() as *mut c_void,
-            private_key.len(),
-        )
-    };
-    ensure!(size == private_key.len());
-    Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            ptr::null_mut(),
-            0,
-        )
-    };
-    let mut signature = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `signature`.
-    // And `signature` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            signature.as_mut_ptr() as *mut c_void,
-            signature.len(),
-        )
-    };
-    ensure!(size == signature.len());
-    Ok(signature.into_boxed_slice())
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 1ba156f..58b394a 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -39,7 +39,7 @@
         "libanyhow",
         "libavflog",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 00ddff8..52635ad 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -14,35 +14,26 @@
 
 //! Main executable of VM attestation for end-to-end testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::Result;
 use avflog::LogResult;
 use com_android_virt_vm_attestation_testservice::{
     aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
         AttestationStatus::AttestationStatus, BnAttestationService, IAttestationService,
         SigningResult::SigningResult, PORT,
     },
-    binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+    binder::{self, BinderFeatures, Interface, IntoBinderResult, Strong},
 };
 use log::{error, info};
 use std::{
-    ffi::{c_void, CStr},
     panic,
-    ptr::{self, NonNull},
-    result,
     sync::{Arc, Mutex},
 };
-use vm_payload_bindgen::{
-    AIBinder, AVmAttestationResult, AVmAttestationResult_free,
-    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
-    AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
-    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
-    AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
-};
+use vm_payload::{AttestationError, AttestationResult};
 
-/// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -61,18 +52,7 @@
 fn try_main() -> Result<()> {
     info!("Welcome to Service VM Client!");
 
-    let mut service = AttestationService::new_binder().as_binder();
-    let service = service.as_native_mut() as *mut AIBinder;
-    let param = ptr::null_mut();
-    // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
-    // is the same type as `sys::AIBinder`. It is safe for `on_ready` to be invoked at any time,
-    // with any parameter.
-    unsafe { AVmPayload_runVsockRpcServer(service, PORT.try_into()?, Some(on_ready), param) };
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
-    // SAFETY: It is safe to call `AVmPayload_notifyPayloadReady` at any time.
-    unsafe { AVmPayload_notifyPayloadReady() };
+    vm_payload::run_single_vsock_service(AttestationService::new_binder(), PORT.try_into()?)
 }
 
 struct AttestationService {
@@ -88,11 +68,11 @@
     }
 }
 
+#[allow(non_snake_case)]
 impl IAttestationService for AttestationService {
     fn requestAttestationForTesting(&self) -> binder::Result<()> {
         const CHALLENGE: &[u8] = &[0xaa; 32];
-        let res = AttestationResult::request_attestation_for_testing(CHALLENGE)
-            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+        let res = vm_payload::restricted::request_attestation_for_testing(CHALLENGE)
             .with_log()
             .or_service_specific_exception(-1)?;
         *self.res.lock().unwrap() = Some(res);
@@ -104,218 +84,46 @@
         challenge: &[u8],
         message: &[u8],
     ) -> binder::Result<SigningResult> {
-        let res = match AttestationResult::request_attestation(challenge) {
+        let res: AttestationResult = match vm_payload::request_attestation(challenge) {
             Ok(res) => res,
-            Err(status) => {
-                let status = to_attestation_status(status);
+            Err(e) => {
+                let status = to_attestation_status(e);
                 return Ok(SigningResult { certificateChain: vec![], signature: vec![], status });
             }
         };
-        let certificate_chain =
-            res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+
+        let certificate_chain: Vec<u8> = res.certificate_chain().flatten().collect();
         let status = AttestationStatus::OK;
-        let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
+        let signature = res.sign_message(message);
+
         Ok(SigningResult { certificateChain: certificate_chain, signature, status })
     }
 
     fn validateAttestationResult(&self) -> binder::Result<()> {
         // TODO(b/191073073): Returns the attestation result to the host for validation.
-        self.res.lock().unwrap().as_ref().unwrap().log().or_service_specific_exception(-1)
-    }
-}
-
-fn to_attestation_status(status: AVmAttestationStatus) -> AttestationStatus {
-    match status {
-        AVmAttestationStatus::ATTESTATION_OK => AttestationStatus::OK,
-        AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
-            AttestationStatus::ERROR_INVALID_CHALLENGE
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
-            AttestationStatus::ERROR_ATTESTATION_FAILED
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => AttestationStatus::ERROR_UNSUPPORTED,
-    }
-}
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-// Safety: `AttestationResult` is not `Send` because it contains a raw pointer to a C struct.
-unsafe impl Send for AttestationResult {}
-
-impl AttestationResult {
-    fn request_attestation_for_testing(
-        challenge: &[u8],
-    ) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestationForTesting(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn certificate_chain(&self) -> Result<Vec<u8>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::new();
-        for i in 0..num_certs {
-            certs.extend(get_certificate_at(self.as_ref(), i)?.iter());
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
-        sign_with_attested_key(self.as_ref(), message)
-    }
-
-    fn log(&self) -> Result<()> {
-        let cert_chain = self.certificate_chain()?;
-        info!("Attestation result certificateChain = {:?}", cert_chain);
-
-        let private_key = self.private_key()?;
-        info!("Attestation result privateKey = {:?}", private_key);
-
-        let message = b"Hello from Service VM client";
-        info!("Signing message: {:?}", message);
-        let signature = self.sign(message)?;
-        info!("Signature: {:?}", signature);
+        log(self.res.lock().unwrap().as_ref().unwrap());
         Ok(())
     }
 }
 
-impl AsRef<AVmAttestationResult> for AttestationResult {
-    fn as_ref(&self) -> &AVmAttestationResult {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`.
-        unsafe { self.0.as_ref() }
+fn log(res: &AttestationResult) {
+    for (i, cert) in res.certificate_chain().enumerate() {
+        info!("Attestation result certificate {i} = {cert:?}");
     }
+
+    let private_key = res.private_key();
+    info!("Attestation result privateKey = {private_key:?}");
+
+    let message = b"Hello from Service VM client";
+    info!("Signing message: {message:?}");
+    let signature = res.sign_message(message);
+    info!("Signature: {signature:?}");
 }
 
-impl Drop for AttestationResult {
-    fn drop(&mut self) {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`, and not freed elsewhere.
-        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
+fn to_attestation_status(e: AttestationError) -> AttestationStatus {
+    match e {
+        AttestationError::InvalidChallenge => AttestationStatus::ERROR_INVALID_CHALLENGE,
+        AttestationError::AttestationFailed => AttestationStatus::ERROR_ATTESTATION_FAILED,
+        AttestationError::AttestationUnsupported => AttestationStatus::ERROR_UNSUPPORTED,
     }
 }
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
-    let mut cert = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `cert`.
-    // And `cert` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getCertificateAt(
-            res,
-            index,
-            cert.as_mut_ptr() as *mut c_void,
-            cert.len(),
-        )
-    };
-    ensure!(size == cert.len());
-    Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
-    let mut private_key = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `private_key`.
-    // And `private_key` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getPrivateKey(
-            res,
-            private_key.as_mut_ptr() as *mut c_void,
-            private_key.len(),
-        )
-    };
-    ensure!(size == private_key.len());
-    Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Vec<u8>> {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            ptr::null_mut(),
-            0,
-        )
-    };
-    let mut signature = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `signature`.
-    // And `signature` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            signature.as_mut_ptr() as *mut c_void,
-            signature.len(),
-        )
-    };
-    ensure!(size <= signature.len());
-    signature.truncate(size);
-    Ok(signature)
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
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 e02db39..026cf3f 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
@@ -20,8 +20,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -191,6 +191,9 @@
             assume().withMessage("Skip where protected VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_PROTECTED_VM)
                     .isNotEqualTo(0);
+            assume().withMessage("Testing protected VMs on GSI isn't supported. b/272443823")
+                    .that(isGsi())
+                    .isFalse();
         } else {
             assume().withMessage("Skip where VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM)
@@ -212,12 +215,17 @@
                 .that(mCtx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
                 .isTrue();
         int vendorApiLevel = getVendorApiLevel();
-        boolean isGsi = new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+        boolean isGsi = isGsi();
+        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();
     }
 
+    protected boolean isGsi() {
+        return new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+    }
+
     protected static int getVendorApiLevel() {
         return SystemProperties.getInt("ro.board.api_level", 0);
     }
@@ -548,6 +556,7 @@
         public int mFileMode;
         public int mMountFlags;
         public String mConsoleInput;
+        public byte[] mInstanceSecret;
 
         public void assertNoException() {
             if (mException != null) {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 471aea7..e32ff88 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -55,6 +55,7 @@
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
         "MicrodroidCrashNativeLib",
+        "libmicrodroid_testlib_rust",
         "libvm_attestation_test_payload",
     ],
     min_sdk_version: "33",
@@ -166,3 +167,22 @@
     header_libs: ["vm_payload_headers"],
     stl: "libc++_static",
 }
+
+// A payload written in Rust, using the Rust wrapper for the VM payload API.
+rust_ffi_shared {
+    name: "libmicrodroid_testlib_rust",
+    crate_name: "microdroid_testlib_rust",
+    defaults: ["avf_build_flags_rust"],
+    prefer_rlib: true,
+    srcs: ["src/native/testbinary.rs"],
+    compile_multilib: "both",
+    rustlibs: [
+        "com.android.microdroid.testservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libavflog",
+        "libcstr",
+        "liblog_rust",
+        "libvm_payload_rs",
+    ],
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 4141903..4d0f5eb 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -197,6 +197,7 @@
                             tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
                             tr.mApkContentsPath = ts.getApkContentsPath();
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
                         });
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
@@ -204,6 +205,7 @@
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
     }
 
     @Test
@@ -2363,6 +2365,63 @@
         runVmTestService(TAG, vm, (ts, tr) -> {}).assertNoException();
     }
 
+    @Test
+    public void createAndRunRustVm() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mAddInteger = ts.addInteger(37, 73);
+                            tr.mApkContentsPath = ts.getApkContentsPath();
+                            tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
+        assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
+    }
+
+    @Test
+    public void createAndRunRustVmWithEncryptedStorage() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> tr.mEncryptedStoragePath = ts.getEncryptedStoragePath());
+        testResults.assertNoException();
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
+    }
+
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
         return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
     }
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
new file mode 100644
index 0000000..85b411e
--- /dev/null
+++ b/tests/testapk/src/native/testbinary.rs
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+//! A VM payload that exists to allow testing of the Rust wrapper for the VM payload APIs.
+
+use anyhow::Result;
+use com_android_microdroid_testservice::{
+    aidl::com::android::microdroid::testservice::{
+        IAppCallback::IAppCallback,
+        ITestService::{BnTestService, ITestService, PORT},
+    },
+    binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong},
+};
+use cstr::cstr;
+use log::{error, info};
+use std::panic;
+use std::process::exit;
+use std::string::String;
+use std::vec::Vec;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("microdroid_testlib_rust")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+    // Redirect panic messages to logcat.
+    panic::set_hook(Box::new(|panic_info| {
+        error!("{panic_info}");
+    }));
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Welcome to the Rust test binary");
+
+    vm_payload::run_single_vsock_service(TestService::new_binder(), PORT.try_into()?)
+}
+
+struct TestService {}
+
+impl Interface for TestService {}
+
+impl TestService {
+    fn new_binder() -> Strong<dyn ITestService> {
+        BnTestService::new_binder(TestService {}, BinderFeatures::default())
+    }
+}
+
+impl ITestService for TestService {
+    fn quit(&self) -> BinderResult<()> {
+        exit(0)
+    }
+
+    fn addInteger(&self, a: i32, b: i32) -> BinderResult<i32> {
+        a.checked_add(b).ok_or_else(|| Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None))
+    }
+
+    fn getApkContentsPath(&self) -> BinderResult<String> {
+        Ok(vm_payload::apk_contents_path().to_string_lossy().to_string())
+    }
+
+    fn getEncryptedStoragePath(&self) -> BinderResult<String> {
+        Ok(vm_payload::encrypted_storage_path()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or("".to_string()))
+    }
+
+    fn insecurelyExposeVmInstanceSecret(&self) -> BinderResult<Vec<u8>> {
+        let mut secret = vec![0u8; 32];
+        vm_payload::get_vm_instance_secret(b"identifier", secret.as_mut_slice());
+        Ok(secret)
+    }
+
+    // Everything below here is unimplemented. Implementations may be added as needed.
+
+    fn readProperty(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn insecurelyExposeAttestationCdi(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn getBcc(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn runEchoReverseServer(&self) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn getEffectiveCapabilities(&self) -> BinderResult<Vec<String>> {
+        unimplemented()
+    }
+    fn getUid(&self) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn writeToFile(&self, _: &str, _: &str) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readFromFile(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn getFilePermissions(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn getMountFlags(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn requestCallback(&self, _: &Strong<dyn IAppCallback + 'static>) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readLineFromConsole(&self) -> BinderResult<String> {
+        unimplemented()
+    }
+}
+
+fn unimplemented<T>() -> BinderResult<T> {
+    let message = cstr!("Got a call to an unimplemented ITestService method in testbinary.rs");
+    error!("{message:?}");
+    Err(Status::new_exception(ExceptionCode::UNSUPPORTED_OPERATION, Some(message)))
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index ac70509..9df376a 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -1240,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 3722d4d..ee5f5cd 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -316,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,
 }
@@ -335,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
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_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 ae8d1da..70da37b 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -282,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)
@@ -643,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 {
@@ -657,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>>,
@@ -737,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() };
@@ -818,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,
@@ -828,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/vm_payload/Android.bp b/vm_payload/Android.bp
index 229f533..cf2a002 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -39,8 +39,8 @@
     visibility: [":__subpackages__"],
 }
 
-// Rust wrappers round the C API for Rust clients.
-// (Yes, this involves going Rust -> C -> Rust.)
+// Access to the C API for Rust code.
+// This shouldn't be used directly - prefer libvm_payload_rs (below)
 rust_bindgen {
     name: "libvm_payload_bindgen",
     wrapper_src: "include-restricted/vm_payload_restricted.h",
@@ -51,15 +51,31 @@
     bindgen_flags: [
         "--default-enum-style rust",
     ],
-    visibility: [
-        "//packages/modules/Virtualization/compos",
-        "//packages/modules/Virtualization/service_vm:__subpackages__",
-    ],
     shared_libs: [
         "libvm_payload#current",
     ],
 }
 
+// Wrapper library for the raw C API for use by Rust clients.
+// (Yes, this involves going Rust -> C -> Rust.)
+// This is not a stable API - we may change it in subsequent versions.
+// But it is made available as an rlib so it is linked into any
+// code using it, leaving only dependencies on stable APIs.
+// So code built with it should run unchanged on future versions.
+rust_library_rlib {
+    name: "libvm_payload_rs",
+    crate_name: "vm_payload",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["wrapper/lib.rs"],
+    rustlibs: [
+        "libbinder_rs",
+        "libstatic_assertions",
+        "libvm_payload_bindgen",
+    ],
+    apex_available: ["com.android.compos"],
+    visibility: ["//visibility:public"],
+}
+
 // Shared library for clients to link against.
 cc_library_shared {
     name: "libvm_payload",
diff --git a/vm_payload/README.md b/vm_payload/README.md
index 4b1e6f3..66fd532 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -70,3 +70,16 @@
 See [AIDL
 backends](https://source.android.com/docs/core/architecture/aidl/aidl-backends)
 for information on using AIDL with the NDK Binder from C++.
+
+## Rust
+
+A Rust wrapper library for the VM Payload API is available (as an rlib) for VM
+payloads written in Rust.
+
+This wrapper is not guaranteed to be stable; we may change it in future
+versions. But payload code built using it will depend only on the C VM Payload
+API and the NDK APIs that are available to the payload, so should run unchanged
+on future versions.
+
+See [wrapper/lib.rs](wrapper/lib.rs) and `libvm_payload_rs` in
+[Android.bp](Android.bp).
diff --git a/vm_payload/wrapper/attestation.rs b/vm_payload/wrapper/attestation.rs
new file mode 100644
index 0000000..e0055d5
--- /dev/null
+++ b/vm_payload/wrapper/attestation.rs
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+
+use std::error::Error;
+use std::ffi::{c_void, CStr};
+use std::fmt::{self, Display};
+use std::iter::FusedIterator;
+use std::ptr::{self, NonNull};
+
+use vm_payload_bindgen::{
+    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+    AVmPayload_requestAttestation, AVmPayload_requestAttestationForTesting,
+};
+
+/// Holds the result of a successful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Debug)]
+pub struct AttestationResult {
+    result: NonNull<AVmAttestationResult>,
+}
+
+/// Error type that can be returned from an unsuccessful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
+pub enum AttestationError {
+    /// The challenge size was not between 0 and 64 bytes (inclusive).
+    InvalidChallenge,
+    /// The attempt to attest the VM failed. A subsequent request may succeed.
+    AttestationFailed,
+    /// VM attestation is not supported in the current environment.
+    AttestationUnsupported,
+}
+
+impl Error for AttestationError {}
+
+impl Display for AttestationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        let status = match self {
+            Self::InvalidChallenge => AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
+            Self::AttestationFailed => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
+            Self::AttestationUnsupported => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+        };
+        // SAFETY: AVmAttestationStatus_toString always returns a non-null pointer to a
+        // nul-terminated C string with static lifetime (which is valid UTF-8).
+        let c_str = unsafe { CStr::from_ptr(AVmAttestationStatus_toString(status)) };
+        let str = c_str.to_str().expect("Invalid UTF-8 for AVmAttestationStatus");
+        f.write_str(str)
+    }
+}
+
+impl Drop for AttestationResult {
+    fn drop(&mut self) {
+        let ptr = self.result.as_ptr();
+
+        // SAFETY: The `result` field is private, and only populated with a successful call to
+        // `AVmPayload_requestAttestation`, and not freed elsewhere.
+        unsafe { AVmAttestationResult_free(ptr) };
+    }
+}
+
+// SAFETY: The API functions that accept the `AVmAttestationResult` pointer are all safe to call
+// from any thread, including `AVmAttestationResult_free` which is called only on drop.
+unsafe impl Send for AttestationResult {}
+
+// SAFETY: There is no interior mutation here; any future functions that might mutate the data would
+// require a non-const pointer and hence need `&mut self` here. The only existing such function is
+// `AVmAttestationResult_free` where we take a mutable reference guaranteeing no other references
+// exist. The raw API functions are safe to call from any thread.
+unsafe impl Sync for AttestationResult {}
+
+/// Requests the remote attestation of this VM.
+///
+/// On success the supplied [`challenge`] will be included in the certificate chain accessible from
+/// the [`AttestationResult`]; this can be used as proof of the freshness of the attestation.
+///
+/// The challenge should be no more than 64 bytes long or the request will fail.
+pub fn request_attestation(challenge: &[u8]) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: We only read the challenge within its bounds and the function does not retain any
+    // reference to it.
+    let status = unsafe {
+        AVmPayload_requestAttestation(
+            challenge.as_ptr() as *const c_void,
+            challenge.len(),
+            &mut result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+/// A variant of [`request_attestation`] used for testing purposes. This should not be used by
+/// normal VMs, and is not available to app owned VMs.
+pub fn request_attestation_for_testing(
+    challenge: &[u8],
+) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: We only read the challenge within its bounds and the function does not retain any
+    // reference to it.
+    let status = unsafe {
+        AVmPayload_requestAttestationForTesting(
+            challenge.as_ptr() as *const c_void,
+            challenge.len(),
+            &mut result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+impl AttestationResult {
+    fn new(
+        status: AVmAttestationStatus,
+        result: *mut AVmAttestationResult,
+    ) -> Result<AttestationResult, AttestationError> {
+        match status {
+            AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+                Err(AttestationError::InvalidChallenge)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+                Err(AttestationError::AttestationFailed)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+                Err(AttestationError::AttestationUnsupported)
+            }
+            AVmAttestationStatus::ATTESTATION_OK => {
+                let result = NonNull::new(result)
+                    .expect("Attestation succeeded but the attestation result is null");
+                Ok(AttestationResult { result })
+            }
+        }
+    }
+
+    fn as_const_ptr(&self) -> *const AVmAttestationResult {
+        self.result.as_ptr().cast_const()
+    }
+
+    /// Returns the attested private key. This is the ECDSA P-256 private key corresponding to the
+    /// public key described by the leaf certificate in the attested
+    /// [certificate chain](AttestationResult::certificate_chain). It is a DER-encoded
+    /// `ECPrivateKey` structure as specified in
+    /// [RFC 5915 s3](https://datatracker.ietf.org/doc/html/rfc5915#section-3).
+    ///
+    /// Note: The [`sign_message`](AttestationResult::sign_message) method allows signing with the
+    /// key without retrieving it.
+    pub fn private_key(&self) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case.
+            unsafe { AVmAttestationResult_getPrivateKey(ptr, ptr::null_mut(), 0) };
+
+        let mut private_key = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `private_key`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getPrivateKey(
+                ptr,
+                private_key.as_mut_ptr() as *mut c_void,
+                private_key.len(),
+            )
+        };
+        assert_eq!(size, private_key.len());
+        private_key
+    }
+
+    /// Signs the given message using the attested private key. The signature uses ECDSA P-256; the
+    /// message is first hashed with SHA-256 and then it is signed with the attested EC P-256
+    /// [private key](AttestationResult::private_key).
+    ///
+    /// The signature is a DER-encoded `ECDSASignature`` structure as described in
+    /// [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979).
+    pub fn sign_message(&self, message: &[u8]) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+        // writes no data since we pass a zero size, and null is explicitly allowed for the
+        // destination in that case.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                ptr::null_mut(),
+                0,
+            )
+        };
+
+        let mut signature = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `signature`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                signature.as_mut_ptr() as *mut c_void,
+                signature.len(),
+            )
+        };
+        assert!(size <= signature.len());
+        signature.truncate(size);
+        signature
+    }
+
+    /// Returns an iterator over the certificates forming the certificate chain for the VM, and its
+    /// public key, obtained by the attestation process.
+    ///
+    /// The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+    /// the attestation key's certificate chain. It starts with the leaf certificate covering the
+    /// attested public key and ends with the root certificate.
+    pub fn certificate_chain(&self) -> CertIterator {
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid.
+        let count = unsafe { AVmAttestationResult_getCertificateCount(self.as_const_ptr()) };
+
+        CertIterator { result: self, count, current: 0 }
+    }
+
+    fn certificate(&self, index: usize) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case. The function will panic if `index` is out of range (which
+            // is safe).
+            unsafe { AVmAttestationResult_getCertificateAt(ptr, index, ptr::null_mut(), 0) };
+
+        let mut cert = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `cert`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getCertificateAt(
+                ptr,
+                index,
+                cert.as_mut_ptr() as *mut c_void,
+                cert.len(),
+            )
+        };
+        assert_eq!(size, cert.len());
+        cert
+    }
+}
+
+/// An iterator over the DER-encoded X.509 certificates containin in an [`AttestationResult`].
+/// See [`certificate_chain`](AttestationResult::certificate_chain) for more details.
+pub struct CertIterator<'a> {
+    result: &'a AttestationResult,
+    count: usize,
+    current: usize, // Invariant: current <= count
+}
+
+impl<'a> Iterator for CertIterator<'a> {
+    type Item = Vec<u8>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.current < self.count {
+            let cert = self.result.certificate(self.current);
+            self.current += 1;
+            Some(cert)
+        } else {
+            None
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let size = self.count - self.current;
+        (size, Some(size))
+    }
+}
+
+impl<'a> ExactSizeIterator for CertIterator<'a> {}
+impl<'a> FusedIterator for CertIterator<'a> {}
diff --git a/vm_payload/wrapper/lib.rs b/vm_payload/wrapper/lib.rs
new file mode 100644
index 0000000..d3f03d7
--- /dev/null
+++ b/vm_payload/wrapper/lib.rs
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+
+//! Rust wrapper for the VM Payload API, allowing virtual machine payload code to be written in
+//! Rust. This wraps the raw C API, accessed via bindgen, into a more idiomatic Rust interface.
+//!
+//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/README.md`
+//! for more information on the VM Payload API.
+
+mod attestation;
+
+pub use attestation::{request_attestation, AttestationError, AttestationResult};
+use binder::unstable_api::AsNative;
+use binder::{FromIBinder, Strong};
+use std::ffi::{c_void, CStr, OsStr};
+use std::os::unix::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+use vm_payload_bindgen::{
+    AIBinder, AVmPayload_getApkContentsPath, AVmPayload_getEncryptedStoragePath,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
+};
+
+/// The functions declared here are restricted to VMs created with a config file;
+/// they will fail, or panic, if called in other VMs. The ability to create such VMs
+/// requires the android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission, and is
+/// therefore not available to privileged or third party apps.
+///
+/// These functions can be used by tests, if the permission is granted via shell.
+pub mod restricted {
+    pub use crate::attestation::request_attestation_for_testing;
+}
+
+/// Marks the main function of the VM payload.
+///
+/// When the VM is run, this function is called. If it returns, the VM ends normally with a 0 exit
+/// code.
+///
+/// Example:
+///
+/// ```rust
+/// use log::info;
+///
+/// vm_payload::main!(vm_main);
+///
+/// fn vm_main() {
+///     android_logger::init_once(
+///          android_logger::Config::default()
+///             .with_tag("example_vm_payload")
+///             .with_max_level(log::LevelFilter::Info),
+///     );
+///     info!("Hello world");
+/// }
+/// ```
+#[macro_export]
+macro_rules! main {
+    ($name:path) => {
+        // Export a symbol with a name matching the extern declaration below.
+        #[export_name = "rust_main"]
+        fn __main() {
+            // Ensure that the main function provided by the application has the correct type.
+            $name()
+        }
+    };
+}
+
+// This is the real C entry point for the VM; we just forward to the Rust entry point.
+#[allow(non_snake_case)]
+#[no_mangle]
+extern "C" fn AVmPayload_main() {
+    extern "Rust" {
+        fn rust_main();
+    }
+
+    // SAFETY: rust_main is provided by the application using the `main!` macro above, which makes
+    // sure it has the right type.
+    unsafe { rust_main() }
+}
+
+/// Notifies the host that the payload is ready.
+///
+/// If the host app has set a `VirtualMachineCallback` for the VM, its
+/// `onPayloadReady` method will be called.
+///
+/// Note that subsequent calls to this function after the first have no effect;
+/// `onPayloadReady` is never called more than once.
+pub fn notify_payload_ready() {
+    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
+    // call at any time.
+    unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+/// Runs a binder RPC server, serving the supplied binder service implementation on the given vsock
+/// port.
+///
+/// If and when the server is ready for connections (i.e. it is listening on the port),
+/// [`notify_payload_ready`] is called to notify the host that the server is ready. This is
+/// appropriate for VM payloads that serve a single binder service - which is common.
+///
+/// Note that this function does not return. The calling thread joins the binder
+/// thread pool to handle incoming messages.
+pub fn run_single_vsock_service<T>(service: Strong<T>, port: u32) -> !
+where
+    T: FromIBinder + ?Sized,
+{
+    extern "C" fn on_ready(_param: *mut c_void) {
+        notify_payload_ready();
+    }
+
+    let mut service = service.as_binder();
+    // The cast here is needed because the compiler doesn't know that our vm_payload_bindgen
+    // AIBinder is the same type as binder_ndk_sys::AIBinder.
+    let service = service.as_native_mut() as *mut AIBinder;
+    let param = ptr::null_mut();
+    // SAFETY: We have a strong reference to the service, so the raw pointer remains valid. It is
+    // safe for on_ready to be invoked at any time, with any parameter.
+    unsafe { AVmPayload_runVsockRpcServer(service, port, Some(on_ready), param) }
+}
+
+/// Gets the path to the contents of the APK containing the VM payload. It is a directory, under
+/// which are the unzipped contents of the APK containing the payload, all read-only
+/// but accessible to the payload.
+pub fn apk_contents_path() -> &'static Path {
+    // SAFETY: AVmPayload_getApkContentsPath always returns a non-null pointer to a
+    // nul-terminated C string with static lifetime.
+    let c_str = unsafe { CStr::from_ptr(AVmPayload_getApkContentsPath()) };
+    Path::new(OsStr::from_bytes(c_str.to_bytes()))
+}
+
+/// Gets the path to the encrypted persistent storage for the VM, if any. This is
+/// a directory under which any files or directories created will be stored on
+/// behalf of the VM by the host app. All data is encrypted using a key known
+/// only to the VM, so the host cannot decrypt it, but may delete it.
+///
+/// Returns `None` if no encrypted storage was requested in the VM configuration.
+pub fn encrypted_storage_path() -> Option<&'static Path> {
+    // SAFETY: AVmPayload_getEncryptedStoragePath returns either null or a pointer to a
+    // nul-terminated C string with static lifetime.
+    let ptr = unsafe { AVmPayload_getEncryptedStoragePath() };
+    if ptr.is_null() {
+        None
+    } else {
+        // SAFETY: We know the pointer is not null, and so it is a valid C string.
+        let c_str = unsafe { CStr::from_ptr(ptr) };
+        Some(Path::new(OsStr::from_bytes(c_str.to_bytes())))
+    }
+}
+
+/// Retrieves all or part of a 32-byte secret that is bound to this unique VM
+/// instance and the supplied identifier. The secret can be used e.g. as an
+/// encryption key.
+///
+/// Every VM has a secret that is derived from a device-specific value known to
+/// the hypervisor, the code that runs in the VM and its non-modifiable
+/// configuration; it is not made available to the host OS.
+///
+/// This function performs a further derivation from the VM secret and the
+/// supplied identifier. As long as the VM identity doesn't change the same value
+/// will be returned for the same identifier, even if the VM is stopped &
+/// restarted or the device rebooted.
+///
+/// If multiple secrets are required for different purposes, a different
+/// identifier should be used for each. The identifiers otherwise are arbitrary
+/// byte sequences and do not need to be kept secret; typically they are
+/// hardcoded in the calling code.
+///
+/// The secret is returned in [`secret`], truncated to its size, which must be between
+/// 1 and 32 bytes (inclusive) or the function will panic.
+pub fn get_vm_instance_secret(identifier: &[u8], secret: &mut [u8]) {
+    let secret_size = secret.len();
+    assert!((1..=32).contains(&secret_size), "VM instance secrets can be up to 32 bytes long");
+
+    // SAFETY: The function only reads from `[identifier]` within its bounds, and only writes to
+    // `[secret]` within its bounds. Neither reference is retained, and we know neither is null.
+    unsafe {
+        AVmPayload_getVmInstanceSecret(
+            identifier.as_ptr() as *const c_void,
+            identifier.len(),
+            secret.as_mut_ptr() as *mut c_void,
+            secret_size,
+        )
+    }
+}