Merge "Define multialg variants of the libdiced_open_dice targets." into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index c427337..efe651e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,21 +18,12 @@
 import android.app.Application as AndroidApplication
 import android.app.NotificationChannel
 import android.app.NotificationManager
-import android.content.ComponentName
 import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.IBinder
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ProcessLifecycleOwner
 
 public class Application : AndroidApplication() {
     override fun onCreate() {
         super.onCreate()
         setupNotificationChannels()
-        val lifecycleObserver = ApplicationLifecycleObserver()
-        ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
     }
 
     private fun setupNotificationChannels() {
@@ -61,45 +52,4 @@
 
         fun getInstance(c: Context): Application = c.getApplicationContext() as Application
     }
-
-    /**
-     * Observes application lifecycle events and interacts with the VmLauncherService to manage
-     * virtual machine state based on application lifecycle transitions. This class binds to the
-     * VmLauncherService and notifies it of application lifecycle events (onStart, onStop), allowing
-     * the service to manage the VM accordingly.
-     */
-    inner class ApplicationLifecycleObserver() : DefaultLifecycleObserver {
-        private var vmLauncherService: VmLauncherService? = null
-        private val connection =
-            object : ServiceConnection {
-                override fun onServiceConnected(className: ComponentName, service: IBinder) {
-                    val binder = service as VmLauncherService.VmLauncherServiceBinder
-                    vmLauncherService = binder.getService()
-                }
-
-                override fun onServiceDisconnected(arg0: ComponentName) {
-                    vmLauncherService = null
-                }
-            }
-
-        override fun onCreate(owner: LifecycleOwner) {
-            super.onCreate(owner)
-            bindToVmLauncherService()
-        }
-
-        override fun onStart(owner: LifecycleOwner) {
-            super.onStart(owner)
-            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_START)
-        }
-
-        override fun onStop(owner: LifecycleOwner) {
-            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_STOP)
-            super.onStop(owner)
-        }
-
-        fun bindToVmLauncherService() {
-            val intent = Intent(this@Application, VmLauncherService::class.java)
-            this@Application.bindService(intent, connection, 0) // No BIND_AUTO_CREATE
-        }
-    }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
deleted file mode 100644
index 4e26c3c..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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
-
-enum class ApplicationLifeCycleEvent {
-    APP_ON_START,
-    APP_ON_STOP,
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
index 887ae02..e035ad4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
@@ -28,6 +28,7 @@
 import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse
 import com.android.virtualization.terminal.proto.ShutdownQueueOpeningRequest
 import com.android.virtualization.terminal.proto.ShutdownRequestItem
+import io.grpc.stub.ServerCallStreamObserver
 import io.grpc.stub.StreamObserver
 
 internal class DebianServiceImpl(context: Context) : DebianServiceImplBase() {
@@ -79,8 +80,15 @@
         request: ShutdownQueueOpeningRequest?,
         responseObserver: StreamObserver<ShutdownRequestItem?>,
     ) {
+        val serverCallStreamObserver = responseObserver as ServerCallStreamObserver<ShutdownRequestItem?>
+        serverCallStreamObserver.setOnCancelHandler {
+            shutdownRunnable = null
+        }
         Log.d(TAG, "openShutdownRequestQueue")
         shutdownRunnable = Runnable {
+            if (serverCallStreamObserver.isCancelled()) {
+                return@Runnable
+            }
             responseObserver.onNext(ShutdownRequestItem.newBuilder().build())
             responseObserver.onCompleted()
             shutdownRunnable = null
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
index be1f922..54754ff 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
@@ -146,8 +146,8 @@
     companion object {
         private const val DIR_IN_SDCARD = "linux"
         private const val ARCHIVE_NAME = "images.tar.gz"
-        private const val BUILD_TAG = "latest" // TODO: use actual tag name
-        private const val HOST_URL = "https://dl.google.com/android/ferrochrome/$BUILD_TAG"
+        private val BUILD_TAG = Integer.toString(Build.VERSION.SDK_INT_FULL)
+        private val HOST_URL = "https://dl.google.com/android/ferrochrome/$BUILD_TAG"
 
         fun getSdcardPathForTesting(): Path {
             return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 662fef5..35c5570 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -29,7 +29,6 @@
 import android.os.ConditionVariable
 import android.os.Environment
 import android.os.SystemProperties
-import android.os.Trace
 import android.provider.Settings
 import android.util.DisplayMetrics
 import android.util.Log
@@ -328,7 +327,7 @@
 
         val stopIntent = Intent()
         stopIntent.setClass(this, VmLauncherService::class.java)
-        stopIntent.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE)
+        stopIntent.setAction(VmLauncherService.ACTION_SHUTDOWN_VM)
         val stopPendingIntent =
             PendingIntent.getService(
                 this,
@@ -363,7 +362,6 @@
                 )
                 .build()
 
-        Trace.beginAsyncSection("executeTerminal", 0)
         run(this, this, notification, getDisplayInfo())
     }
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
new file mode 100644
index 0000000..7647d9b
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.system.virtualmachine.VirtualMachine
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * MemBalloonController is responsible for adjusting the memory ballon size of a VM depending on
+ * whether the app is visible or running in the background
+ */
+class MemBalloonController(val context: Context, val vm: VirtualMachine) {
+    companion object {
+        private const val INITIAL_PERCENT = 10
+        private const val MAX_PERCENT = 50
+        private const val INFLATION_STEP_PERCENT = 5
+        private const val INFLATION_PERIOD_SEC = 60L
+
+        private val mainHandler = Handler(Looper.getMainLooper())
+
+        private fun runOnMainThread(runnable: Runnable) {
+            mainHandler.post(runnable)
+        }
+    }
+
+    private val executor =
+        Executors.newSingleThreadScheduledExecutor(
+            TerminalThreadFactory(context.getApplicationContext())
+        )
+
+    private val observer =
+        object : DefaultLifecycleObserver {
+
+            // If the app is started or resumed, give deflate the balloon to 0 to give maximum
+            // available memory to the virtual machine
+            override fun onResume(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                executor.submit({
+                    Log.v(TAG, "app resumed. deflating mem balloon to the minimum")
+                    vm.setMemoryBalloonByPercent(0)
+                })
+            }
+
+            // If the app goes into background, progressively inflate the balloon from
+            // INITIAL_PERCENT until it reaches MAX_PERCENT
+            override fun onStop(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                balloonPercent = INITIAL_PERCENT
+                ongoingInflation =
+                    executor.scheduleAtFixedRate(
+                        {
+                            if (balloonPercent <= MAX_PERCENT) {
+                                Log.v(TAG, "inflating mem balloon to ${balloonPercent} %")
+                                vm.setMemoryBalloonByPercent(balloonPercent)
+                                balloonPercent += INFLATION_STEP_PERCENT
+                            } else {
+                                Log.v(TAG, "mem balloon is inflated to its max (${MAX_PERCENT} %)")
+                                ongoingInflation!!.cancel(false)
+                            }
+                        },
+                        0 /* initialDelay */,
+                        INFLATION_PERIOD_SEC,
+                        TimeUnit.SECONDS,
+                    )
+            }
+        }
+
+    private var balloonPercent = 0
+    private var ongoingInflation: ScheduledFuture<*>? = null
+
+    fun start() {
+        // addObserver is @MainThread
+        runOnMainThread({ ProcessLifecycleOwner.get().lifecycle.addObserver(observer) })
+    }
+
+    fun stop() {
+        // removeObserver is @MainThread
+        runOnMainThread({
+            ProcessLifecycleOwner.get().lifecycle.removeObserver(observer)
+            executor.shutdown()
+        })
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
index 5c01ead..7e78235 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalTabFragment.kt
@@ -19,7 +19,6 @@
 import android.graphics.Bitmap
 import android.net.http.SslError
 import android.os.Bundle
-import android.os.Trace
 import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
@@ -145,7 +144,6 @@
                 object : WebView.VisualStateCallback() {
                     override fun onComplete(completedRequestId: Long) {
                         if (completedRequestId == requestId) {
-                            Trace.endAsyncSection("executeTerminal", 0)
                             bootProgressView.visibility = View.GONE
                             terminalView.visibility = View.VISIBLE
                             terminalView.mapTouchToMouseEvent()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 94b7011..1857175 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -31,14 +31,13 @@
 import android.os.Parcel
 import android.os.Parcelable
 import android.os.ResultReceiver
-import android.os.Trace
+import android.os.SystemProperties
 import android.system.virtualmachine.VirtualMachine
 import android.system.virtualmachine.VirtualMachineCustomImageConfig
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig
 import android.system.virtualmachine.VirtualMachineException
 import android.util.Log
 import android.widget.Toast
-import com.android.internal.annotations.GuardedBy
 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
 import com.android.virtualization.terminal.Runner.Companion.create
@@ -55,7 +54,6 @@
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
-import java.lang.Math.min
 import java.lang.RuntimeException
 import java.net.InetSocketAddress
 import java.net.SocketAddress
@@ -66,55 +64,14 @@
 import java.util.concurrent.TimeUnit
 
 class VmLauncherService : Service() {
-    inner class VmLauncherServiceBinder : android.os.Binder() {
-        fun getService(): VmLauncherService = this@VmLauncherService
-    }
-
-    private val binder = VmLauncherServiceBinder()
+    private lateinit var executorService: ExecutorService
 
     // TODO: using lateinit for some fields to avoid null
-    private var executorService: ExecutorService? = null
     private var virtualMachine: VirtualMachine? = null
     private var resultReceiver: ResultReceiver? = null
     private var server: Server? = null
     private var debianService: DebianServiceImpl? = null
     private var portNotifier: PortNotifier? = null
-    private var mLock = Object()
-    @GuardedBy("mLock") private var currentMemBalloonPercent = 0
-
-    @GuardedBy("mLock") private val inflateMemBalloonHandler = Handler(Looper.getMainLooper())
-    private val inflateMemBalloonTask: Runnable =
-        object : Runnable {
-            override fun run() {
-                synchronized(mLock) {
-                    if (
-                        currentMemBalloonPercent < INITIAL_MEM_BALLOON_PERCENT ||
-                            currentMemBalloonPercent > MAX_MEM_BALLOON_PERCENT
-                    ) {
-                        Log.e(
-                            TAG,
-                            "currentBalloonPercent=$currentMemBalloonPercent is invalid," +
-                                " should be in range: " +
-                                "$INITIAL_MEM_BALLOON_PERCENT~$MAX_MEM_BALLOON_PERCENT",
-                        )
-                        return
-                    }
-                    // Increases the balloon size by MEM_BALLOON_PERCENT_STEP% every time
-                    if (currentMemBalloonPercent < MAX_MEM_BALLOON_PERCENT) {
-                        currentMemBalloonPercent =
-                            min(
-                                MAX_MEM_BALLOON_PERCENT,
-                                currentMemBalloonPercent + MEM_BALLOON_PERCENT_STEP,
-                            )
-                        virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                        inflateMemBalloonHandler.postDelayed(
-                            this,
-                            MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                        )
-                    }
-                }
-            }
-        }
 
     interface VmLauncherServiceCallback {
         fun onVmStart()
@@ -127,49 +84,16 @@
     }
 
     override fun onBind(intent: Intent?): IBinder? {
-        return binder
+        return null
     }
 
-    /**
-     * Processes application lifecycle events and adjusts the virtual machine's memory balloon
-     * accordingly.
-     *
-     * @param event The application lifecycle event.
-     */
-    fun processAppLifeCycleEvent(event: ApplicationLifeCycleEvent) {
-        when (event) {
-            // When the app starts, reset the memory balloon to 0%.
-            // This gives the app maximum available memory.
-            ApplicationLifeCycleEvent.APP_ON_START -> {
-                synchronized(mLock) {
-                    inflateMemBalloonHandler.removeCallbacks(inflateMemBalloonTask)
-                    currentMemBalloonPercent = 0
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                }
-            }
-            ApplicationLifeCycleEvent.APP_ON_STOP -> {
-                // When the app stops, inflate the memory balloon to INITIAL_MEM_BALLOON_PERCENT.
-                // Inflate the balloon by MEM_BALLOON_PERCENT_STEP every
-                // MEM_BALLOON_INFLATE_INTERVAL_MILLIS milliseconds until reaching
-                // MAX_MEM_BALLOON_PERCENT of total memory. This allows the system to reclaim
-                // memory while the app is in the background.
-                synchronized(mLock) {
-                    currentMemBalloonPercent = INITIAL_MEM_BALLOON_PERCENT
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                    inflateMemBalloonHandler.postDelayed(
-                        inflateMemBalloonTask,
-                        MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                    )
-                }
-            }
-            else -> {
-                Log.e(TAG, "unrecognized lifecycle event: $event")
-            }
-        }
+    override fun onCreate() {
+        super.onCreate()
+        executorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
     }
 
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
-        if (intent.action == ACTION_STOP_VM_LAUNCHER_SERVICE) {
+        if (intent.action == ACTION_SHUTDOWN_VM) {
             if (debianService != null && debianService!!.shutdownDebian()) {
                 // During shutdown, change the notification content to indicate that it's closing
                 val notification = createNotificationForTerminalClose()
@@ -185,7 +109,6 @@
             Log.d(TAG, "VM instance is already started")
             return START_NOT_STICKY
         }
-        executorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
 
         val image = InstalledImage.getDefault(this)
         val json = ConfigJson.from(this, image.configPath)
@@ -201,15 +124,12 @@
         }
         val config = configBuilder.build()
 
-        Trace.beginSection("vmCreate")
         val runner: Runner =
             try {
                 create(this, config)
             } catch (e: VirtualMachineException) {
                 throw RuntimeException("cannot create runner", e)
             }
-        Trace.endSection()
-        Trace.beginAsyncSection("debianBoot", 0)
 
         virtualMachine = runner.vm
         resultReceiver =
@@ -218,12 +138,16 @@
                 ResultReceiver::class.java,
             )
 
+        val mbc = MemBalloonController(this, virtualMachine!!)
+        mbc.start()
+
         runner.exitStatus.thenAcceptAsync { success: Boolean ->
+            mbc.stop()
             resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
         val logDir = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
-        Logger.setup(virtualMachine!!, logDir, executorService!!)
+        Logger.setup(virtualMachine!!, logDir, executorService)
 
         val notification =
             intent.getParcelableExtra<Notification?>(EXTRA_NOTIFICATION, Notification::class.java)
@@ -300,7 +224,7 @@
     private fun createNotificationForTerminalClose(): Notification {
         val stopIntent = Intent()
         stopIntent.setClass(this, VmLauncherService::class.java)
-        stopIntent.setAction(ACTION_STOP_VM_LAUNCHER_SERVICE)
+        stopIntent.setAction(ACTION_SHUTDOWN_VM)
         val stopPendingIntent =
             PendingIntent.getService(
                 this,
@@ -423,7 +347,7 @@
             return
         }
 
-        executorService!!.execute(
+        executorService.execute(
             Runnable {
                 // TODO(b/373533555): we can use mDNS for that.
                 val debianServicePortFile = File(filesDir, "debian_service_port")
@@ -451,10 +375,9 @@
                     Log.e(TAG, "failed to stop a VM instance", e)
                 }
             }
-            executorService?.shutdownNow()
-            executorService = null
             virtualMachine = null
         }
+        executorService.shutdownNow()
         super.onDestroy()
     }
 
@@ -468,8 +391,7 @@
         private const val ACTION_START_VM_LAUNCHER_SERVICE =
             "android.virtualization.START_VM_LAUNCHER_SERVICE"
         const val EXTRA_DISPLAY_INFO = "EXTRA_DISPLAY_INFO"
-        const val ACTION_STOP_VM_LAUNCHER_SERVICE: String =
-            "android.virtualization.STOP_VM_LAUNCHER_SERVICE"
+        const val ACTION_SHUTDOWN_VM: String = "android.virtualization.ACTION_SHUTDOWN_VM"
 
         private const val RESULT_START = 0
         private const val RESULT_STOP = 1
@@ -479,12 +401,18 @@
         private const val KEY_TERMINAL_IPADDRESS = "address"
         private const val KEY_TERMINAL_PORT = "port"
 
-        private const val VM_BOOT_TIMEOUT_SECONDS = 20
+        private val VM_BOOT_TIMEOUT_SECONDS: Int =
+            {
+                val deviceName = SystemProperties.get("ro.product.vendor.device", "")
+                val cuttlefish = deviceName.startsWith("vsoc_")
+                val goldfish = deviceName.startsWith("emu64")
 
-        private const val INITIAL_MEM_BALLOON_PERCENT = 10
-        private const val MAX_MEM_BALLOON_PERCENT = 50
-        private const val MEM_BALLOON_INFLATE_INTERVAL_MILLIS = 60000L
-        private const val MEM_BALLOON_PERCENT_STEP = 5
+                if (cuttlefish || goldfish) {
+                    3 * 60
+                } else {
+                    30
+                }
+            }()
 
         private fun getMyIntent(context: Context): Intent {
             return Intent(context.getApplicationContext(), VmLauncherService::class.java)
@@ -530,7 +458,7 @@
 
         fun stop(context: Context) {
             val i = getMyIntent(context)
-            i.setAction(ACTION_STOP_VM_LAUNCHER_SERVICE)
+            i.setAction(ACTION_SHUTDOWN_VM)
             context.startService(i)
         }
     }
diff --git a/guest/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
index 9afbcc3..30624cd 100644
--- a/guest/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -41,7 +41,6 @@
 use alloc::boxed::Box;
 use bssl_avf::Digester;
 use diced_open_dice::{bcc_handover_parse, DiceArtifacts, DiceContext, Hidden, VM_KEY_ALGORITHM};
-use hypervisor_backends::get_mem_sharer;
 use libfdt::Fdt;
 use log::{debug, error, info, trace, warn};
 use pvmfw_avb::verify_payload;
@@ -99,15 +98,7 @@
     }
 
     let guest_page_size = verified_boot_data.page_size.unwrap_or(SIZE_4KB);
-    // TODO(ptosi): Cache the (single?) granule once, in vmbase.
-    let hyp_page_size = if let Some(mem_sharer) = get_mem_sharer() {
-        Some(mem_sharer.granule().map_err(|e| {
-            error!("Failed to get granule size: {e}");
-            RebootReason::InternalError
-        })?)
-    } else {
-        None
-    };
+    let hyp_page_size = hypervisor_backends::get_granule_size();
     let _ =
         sanitize_device_tree(untrusted_fdt, vm_dtbo, vm_ref_dt, guest_page_size, hyp_page_size)?;
     let fdt = untrusted_fdt; // DT has now been sanitized.
diff --git a/libs/libhypervisor_backends/src/hypervisor.rs b/libs/libhypervisor_backends/src/hypervisor.rs
index aa65133..7c274f5 100644
--- a/libs/libhypervisor_backends/src/hypervisor.rs
+++ b/libs/libhypervisor_backends/src/hypervisor.rs
@@ -152,3 +152,8 @@
 pub fn get_device_assigner() -> Option<&'static dyn DeviceAssigningHypervisor> {
     get_hypervisor().as_device_assigner()
 }
+
+/// Gets the unique hypervisor granule size, if any.
+pub fn get_granule_size() -> Option<usize> {
+    get_hypervisor().get_granule_size()
+}
diff --git a/libs/libhypervisor_backends/src/hypervisor/common.rs b/libs/libhypervisor_backends/src/hypervisor/common.rs
index bfe638f..f229e14 100644
--- a/libs/libhypervisor_backends/src/hypervisor/common.rs
+++ b/libs/libhypervisor_backends/src/hypervisor/common.rs
@@ -32,6 +32,13 @@
     fn as_device_assigner(&self) -> Option<&dyn DeviceAssigningHypervisor> {
         None
     }
+
+    /// Returns the granule used by all APIs (MEM_SHARE, MMIO_GUARD, device assignment, ...).
+    ///
+    /// If no such API is supported or if they support different granule sizes, returns None.
+    fn get_granule_size(&self) -> Option<usize> {
+        None
+    }
 }
 
 pub trait MmioGuardedHypervisor {
diff --git a/libs/libhypervisor_backends/src/hypervisor/geniezone.rs b/libs/libhypervisor_backends/src/hypervisor/geniezone.rs
index 76e010b..0913ff3 100644
--- a/libs/libhypervisor_backends/src/hypervisor/geniezone.rs
+++ b/libs/libhypervisor_backends/src/hypervisor/geniezone.rs
@@ -84,6 +84,10 @@
     fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
         Some(self)
     }
+
+    fn get_granule_size(&self) -> Option<usize> {
+        <Self as MemSharingHypervisor>::granule(self).ok()
+    }
 }
 
 impl MmioGuardedHypervisor for GeniezoneHypervisor {
diff --git a/libs/libhypervisor_backends/src/hypervisor/kvm_aarch64.rs b/libs/libhypervisor_backends/src/hypervisor/kvm_aarch64.rs
index 233097b..f183107 100644
--- a/libs/libhypervisor_backends/src/hypervisor/kvm_aarch64.rs
+++ b/libs/libhypervisor_backends/src/hypervisor/kvm_aarch64.rs
@@ -90,6 +90,10 @@
     fn as_device_assigner(&self) -> Option<&dyn DeviceAssigningHypervisor> {
         Some(self)
     }
+
+    fn get_granule_size(&self) -> Option<usize> {
+        <Self as MemSharingHypervisor>::granule(self).ok()
+    }
 }
 
 impl MmioGuardedHypervisor for ProtectedKvmHypervisor {
diff --git a/libs/libhypervisor_backends/src/hypervisor/kvm_x86.rs b/libs/libhypervisor_backends/src/hypervisor/kvm_x86.rs
index 7f9ea4d..d72f788 100644
--- a/libs/libhypervisor_backends/src/hypervisor/kvm_x86.rs
+++ b/libs/libhypervisor_backends/src/hypervisor/kvm_x86.rs
@@ -84,6 +84,10 @@
     fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
         Some(self)
     }
+
+    fn get_granule_size(&self) -> Option<usize> {
+        <Self as MemSharingHypervisor>::granule(self).ok()
+    }
 }
 
 macro_rules! vmcall {
diff --git a/libs/libhypervisor_backends/src/lib.rs b/libs/libhypervisor_backends/src/lib.rs
index 33dc5ad..3c81ac8 100644
--- a/libs/libhypervisor_backends/src/lib.rs
+++ b/libs/libhypervisor_backends/src/lib.rs
@@ -24,5 +24,6 @@
 
 pub use error::{Error, Result};
 pub use hypervisor::{
-    get_device_assigner, get_mem_sharer, get_mmio_guard, DeviceAssigningHypervisor, KvmError,
+    get_device_assigner, get_granule_size, get_mem_sharer, get_mmio_guard,
+    DeviceAssigningHypervisor, KvmError,
 };