Merge changes I5f3920b7,I013dce63 into main

* changes:
  libavf: Add callback support
  libvmclient: Take VmCallback in VmInstance::start()
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 8cb01d7..5dd060a 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -49,6 +49,12 @@
     },
     {
       "name": "vm_accessor_test"
+    },
+    {
+      "name": "avf_backcompat_tests"
+    },
+    {
+      "name": "old_images_avf_test"
     }
   ],
   "avf-postsubmit": [
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 59f18df..2bac412 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -11,12 +11,17 @@
     asset_dirs: ["assets"],
     resource_dirs: ["res"],
     static_libs: [
+        // TODO(b/330257000): will be removed when binder RPC is used
+        "android.system.virtualizationservice_internal-java",
         "androidx-constraintlayout_constraintlayout",
         "androidx.window_window",
         "apache-commons-compress",
+        "avf_aconfig_flags_java",
         "com.google.android.material_material",
         "debian-service-grpclib-lite",
         "gson",
+        // TODO(b/331708504): will be removed when AVF framework handles surface
+        "libcrosvm_android_display_service-java",
         "VmTerminalApp.aidl-java",
         "MicrodroidTestHelper", // for DeviceProperties class
     ],
@@ -36,6 +41,7 @@
         //optimize: true,
         proguard_flags_files: ["proguard.flags"],
         shrink_resources: true,
+        keep_runtime_invisible_annotations: true,
     },
     apex_available: [
         "com.android.virt",
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 726004c..c11b1a0 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -35,7 +35,8 @@
         android:theme="@style/VmTerminalAppTheme"
         android:usesCleartextTraffic="true"
         android:supportsRtl="true"
-        android:enabled="false">
+        android:enabled="false"
+        android:name=".Application">
         <activity android:name=".MainActivity"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode|screenLayout|smallestScreenSize"
                   android:exported="true">
@@ -46,6 +47,11 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity android:name=".DisplayActivity"
+            android:screenOrientation="landscape"
+            android:resizeableActivity="false"
+            android:theme="@style/FullscreenTheme"
+            android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode|screenLayout|smallestScreenSize" />
         <activity android:name=".SettingsActivity"
             android:label="@string/action_settings" />
         <activity android:name=".SettingsDiskResizeActivity"
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
new file mode 100644
index 0000000..efe651e
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal
+
+import android.app.Application as AndroidApplication
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+
+public class Application : AndroidApplication() {
+    override fun onCreate() {
+        super.onCreate()
+        setupNotificationChannels()
+    }
+
+    private fun setupNotificationChannels() {
+        val nm = getSystemService<NotificationManager>(NotificationManager::class.java)
+
+        nm.createNotificationChannel(
+            NotificationChannel(
+                CHANNEL_LONG_RUNNING_ID,
+                getString(R.string.notification_channel_long_running_name),
+                NotificationManager.IMPORTANCE_DEFAULT,
+            )
+        )
+
+        nm.createNotificationChannel(
+            NotificationChannel(
+                CHANNEL_SYSTEM_EVENTS_ID,
+                getString(R.string.notification_channel_system_events_name),
+                NotificationManager.IMPORTANCE_HIGH,
+            )
+        )
+    }
+
+    companion object {
+        const val CHANNEL_LONG_RUNNING_ID = "long_running"
+        const val CHANNEL_SYSTEM_EVENTS_ID = "system_events"
+
+        fun getInstance(c: Context): Application = c.getApplicationContext() as Application
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
index e7ac8d9..70bc5e4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
@@ -16,8 +16,6 @@
 package com.android.virtualization.terminal
 
 import android.Manifest
-import android.app.NotificationChannel
-import android.app.NotificationManager
 import android.content.pm.PackageManager
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
@@ -25,18 +23,6 @@
 abstract class BaseActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        val notificationManager =
-            getSystemService<NotificationManager>(NotificationManager::class.java)
-        if (notificationManager.getNotificationChannel(this.packageName) == null) {
-            val channel =
-                NotificationChannel(
-                    this.packageName,
-                    getString(R.string.app_name),
-                    NotificationManager.IMPORTANCE_HIGH,
-                )
-            notificationManager.createNotificationChannel(channel)
-        }
-
         if (this !is ErrorActivity) {
             val currentThread = Thread.currentThread()
             if (currentThread.uncaughtExceptionHandler !is TerminalExceptionHandler) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DisplayActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/DisplayActivity.kt
new file mode 100644
index 0000000..290cf5a
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DisplayActivity.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal
+
+import android.os.Bundle
+import android.system.virtualmachine.VirtualMachineManager
+import android.view.SurfaceView
+import android.view.WindowInsets
+import android.view.WindowInsetsController
+
+class DisplayActivity : BaseActivity() {
+    private lateinit var displayProvider: DisplayProvider
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_display)
+        val mainView = findViewById<SurfaceView>(R.id.surface_view)
+        val cursorView = findViewById<SurfaceView>(R.id.cursor_surface_view)
+        makeFullscreen()
+        // Connect the views to the VM
+        displayProvider = DisplayProvider(mainView, cursorView)
+        val vmm =
+            applicationContext.getSystemService<VirtualMachineManager>(
+                VirtualMachineManager::class.java
+            )
+        val debianVm = vmm.get("debian")
+        if (debianVm != null) {
+            InputForwarder(
+                this,
+                debianVm,
+                findViewById(R.id.background_touch_view),
+                findViewById(R.id.surface_view),
+                findViewById(R.id.surface_view),
+            )
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        displayProvider.notifyDisplayIsGoingToInvisible()
+    }
+
+    private fun makeFullscreen() {
+        val w = window
+        w.setDecorFitsSystemWindows(false)
+        val insetsCtrl = w.insetsController
+        insetsCtrl?.hide(WindowInsets.Type.systemBars())
+        insetsCtrl?.setSystemBarsBehavior(
+            WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+        )
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DisplayProvider.kt b/android/TerminalApp/java/com/android/virtualization/terminal/DisplayProvider.kt
new file mode 100644
index 0000000..a04e056
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DisplayProvider.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virtualization.terminal
+
+import android.crosvm.ICrosvmAndroidDisplayService
+import android.graphics.PixelFormat
+import android.os.ParcelFileDescriptor
+import android.os.RemoteException
+import android.os.ServiceManager
+import android.system.virtualizationservice_internal.IVirtualizationServiceInternal
+import android.util.Log
+import android.view.SurfaceControl
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import com.android.virtualization.terminal.DisplayProvider.CursorHandler
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.io.IOException
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import libcore.io.IoBridge
+
+/** Provides Android-side surface from given SurfaceView to a VM instance as a display for that */
+internal class DisplayProvider(
+    private val mainView: SurfaceView,
+    private val cursorView: SurfaceView,
+) {
+    private val virtService: IVirtualizationServiceInternal by lazy {
+        val b = ServiceManager.waitForService("android.system.virtualizationservice")
+        IVirtualizationServiceInternal.Stub.asInterface(b)
+    }
+    private var cursorHandler: CursorHandler? = null
+
+    init {
+        mainView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT)
+        mainView.holder.addCallback(Callback(SurfaceKind.MAIN))
+        cursorView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT)
+        cursorView.holder.addCallback(Callback(SurfaceKind.CURSOR))
+        cursorView.holder.setFormat(PixelFormat.RGBA_8888)
+        // TODO: do we need this z-order?
+        cursorView.setZOrderMediaOverlay(true)
+    }
+
+    fun notifyDisplayIsGoingToInvisible() {
+        // When the display is going to be invisible (by putting in the background), save the frame
+        // of the main surface so that we can re-draw it next time the display becomes visible. This
+        // is to save the duration of time where nothing is drawn by VM.
+        try {
+            getDisplayService().saveFrameForSurface(false /* forCursor */)
+        } catch (e: RemoteException) {
+            throw RuntimeException("Failed to save frame for the main surface", e)
+        }
+    }
+
+    @Synchronized
+    private fun getDisplayService(): ICrosvmAndroidDisplayService {
+        try {
+            val b = virtService.waitDisplayService()
+            return ICrosvmAndroidDisplayService.Stub.asInterface(b)
+        } catch (e: Exception) {
+            throw RuntimeException("Error while getting display service", e)
+        }
+    }
+
+    enum class SurfaceKind {
+        MAIN,
+        CURSOR,
+    }
+
+    inner class Callback(private val surfaceKind: SurfaceKind) : SurfaceHolder.Callback {
+        fun isForCursor(): Boolean {
+            return surfaceKind == SurfaceKind.CURSOR
+        }
+
+        override fun surfaceCreated(holder: SurfaceHolder) {
+            try {
+                getDisplayService().setSurface(holder.getSurface(), isForCursor())
+            } catch (e: Exception) {
+                // TODO: don't consume this exception silently. For some unknown reason, setSurface
+                // call above throws IllegalArgumentException and that fails the surface
+                // configuration.
+                Log.e(TAG, "Failed to present surface $surfaceKind to VM", e)
+            }
+            try {
+                when (surfaceKind) {
+                    SurfaceKind.MAIN -> getDisplayService().drawSavedFrameForSurface(isForCursor())
+                    SurfaceKind.CURSOR -> {
+                        val stream = createNewCursorStream()
+                        getDisplayService().setCursorStream(stream)
+                    }
+                }
+            } catch (e: Exception) {
+                // TODO: don't consume exceptions here too
+                Log.e(TAG, "Failed to configure surface $surfaceKind", e)
+            }
+        }
+
+        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+            // TODO: support resizeable display. We could actually change the display size that the
+            // VM sees, or keep the size and render it by fitting it in the new surface.
+        }
+
+        override fun surfaceDestroyed(holder: SurfaceHolder) {
+            try {
+                getDisplayService().removeSurface(isForCursor())
+            } catch (e: RemoteException) {
+                throw RuntimeException("Error while destroying surface for $surfaceKind", e)
+            }
+        }
+    }
+
+    private fun createNewCursorStream(): ParcelFileDescriptor? {
+        cursorHandler?.interrupt()
+        var pfds: Array<ParcelFileDescriptor> =
+            try {
+                ParcelFileDescriptor.createSocketPair()
+            } catch (e: IOException) {
+                throw RuntimeException("Failed to create socketpair for cursor stream", e)
+            }
+        cursorHandler = CursorHandler(pfds[0]).also { it.start() }
+        return pfds[1]
+    }
+
+    /**
+     * Thread reading cursor coordinate from a stream, and updating the position of the cursor
+     * surface accordingly.
+     */
+    private inner class CursorHandler(private val stream: ParcelFileDescriptor) : Thread() {
+        private val cursor: SurfaceControl = this@DisplayProvider.cursorView.surfaceControl
+        private val transaction: SurfaceControl.Transaction = SurfaceControl.Transaction()
+
+        init {
+            val main = this@DisplayProvider.mainView.surfaceControl
+            transaction.reparent(cursor, main).apply()
+        }
+
+        override fun run() {
+            try {
+                val byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */)
+                byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
+                while (true) {
+                    if (interrupted()) {
+                        Log.d(TAG, "CursorHandler thread interrupted!")
+                        return
+                    }
+                    byteBuffer.clear()
+                    val bytes =
+                        IoBridge.read(
+                            stream.fileDescriptor,
+                            byteBuffer.array(),
+                            0,
+                            byteBuffer.array().size,
+                        )
+                    if (bytes == -1) {
+                        Log.e(TAG, "cannot read from cursor stream, stop the handler")
+                        return
+                    }
+                    val x = (byteBuffer.getInt() and -0x1).toFloat()
+                    val y = (byteBuffer.getInt() and -0x1).toFloat()
+                    transaction.setPosition(cursor, x, y).apply()
+                }
+            } catch (e: IOException) {
+                Log.e(TAG, "failed to run CursorHandler", e)
+            }
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InputForwarder.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InputForwarder.kt
new file mode 100644
index 0000000..117ce94
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InputForwarder.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal
+
+import android.content.Context
+import android.hardware.input.InputManager
+import android.os.Handler
+import android.system.virtualmachine.VirtualMachine
+import android.util.Log
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+
+/** Forwards input events (touch, mouse, ...) from Android to VM */
+internal class InputForwarder(
+    private val context: Context,
+    vm: VirtualMachine,
+    touchReceiver: View,
+    mouseReceiver: View,
+    keyReceiver: View,
+) {
+    private val virtualMachine: VirtualMachine = vm
+    private var inputDeviceListener: InputManager.InputDeviceListener? = null
+    private var isTabletMode = false
+
+    init {
+        val config = vm.config.customImageConfig
+
+        checkNotNull(config)
+
+        if (config.useTouch() == true) {
+            setupTouchReceiver(touchReceiver)
+        }
+        if (config.useMouse() || config.useTrackpad()) {
+            setupMouseReceiver(mouseReceiver)
+        }
+        if (config.useKeyboard()) {
+            setupKeyReceiver(keyReceiver)
+        }
+        if (config.useSwitches()) {
+            // Any view's handler is fine.
+            setupTabletModeHandler(touchReceiver.getHandler())
+        }
+    }
+
+    fun cleanUp() {
+        if (inputDeviceListener != null) {
+            val im = context.getSystemService<InputManager>(InputManager::class.java)
+            im.unregisterInputDeviceListener(inputDeviceListener)
+            inputDeviceListener = null
+        }
+    }
+
+    private fun setupTouchReceiver(receiver: View) {
+        receiver.setOnTouchListener(
+            View.OnTouchListener { v: View?, event: MotionEvent? ->
+                virtualMachine.sendMultiTouchEvent(event)
+            }
+        )
+    }
+
+    private fun setupMouseReceiver(receiver: View) {
+        receiver.requestUnbufferedDispatch(InputDevice.SOURCE_ANY)
+        receiver.setOnCapturedPointerListener { v: View?, event: MotionEvent? ->
+            val eventSource = event!!.source
+            if ((eventSource and InputDevice.SOURCE_CLASS_POSITION) != 0) {
+                return@setOnCapturedPointerListener virtualMachine.sendTrackpadEvent(event)
+            }
+            virtualMachine.sendMouseEvent(event)
+        }
+    }
+
+    private fun setupKeyReceiver(receiver: View) {
+        receiver.setOnKeyListener { v: View?, code: Int, event: KeyEvent? ->
+            // TODO: this is guest-os specific. It shouldn't be handled here.
+            if (isVolumeKey(code)) {
+                return@setOnKeyListener false
+            }
+            virtualMachine.sendKeyEvent(event)
+        }
+    }
+
+    private fun setupTabletModeHandler(handler: Handler?) {
+        val im = context.getSystemService<InputManager?>(InputManager::class.java)
+        inputDeviceListener =
+            object : InputManager.InputDeviceListener {
+                override fun onInputDeviceAdded(deviceId: Int) {
+                    setTabletModeConditionally()
+                }
+
+                override fun onInputDeviceRemoved(deviceId: Int) {
+                    setTabletModeConditionally()
+                }
+
+                override fun onInputDeviceChanged(deviceId: Int) {
+                    setTabletModeConditionally()
+                }
+            }
+        im!!.registerInputDeviceListener(inputDeviceListener, handler)
+    }
+
+    fun setTabletModeConditionally() {
+        val tabletModeNeeded = !hasPhysicalKeyboard()
+        if (tabletModeNeeded != isTabletMode) {
+            val mode = if (tabletModeNeeded) "tablet mode" else "desktop mode"
+            Log.d(TAG, "switching to $mode")
+            isTabletMode = tabletModeNeeded
+            virtualMachine.sendTabletModeEvent(tabletModeNeeded)
+        }
+    }
+
+    companion object {
+        private fun isVolumeKey(keyCode: Int): Boolean {
+            return keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
+                keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+                keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+        }
+
+        private fun hasPhysicalKeyboard(): Boolean {
+            for (id in InputDevice.getDeviceIds()) {
+                val d = InputDevice.getDevice(id)
+                if (!d!!.isVirtual && d.isEnabled && d.isFullKeyboard) {
+                    return true
+                }
+            }
+            return false
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
index 423d66b..7180e87 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
@@ -71,7 +71,7 @@
                 PendingIntent.FLAG_IMMUTABLE,
             )
         notification =
-            Notification.Builder(this, this.packageName)
+            Notification.Builder(this, Application.CHANNEL_LONG_RUNNING_ID)
                 .setSilent(true)
                 .setSmallIcon(R.drawable.ic_launcher_foreground)
                 .setContentTitle(getString(R.string.installer_notif_title_text))
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index bf2f573..71f10f9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -56,6 +56,7 @@
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import com.android.internal.annotations.VisibleForTesting
 import com.android.microdroid.test.common.DeviceProperties
+import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.CertificateUtils.createOrGetKey
 import com.android.virtualization.terminal.CertificateUtils.writeCertificateToFile
 import com.android.virtualization.terminal.ErrorActivity.Companion.start
@@ -87,6 +88,7 @@
     private val bootCompleted = ConditionVariable()
     private lateinit var manageExternalStorageActivityResultLauncher: ActivityResultLauncher<Intent>
     private lateinit var modifierKeysController: ModifierKeysController
+    private var displayMenu: MenuItem? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -209,7 +211,10 @@
                     view: WebView?,
                     request: WebResourceRequest?,
                 ): Boolean {
-                    return false
+                    val intent = Intent(Intent.ACTION_VIEW, request?.url)
+                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    startActivity(intent)
+                    return true
                 }
 
                 override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
@@ -253,6 +258,10 @@
                                     Trace.endAsyncSection("executeTerminal", 0)
                                     findViewById<View?>(R.id.boot_progress).visibility = View.GONE
                                     terminalContainer.visibility = View.VISIBLE
+                                    if (terminalGuiSupport()) {
+                                        displayMenu?.setVisible(true)
+                                        displayMenu?.setEnabled(true)
+                                    }
                                     bootCompleted.open()
                                     modifierKeysController.update()
                                     terminalView.mapTouchToMouseEvent()
@@ -293,6 +302,8 @@
             info,
             executorService,
             object : NsdManager.ServiceInfoCallback {
+                var loaded: Boolean = false
+
                 override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
 
                 override fun onServiceInfoCallbackUnregistered() {}
@@ -300,13 +311,15 @@
                 override fun onServiceLost() {}
 
                 override fun onServiceUpdated(info: NsdServiceInfo) {
-                    nsdManager.unregisterServiceInfoCallback(this)
-
                     Log.i(TAG, "Service found: $info")
                     val ipAddress = info.hostAddresses[0].hostAddress
                     val port = info.port
                     val url = getTerminalServiceUrl(ipAddress, port)
-                    runOnUiThread(Runnable { terminalView.loadUrl(url.toString()) })
+                    if (!loaded) {
+                        loaded = true
+                        nsdManager.unregisterServiceInfoCallback(this)
+                        runOnUiThread(Runnable { terminalView.loadUrl(url.toString()) })
+                    }
                 }
             },
         )
@@ -337,6 +350,11 @@
 
     override fun onCreateOptionsMenu(menu: Menu?): Boolean {
         menuInflater.inflate(R.menu.main_menu, menu)
+        displayMenu =
+            menu?.findItem(R.id.menu_item_display).also {
+                it?.setVisible(terminalGuiSupport())
+                it?.setEnabled(false)
+            }
         return true
     }
 
@@ -346,6 +364,10 @@
             val intent = Intent(this, SettingsActivity::class.java)
             this.startActivity(intent)
             return true
+        } else if (id == R.id.menu_item_display) {
+            val intent = Intent(this, DisplayActivity::class.java)
+            this.startActivity(intent)
+            return true
         }
         return super.onOptionsItemSelected(item)
     }
@@ -409,7 +431,7 @@
             )
         val icon = Icon.createWithResource(resources, R.drawable.ic_launcher_foreground)
         val notification: Notification =
-            Notification.Builder(this, this.packageName)
+            Notification.Builder(this, Application.CHANNEL_LONG_RUNNING_ID)
                 .setSilent(true)
                 .setSmallIcon(R.drawable.ic_launcher_foreground)
                 .setContentTitle(resources.getString(R.string.service_notification_title))
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
index 7c48303..7e58b36 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
@@ -100,7 +100,7 @@
                 )
                 .build()
         val notification: Notification =
-            Notification.Builder(context, context.getPackageName())
+            Notification.Builder(context, Application.CHANNEL_SYSTEM_EVENTS_ID)
                 .setSmallIcon(R.drawable.ic_launcher_foreground)
                 .setContentTitle(title)
                 .setContentText(content)
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 8c0368d..346056a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -36,6 +36,7 @@
 import android.system.virtualmachine.VirtualMachineException
 import android.util.Log
 import android.widget.Toast
+import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
 import com.android.virtualization.terminal.Runner.Companion.create
 import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
@@ -149,6 +150,8 @@
             info,
             executorService!!,
             object : NsdManager.ServiceInfoCallback {
+                var started: Boolean = false
+
                 override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
 
                 override fun onServiceInfoCallbackUnregistered() {}
@@ -156,9 +159,12 @@
                 override fun onServiceLost() {}
 
                 override fun onServiceUpdated(info: NsdServiceInfo) {
-                    nsdManager.unregisterServiceInfoCallback(this)
                     Log.i(TAG, "Service found: $info")
-                    startDebianServer(info.hostAddresses[0].hostAddress)
+                    if (!started) {
+                        started = true
+                        nsdManager.unregisterServiceInfoCallback(this)
+                        startDebianServer(info.hostAddresses[0].hostAddress)
+                    }
                 }
             },
         )
@@ -182,7 +188,7 @@
             resources.getString(R.string.service_notification_force_quit_action)
         val stopNotificationTitle: String? =
             resources.getString(R.string.service_notification_close_title)
-        return Notification.Builder(this, this.packageName)
+        return Notification.Builder(this, Application.CHANNEL_SYSTEM_EVENTS_ID)
             .setSmallIcon(R.drawable.ic_launcher_foreground)
             .setContentTitle(stopNotificationTitle)
             .setOngoing(true)
@@ -212,6 +218,21 @@
             changed = true
         }
 
+        // TODO(jeongik): let it configurable
+        if (terminalGuiSupport()) {
+            builder
+                .setDisplayConfig(
+                    VirtualMachineCustomImageConfig.DisplayConfig.Builder()
+                        .setWidth(1920)
+                        .setHeight(1080)
+                        .build()
+                )
+                .useKeyboard(true)
+                .useMouse(true)
+                .useTouch(true)
+            changed = true
+        }
+
         val image = InstalledImage.getDefault(this)
         if (image.hasBackup()) {
             val backup = image.backupFile
diff --git a/android/TerminalApp/proguard.flags b/android/TerminalApp/proguard.flags
index 88b8a9c..04a2140 100644
--- a/android/TerminalApp/proguard.flags
+++ b/android/TerminalApp/proguard.flags
@@ -4,7 +4,10 @@
 -keepattributes Signature
 
 # For using GSON @Expose annotation
--keepattributes *Annotation*
+-keepattributes RuntimeVisibleAnnotations,
+                RuntimeVisibleParameterAnnotations,
+                RuntimeVisibleTypeAnnotations,
+                AnnotationDefault
 
 # Gson specific classes
 -dontwarn sun.misc.**
diff --git a/android/TerminalApp/res/drawable/ic_display.xml b/android/TerminalApp/res/drawable/ic_display.xml
new file mode 100644
index 0000000..86bdb5d
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_display.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  Copyright 2025 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M240,840L240,760L280,720L160,720Q127,720 103.5,696.5Q80,673 80,640L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,640Q880,673 856.5,696.5Q833,720 800,720L680,720L720,760L720,840L240,840ZM160,640L800,640Q800,640 800,640Q800,640 800,640L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,640Q160,640 160,640Q160,640 160,640ZM160,640Q160,640 160,640Q160,640 160,640L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,640Q160,640 160,640Q160,640 160,640Z" />
+</vector>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/activity_display.xml b/android/TerminalApp/res/layout/activity_display.xml
new file mode 100644
index 0000000..48e49fe
--- /dev/null
+++ b/android/TerminalApp/res/layout/activity_display.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  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.
+ -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".DisplayActivity">
+    <View
+        android:id="@+id/background_touch_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        />
+    <!-- the size should be match_parent -->
+    <SurfaceView
+        android:id="@+id/surface_view"
+        android:layout_width="1920px"
+        android:layout_height="1080px"
+        android:focusable="true"
+        android:focusableInTouchMode="true"
+        android:focusedByDefault="true"
+        android:defaultFocusHighlightEnabled="true">
+        <requestFocus />
+    </SurfaceView>
+    <!-- A cursor size in virtio-gpu spec is always 64x64 -->
+    <SurfaceView
+        android:id="@+id/cursor_surface_view"
+        android:layout_width="64px"
+        android:layout_height="64px">
+    </SurfaceView>
+
+</merge>
\ No newline at end of file
diff --git a/android/TerminalApp/res/menu/main_menu.xml b/android/TerminalApp/res/menu/main_menu.xml
index 42ba85d..dbb788c 100644
--- a/android/TerminalApp/res/menu/main_menu.xml
+++ b/android/TerminalApp/res/menu/main_menu.xml
@@ -20,4 +20,9 @@
         android:icon="@drawable/ic_settings"
         android:title="@string/action_settings"
         app:showAsAction="always"/>
+    <item android:id="@+id/menu_item_display"
+        android:icon="@drawable/ic_display"
+        android:enabled="false"
+        android:title="@string/action_display"
+        app:showAsAction="always"/>
 </menu>
diff --git a/android/TerminalApp/res/values-iw/strings.xml b/android/TerminalApp/res/values-iw/strings.xml
index 8859e5f..729b22b 100644
--- a/android/TerminalApp/res/values-iw/strings.xml
+++ b/android/TerminalApp/res/values-iw/strings.xml
@@ -20,7 +20,7 @@
     <string name="terminal_display" msgid="4810127497644015237">"תצוגת טרמינל"</string>
     <string name="terminal_input" msgid="4602512831433433551">"סמן"</string>
     <string name="empty_line" msgid="5012067143408427178">"שורה ריקה"</string>
-    <string name="double_tap_to_edit_text" msgid="2344363097580051316">"כדי להקליד טקסט צריך להקיש הקשה כפולה"</string>
+    <string name="double_tap_to_edit_text" msgid="2344363097580051316">"כדי להקליד טקסט צריך ללחוץ לחיצה כפולה"</string>
     <string name="installer_title_text" msgid="500663060973466805">"התקנה של טרמינל Linux"</string>
     <string name="installer_desc_text_format" msgid="5935117404303982823">"כדי להפעיל את טרמינל Linux, צריך להוריד נתונים בנפח של בערך <xliff:g id="EXPECTED_SIZE">%1$s</xliff:g> דרך הרשת.\nלהמשיך?"</string>
     <string name="installer_wait_for_wifi_checkbox_text" msgid="5812378362605046639">"הורדה רק באמצעות Wi-Fi"</string>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index d3440d3..44009c3 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -55,6 +55,9 @@
     <!-- Action bar icon name for the settings view CHAR LIMIT=none] -->
     <string name="action_settings">Settings</string>
 
+    <!-- Action bar icon name for showing the display activity CHAR LIMIT=none] -->
+    <string name="action_display">Display</string>
+
     <!-- Toast message to notify that preparing terminal to start [CHAR LIMIT=none] -->
     <string name="vm_creation_message">Preparing terminal</string>
     <!-- Toast message to notify that terminal is stopping [CHAR LIMIT=none] -->
@@ -172,4 +175,10 @@
 
     <!-- This string is for toast message to notify that VirGL is enabled. [CHAR LIMIT=40] -->
     <string name="virgl_enabled"><xliff:g>VirGL</xliff:g> is enabled</string>
+
+    <!-- This is the name of the notification channel for long-runnint tasks [CHAR LIMIT=none] -->
+    <string name="notification_channel_long_running_name">Long running tasks</string>
+
+    <!-- This is the name of the notification channel for system events [CHAR LIMIT=none] -->
+    <string name="notification_channel_system_events_name">System events</string>
 </resources>
diff --git a/android/TerminalApp/res/values/styles.xml b/android/TerminalApp/res/values/styles.xml
index 3fb8e7d..13f070f 100644
--- a/android/TerminalApp/res/values/styles.xml
+++ b/android/TerminalApp/res/values/styles.xml
@@ -27,4 +27,15 @@
     <style name="VmTerminalAppTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
         <item name="android:windowLightStatusBar" tools:targetApi="m">?android:attr/isLightTheme</item>
     </style>
+    <style name="FullscreenTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
+        <item name="android:navigationBarColor">
+            @android:color/transparent
+        </item>
+        <item name="android:statusBarColor">
+            @android:color/transparent
+        </item>
+        <item name="android:windowLayoutInDisplayCutoutMode">
+            always
+        </item>
+    </style>
 </resources>
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 57779bf..33f3be1 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -2033,7 +2033,7 @@
     Owned(T),
 }
 
-impl<'a, T> AsRef<T> for BorrowedOrOwned<'a, T> {
+impl<T> AsRef<T> for BorrowedOrOwned<'_, T> {
     fn as_ref(&self) -> &T {
         match self {
             Self::Borrowed(b) => b,
diff --git a/build/debian/fai_config/files/etc/avahi/services/ttyd.service/AVF b/build/debian/fai_config/files/etc/avahi/services/ttyd.service/AVF
deleted file mode 100644
index 64f9d0a..0000000
--- a/build/debian/fai_config/files/etc/avahi/services/ttyd.service/AVF
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
-<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
-
-<service-group>
-
-  <name>ttyd</name>
-
-  <service protocol="ipv4">
-    <type>_http._tcp</type>
-    <port>7681</port>
-  </service>
-
-</service-group>
diff --git a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
index 4a32f2b..d86bab0 100644
--- a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
+++ b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
@@ -3,11 +3,14 @@
 After=syslog.target
 After=network.target
 After=virtiofs_internal.service
+
 [Service]
 ExecStart=/usr/local/bin/ttyd --ssl --ssl-cert /etc/ttyd/server.crt --ssl-key /etc/ttyd/server.key --ssl-ca /mnt/internal/ca.crt -t disableLeaveAlert=true -W login -f droid
+ExecStartPost=/usr/bin/avahi-publish-service ttyd _http._tcp 7681
 Type=simple
 Restart=always
 User=root
 Group=root
+
 [Install]
 WantedBy=multi-user.target
diff --git a/build/debian/fai_config/package_config/AVF b/build/debian/fai_config/package_config/AVF
index 98b558b..bf51fdb 100644
--- a/build/debian/fai_config/package_config/AVF
+++ b/build/debian/fai_config/package_config/AVF
@@ -8,3 +8,5 @@
 forwarder-guest
 forwarder-guest-launcher
 shutdown-runner
+weston
+xwayland
diff --git a/guest/apkdmverity/src/main.rs b/guest/apkdmverity/src/main.rs
index d2f88ae..2fc964b 100644
--- a/guest/apkdmverity/src/main.rs
+++ b/guest/apkdmverity/src/main.rs
@@ -27,6 +27,7 @@
 use apkverify::{HashAlgorithm, V4Signature};
 use clap::{arg, Arg, ArgAction, Command};
 use dm::loopdevice;
+use dm::loopdevice::LoopConfigOptions;
 use dm::util;
 use dm::verity::{DmVerityHashAlgorithm, DmVerityTargetBuilder};
 use itertools::Itertools;
@@ -109,9 +110,13 @@
         }
         (
             loopdevice::attach(
-                &apk, 0, apk_size, /* direct_io */ true, /* writable */ false,
+                &apk,
+                0,
+                apk_size,
+                &LoopConfigOptions { direct_io: true, ..Default::default() },
             )
-            .context("Failed to attach APK to a loop device")?,
+            .context("Failed to attach APK to a loop device")?
+            .path,
             apk_size,
         )
     };
@@ -125,10 +130,9 @@
     // Due to unknown reason(b/191344832), we can't enable "direct IO" for the IDSIG file (backing
     // the hash). For now we don't use "direct IO" but it seems OK since the IDSIG file is very
     // small and the benefit of direct-IO would be negliable.
-    let hash_device = loopdevice::attach(
-        &idsig, offset, size, /* direct_io */ false, /* writable */ false,
-    )
-    .context("Failed to attach idsig to a loop device")?;
+    let hash_device = loopdevice::attach(&idsig, offset, size, &LoopConfigOptions::default())
+        .context("Failed to attach idsig to a loop device")?
+        .path;
 
     // Build a dm-verity target spec from the information from the idsig file. The apk and the
     // idsig files are used as the data device and the hash device, respectively.
@@ -347,18 +351,17 @@
         // of the data device is done in the scopeguard for the return value of `enable_verity`
         // below. Only the idsig_loop_device needs detatching.
         let apk_loop_device = loopdevice::attach(
-            &apk_path, 0, apk_size, /* direct_io */ true, /* writable */ false,
+            &apk_path,
+            0,
+            apk_size,
+            &LoopConfigOptions { direct_io: true, ..Default::default() },
         )
-        .unwrap();
+        .unwrap()
+        .path;
         let idsig_loop_device = scopeguard::guard(
-            loopdevice::attach(
-                &idsig_path,
-                0,
-                idsig_size,
-                /* direct_io */ false,
-                /* writable */ false,
-            )
-            .unwrap(),
+            loopdevice::attach(&idsig_path, 0, idsig_size, &LoopConfigOptions::default())
+                .unwrap()
+                .path,
             |dev| loopdevice::detach(dev).unwrap(),
         );
 
diff --git a/guest/authfs/src/common.rs b/guest/authfs/src/common.rs
index 6556fde..fc5af89 100644
--- a/guest/authfs/src/common.rs
+++ b/guest/authfs/src/common.rs
@@ -18,7 +18,7 @@
 pub const CHUNK_SIZE: u64 = 4096;
 
 pub fn divide_roundup(dividend: u64, divisor: u64) -> u64 {
-    (dividend + divisor - 1) / divisor
+    dividend.div_ceil(divisor)
 }
 
 /// Given `offset` and `length`, generates (offset, size) tuples that together form the same length,
diff --git a/guest/authfs/src/fsverity/metadata/metadata.rs b/guest/authfs/src/fsverity/metadata/metadata.rs
index 54d0145..2e78190 100644
--- a/guest/authfs/src/fsverity/metadata/metadata.rs
+++ b/guest/authfs/src/fsverity/metadata/metadata.rs
@@ -131,8 +131,7 @@
     };
 
     // merkle tree is at the next 4K boundary
-    let merkle_tree_offset =
-        (metadata_file.stream_position()? + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
+    let merkle_tree_offset = (metadata_file.stream_position()?).div_ceil(CHUNK_SIZE) * CHUNK_SIZE;
 
     Ok(Box::new(FSVerityMetadata { header, digest, signature, metadata_file, merkle_tree_offset }))
 }
diff --git a/guest/authfs/src/fusefs.rs b/guest/authfs/src/fusefs.rs
index fa4076d..9e49046 100644
--- a/guest/authfs/src/fusefs.rs
+++ b/guest/authfs/src/fusefs.rs
@@ -816,7 +816,7 @@
                     // FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a
                     // kernel change (see b/196635431). Until it's possible, use
                     // xattr to expose what we need as an authfs specific API.
-                    if name != CStr::from_bytes_with_nul(b"authfs.fsverity.digest\0").unwrap() {
+                    if name != c"authfs.fsverity.digest" {
                         return Err(io::Error::from_raw_os_error(libc::ENODATA));
                     }
 
diff --git a/guest/pvmfw/avb/tests/api_test.rs b/guest/pvmfw/avb/tests/api_test.rs
index 23e05d4..29a6277 100644
--- a/guest/pvmfw/avb/tests/api_test.rs
+++ b/guest/pvmfw/avb/tests/api_test.rs
@@ -23,7 +23,6 @@
 use std::{
     fs,
     mem::{offset_of, size_of},
-    ptr,
 };
 use utils::*;
 
@@ -414,9 +413,9 @@
     // vbmeta_header is unaligned; copy flags to local variable
     let vbmeta_header_flags = vbmeta_header.flags;
     assert_eq!(0, vbmeta_header_flags, "The disable flag should not be set in the latest kernel.");
-    let flags_addr = ptr::addr_of!(vbmeta_header.flags) as *const u8;
+    let flags_addr = (&raw const vbmeta_header.flags).cast::<u8>();
     // SAFETY: It is safe as both raw pointers `flags_addr` and `vbmeta_header` are not null.
-    let flags_offset = unsafe { flags_addr.offset_from(ptr::addr_of!(vbmeta_header) as *const u8) };
+    let flags_offset = unsafe { flags_addr.offset_from((&raw const vbmeta_header).cast::<u8>()) };
     let flags_offset = usize::try_from(footer.vbmeta_offset)? + usize::try_from(flags_offset)?;
 
     // Act.
diff --git a/guest/pvmfw/src/dice.rs b/guest/pvmfw/src/dice.rs
index 78bd6b8..4df10b3 100644
--- a/guest/pvmfw/src/dice.rs
+++ b/guest/pvmfw/src/dice.rs
@@ -156,9 +156,7 @@
     fn generate_config_descriptor(&self, instance_hash: Option<Hash>) -> Result<Vec<u8>> {
         let mut config = Vec::with_capacity(4);
         config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!("vm_entry")?));
-        if cfg!(dice_changes) {
-            config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
-        }
+        config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
         if self.rkp_vm_marker {
             config.push((cbor!(RKP_VM_MARKER_KEY)?, Value::Null))
         }
@@ -245,14 +243,7 @@
         assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
         assert_eq!(config_map.get(&COMPONENT_VERSION_KEY), None);
         assert_eq!(config_map.get(&RESETTABLE_KEY), None);
-        if cfg!(dice_changes) {
-            assert_eq!(
-                config_map.get(&SECURITY_VERSION_KEY).unwrap().as_integer().unwrap(),
-                42.into()
-            );
-        } else {
-            assert_eq!(config_map.get(&SECURITY_VERSION_KEY), None);
-        }
+        assert_eq!(config_map.get(&SECURITY_VERSION_KEY).unwrap().as_integer().unwrap(), 42.into());
         assert_eq!(config_map.get(&RKP_VM_MARKER_KEY), None);
     }
 
diff --git a/guest/pvmfw/src/fdt.rs b/guest/pvmfw/src/fdt.rs
index 29212f9..818d342 100644
--- a/guest/pvmfw/src/fdt.rs
+++ b/guest/pvmfw/src/fdt.rs
@@ -112,6 +112,24 @@
     Ok(None)
 }
 
+/// Read /avf/untrusted/instance-id, if present.
+pub fn read_instance_id(fdt: &Fdt) -> libfdt::Result<Option<&[u8]>> {
+    read_avf_untrusted_prop(fdt, c"instance-id")
+}
+
+/// Read /avf/untrusted/defer-rollback-protection, if present.
+pub fn read_defer_rollback_protection(fdt: &Fdt) -> libfdt::Result<Option<&[u8]>> {
+    read_avf_untrusted_prop(fdt, c"defer-rollback-protection")
+}
+
+fn read_avf_untrusted_prop<'a>(fdt: &'a Fdt, prop: &CStr) -> libfdt::Result<Option<&'a [u8]>> {
+    if let Some(node) = fdt.node(c"/avf/untrusted")? {
+        node.getprop(prop)
+    } else {
+        Ok(None)
+    }
+}
+
 fn patch_initrd_range(fdt: &mut Fdt, initrd_range: &Range<usize>) -> libfdt::Result<()> {
     let start = u32::try_from(initrd_range.start).unwrap();
     let end = u32::try_from(initrd_range.end).unwrap();
diff --git a/guest/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
index 0a3dca6..afa64e0 100644
--- a/guest/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -35,22 +35,20 @@
 use crate::bcc::Bcc;
 use crate::dice::PartialInputs;
 use crate::entry::RebootReason;
-use crate::fdt::{modify_for_next_stage, sanitize_device_tree};
+use crate::fdt::{modify_for_next_stage, read_instance_id, sanitize_device_tree};
 use crate::rollback::perform_rollback_protection;
 use alloc::borrow::Cow;
 use alloc::boxed::Box;
 use bssl_avf::Digester;
 use diced_open_dice::{bcc_handover_parse, DiceArtifacts, DiceContext, Hidden, VM_KEY_ALGORITHM};
-use libfdt::{Fdt, FdtNode};
+use libfdt::Fdt;
 use log::{debug, error, info, trace, warn};
 use pvmfw_avb::verify_payload;
 use pvmfw_avb::DebugLevel;
 use pvmfw_embedded_key::PUBLIC_KEY;
-use vmbase::fdt::pci::{PciError, PciInfo};
 use vmbase::heap;
-use vmbase::memory::{flush, init_shared_pool, SIZE_4KB};
+use vmbase::memory::{flush, SIZE_4KB};
 use vmbase::rand;
-use vmbase::virtio::pci;
 
 fn main<'a>(
     untrusted_fdt: &mut Fdt,
@@ -77,8 +75,6 @@
     })?;
     trace!("BCC: {bcc_handover:x?}");
 
-    let cdi_seal = bcc_handover.cdi_seal();
-
     let bcc = Bcc::new(bcc_handover.bcc()).map_err(|e| {
         error!("{e}");
         RebootReason::InvalidBcc
@@ -102,19 +98,8 @@
     }
 
     let guest_page_size = verified_boot_data.page_size.unwrap_or(SIZE_4KB);
-    let fdt_info = sanitize_device_tree(untrusted_fdt, vm_dtbo, vm_ref_dt, guest_page_size)?;
+    let _ = sanitize_device_tree(untrusted_fdt, vm_dtbo, vm_ref_dt, guest_page_size)?;
     let fdt = untrusted_fdt; // DT has now been sanitized.
-    let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
-    debug!("PCI: {:#x?}", pci_info);
-    // Set up PCI bus for VirtIO devices.
-    let mut pci_root = pci::initialize(pci_info).map_err(|e| {
-        error!("Failed to initialize PCI: {e}");
-        RebootReason::InternalError
-    })?;
-    init_shared_pool(fdt_info.swiotlb_info.fixed_range()).map_err(|e| {
-        error!("Failed to initialize shared pool: {e}");
-        RebootReason::InternalError
-    })?;
 
     let next_bcc_size = guest_page_size;
     let next_bcc = heap::aligned_boxed_slice(next_bcc_size, guest_page_size).ok_or_else(|| {
@@ -129,13 +114,12 @@
         RebootReason::InternalError
     })?;
 
-    let instance_hash = Some(salt_from_instance_id(fdt)?);
+    let instance_hash = salt_from_instance_id(fdt)?;
     let (new_instance, salt, defer_rollback_protection) = perform_rollback_protection(
         fdt,
         &verified_boot_data,
         &dice_inputs,
-        &mut pci_root,
-        cdi_seal,
+        bcc_handover.cdi_seal(),
         instance_hash,
     )?;
     trace!("Got salt for instance: {salt:x?}");
@@ -204,8 +188,14 @@
 
 // Get the "salt" which is one of the input for DICE derivation.
 // This provides differentiation of secrets for different VM instances with same payloads.
-fn salt_from_instance_id(fdt: &Fdt) -> Result<Hidden, RebootReason> {
-    let id = instance_id(fdt)?;
+fn salt_from_instance_id(fdt: &Fdt) -> Result<Option<Hidden>, RebootReason> {
+    let Some(id) = read_instance_id(fdt).map_err(|e| {
+        error!("Failed to get instance-id in DT: {e}");
+        RebootReason::InvalidFdt
+    })?
+    else {
+        return Ok(None);
+    };
     let salt = Digester::sha512()
         .digest(&[&b"InstanceId:"[..], id].concat())
         .map_err(|e| {
@@ -214,46 +204,5 @@
         })?
         .try_into()
         .map_err(|_| RebootReason::InternalError)?;
-    Ok(salt)
-}
-
-fn instance_id(fdt: &Fdt) -> Result<&[u8], RebootReason> {
-    let node = avf_untrusted_node(fdt)?;
-    let id = node.getprop(c"instance-id").map_err(|e| {
-        error!("Failed to get instance-id in DT: {e}");
-        RebootReason::InvalidFdt
-    })?;
-    id.ok_or_else(|| {
-        error!("Missing instance-id");
-        RebootReason::InvalidFdt
-    })
-}
-
-fn avf_untrusted_node(fdt: &Fdt) -> Result<FdtNode, RebootReason> {
-    let node = fdt.node(c"/avf/untrusted").map_err(|e| {
-        error!("Failed to get /avf/untrusted node: {e}");
-        RebootReason::InvalidFdt
-    })?;
-    node.ok_or_else(|| {
-        error!("/avf/untrusted node is missing in DT");
-        RebootReason::InvalidFdt
-    })
-}
-
-/// Logs the given PCI error and returns the appropriate `RebootReason`.
-fn handle_pci_error(e: PciError) -> RebootReason {
-    error!("{}", e);
-    match e {
-        PciError::FdtErrorPci(_)
-        | PciError::FdtNoPci
-        | PciError::FdtErrorReg(_)
-        | PciError::FdtMissingReg
-        | PciError::FdtRegEmpty
-        | PciError::FdtRegMissingSize
-        | PciError::CamWrongSize(_)
-        | PciError::FdtErrorRanges(_)
-        | PciError::FdtMissingRanges
-        | PciError::RangeAddressMismatch { .. }
-        | PciError::NoSuitableRange => RebootReason::InvalidFdt,
-    }
+    Ok(Some(salt))
 }
diff --git a/guest/pvmfw/src/rollback.rs b/guest/pvmfw/src/rollback.rs
index f7723d7..74b2cd8 100644
--- a/guest/pvmfw/src/rollback.rs
+++ b/guest/pvmfw/src/rollback.rs
@@ -16,16 +16,20 @@
 
 use crate::dice::PartialInputs;
 use crate::entry::RebootReason;
+use crate::fdt::read_defer_rollback_protection;
 use crate::instance::EntryBody;
 use crate::instance::Error as InstanceError;
 use crate::instance::{get_recorded_entry, record_instance_entry};
 use diced_open_dice::Hidden;
-use libfdt::{Fdt, FdtNode};
+use libfdt::Fdt;
 use log::{error, info};
 use pvmfw_avb::Capability;
 use pvmfw_avb::VerifiedBootData;
 use virtio_drivers::transport::pci::bus::PciRoot;
+use vmbase::fdt::{pci::PciInfo, SwiotlbInfo};
+use vmbase::memory::init_shared_pool;
 use vmbase::rand;
+use vmbase::virtio::pci;
 
 /// Performs RBP based on the input payload, current DICE chain, and host-controlled platform.
 ///
@@ -37,7 +41,6 @@
     fdt: &Fdt,
     verified_boot_data: &VerifiedBootData,
     dice_inputs: &PartialInputs,
-    pci_root: &mut PciRoot,
     cdi_seal: &[u8],
     instance_hash: Option<Hidden>,
 ) -> Result<(bool, Hidden, bool), RebootReason> {
@@ -53,7 +56,7 @@
         skip_rollback_protection()?;
         Ok((false, instance_hash.unwrap(), false))
     } else {
-        perform_legacy_rollback_protection(dice_inputs, pci_root, cdi_seal, instance_hash)
+        perform_legacy_rollback_protection(fdt, dice_inputs, cdi_seal, instance_hash)
     }
 }
 
@@ -92,17 +95,18 @@
 
 /// Performs RBP using instance.img where updates require clearing old entries, causing new CDIs.
 fn perform_legacy_rollback_protection(
+    fdt: &Fdt,
     dice_inputs: &PartialInputs,
-    pci_root: &mut PciRoot,
     cdi_seal: &[u8],
     instance_hash: Option<Hidden>,
 ) -> Result<(bool, Hidden, bool), RebootReason> {
     info!("Fallback to instance.img based rollback checks");
-    let (recorded_entry, mut instance_img, header_index) = get_recorded_entry(pci_root, cdi_seal)
-        .map_err(|e| {
-        error!("Failed to get entry from instance.img: {e}");
-        RebootReason::InternalError
-    })?;
+    let mut pci_root = initialize_instance_img_device(fdt)?;
+    let (recorded_entry, mut instance_img, header_index) =
+        get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
+            error!("Failed to get entry from instance.img: {e}");
+            RebootReason::InternalError
+        })?;
     let (new_instance, salt) = if let Some(entry) = recorded_entry {
         check_dice_measurements_match_entry(dice_inputs, &entry)?;
         let salt = instance_hash.unwrap_or(entry.salt);
@@ -155,24 +159,34 @@
 }
 
 fn should_defer_rollback_protection(fdt: &Fdt) -> Result<bool, RebootReason> {
-    let node = avf_untrusted_node(fdt)?;
-    let defer_rbp = node
-        .getprop(c"defer-rollback-protection")
-        .map_err(|e| {
-            error!("Failed to get defer-rollback-protection property in DT: {e}");
-            RebootReason::InvalidFdt
-        })?
-        .is_some();
-    Ok(defer_rbp)
-}
-
-fn avf_untrusted_node(fdt: &Fdt) -> Result<FdtNode, RebootReason> {
-    let node = fdt.node(c"/avf/untrusted").map_err(|e| {
-        error!("Failed to get /avf/untrusted node: {e}");
+    let defer_rbp = read_defer_rollback_protection(fdt).map_err(|e| {
+        error!("Failed to get defer-rollback-protection property in DT: {e}");
         RebootReason::InvalidFdt
     })?;
-    node.ok_or_else(|| {
-        error!("/avf/untrusted node is missing in DT");
+    Ok(defer_rbp.is_some())
+}
+
+/// Set up PCI bus and VirtIO-blk device containing the instance.img partition.
+fn initialize_instance_img_device(fdt: &Fdt) -> Result<PciRoot, RebootReason> {
+    let pci_info = PciInfo::from_fdt(fdt).map_err(|e| {
+        error!("Failed to detect PCI from DT: {e}");
         RebootReason::InvalidFdt
-    })
+    })?;
+    let swiotlb_range = SwiotlbInfo::new_from_fdt(fdt)
+        .map_err(|e| {
+            error!("Failed to detect swiotlb from DT: {e}");
+            RebootReason::InvalidFdt
+        })?
+        .and_then(|info| info.fixed_range());
+
+    let pci_root = pci::initialize(pci_info).map_err(|e| {
+        error!("Failed to initialize PCI: {e}");
+        RebootReason::InternalError
+    })?;
+    init_shared_pool(swiotlb_range).map_err(|e| {
+        error!("Failed to initialize shared pool: {e}");
+        RebootReason::InternalError
+    })?;
+
+    Ok(pci_root)
 }
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index 48e3ccb..c650046 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -54,13 +54,9 @@
 const INSTANCE_IMG_PATH: &str = "/data/local/tmp/rialto_test/arm64/instance.img";
 const TEST_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
 
-#[cfg(dice_changes)]
 #[test]
 fn process_requests_in_protected_vm() -> Result<()> {
     if hypervisor_props::is_protected_vm_supported()? {
-        // The test is skipped if the feature flag |dice_changes| is not enabled, because when
-        // the flag is off, the DICE chain is truncated in the pvmfw, and the service VM cannot
-        // verify the chain due to the missing entries in the chain.
         check_processing_requests(VmType::ProtectedVm, None)
     } else {
         warn!("pVMs are not supported on device, skipping test");
diff --git a/guest/trusty/security_vm/launcher/Android.bp b/guest/trusty/security_vm/launcher/Android.bp
index fea8873..ff628fd 100644
--- a/guest/trusty/security_vm/launcher/Android.bp
+++ b/guest/trusty/security_vm/launcher/Android.bp
@@ -18,56 +18,3 @@
         false: false,
     }),
 }
-
-prebuilt_etc {
-    name: "lk_trusty.elf",
-    system_ext_specific: true,
-    relative_install_path: "vm/trusty_vm",
-    filename: "lk_trusty.elf",
-    arch: {
-        x86_64: {
-            src: ":trusty_security_vm_signed",
-        },
-        arm64: {
-            src: ":trusty_security_vm_signed",
-        },
-    },
-    src: ":empty_file",
-}
-
-filegroup {
-    name: "trusty_vm_sign_key",
-    srcs: [":avb_testkey_rsa4096"],
-}
-
-// python -c "import hashlib; print(hashlib.sha256(b'trusty_security_vm_salt').hexdigest())"
-trusty_security_vm_salt = "75a71e967c1a1e0f805cca20465e7acf83e6a04e567a67c426d8b5a94f8d61c5"
-
-TRUSTY_SECURITY_VM_VERSION = 1
-
-avb_add_hash_footer {
-    name: "trusty_security_vm_signed",
-    filename: "trusty_security_vm_signed",
-    partition_name: "boot",
-    private_key: ":trusty_vm_sign_key",
-    salt: trusty_security_vm_salt,
-    rollback_index: TRUSTY_SECURITY_VM_VERSION,
-    props: [
-        {
-            name: "com.android.virt.cap",
-            value: "trusty_security_vm",
-        },
-    ],
-    src: ":empty_file",
-    enabled: false,
-    arch: {
-        x86_64: {
-            src: ":trusty-lk.elf",
-            enabled: true,
-        },
-        arm64: {
-            src: ":trusty-test-lk.elf",
-            enabled: true,
-        },
-    },
-}
diff --git a/guest/trusty/security_vm/vm/Android.bp b/guest/trusty/security_vm/vm/Android.bp
new file mode 100644
index 0000000..f23385b
--- /dev/null
+++ b/guest/trusty/security_vm/vm/Android.bp
@@ -0,0 +1,56 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+prebuilt_etc {
+    name: "lk_trusty.elf",
+    system_ext_specific: true,
+    relative_install_path: "vm/trusty_vm",
+    filename: "lk_trusty.elf",
+    arch: {
+        x86_64: {
+            src: ":trusty_security_vm_signed",
+        },
+        arm64: {
+            src: ":trusty_security_vm_signed",
+        },
+    },
+    src: ":empty_file",
+}
+
+filegroup {
+    name: "trusty_vm_sign_key",
+    srcs: [":avb_testkey_rsa4096"],
+}
+
+// python -c "import hashlib; print(hashlib.sha256(b'trusty_security_vm_salt').hexdigest())"
+trusty_security_vm_salt = "75a71e967c1a1e0f805cca20465e7acf83e6a04e567a67c426d8b5a94f8d61c5"
+
+TRUSTY_SECURITY_VM_VERSION = 1
+
+avb_add_hash_footer {
+    name: "trusty_security_vm_signed",
+    filename: "trusty_security_vm_signed",
+    partition_name: "boot",
+    private_key: ":trusty_vm_sign_key",
+    salt: trusty_security_vm_salt,
+    rollback_index: TRUSTY_SECURITY_VM_VERSION,
+    props: [
+        {
+            name: "com.android.virt.cap",
+            value: "trusty_security_vm",
+        },
+    ],
+    src: ":empty_file",
+    enabled: false,
+    arch: {
+        x86_64: {
+            src: ":trusty-lk.elf",
+            enabled: true,
+        },
+        arm64: {
+            src: ":trusty-test-lk.elf",
+            enabled: true,
+        },
+    },
+}
diff --git a/guest/vmbase_example/Android.bp b/guest/vmbase_example/Android.bp
index ab21191..e5dfc2a 100644
--- a/guest/vmbase_example/Android.bp
+++ b/guest/vmbase_example/Android.bp
@@ -12,6 +12,7 @@
         "libdiced_open_dice_nostd",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
+        "libspin_nostd",
         "libvirtio_drivers",
         "libvmbase",
     ],
diff --git a/guest/vmbase_example/src/main.rs b/guest/vmbase_example/src/main.rs
index f5b41bd..b7d2f95 100644
--- a/guest/vmbase_example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -26,9 +26,9 @@
 use crate::layout::print_addresses;
 use crate::pci::check_pci;
 use alloc::{vec, vec::Vec};
-use core::ptr::addr_of_mut;
 use libfdt::Fdt;
 use log::{debug, error, info, trace, warn, LevelFilter};
+use spin::mutex::SpinMutex;
 use vmbase::{
     bionic, configure_heap,
     fdt::pci::PciInfo,
@@ -39,8 +39,8 @@
 };
 
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
-static mut ZEROED_DATA: [u32; 10] = [0; 10];
-static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
+static ZEROED_DATA: SpinMutex<[u32; 10]> = SpinMutex::new([0; 10]);
+static MUTABLE_DATA: SpinMutex<[u32; 4]> = SpinMutex::new([1, 2, 3, 4]);
 
 generate_image_header!();
 main!(main);
@@ -103,22 +103,16 @@
 
 fn check_data() {
     info!("INITIALISED_DATA: {:?}", INITIALISED_DATA.as_ptr());
-    // SAFETY: We only print the addresses of the static mutable variable, not actually access it.
-    info!("ZEROED_DATA: {:?}", unsafe { ZEROED_DATA.as_ptr() });
-    // SAFETY: We only print the addresses of the static mutable variable, not actually access it.
-    info!("MUTABLE_DATA: {:?}", unsafe { MUTABLE_DATA.as_ptr() });
 
     assert_eq!(INITIALISED_DATA[0], 1);
     assert_eq!(INITIALISED_DATA[1], 2);
     assert_eq!(INITIALISED_DATA[2], 3);
     assert_eq!(INITIALISED_DATA[3], 4);
 
-    // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
-    // chance of concurrent access.
-    let zeroed_data = unsafe { &mut *addr_of_mut!(ZEROED_DATA) };
-    // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
-    // chance of concurrent access.
-    let mutable_data = unsafe { &mut *addr_of_mut!(MUTABLE_DATA) };
+    let zeroed_data = &mut *ZEROED_DATA.lock();
+    let mutable_data = &mut *MUTABLE_DATA.lock();
+    info!("ZEROED_DATA: {:?}", zeroed_data.as_ptr());
+    info!("MUTABLE_DATA: {:?}", mutable_data.as_ptr());
 
     for element in zeroed_data.iter() {
         assert_eq!(*element, 0);
diff --git a/libs/apkverify/src/hashtree.rs b/libs/apkverify/src/hashtree.rs
index 00d8292..54e879b 100644
--- a/libs/apkverify/src/hashtree.rs
+++ b/libs/apkverify/src/hashtree.rs
@@ -84,7 +84,7 @@
             let mut level0 = Cursor::new(&mut hash_tree[cur.start..cur.end]);
 
             let mut a_block = vec![0; block_size];
-            let mut num_blocks = (input_size + block_size - 1) / block_size;
+            let mut num_blocks = input_size.div_ceil(block_size);
             while num_blocks > 0 {
                 input.read_exact(&mut a_block)?;
                 let h = hash_one_block(&a_block, salt, block_size, algorithm)?;
@@ -138,7 +138,7 @@
         if input_size <= block_size {
             break;
         }
-        let num_blocks = (input_size + block_size - 1) / block_size;
+        let num_blocks = input_size.div_ceil(block_size);
         let hashes_size = round_to_multiple(num_blocks * digest_size, block_size);
         level_sizes.push(hashes_size);
     }
diff --git a/libs/bssl/src/cbb.rs b/libs/bssl/src/cbb.rs
index a48c714..282a77d 100644
--- a/libs/bssl/src/cbb.rs
+++ b/libs/bssl/src/cbb.rs
@@ -40,13 +40,13 @@
     }
 }
 
-impl<'a> AsRef<CBB> for CbbFixed<'a> {
+impl AsRef<CBB> for CbbFixed<'_> {
     fn as_ref(&self) -> &CBB {
         &self.cbb
     }
 }
 
-impl<'a> AsMut<CBB> for CbbFixed<'a> {
+impl AsMut<CBB> for CbbFixed<'_> {
     fn as_mut(&mut self) -> &mut CBB {
         &mut self.cbb
     }
diff --git a/libs/bssl/src/cbs.rs b/libs/bssl/src/cbs.rs
index 12671cf..166484c 100644
--- a/libs/bssl/src/cbs.rs
+++ b/libs/bssl/src/cbs.rs
@@ -42,13 +42,13 @@
     }
 }
 
-impl<'a> AsRef<CBS> for Cbs<'a> {
+impl AsRef<CBS> for Cbs<'_> {
     fn as_ref(&self) -> &CBS {
         &self.cbs
     }
 }
 
-impl<'a> AsMut<CBS> for Cbs<'a> {
+impl AsMut<CBS> for Cbs<'_> {
     fn as_mut(&mut self) -> &mut CBS {
         &mut self.cbs
     }
diff --git a/libs/bssl/src/ec_key.rs b/libs/bssl/src/ec_key.rs
index 3e2e382..da9eb77 100644
--- a/libs/bssl/src/ec_key.rs
+++ b/libs/bssl/src/ec_key.rs
@@ -471,7 +471,7 @@
 /// Wrapper of an `EC_GROUP` reference.
 struct EcGroup<'a>(&'a EC_GROUP);
 
-impl<'a> EcGroup<'a> {
+impl EcGroup<'_> {
     /// Returns the NID that identifies the EC group of the key.
     fn curve_nid(&self) -> i32 {
         // SAFETY: It is safe since the inner pointer is valid and points to an initialized
@@ -518,7 +518,7 @@
     }
 }
 
-impl<'a> AsRef<EC_GROUP> for EcGroup<'a> {
+impl AsRef<EC_GROUP> for EcGroup<'_> {
     fn as_ref(&self) -> &EC_GROUP {
         self.0
     }
diff --git a/libs/devicemapper/Android.bp b/libs/devicemapper/Android.bp
index 5332469..6b7f680 100644
--- a/libs/devicemapper/Android.bp
+++ b/libs/devicemapper/Android.bp
@@ -8,7 +8,6 @@
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
     edition: "2021",
-    prefer_rlib: true,
     rustlibs: [
         "libanyhow",
         "libbitflags",
@@ -18,16 +17,12 @@
         "libuuid",
         "libzerocopy",
     ],
-    multilib: {
-        lib32: {
-            enabled: false,
-        },
-    },
 }
 
 rust_library {
     name: "libdm_rust",
     defaults: ["libdm_rust.defaults"],
+    host_supported: true,
 }
 
 rust_test {
diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs
index 75417ed..1326caf 100644
--- a/libs/devicemapper/src/crypt.rs
+++ b/libs/devicemapper/src/crypt.rs
@@ -87,7 +87,7 @@
     opt_params: Vec<&'a str>,
 }
 
-impl<'a> Default for DmCryptTargetBuilder<'a> {
+impl Default for DmCryptTargetBuilder<'_> {
     fn default() -> Self {
         DmCryptTargetBuilder {
             cipher: CipherType::AES256HCTR2,
diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs
index a8f3049..a8c2833 100644
--- a/libs/devicemapper/src/lib.rs
+++ b/libs/devicemapper/src/lib.rs
@@ -235,6 +235,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::loopdevice::LoopConfigOptions;
     use crypt::{CipherType, DmCryptTargetBuilder};
     use rdroidtest::{ignore_if, rdroidtest};
     use rustutils::system_properties;
@@ -328,10 +329,10 @@
             backing_file,
             0,
             sz,
-            /* direct_io */ true,
-            /* writable */ true,
+            &LoopConfigOptions { direct_io: true, writable: true, ..Default::default() },
         )
-        .unwrap();
+        .unwrap()
+        .path;
         let device_diff = device.to_owned() + "_diff";
 
         scopeguard::defer! {
@@ -372,10 +373,10 @@
             backing_file,
             0,
             sz,
-            /* direct_io */ true,
-            /* writable */ true,
+            &LoopConfigOptions { direct_io: true, writable: true, ..Default::default() },
         )
-        .unwrap();
+        .unwrap()
+        .path;
         let device_diff = device.to_owned() + "_diff";
         scopeguard::defer! {
             loopdevice::detach(&data_device).unwrap();
diff --git a/libs/devicemapper/src/loopdevice.rs b/libs/devicemapper/src/loopdevice.rs
index 130c1c4..b830eda 100644
--- a/libs/devicemapper/src/loopdevice.rs
+++ b/libs/devicemapper/src/loopdevice.rs
@@ -59,14 +59,31 @@
     Ok(unsafe { _loop_clr_fd(device_file.as_raw_fd()) }?)
 }
 
+/// LOOP_CONFIGURE ioctl operation flags.
+#[derive(Default)]
+pub struct LoopConfigOptions {
+    /// Whether to use direct I/O
+    pub direct_io: bool,
+    /// Whether the device is writable
+    pub writable: bool,
+    /// Whether to autodestruct the device on last close
+    pub autoclear: bool,
+}
+
+pub struct LoopDevice {
+    /// The loop device file
+    pub file: File,
+    /// Path to the loop device
+    pub path: PathBuf,
+}
+
 /// Creates a loop device and attach the given file at `path` as the backing store.
 pub fn attach<P: AsRef<Path>>(
     path: P,
     offset: u64,
     size_limit: u64,
-    direct_io: bool,
-    writable: bool,
-) -> Result<PathBuf> {
+    options: &LoopConfigOptions,
+) -> Result<LoopDevice> {
     // Attaching a file to a loop device can make a race condition; a loop device number obtained
     // from LOOP_CTL_GET_FREE might have been used by another thread or process. In that case the
     // subsequent LOOP_CONFIGURE ioctl returns with EBUSY. Try until it succeeds.
@@ -80,8 +97,8 @@
 
     let begin = Instant::now();
     loop {
-        match try_attach(&path, offset, size_limit, direct_io, writable) {
-            Ok(loop_dev) => return Ok(loop_dev),
+        match try_attach(&path, offset, size_limit, options) {
+            Ok(loop_device) => return Ok(loop_device),
             Err(e) => {
                 if begin.elapsed() > TIMEOUT {
                     return Err(e);
@@ -102,9 +119,8 @@
     path: P,
     offset: u64,
     size_limit: u64,
-    direct_io: bool,
-    writable: bool,
-) -> Result<PathBuf> {
+    options: &LoopConfigOptions,
+) -> Result<LoopDevice> {
     // Get a free loop device
     wait_for_path(LOOP_CONTROL)?;
     let ctrl_file = OpenOptions::new()
@@ -117,8 +133,8 @@
     // Construct the loop_info64 struct
     let backing_file = OpenOptions::new()
         .read(true)
-        .write(writable)
-        .custom_flags(if direct_io { O_DIRECT } else { 0 })
+        .write(options.writable)
+        .custom_flags(if options.direct_io { O_DIRECT } else { 0 })
         .open(&path)
         .context(format!("failed to open {:?}", path.as_ref()))?;
     let mut config = loop_config::new_zeroed();
@@ -127,14 +143,18 @@
     config.info.lo_offset = offset;
     config.info.lo_sizelimit = size_limit;
 
-    if !writable {
+    if !options.writable {
         config.info.lo_flags = Flag::LO_FLAGS_READ_ONLY;
     }
 
-    if direct_io {
+    if options.direct_io {
         config.info.lo_flags.insert(Flag::LO_FLAGS_DIRECT_IO);
     }
 
+    if options.autoclear {
+        config.info.lo_flags.insert(Flag::LO_FLAGS_AUTOCLEAR);
+    }
+
     // Configure the loop device to attach the backing file
     let device_path = format!("{}{}", LOOP_DEV_PREFIX, num);
     wait_for_path(&device_path)?;
@@ -146,7 +166,7 @@
     loop_configure(&device_file, &config)
         .context(format!("Failed to configure {:?}", &device_path))?;
 
-    Ok(PathBuf::from(device_path))
+    Ok(LoopDevice { file: device_file, path: PathBuf::from(device_path) })
 }
 
 /// Detaches backing file from the loop device `path`.
@@ -185,7 +205,10 @@
         let a_file = a_dir.path().join("test");
         let a_size = 4096u64;
         create_empty_file(&a_file, a_size);
-        let dev = attach(a_file, 0, a_size, /* direct_io */ true, /* writable */ false).unwrap();
+        let dev =
+            attach(a_file, 0, a_size, &LoopConfigOptions { direct_io: true, ..Default::default() })
+                .unwrap()
+                .path;
         scopeguard::defer! {
             detach(&dev).unwrap();
         }
@@ -198,7 +221,7 @@
         let a_file = a_dir.path().join("test");
         let a_size = 4096u64;
         create_empty_file(&a_file, a_size);
-        let dev = attach(a_file, 0, a_size, /* direct_io */ false, /* writable */ false).unwrap();
+        let dev = attach(a_file, 0, a_size, &LoopConfigOptions::default()).unwrap().path;
         scopeguard::defer! {
             detach(&dev).unwrap();
         }
@@ -211,11 +234,34 @@
         let a_file = a_dir.path().join("test");
         let a_size = 4096u64;
         create_empty_file(&a_file, a_size);
-        let dev = attach(a_file, 0, a_size, /* direct_io */ true, /* writable */ true).unwrap();
+        let dev = attach(
+            a_file,
+            0,
+            a_size,
+            &LoopConfigOptions { direct_io: true, writable: true, ..Default::default() },
+        )
+        .unwrap()
+        .path;
         scopeguard::defer! {
             detach(&dev).unwrap();
         }
         assert!(is_direct_io(&dev));
         assert!(is_direct_io_writable(&dev));
     }
+
+    #[rdroidtest]
+    fn attach_loop_device_autoclear() {
+        let a_dir = tempfile::TempDir::new().unwrap();
+        let a_file = a_dir.path().join("test");
+        let a_size = 4096u64;
+        create_empty_file(&a_file, a_size);
+        let dev =
+            attach(a_file, 0, a_size, &LoopConfigOptions { autoclear: true, ..Default::default() })
+                .unwrap();
+        drop(dev.file);
+
+        let dev_size_path =
+            Path::new("/sys/block").join(dev.path.file_name().unwrap()).join("size");
+        assert_eq!("0", fs::read_to_string(dev_size_path).unwrap().trim());
+    }
 }
diff --git a/libs/devicemapper/src/verity.rs b/libs/devicemapper/src/verity.rs
index 09087da..100320b 100644
--- a/libs/devicemapper/src/verity.rs
+++ b/libs/devicemapper/src/verity.rs
@@ -66,7 +66,7 @@
     }
 }
 
-impl<'a> Default for DmVerityTargetBuilder<'a> {
+impl Default for DmVerityTargetBuilder<'_> {
     fn default() -> Self {
         DmVerityTargetBuilder {
             version: DmVerityVersion::V1,
diff --git a/libs/dice/driver/src/lib.rs b/libs/dice/driver/src/lib.rs
index b5c1f12..245bf11 100644
--- a/libs/dice/driver/src/lib.rs
+++ b/libs/dice/driver/src/lib.rs
@@ -185,7 +185,6 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use core::ffi::CStr;
     use diced_open_dice::{
         hash, retry_bcc_format_config_descriptor, DiceConfigValues, HIDDEN_SIZE,
     };
@@ -233,10 +232,7 @@
 
         let dice = DiceDriver::from_file(&file_path)?;
 
-        let values = DiceConfigValues {
-            component_name: Some(CStr::from_bytes_with_nul(b"test\0")?),
-            ..Default::default()
-        };
+        let values = DiceConfigValues { component_name: Some(c"test"), ..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())?;
diff --git a/libs/dice/open_dice/src/bcc.rs b/libs/dice/open_dice/src/bcc.rs
index a3ddd76..1d9039d 100644
--- a/libs/dice/open_dice/src/bcc.rs
+++ b/libs/dice/open_dice/src/bcc.rs
@@ -172,7 +172,7 @@
     bcc: Option<&'a [u8]>,
 }
 
-impl<'a> DiceArtifacts for BccHandover<'a> {
+impl DiceArtifacts for BccHandover<'_> {
     fn cdi_attest(&self) -> &[u8; CDI_SIZE] {
         self.cdi_attest
     }
diff --git a/libs/dice/open_dice/src/error.rs b/libs/dice/open_dice/src/error.rs
index 9089432..c9eb5cc 100644
--- a/libs/dice/open_dice/src/error.rs
+++ b/libs/dice/open_dice/src/error.rs
@@ -31,6 +31,8 @@
     PlatformError,
     /// Unsupported key algorithm.
     UnsupportedKeyAlgorithm(coset::iana::Algorithm),
+    /// A failed fallible allocation. Used in no_std environments.
+    MemoryAllocationError,
 }
 
 /// This makes `DiceError` accepted by anyhow.
@@ -48,6 +50,7 @@
             Self::UnsupportedKeyAlgorithm(algorithm) => {
                 write!(f, "Unsupported key algorithm: {algorithm:?}")
             }
+            Self::MemoryAllocationError => write!(f, "Memory allocation failed"),
         }
     }
 }
diff --git a/libs/dice/open_dice/src/retry.rs b/libs/dice/open_dice/src/retry.rs
index 6e75e91..803673d 100644
--- a/libs/dice/open_dice/src/retry.rs
+++ b/libs/dice/open_dice/src/retry.rs
@@ -13,9 +13,9 @@
 // limitations under the License.
 
 //! This module implements a retry version for multiple DICE functions that
-//! require preallocated output buffer. As the retry functions require
-//! memory allocation on heap, currently we only expose these functions in
-//! std environment.
+//! require preallocated output buffer. When running without std the allocation
+//! of this buffer may fail and callers will see Error::MemoryAllocationError.
+//! When running with std, allocation may fail.
 
 use crate::bcc::{bcc_format_config_descriptor, bcc_main_flow, DiceConfigValues};
 use crate::dice::{
@@ -62,6 +62,9 @@
     let mut buffer = Vec::new();
     match f(&mut buffer) {
         Err(DiceError::BufferTooSmall(actual_size)) => {
+            #[cfg(not(feature = "std"))]
+            buffer.try_reserve_exact(actual_size).map_err(|_| DiceError::MemoryAllocationError)?;
+
             buffer.resize(actual_size, 0);
             f(&mut buffer)?;
         }
diff --git a/libs/dice/sample_inputs/src/sample_inputs.rs b/libs/dice/sample_inputs/src/sample_inputs.rs
index c323bc4..adca46b 100644
--- a/libs/dice/sample_inputs/src/sample_inputs.rs
+++ b/libs/dice/sample_inputs/src/sample_inputs.rs
@@ -18,7 +18,6 @@
 use alloc::vec;
 use alloc::vec::Vec;
 use ciborium::{de, ser, value::Value};
-use core::ffi::CStr;
 use coset::{iana, Algorithm, AsCborValue, CoseKey, KeyOperation, KeyType, Label};
 use diced_open_dice::{
     derive_cdi_private_key_seed, keypair_from_seed, retry_bcc_format_config_descriptor,
@@ -115,7 +114,7 @@
 
     // Gets the ABL certificate to as the root certificate of DICE chain.
     let config_values = DiceConfigValues {
-        component_name: Some(CStr::from_bytes_with_nul(b"ABL\0").unwrap()),
+        component_name: Some(c"ABL"),
         component_version: Some(1),
         resettable: true,
         security_version: Some(10),
@@ -148,7 +147,7 @@
 
     // Appends AVB certificate to DICE chain.
     let config_values = DiceConfigValues {
-        component_name: Some(CStr::from_bytes_with_nul(b"AVB\0").unwrap()),
+        component_name: Some(c"AVB"),
         component_version: Some(1),
         resettable: true,
         security_version: Some(11),
@@ -173,7 +172,7 @@
 
     // Appends Android certificate to DICE chain.
     let config_values = DiceConfigValues {
-        component_name: Some(CStr::from_bytes_with_nul(b"Android\0").unwrap()),
+        component_name: Some(c"Android"),
         component_version: Some(12),
         resettable: true,
         security_version: Some(12),
diff --git a/libs/libfdt/src/iterators.rs b/libs/libfdt/src/iterators.rs
index 743c52b..1c66e4d 100644
--- a/libs/libfdt/src/iterators.rs
+++ b/libs/libfdt/src/iterators.rs
@@ -66,7 +66,7 @@
     }
 }
 
-impl<'a> Iterator for CellIterator<'a> {
+impl Iterator for CellIterator<'_> {
     type Item = u32;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -118,7 +118,7 @@
     }
 }
 
-impl<'a> Iterator for RegIterator<'a> {
+impl Iterator for RegIterator<'_> {
     type Item = Reg<u64>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -161,7 +161,7 @@
     }
 }
 
-impl<'a> Iterator for MemRegIterator<'a> {
+impl Iterator for MemRegIterator<'_> {
     type Item = Range<usize>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -215,8 +215,8 @@
     }
 }
 
-impl<'a, A: FromAddrCells, P: FromAddrCells, S: FromSizeCells> Iterator
-    for RangesIterator<'a, A, P, S>
+impl<A: FromAddrCells, P: FromAddrCells, S: FromSizeCells> Iterator
+    for RangesIterator<'_, A, P, S>
 {
     type Item = AddressRange<A, P, S>;
 
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 0dcd31a..47f4817 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -344,7 +344,7 @@
     }
 }
 
-impl<'a> PartialEq for FdtNode<'a> {
+impl PartialEq for FdtNode<'_> {
     fn eq(&self, other: &Self) -> bool {
         self.fdt.as_ptr() == other.fdt.as_ptr() && self.offset == other.offset
     }
diff --git a/libs/libservice_vm_requests/src/cert.rs b/libs/libservice_vm_requests/src/cert.rs
index e31d870..de5ae1a 100644
--- a/libs/libservice_vm_requests/src/cert.rs
+++ b/libs/libservice_vm_requests/src/cert.rs
@@ -58,7 +58,7 @@
     vm_components: Vec<VmComponent<'a>>,
 }
 
-impl<'a> AssociatedOid for AttestationExtension<'a> {
+impl AssociatedOid for AttestationExtension<'_> {
     const OID: ObjectIdentifier = AVF_ATTESTATION_EXTENSION_V1;
 }
 
diff --git a/libs/libvm_payload/src/lib.rs b/libs/libvm_payload/src/lib.rs
index cbadec2..14aff99 100644
--- a/libs/libvm_payload/src/lib.rs
+++ b/libs/libvm_payload/src/lib.rs
@@ -27,7 +27,7 @@
 use rpcbinder::{RpcServer, RpcSession};
 use openssl::{ec::EcKey, sha::sha256, ecdsa::EcdsaSig};
 use std::convert::Infallible;
-use std::ffi::{CString, CStr};
+use std::ffi::CString;
 use std::fmt::Debug;
 use std::os::raw::{c_char, c_void};
 use std::path::Path;
@@ -376,20 +376,16 @@
 #[no_mangle]
 pub extern "C" fn AVmAttestationStatus_toString(status: AVmAttestationStatus) -> *const c_char {
     let message = match status {
-        AVmAttestationStatus::ATTESTATION_OK => {
-            CStr::from_bytes_with_nul(b"The remote attestation completes successfully.\0").unwrap()
-        }
+        AVmAttestationStatus::ATTESTATION_OK => c"The remote attestation completes successfully.",
         AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
-            CStr::from_bytes_with_nul(b"The challenge size is not between 0 and 64.\0").unwrap()
+            c"The challenge size is not between 0 and 64."
         }
         AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
-            CStr::from_bytes_with_nul(b"Failed to attest the VM. Please retry at a later time.\0")
-                .unwrap()
+            c"Failed to attest the VM. Please retry at a later time."
         }
-        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
-            b"Remote attestation is not supported in the current environment.\0",
-        )
-        .unwrap(),
+        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+            c"Remote attestation is not supported in the current environment."
+        }
     };
     message.as_ptr()
 }
diff --git a/libs/libvm_payload/wrapper/attestation.rs b/libs/libvm_payload/wrapper/attestation.rs
index e0055d5..69fef4f 100644
--- a/libs/libvm_payload/wrapper/attestation.rs
+++ b/libs/libvm_payload/wrapper/attestation.rs
@@ -265,7 +265,7 @@
     current: usize, // Invariant: current <= count
 }
 
-impl<'a> Iterator for CertIterator<'a> {
+impl Iterator for CertIterator<'_> {
     type Item = Vec<u8>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -284,5 +284,5 @@
     }
 }
 
-impl<'a> ExactSizeIterator for CertIterator<'a> {}
-impl<'a> FusedIterator for CertIterator<'a> {}
+impl ExactSizeIterator for CertIterator<'_> {}
+impl FusedIterator for CertIterator<'_> {}
diff --git a/libs/libvmbase/src/bionic.rs b/libs/libvmbase/src/bionic.rs
index 37b6e45..2b59493 100644
--- a/libs/libvmbase/src/bionic.rs
+++ b/libs/libvmbase/src/bionic.rs
@@ -20,7 +20,6 @@
 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;
 
@@ -71,11 +70,10 @@
 pub static mut ERRNO: c_int = 0;
 
 #[no_mangle]
-#[allow(unused_unsafe)]
+// SAFETY: C functions which call this are only called from the main thread, not from exception
+// handlers.
 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 { addr_of_mut!(ERRNO) as *mut _ }
+    (&raw mut ERRNO).cast()
 }
 
 fn set_errno(value: c_int) {
@@ -88,15 +86,20 @@
     unsafe { ERRNO }
 }
 
+/// # Safety
+///
+/// `buffer` must point to an allocation of at least `length` bytes which is valid to write to and
+/// has no concurrent access while this function is running.
 #[no_mangle]
-extern "C" fn getentropy(buffer: *mut c_void, length: usize) -> c_int {
+unsafe extern "C" fn getentropy(buffer: *mut c_void, length: usize) -> c_int {
     if length > 256 {
         // The maximum permitted value for the length argument is 256.
         set_errno(EIO);
         return -1;
     }
 
-    // SAFETY: Just like libc, we need to assume that `ptr` is valid.
+    // SAFETY: The caller promised that `buffer` is a valid pointer to at least `length` bytes with
+    // no concurrent access.
     let buffer = unsafe { slice::from_raw_parts_mut(buffer.cast::<u8>(), length) };
     fill_with_entropy(buffer).unwrap();
 
@@ -169,9 +172,13 @@
 #[no_mangle]
 static stderr: CFilePtr = CFilePtr::Stderr;
 
+/// # Safety
+///
+/// `c_str` must be a valid pointer to a NUL-terminated string which is not modified before this
+/// function returns.
 #[no_mangle]
-extern "C" fn fputs(c_str: *const c_char, stream: usize) -> c_int {
-    // SAFETY: Just like libc, we need to assume that `s` is a valid NULL-terminated string.
+unsafe extern "C" fn fputs(c_str: *const c_char, stream: usize) -> c_int {
+    // SAFETY: The caller promised that `c_str` is a valid NUL-terminated string.
     let c_str = unsafe { CStr::from_ptr(c_str) };
 
     if let (Ok(s), Ok(f)) = (c_str.to_str(), CFilePtr::try_from(stream)) {
@@ -183,11 +190,16 @@
     }
 }
 
+/// # Safety
+///
+/// `ptr` must be a valid pointer to an array of at least `size * nmemb` initialised bytes, which
+/// are not modified before this function returns.
 #[no_mangle]
-extern "C" fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: usize) -> usize {
+unsafe extern "C" fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: usize) -> usize {
     let length = size.saturating_mul(nmemb);
 
-    // SAFETY: Just like libc, we need to assume that `ptr` is valid.
+    // SAFETY: The caller promised that `ptr` is a valid pointer to at least `size * nmemb`
+    // initialised bytes, and `length` is no more than that.
     let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, length) };
 
     if let (Ok(s), Ok(f)) = (str::from_utf8(bytes), CFilePtr::try_from(stream)) {
@@ -203,12 +215,16 @@
     cstr_error(n).as_ptr().cast_mut().cast()
 }
 
+/// # Safety
+///
+/// `s` must be a valid pointer to a NUL-terminated string which is not modified before this
+/// function returns.
 #[no_mangle]
-extern "C" fn perror(s: *const c_char) {
+unsafe extern "C" fn perror(s: *const c_char) {
     let prefix = if s.is_null() {
         None
     } else {
-        // SAFETY: Just like libc, we need to assume that `s` is a valid NULL-terminated string.
+        // SAFETY: The caller promised that `s` is a valid NUL-terminated string.
         let c_str = unsafe { CStr::from_ptr(s) };
         if c_str.is_empty() {
             None
diff --git a/libs/libvmbase/src/layout.rs b/libs/libvmbase/src/layout.rs
index cf3a8fc..4c45eb2 100644
--- a/libs/libvmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -22,7 +22,6 @@
 use crate::memory::{max_stack_size, page_4kb_of, PAGE_SIZE};
 use aarch64_paging::paging::VirtualAddress;
 use core::ops::Range;
-use core::ptr::addr_of;
 use static_assertions::const_assert_eq;
 
 /// First address that can't be translated by a level 1 TTBR0_EL1.
@@ -44,9 +43,7 @@
 #[macro_export]
 macro_rules! linker_addr {
     ($symbol:ident) => {{
-        // SAFETY: We're just getting the address of an extern static symbol provided by the linker,
-        // not dereferencing it.
-        let addr = unsafe { addr_of!($crate::linker::$symbol) as usize };
+        let addr = (&raw const $crate::linker::$symbol) as usize;
         VirtualAddress(addr)
     }};
 }
@@ -132,5 +129,5 @@
     // SAFETY: __stack_chk_guard shouldn't have any mutable aliases unless the stack overflows. If
     // it does, then there could be undefined behaviour all over the program, but we want to at
     // least have a chance at catching it.
-    unsafe { addr_of!(__stack_chk_guard).read_volatile() }
+    unsafe { (&raw const __stack_chk_guard).read_volatile() }
 }
diff --git a/libs/statslog_virtualization/Android.bp b/libs/statslog_virtualization/Android.bp
index 2860e6c..f33a147 100644
--- a/libs/statslog_virtualization/Android.bp
+++ b/libs/statslog_virtualization/Android.bp
@@ -72,4 +72,7 @@
     rustlibs: [
         "libstatslog_virtualization_rust_header",
     ],
+    flags: [
+        "-A clippy::needless-lifetimes",
+    ],
 }
diff --git a/tests/aidl/Android.bp b/tests/aidl/Android.bp
index ed4e8ff..63db488 100644
--- a/tests/aidl/Android.bp
+++ b/tests/aidl/Android.bp
@@ -17,5 +17,8 @@
         cpp: {
             enabled: true,
         },
+        ndk: {
+            min_sdk_version: "UpsideDownCake",
+        },
     },
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 4f9806a..e8673ce 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -101,6 +101,9 @@
     private static final String INSTANCE_IMG = TEST_ROOT + "instance.img";
     private static final String INSTANCE_ID_FILE = TEST_ROOT + "instance_id";
 
+    private static final String DEBUG_LEVEL_FULL = "full --enable-earlycon";
+    private static final String DEBUG_LEVEL_NONE = "none";
+
     private static final int MIN_MEM_ARM64 = 170;
     private static final int MIN_MEM_X86_64 = 196;
 
@@ -465,7 +468,7 @@
         try {
             microdroid =
                     MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                            .debugLevel("full")
+                            .debugLevel(DEBUG_LEVEL_FULL)
                             .memoryMib(minMemorySize())
                             .cpuTopology("match_host")
                             .protectedVm(true)
@@ -495,7 +498,7 @@
         // Act
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(true)
@@ -644,7 +647,7 @@
 
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -751,7 +754,7 @@
                                 VIRT_APEX + "bin/vm",
                                 "run-app",
                                 "--debug",
-                                debuggable ? "full" : "none",
+                                debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE,
                                 apkPath,
                                 idsigPath,
                                 instanceImgPath));
@@ -871,7 +874,7 @@
         final String configPath = "assets/vm_config_apex.json"; // path inside the APK
         ITestDevice microdroid =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -1023,7 +1026,7 @@
         final String configPath = "assets/vm_config.json"; // path inside the APK
         testMicrodroidBootsWithBuilder(
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -1061,7 +1064,7 @@
         final String configPath = "assets/vm_config.json";
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -1175,7 +1178,7 @@
                                 "shell",
                                 VIRT_APEX + "bin/vm",
                                 "run-app",
-                                "--debug full",
+                                "--debug " + DEBUG_LEVEL_FULL,
                                 "--console " + CONSOLE_PATH,
                                 "--payload-binary-name",
                                 "MicrodroidEmptyPayloadJniLib.so",
@@ -1357,7 +1360,7 @@
 
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -1403,7 +1406,7 @@
         final String configPath = "assets/vm_config.json";
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
@@ -1434,7 +1437,7 @@
         // Start the VM with the dump DT option.
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(mem_size)
                         .cpuTopology("one_cpu")
                         .protectedVm(false)
@@ -1464,7 +1467,7 @@
         // Start the VM with the dump DT option.
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
+                        .debugLevel(DEBUG_LEVEL_FULL)
                         .memoryMib(mem_size)
                         .cpuTopology("one_cpu")
                         .protectedVm(true)
diff --git a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts
index 095eb54..de9f7c5 100644
--- a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts
+++ b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_golden.dts
@@ -49,7 +49,7 @@
         };
 
         chosen {
-                bootargs = "panic=-1 crashkernel=17M";
+                bootargs = "panic=-1 crashkernel=17M earlycon=uart8250,mmio,0x3f8 keep_bootcon";
                 kaslr-seed = <>;
                 linux,initrd-end = <0x81200360>;
                 linux,initrd-start = <0x81000000>;
diff --git a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts
index f2ebdf9..f09e4ff 100644
--- a/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts
+++ b/tests/hostside/java/com/android/microdroid/test/goldens/dt_dump_protected_golden.dts
@@ -49,7 +49,7 @@
         };
 
         chosen {
-                bootargs = "panic=-1 crashkernel=31M";
+                bootargs = "panic=-1 crashkernel=31M earlycon=uart8250,mmio,0x3f8 keep_bootcon";
                 kaslr-seed = <>;
                 linux,initrd-end = <0x81202104>;
                 linux,initrd-start = <0x81000000>;
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index d0e045b..99300e2 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -173,6 +173,8 @@
         "liblog",
         "libprotobuf-cpp-lite-ndk",
     ],
+    // We've added support for updatable payloads in Android U.
+    min_sdk_version: "UpsideDownCake",
 }
 
 cc_library_shared {
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 8502ec3..a2b4747 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1874,22 +1874,13 @@
         return false;
     }
 
-    private void ensureUpdatableVmSupported() throws Exception {
-        if (getVendorApiLevel() >= 202504 && deviceCapableOfProtectedVm()) {
-            assertTrue(
-                    "Missing Updatable VM support, have you declared Secretkeeper interface?",
-                    isUpdatableVmSupported());
-        } else {
-            assumeTrue("Device does not support Updatable VM", isUpdatableVmSupported());
-        }
-    }
-
     @Test
     public void rollbackProtectedDataOfPayload() throws Exception {
         assumeSupportedDevice();
         // Rollback protected data is only possible if Updatable VMs is supported -
         // which implies Secretkeeper support.
-        ensureUpdatableVmSupported();
+        assumeTrue("Missing Updatable VM support", isUpdatableVmSupported());
+
         byte[] value1 = new byte[32];
         Arrays.fill(value1, (byte) 0xcc);
         byte[] value2 = new byte[32];
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 06c7e9d..2ab73a4 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -348,25 +348,40 @@
         }
 
         ScopedAStatus insecurelyReadPayloadRpData(std::array<uint8_t, 32>* out) override {
-            int32_t ret = AVmPayload_readRollbackProtectedSecret(out->data(), 32);
-            if (ret != 32) {
-                return ScopedAStatus::fromServiceSpecificError(ret);
+            if (__builtin_available(android 36, *)) {
+                int32_t ret = AVmPayload_readRollbackProtectedSecret(out->data(), 32);
+                if (ret != 32) {
+                    return ScopedAStatus::fromServiceSpecificError(ret);
+                }
+                return ScopedAStatus::ok();
+            } else {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "not available before SDK 36");
             }
-            return ScopedAStatus::ok();
         }
 
         ScopedAStatus insecurelyWritePayloadRpData(
                 const std::array<uint8_t, 32>& inputData) override {
-            int32_t ret = AVmPayload_writeRollbackProtectedSecret(inputData.data(), 32);
-            if (ret != 32) {
-                return ScopedAStatus::fromServiceSpecificError(ret);
+            if (__builtin_available(android 36, *)) {
+                int32_t ret = AVmPayload_writeRollbackProtectedSecret(inputData.data(), 32);
+                if (ret != 32) {
+                    return ScopedAStatus::fromServiceSpecificError(ret);
+                }
+                return ScopedAStatus::ok();
+            } else {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "not available before SDK 36");
             }
-            return ScopedAStatus::ok();
         }
 
         ScopedAStatus isNewInstance(bool* is_new_instance_out) override {
-            *is_new_instance_out = AVmPayload_isNewInstance();
-            return ScopedAStatus::ok();
+            if (__builtin_available(android 36, *)) {
+                *is_new_instance_out = AVmPayload_isNewInstance();
+                return ScopedAStatus::ok();
+            } else {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "not available before SDK 36");
+            }
         }
 
         ScopedAStatus quit() override { exit(0); }
diff --git a/tests/vmshareapp/Android.bp b/tests/vmshareapp/Android.bp
index 5f6dc57..86c48f6 100644
--- a/tests/vmshareapp/Android.bp
+++ b/tests/vmshareapp/Android.bp
@@ -13,4 +13,8 @@
         "MicrodroidPayloadInOtherAppNativeLib",
     ],
     min_sdk_version: "34",
+    sdk_version: "system_current",
+    // Ideally we should set something "latest finalized sdk version" here.
+    // However, soong doesn't seem to provide such functionality.
+    target_sdk_version: "VanillaIceCream",
 }
diff --git a/tests/vts/src/vts_libavf_test.rs b/tests/vts/src/vts_libavf_test.rs
index 8e3c5b5..c59271a 100644
--- a/tests/vts/src/vts_libavf_test.rs
+++ b/tests/vts/src/vts_libavf_test.rs
@@ -16,7 +16,6 @@
 
 use anyhow::{bail, ensure, Context, Result};
 use log::info;
-use std::ffi::CStr;
 use std::fs::File;
 use std::io::{self, BufWriter, Write};
 use std::os::fd::IntoRawFd;
@@ -108,10 +107,7 @@
 
     // SAFETY: config is the only reference to a valid object
     unsafe {
-        AVirtualMachineRawConfig_setName(
-            config,
-            CStr::from_bytes_with_nul(b"vts_libavf_test_rialto\0").unwrap().as_ptr(),
-        );
+        AVirtualMachineRawConfig_setName(config, c"vts_libavf_test_rialto".as_ptr());
         AVirtualMachineRawConfig_setKernel(config, kernel_fd);
         AVirtualMachineRawConfig_setProtectedVm(config, protected_vm);
         AVirtualMachineRawConfig_setMemoryMiB(config, VM_MEMORY_MB);