Merge "TerminalApp: Remove onDestroy() from ApplicationLifecycleObserver" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index d47afc4..1c078ce 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -19,6 +19,3 @@
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
 rustfmt = --config-path=rustfmt.toml
 ktfmt = --kotlinlang-style
-
-[Hook Scripts]
-aosp_hook = ${REPO_ROOT}/frameworks/base/tools/aosp/aosp_sha.sh ${PREUPLOAD_COMMIT} "."
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
index 017ff89..be1f922 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.kt
@@ -65,6 +65,14 @@
         }
     }
 
+    /** Returns path to the archive. */
+    fun getPath(): String {
+        return when (source) {
+            is UrlSource -> source.value.toString()
+            is PathSource -> source.value.toString()
+        }
+    }
+
     /** Returns size of the archive in bytes */
     @Throws(IOException::class)
     fun getSize(): Long {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
index 7180e87..01c3880 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
@@ -150,21 +150,26 @@
 
     private fun downloadFromSdcard(): Boolean {
         val archive = fromSdCard()
+        val archive_path = archive.getPath()
 
         // Installing from sdcard is preferred, but only supported only in debuggable build.
-        if (Build.isDebuggable() && archive.exists()) {
-            Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz")
+        if (!Build.isDebuggable()) {
+            Log.i(TAG, "Non-debuggable build doesn't support installation from $archive_path")
+            return false
+        }
+        if (!archive.exists()) {
+            return false
+        }
 
-            val dest = getDefault(this).installDir
-            try {
-                archive.installTo(dest, null)
-                Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz")
-                return true
-            } catch (e: IOException) {
-                Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz", e)
-            }
-        } else {
-            Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux")
+        Log.i(TAG, "trying to install $archive_path")
+
+        val dest = getDefault(this).installDir
+        try {
+            archive.installTo(dest, null)
+            Log.i(TAG, "image is installed from $archive_path")
+            return true
+        } catch (e: IOException) {
+            Log.i(TAG, "Failed to install $archive_path", e)
         }
         return false
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
index 547f1a7..d02491b 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Logger.kt
@@ -30,6 +30,7 @@
 import java.nio.file.Files
 import java.nio.file.Path
 import java.nio.file.StandardOpenOption
+import java.time.LocalDateTime
 import java.util.concurrent.ExecutorService
 import libcore.io.Streams
 
@@ -37,14 +38,21 @@
  * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
  */
 internal object Logger {
-    fun setup(vm: VirtualMachine, path: Path, executor: ExecutorService) {
+    fun setup(vm: VirtualMachine, dir: Path, executor: ExecutorService) {
+        val tag = vm.name
+
         if (vm.config.debugLevel != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+            Log.i(tag, "Logs are not captured. Non-debuggable VM.")
             return
         }
 
         try {
+            Files.createDirectories(dir)
+            deleteOldLogs(dir, 10)
+            val logPath = dir.resolve(LocalDateTime.now().toString() + ".txt")
             val console = vm.getConsoleOutput()
-            val file = Files.newOutputStream(path, StandardOpenOption.CREATE)
+            val file =
+                Files.newOutputStream(logPath, StandardOpenOption.CREATE)
             executor.submit<Int?> {
                 console.use { console ->
                     LineBufferedOutputStream(file).use { fileOutput ->
@@ -54,7 +62,7 @@
             }
 
             val log = vm.getLogOutput()
-            executor.submit<Unit> { log.use { writeToLogd(it, vm.name) } }
+            executor.submit<Unit> { log.use { writeToLogd(it, tag) } }
         } catch (e: VirtualMachineException) {
             throw RuntimeException(e)
         } catch (e: IOException) {
@@ -62,12 +70,32 @@
         }
     }
 
+    fun deleteOldLogs(dir: Path, numLogsToKeep: Long) {
+        Files.list(dir)
+            .filter { Files.isRegularFile(it) }
+            .sorted(
+                Comparator.comparingLong { f: Path ->
+                        // for some reason, type inference didn't work here!
+                        Files.getLastModifiedTime(f).toMillis()
+                    }
+                    .reversed()
+            )
+            .skip(numLogsToKeep)
+            .forEach {
+                try {
+                    Files.delete(it)
+                } catch (e: IOException) {
+                    // don't bother
+                }
+            }
+    }
+
     @Throws(IOException::class)
-    private fun writeToLogd(input: InputStream?, vmName: String?) {
+    private fun writeToLogd(input: InputStream?, tag: String?) {
         val reader = BufferedReader(InputStreamReader(input))
         reader
             .useLines { lines -> lines.takeWhile { !Thread.interrupted() } }
-            .forEach { Log.d(vmName, it) }
+            .forEach { Log.d(tag, it) }
     }
 
     private class LineBufferedOutputStream(out: OutputStream?) : BufferedOutputStream(out) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 4bfad62..aa1898f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -38,6 +38,7 @@
 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,6 +56,7 @@
 import java.io.FileOutputStream
 import java.io.IOException
 import java.lang.RuntimeException
+import java.lang.Math.min
 import java.net.InetSocketAddress
 import java.net.SocketAddress
 import java.nio.file.Files
@@ -75,6 +77,40 @@
     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()
@@ -99,13 +135,26 @@
             // When the app starts, reset the memory balloon to 0%.
             // This gives the app maximum available memory.
             ApplicationLifeCycleEvent.APP_ON_START -> {
-                virtualMachine?.setMemoryBalloonByPercent(0)
+                synchronized(mLock) {
+                    inflateMemBalloonHandler.removeCallbacks(inflateMemBalloonTask);
+                    currentMemBalloonPercent = 0;
+                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
+                }
             }
             ApplicationLifeCycleEvent.APP_ON_STOP -> {
-                // When the app stops, inflate the memory balloon to 10%.
-                // This allows the system to reclaim memory while the app is in the background.
-                // TODO(b/400590341) Inflate the balloon while the application remains Stop status.
-                virtualMachine?.setMemoryBalloonByPercent(10)
+                // 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")
@@ -139,10 +188,7 @@
         val displaySize = intent.getParcelableExtra(EXTRA_DISPLAY_INFO, DisplayInfo::class.java)
 
         customImageConfigBuilder.setAudioConfig(
-            AudioConfig.Builder()
-                .setUseSpeaker(true)
-                .setUseMicrophone(true)
-                .build()
+            AudioConfig.Builder().setUseSpeaker(true).setUseMicrophone(true).build()
         )
         if (overrideConfigIfNecessary(customImageConfigBuilder, displaySize)) {
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build())
@@ -170,8 +216,8 @@
             resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
-        val logPath = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
-        Logger.setup(virtualMachine!!, logPath, executorService!!)
+        val logDir = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
+        Logger.setup(virtualMachine!!, logDir, executorService!!)
 
         val notification =
             intent.getParcelableExtra<Notification?>(EXTRA_NOTIFICATION, Notification::class.java)
@@ -258,6 +304,21 @@
             )
             Toast.makeText(this, R.string.virgl_enabled, Toast.LENGTH_SHORT).show()
             changed = true
+        } else if (Files.exists(ImageArchive.getSdcardPathForTesting().resolve("gfxstream"))) {
+            // TODO: check if the configuration is right. current config comes from cuttlefish's one
+            builder.setGpuConfig(
+                VirtualMachineCustomImageConfig.GpuConfig.Builder()
+                    .setBackend("gfxstream")
+                    .setRendererUseEgl(false)
+                    .setRendererUseGles(false)
+                    .setRendererUseGlx(false)
+                    .setRendererUseSurfaceless(true)
+                    .setRendererUseVulkan(true)
+                    .setContextTypes(arrayOf<String>("gfxstream-vulkan", "gfxstream-composer"))
+                    .build()
+            )
+            Toast.makeText(this, "gfxstream", Toast.LENGTH_SHORT).show()
+            changed = true
         }
 
         // Set the initial display size
@@ -376,6 +437,11 @@
         private const val RESULT_STOP = 1
         private const val RESULT_ERROR = 2
 
+        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;
+
         private fun getMyIntent(context: Context): Intent {
             return Intent(context.getApplicationContext(), VmLauncherService::class.java)
         }
diff --git a/android/TerminalApp/res/values-gl/strings.xml b/android/TerminalApp/res/values-gl/strings.xml
index ad1cda8..504e687 100644
--- a/android/TerminalApp/res/values-gl/strings.xml
+++ b/android/TerminalApp/res/values-gl/strings.xml
@@ -90,5 +90,5 @@
     <string name="virgl_enabled" msgid="5242525588039698086">"Activouse <xliff:g id="ID_1">VirGL</xliff:g>"</string>
     <string name="notification_channel_long_running_name" msgid="7916541360369402952">"Tarefas de longa duración"</string>
     <string name="notification_channel_system_events_name" msgid="1004951444029742137">"Eventos do sistema"</string>
-    <string name="tab_default_title" msgid="2300417689389397930">"Tabulador"</string>
+    <string name="tab_default_title" msgid="2300417689389397930">"Pestana"</string>
 </resources>
diff --git a/android/vm/vm_shell.sh b/android/vm/vm_shell.sh
index 60d9329..cac5781 100755
--- a/android/vm/vm_shell.sh
+++ b/android/vm/vm_shell.sh
@@ -50,15 +50,13 @@
 }
 
 function list_cids() {
-    local selected_cid=$1
-    local available_cids=$(adb shell /apex/com.android.virt/bin/vm list | awk 'BEGIN { FS="[:,]" } /cid/ { print $2; }')
-    echo "${available_cids}"
+    adb shell /apex/com.android.virt/bin/vm list | awk 'BEGIN { FS="[:,]" } /cid/ { print $2; }'
 }
 
 function handle_connect_cmd() {
     selected_cid=$1
 
-    available_cids=$(list_cids)
+    available_cids=($(list_cids))
 
     if [ -z "${available_cids}" ]; then
         echo No VM is available
@@ -66,11 +64,11 @@
     fi
 
     if [ ! -n "${selected_cid}" ]; then
-        if [ ${#selected_cid[@]} -eq 1 ]; then
+        if [ ${#available_cids[@]} -eq 1 ]; then
             selected_cid=${available_cids[0]}
         else
             PS3="Select CID of VM to adb-shell into: "
-            select cid in ${available_cids}
+            select cid in ${available_cids[@]}
             do
                 selected_cid=${cid}
                 break
diff --git a/android/vm_demo_native/main.cpp b/android/vm_demo_native/main.cpp
index e1acc05..8fc14bf 100644
--- a/android/vm_demo_native/main.cpp
+++ b/android/vm_demo_native/main.cpp
@@ -329,11 +329,15 @@
                                                                       &ARpcSession_free);
     ARpcSession_setMaxIncomingThreads(session.get(), 1);
 
+    auto param = std::make_unique<std::shared_ptr<IVirtualMachine>>(std::move(vm));
+    auto paramDeleteFd = [](void* param) {
+        delete static_cast<std::shared_ptr<IVirtualMachine>*>(param);
+    };
+
     AIBinder* binder = ARpcSession_setupPreconnectedClient(
             session.get(),
             [](void* param) {
-                std::shared_ptr<IVirtualMachine> vm =
-                        *static_cast<std::shared_ptr<IVirtualMachine>*>(param);
+                IVirtualMachine* vm = static_cast<std::shared_ptr<IVirtualMachine>*>(param)->get();
                 ScopedFileDescriptor sock_fd;
                 ScopedAStatus ret = vm->connectVsock(ITestService::PORT, &sock_fd);
                 if (!ret.isOk()) {
@@ -341,7 +345,7 @@
                 }
                 return sock_fd.release();
             },
-            &vm);
+            param.release(), paramDeleteFd);
     if (binder == nullptr) {
         return Error() << "Failed to connect to vm payload";
     }
diff --git a/build/apex/manifest.json b/build/apex/manifest.json
index b32aa7b..e596ce1 100644
--- a/build/apex/manifest.json
+++ b/build/apex/manifest.json
@@ -3,6 +3,7 @@
   "version": 2,
   "requireNativeLibs": [
     "libEGL.so",
-    "libGLESv2.so"
+    "libGLESv2.so",
+    "libvulkan.so"
   ]
 }
diff --git a/build/compos/CompOSPayloadApp/Android.bp b/build/compos/CompOSPayloadApp/Android.bp
index c6192b9..04465b3 100644
--- a/build/compos/CompOSPayloadApp/Android.bp
+++ b/build/compos/CompOSPayloadApp/Android.bp
@@ -5,5 +5,6 @@
 android_app {
     name: "CompOSPayloadApp",
     sdk_version: "current",
+    system_ext_specific: true,
     apex_available: ["com.android.compos"],
 }
diff --git a/guest/forwarder_guest_launcher/debian/service b/guest/forwarder_guest_launcher/debian/service
index 6824c70..ad57a26 100644
--- a/guest/forwarder_guest_launcher/debian/service
+++ b/guest/forwarder_guest_launcher/debian/service
@@ -11,6 +11,8 @@
 RestartSec=1
 User=root
 Group=root
+StandardOutput=journal
+StandardError=journal
 
 [Install]
 WantedBy=multi-user.target
diff --git a/guest/shutdown_runner/debian/service b/guest/shutdown_runner/debian/service
index 7188d36..2668930 100644
--- a/guest/shutdown_runner/debian/service
+++ b/guest/shutdown_runner/debian/service
@@ -10,6 +10,8 @@
 RestartSec=1
 User=root
 Group=root
+StandardOutput=journal
+StandardError=journal
 
 [Install]
 WantedBy=multi-user.target
diff --git a/guest/trusty/security_vm/vm/Android.bp b/guest/trusty/security_vm/vm/Android.bp
index 35d7313..6fa0c32 100644
--- a/guest/trusty/security_vm/vm/Android.bp
+++ b/guest/trusty/security_vm/vm/Android.bp
@@ -29,6 +29,7 @@
     ],
     visibility: [
         "//packages/modules/Virtualization/guest/trusty/test_vm/vm",
+        "//packages/modules/Virtualization/guest/trusty/test_vm_os/vm",
         "//vendor:__subpackages__",
     ],
 }
diff --git a/guest/trusty/test_vm_os/Android.bp b/guest/trusty/test_vm_os/Android.bp
new file mode 100644
index 0000000..ab0d5d8
--- /dev/null
+++ b/guest/trusty/test_vm_os/Android.bp
@@ -0,0 +1,63 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+    default_team: "trendy_team_trusty",
+}
+
+prebuilt_etc {
+    name: "test_vm_os.trusty_test_vm_config",
+    enabled: false,
+    installable: false,
+    arch: {
+        arm64: {
+            src: "trusty-test_vm-config-arm64.json",
+            enabled: true,
+        },
+        x86_64: {
+            src: "trusty-test_vm-config-x86_64.json",
+            enabled: true,
+        },
+    },
+    filename: "trusty-test_vm-config.json",
+}
+
+sh_test {
+    name: "TrustyVmUnitTests",
+    src: "trusty-ut-ctrl.sh",
+    enabled: false,
+    arch: {
+        arm64: {
+            enabled: true,
+        },
+        x86_64: {
+            enabled: true,
+        },
+    },
+    filename_from_src: true,
+    data: [
+        ":trusty_test_vm_os_elf",
+        ":test_vm_os.trusty_test_vm_config",
+        "trusty-vm-launcher.sh",
+        "trusty-wait-ready.sh",
+        ":trusty-ut-ctrl.system",
+    ],
+    // TODO(b/378367793) use the AndroidTest.xml generated from the trusty
+    // test-map for test_vm payload
+    test_config_template: "AndroidTest.xml",
+    test_suites: [
+        "general-tests",
+    ],
+}
diff --git a/guest/trusty/test_vm_os/AndroidTest.xml b/guest/trusty/test_vm_os/AndroidTest.xml
new file mode 100644
index 0000000..be5c467
--- /dev/null
+++ b/guest/trusty/test_vm_os/AndroidTest.xml
@@ -0,0 +1,82 @@
+<?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.
+  -->
+    <configuration description="Runs {MODULE}">
+    <!-- object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController" -->
+        <!--Skip the test when trusty VM is not enabled. -->
+        <!--option name="run-command" value="getprop trusty.test_vm.nonsecure_vm_ready | grep 1" /-->
+    <!--/object-->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <!-- Target Preparers - Run Shell Commands -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push-file" key="trusty-ut-ctrl.system" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl" />
+        <option name="push-file" key="trusty-ut-ctrl.sh" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh" />
+        <option name="push-file" key="trusty-vm-launcher.sh" value="/data/local/tmp/trusty_test_vm_os/trusty-vm-launcher.sh" />
+        <option name="push-file" key="trusty-wait-ready.sh" value="/data/local/tmp/trusty_test_vm_os/trusty-wait-ready.sh" />
+        <option name="push-file" key="trusty-test_vm-config.json" value="/data/local/tmp/trusty_test_vm_os/trusty-test_vm-config.json" />
+        <option name="push-file" key="trusty_test_vm_os.elf" value="/data/local/tmp/trusty_test_vm_os/trusty_test_vm_os.elf" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <!--Note: the first run-command shall not expect the background command to have started -->
+        <option name="run-bg-command" value="sh /data/local/tmp/trusty_test_vm_os/trusty-vm-launcher.sh" />
+        <option name="run-command" value="sh /data/local/tmp/trusty_test_vm_os/trusty-wait-ready.sh" />
+        <option name="run-command" value="start storageproxyd_test_vm_os" />
+        <option name="teardown-command" value="stop storageproxyd_test_vm_os" />
+        <option name="teardown-command" value="killall storageproxyd_test_vm_os || true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
+        <option name="parse-gtest" value="true" />
+        <option name="abort-if-device-lost" value="true"/>
+        <option name="abort-if-root-lost" value="true" />
+        <option name="per-binary-timeout" value="10m" />
+        <option name="test-command-line" key="com.android.kernel.mmutest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.mmutest"/>
+        <option name="test-command-line" key="com.android.kernel.threadtest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.threadtest"/>
+        <option name="test-command-line" key="com.android.kernel.iovectest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.iovectest"/>
+        <!--TODO(b/400064847) enable kernel.timertest when Trusty VM supports more than 2 VCPU"/-->
+        <!--option name="test-command-line" key="com.android.kernel.timertest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.timertest"/-->
+        <option name="test-command-line" key="com.android.kernel.btitest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.btitest"/>
+        <option name="test-command-line" key="com.android.kernel.cachetest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.cachetest"/>
+        <option name="test-command-line" key="com.android.kernel.console-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.console-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.dpc-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.dpc-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.iovectest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.iovectest"/>
+        <option name="test-command-line" key="com.android.kernel.ktipc.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.ktipc.test"/>
+        <option name="test-command-line" key="com.android.kernel.memorytest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.memorytest"/>
+        <option name="test-command-line" key="com.android.kernel.pactest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.pactest"/>
+        <option name="test-command-line" key="com.android.kernel.uirq-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.uirq-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.usercopy-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.usercopy-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.userscstest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.kernel.userscstest"/>
+        <option name="test-command-line" key="com.android.manifesttest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.manifesttest"/>
+        <option name="test-command-line" key="com.android.memref.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.memref.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.memref.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.rust.memref.test"/>
+        <option name="test-command-line" key="com.android.timer-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.timer-unittest"/>
+        <option name="test-command-line" key="com.android.ipc-unittest.ctrl" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.ipc-unittest.ctrl"/>
+        <!--option name="test-command-line" key="com.android.trusty.cfitest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.cfitest"/-->
+        <option name="test-command-line" key="com.android.trusty.crashtest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.crashtest"/>
+        <option name="test-command-line" key="com.android.trusty.dlmalloctest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.dlmalloctest"/>
+        <option name="test-command-line" key="com.android.trusty.rust.tipc.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.rust.tipc.test"/>
+        <option name="test-command-line" key="com.android.uirq-unittest" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.uirq-unittest"/>
+    </test>
+    <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
+        <option name="parse-gtest" value="true" />
+        <!--option name="abort-if-device-lost" value="true" /-->
+        <!--option name="abort-if-root-lost" value="true" /-->
+        <option name="per-binary-timeout" value="40m" />
+        <option name="test-command-line" key="com.android.trusty.rust.binder_rpc_test.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.rust.binder_rpc_test.test"/>
+        <option name="test-command-line" key="com.android.trusty.binder.test" value="/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl.sh com.android.trusty.binder.test"/>
+    </test>
+    </configuration>
diff --git a/guest/trusty/test_vm_os/README.md b/guest/trusty/test_vm_os/README.md
new file mode 100644
index 0000000..4d65d9f
--- /dev/null
+++ b/guest/trusty/test_vm_os/README.md
@@ -0,0 +1,7 @@
+## test_vm_os
+
+The Trusty test_vm_os is meant to test the Trusty OS as a VM,
+its payload ought to include the test TAs for different test types:
+- Trusty kernel OS test
+- Trusty/Binder IPC tests
+- Trusty user-space tests for service TAs (DT tree for example)
diff --git a/guest/trusty/test_vm_os/TEST_MAPPING b/guest/trusty/test_vm_os/TEST_MAPPING
new file mode 100644
index 0000000..1506720
--- /dev/null
+++ b/guest/trusty/test_vm_os/TEST_MAPPING
@@ -0,0 +1,9 @@
+{
+  "trusty_test_vm_presubmit": [
+  ],
+  "trusty_test_vm_postsubmit": [
+    {
+        "name": "TrustyVMOS_UnitTests"
+    }
+  ]
+}
diff --git a/guest/trusty/test_vm_os/trusty-test_vm-config-arm64.json b/guest/trusty/test_vm_os/trusty-test_vm-config-arm64.json
new file mode 100644
index 0000000..9d60892
--- /dev/null
+++ b/guest/trusty/test_vm_os/trusty-test_vm-config-arm64.json
@@ -0,0 +1,8 @@
+{
+    "name": "trusty_test_vm",
+    "kernel": "/data/local/tmp/trusty_test_vm_os/trusty_test_vm_os.elf",
+    "platform_version": "1.0",
+    "cpu_topology": "one_cpu",
+    "memory_mib": 112,
+    "protected": true
+}
diff --git a/guest/trusty/test_vm_os/trusty-test_vm-config-x86_64.json b/guest/trusty/test_vm_os/trusty-test_vm-config-x86_64.json
new file mode 100644
index 0000000..5270ac7
--- /dev/null
+++ b/guest/trusty/test_vm_os/trusty-test_vm-config-x86_64.json
@@ -0,0 +1,7 @@
+{
+    "name": "trusty_test_vm",
+    "kernel": "/data/local/tmp/trusty_test_vm_os/trusty_test_vm_os.elf",
+    "platform_version": "1.0",
+    "cpu_topology": "one_cpu",
+    "memory_mib": 112
+}
diff --git a/guest/trusty/test_vm_os/trusty-ut-ctrl.sh b/guest/trusty/test_vm_os/trusty-ut-ctrl.sh
new file mode 100644
index 0000000..860236b
--- /dev/null
+++ b/guest/trusty/test_vm_os/trusty-ut-ctrl.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+#
+# 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.
+
+/data/local/tmp/trusty_test_vm_os/trusty-ut-ctrl -D VSOCK:${2:-$(getprop trusty.test_vm_os.vm_cid)}:1 $1
diff --git a/guest/trusty/test_vm_os/trusty-vm-launcher.sh b/guest/trusty/test_vm_os/trusty-vm-launcher.sh
new file mode 100755
index 0000000..497b188
--- /dev/null
+++ b/guest/trusty/test_vm_os/trusty-vm-launcher.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# 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.
+
+/apex/com.android.virt/bin/vm run /data/local/tmp/trusty_test_vm_os/trusty-test_vm-config.json
diff --git a/guest/trusty/test_vm_os/trusty-wait-ready.sh b/guest/trusty/test_vm_os/trusty-wait-ready.sh
new file mode 100755
index 0000000..0aed284
--- /dev/null
+++ b/guest/trusty/test_vm_os/trusty-wait-ready.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -euo pipefail
+
+function get_cid {
+    local max_cid
+    max_cid=$(/apex/com.android.virt/bin/vm list | awk 'BEGIN { FS="[:,]" } /cid/ { print $2; }' | sort -n | tail -1)
+
+    # return the value trimmed from whitespaces
+    echo "${max_cid}" | xargs
+}
+
+function wait_for_cid {
+    TIMES=${1:-20}
+    X=0
+    local init_cid
+    init_cid=$(get_cid)
+    while [ "$TIMES" -eq 0 ] || [ "$TIMES" -gt "$X" ]
+    do
+      local cid
+      cid=$(get_cid)
+      echo "wait_for_cid: retry $(( X++ )) / $TIMES : init_cid=$init_cid cid=$cid";
+      if [ "$cid" -gt "$init_cid" ]
+      then
+        break
+      else
+        sleep 2
+      fi
+    done
+    setprop trusty.test_vm_os.vm_cid "$cid"
+}
+
+# This script is expected to be started before the trusty_test_vm is started
+# wait_for_cid gets the max cid and wait for it to be updated as an indication
+# that the trusty_test_vm has properly started.
+# wait_for_cid polls for the CID change at 2 seconds intervals
+# the input argument is the max number of retries (20 by default)
+wait_for_cid "$@"
+
+echo trusty.test_vm_os.vm_cid="$(getprop trusty.test_vm_os.vm_cid)"
diff --git a/guest/trusty/test_vm_os/vm/Android.bp b/guest/trusty/test_vm_os/vm/Android.bp
new file mode 100644
index 0000000..2e81828
--- /dev/null
+++ b/guest/trusty/test_vm_os/vm/Android.bp
@@ -0,0 +1,114 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+prebuilt_etc {
+    name: "trusty_test_vm_os_elf",
+    system_ext_specific: true,
+    filename: "trusty_test_vm_os.elf",
+    src: select((os(), arch()), {
+        ("android", "arm64"): ":trusty_test_vm_os_signed",
+        ("android", "x86_64"): ":trusty_test_vm_os_unsigned",
+        (default, default): ":empty_file",
+    }),
+}
+
+cc_binary {
+    name: "trusty_test_vm_os_signed",
+    srcs: [
+        ":trusty_test_vm_os_signed_bin_obj",
+    ],
+    // reuse the common trusty_vm_sections linker script
+    linker_scripts: [
+        ":trusty_vm_sections.ld",
+    ],
+    ldflags: [
+        // Prevent the `trusty_test_vm_os_signed_bin_obj` segment from being garbage collected.
+        "-Wl,--no-gc-sections",
+        // Prevent the build ID segments from being added, as it would corrupt the integrity
+        // of the original signed image.
+        "-Wl,--build-id=none",
+        // Use a standard page size of 4096, smaller than the default 16384, to avoid padding
+        // with extra bytes.
+        "-Wl,-z,max-page-size=4096",
+    ],
+    nocrt: true,
+    no_libcrt: true,
+    static_executable: true,
+    system_shared_libs: [],
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+    strip: {
+        none: true,
+    },
+}
+
+cc_genrule {
+    name: "test_vm_os.S",
+    enabled: false,
+    arch: {
+        arm64: {
+            srcs: [":trusty_test_vm_os_signed_bin"],
+            enabled: true,
+        },
+    },
+    out: ["test_vm_os.S"],
+    cmd: "(" +
+        "    echo '.section .vm_payload_signed.bin';" +
+        "    echo '.globl vm_payload_signed';" +
+        "    echo 'vm_payload_signed:';" +
+        "    echo '.incbin \"'$(in)'\"';" +
+        ") > $(out)",
+    visibility: ["//visibility:private"],
+}
+
+cc_object {
+    name: "trusty_test_vm_os_signed_bin_obj",
+    srcs: [
+        ":test_vm_os.S",
+    ],
+    crt: false,
+    static_libs: ["trusty_test_vm_os_signed_bin"],
+    system_shared_libs: [],
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+    visibility: ["//visibility:private"],
+}
+
+// python -c "import hashlib; print(hashlib.sha256(b'trusty_test_vm_os_salt').hexdigest())"
+trusty_test_vm_os_salt = "74706b35d927b14539a73e14e6e91a2d3be5d46a12c02cf4084bcef5ffee6e4a"
+
+TRUSTY_TEST_VM_OS_VERSION = 1
+
+avb_add_hash_footer {
+    name: "trusty_test_vm_os_signed_bin",
+    filename: "trusty_test_vm_os_signed.bin",
+    partition_name: "boot",
+    private_key: ":trusty_vm_sign_key",
+    salt: trusty_test_vm_os_salt,
+    rollback_index: TRUSTY_TEST_VM_OS_VERSION,
+    props: [
+        {
+            name: "com.android.virt.cap",
+            value: "trusty_security_vm",
+        },
+    ],
+    src: ":trusty_test_vm_os_unsigned",
+    enabled: false,
+    arch: {
+        arm64: {
+            enabled: true,
+        },
+        x86_64: {
+            enabled: true,
+        },
+    },
+}
diff --git a/libs/libservice_vm_requests/src/client_vm.rs b/libs/libservice_vm_requests/src/client_vm.rs
index 4e54510..8ad10fd 100644
--- a/libs/libservice_vm_requests/src/client_vm.rs
+++ b/libs/libservice_vm_requests/src/client_vm.rs
@@ -25,7 +25,7 @@
 use core::result;
 use coset::{AsCborValue, CborSerializable, CoseSign, CoseSign1};
 use der::{Decode, Encode};
-use diced_open_dice::{DiceArtifacts, HASH_SIZE};
+use diced_open_dice::DiceArtifacts;
 use log::{debug, error, info};
 use microdroid_kernel_hashes::{HASH_SIZE as KERNEL_HASH_SIZE, OS_HASHES};
 use service_vm_comm::{ClientVmAttestationParams, Csr, CsrPayload, RequestProcessingError};
@@ -252,7 +252,7 @@
     Ok(false)
 }
 
-fn expected_kernel_authority_hash(service_vm_entry: &Value) -> Result<[u8; HASH_SIZE]> {
+fn expected_kernel_authority_hash(service_vm_entry: &Value) -> Result<Vec<u8>> {
     let cose_sign1 = CoseSign1::from_cbor_value(service_vm_entry.clone())?;
     let payload = cose_sign1.payload.ok_or_else(|| {
         error!("No payload found in the service VM DICE chain entry");
diff --git a/libs/libservice_vm_requests/src/dice.rs b/libs/libservice_vm_requests/src/dice.rs
index ef9d894..ba67450 100644
--- a/libs/libservice_vm_requests/src/dice.rs
+++ b/libs/libservice_vm_requests/src/dice.rs
@@ -19,8 +19,8 @@
 use alloc::vec::Vec;
 use bssl_avf::{ed25519_verify, Digester, EcKey};
 use cbor_util::{
-    cbor_value_type, get_label_value, get_label_value_as_bytes, value_to_array,
-    value_to_byte_array, value_to_bytes, value_to_map, value_to_num, value_to_text,
+    cbor_value_type, get_label_value, get_label_value_as_bytes, value_to_array, value_to_bytes,
+    value_to_map, value_to_num, value_to_text,
 };
 use ciborium::value::Value;
 use core::cell::OnceCell;
@@ -31,7 +31,7 @@
     Algorithm, AsCborValue, CborSerializable, CoseError, CoseKey, CoseSign1, KeyOperation, KeyType,
     Label,
 };
-use diced_open_dice::{DiceMode, HASH_SIZE};
+use diced_open_dice::DiceMode;
 use log::{debug, error, info};
 use service_vm_comm::RequestProcessingError;
 
@@ -288,8 +288,8 @@
 pub(crate) struct DiceChainEntryPayload {
     pub(crate) subject_public_key: PublicKey,
     mode: DiceMode,
-    pub(crate) code_hash: [u8; HASH_SIZE],
-    pub(crate) authority_hash: [u8; HASH_SIZE],
+    pub(crate) code_hash: Vec<u8>,
+    pub(crate) authority_hash: Vec<u8>,
     config_descriptor: ConfigDescriptor,
 }
 
@@ -327,12 +327,12 @@
                 }
                 MODE => builder.mode(to_mode(value)?)?,
                 CODE_HASH => {
-                    let code_hash = value_to_byte_array(value, "DiceChainEntryPayload code_hash")?;
+                    let code_hash = value_to_bytes(value, "DiceChainEntryPayload code_hash")?;
                     builder.code_hash(code_hash)?;
                 }
                 AUTHORITY_HASH => {
                     let authority_hash =
-                        value_to_byte_array(value, "DiceChainEntryPayload authority_hash")?;
+                        value_to_bytes(value, "DiceChainEntryPayload authority_hash")?;
                     builder.authority_hash(authority_hash)?;
                 }
                 CONFIG_DESC => {
@@ -524,8 +524,8 @@
 struct PayloadBuilder {
     subject_public_key: OnceCell<PublicKey>,
     mode: OnceCell<DiceMode>,
-    code_hash: OnceCell<[u8; HASH_SIZE]>,
-    authority_hash: OnceCell<[u8; HASH_SIZE]>,
+    code_hash: OnceCell<Vec<u8>>,
+    authority_hash: OnceCell<Vec<u8>>,
     config_descriptor: OnceCell<ConfigDescriptor>,
 }
 
@@ -552,11 +552,11 @@
         set_once(&self.mode, mode, "mode")
     }
 
-    fn code_hash(&mut self, code_hash: [u8; HASH_SIZE]) -> Result<()> {
+    fn code_hash(&mut self, code_hash: Vec<u8>) -> Result<()> {
         set_once(&self.code_hash, code_hash, "code_hash")
     }
 
-    fn authority_hash(&mut self, authority_hash: [u8; HASH_SIZE]) -> Result<()> {
+    fn authority_hash(&mut self, authority_hash: Vec<u8>) -> Result<()> {
         set_once(&self.authority_hash, authority_hash, "authority_hash")
     }
 
@@ -570,7 +570,9 @@
         // the Open Profile for DICE spec.
         let mode = self.mode.take().unwrap_or(DiceMode::kDiceModeNotInitialized);
         let code_hash = take_value(&mut self.code_hash, "code_hash")?;
+        validate_hash_size(code_hash.len(), "code_hash")?;
         let authority_hash = take_value(&mut self.authority_hash, "authority_hash")?;
+        validate_hash_size(authority_hash.len(), "authority_hash")?;
         let config_descriptor = take_value(&mut self.config_descriptor, "config_descriptor")?;
         Ok(DiceChainEntryPayload {
             subject_public_key,
@@ -581,3 +583,18 @@
         })
     }
 }
+
+fn validate_hash_size(len: usize, name: &str) -> Result<()> {
+    // According to the Android Profile for DICE specification, SHA-256, SHA-384, and SHA-512
+    // are all acceptable hash algorithms.
+    const ACCEPTABLE_HASH_SIZES: [usize; 3] = [32, 48, 64];
+    if ACCEPTABLE_HASH_SIZES.contains(&len) {
+        Ok(())
+    } else {
+        error!(
+            "Invalid hash size for {}: {}. Acceptable hash sizes are: {:?}",
+            name, len, ACCEPTABLE_HASH_SIZES
+        );
+        Err(RequestProcessingError::InvalidDiceChain)
+    }
+}
diff --git a/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
index 67a4716..8452344 100644
--- a/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -59,28 +59,29 @@
         JNIEnv *mEnv;
         jobject mProvider;
         jmethodID mMid;
-    } state;
+    };
 
-    state.mEnv = env;
-    state.mProvider = provider;
-    state.mMid = mid;
+    auto state = std::make_unique<State>(env, provider, mid);
 
     using RequestFun = int (*)(void *);
     RequestFun requestFunc = [](void *param) -> int {
-        State *state = reinterpret_cast<State *>(param);
+        State *state = static_cast<State *>(param);
         int ownedFd = state->mEnv->CallIntMethod(state->mProvider, state->mMid);
         // FD is owned by PFD in Java layer, need to dupe it so that
         // ARpcSession_setupPreconnectedClient can take ownership when it calls unique_fd internally
         return fcntl(ownedFd, F_DUPFD_CLOEXEC, 0);
     };
 
+    auto paramDeleteFunc = [](void *param) { delete static_cast<State *>(param); };
+
     RpcSessionHandle session;
     // We need a thread pool to be able to support linkToDeath, or callbacks
     // (b/268335700). These threads are currently created eagerly, so we don't
     // want too many. The number 1 is chosen after some discussion, and to match
     // the server-side default (mMaxThreads on RpcServer).
     ARpcSession_setMaxIncomingThreads(session.get(), 1);
-    auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &state);
+    auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, state.release(),
+                                                      paramDeleteFunc);
     return AIBinder_toJavaBinder(env, client);
 }
 
diff --git a/tests/backcompat_test/Android.bp b/tests/backcompat_test/Android.bp
index aa1e089..d47487a 100644
--- a/tests/backcompat_test/Android.bp
+++ b/tests/backcompat_test/Android.bp
@@ -14,6 +14,7 @@
         "libanyhow",
         "liblibc",
         "libnix",
+        "librustutils",
         "libvmclient",
         "liblog_rust",
     ],
diff --git a/tests/backcompat_test/src/main.rs b/tests/backcompat_test/src/main.rs
index b92049d..9518c38 100644
--- a/tests/backcompat_test/src/main.rs
+++ b/tests/backcompat_test/src/main.rs
@@ -25,6 +25,7 @@
 use anyhow::anyhow;
 use anyhow::Context;
 use anyhow::Error;
+use anyhow::Result;
 use log::info;
 use std::fs::read_to_string;
 use std::fs::File;
@@ -46,11 +47,11 @@
 
 /// Runs a protected VM and validates it against a golden device tree.
 #[test]
-fn test_device_tree_protected_compat() -> Result<(), Error> {
+fn test_device_tree_protected_compat() -> Result<()> {
     run_test(true, GOLDEN_DEVICE_TREE_PROTECTED)
 }
 
-fn run_test(protected: bool, golden_dt: &str) -> Result<(), Error> {
+fn run_test(protected: bool, golden_dt: &str) -> Result<()> {
     let kernel = Some(open_payload(VMBASE_EXAMPLE_KERNEL_PATH)?);
     android_logger::init_once(
         android_logger::Config::default()
@@ -110,6 +111,7 @@
         .truncate(true)
         .open("dump_dt.dtb")
         .with_context(|| "Failed to open device tree dump file dump_dt.dtb")?;
+    let is_updatable = service.isUpdatableVmSupported()?;
     let vm = VmInstance::create(
         service.as_ref(),
         &config,
@@ -142,7 +144,8 @@
     {
         return Err(anyhow!("failed to execute dtc"));
     }
-    let dtcompare_res = Command::new("./dtcompare")
+    let mut dtcompare_cmd = Command::new("./dtcompare");
+    dtcompare_cmd
         .arg("--dt1")
         .arg("dump_dt_golden.dtb")
         .arg("--dt2")
@@ -162,12 +165,23 @@
         .arg("/chosen/linux,initrd-start")
         .arg("--ignore-path-value")
         .arg("/chosen/linux,initrd-end")
-        .arg("--ignore-path-value")
-        .arg("/avf/secretkeeper_public_key")
         .arg("--ignore-path")
-        .arg("/avf/name")
-        .output()
-        .context("failed to execute dtcompare")?;
+        .arg("/avf/name");
+    // Check if Secretkeeper is advertised. If not, check the vendor API level. Secretkeeper is
+    // required as of 202504, and if missing, the test should fail.
+    // Otherwise, ignore the fields, as they are not required.
+    if is_updatable {
+        dtcompare_cmd.arg("--ignore-path-value").arg("/avf/secretkeeper_public_key");
+    } else if vsr_api_level()? >= 202504 {
+        return Err(anyhow!("Secretkeeper support missing on vendor API >= 202504. Secretkeeper needs to be implemented."));
+    } else {
+        dtcompare_cmd
+            .arg("--ignore-path")
+            .arg("/avf/secretkeeper_public_key")
+            .arg("--ignore-path")
+            .arg("/avf/untrusted/defer-rollback-protection");
+    }
+    let dtcompare_res = dtcompare_cmd.output().context("failed to execute dtcompare")?;
     if !dtcompare_res.status.success() {
         if !Command::new("./dtc_static")
             .arg("-I")
@@ -202,7 +216,18 @@
     Ok(())
 }
 
-fn open_payload(path: &str) -> Result<ParcelFileDescriptor, Error> {
+fn open_payload(path: &str) -> Result<ParcelFileDescriptor> {
     let file = File::open(path).with_context(|| format!("Failed to open VM image {path}"))?;
     Ok(ParcelFileDescriptor::new(file))
 }
+
+fn vsr_api_level() -> Result<i32> {
+    get_sysprop_i32("ro.vendor.api_level")
+}
+
+fn get_sysprop_i32(prop: &str) -> Result<i32> {
+    let Some(val) = rustutils::system_properties::read(prop)? else {
+        return Ok(-1);
+    };
+    val.parse::<i32>().with_context(|| format!("Failed to read {prop}"))
+}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 59a57f1..5513af6 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -1463,7 +1463,7 @@
     }
 
     private void ensureUpdatableVmSupported() throws DeviceNotAvailableException {
-        if (PropertyUtil.isVendorApiLevelAtLeast(getAndroidDevice(), 202504)) {
+        if (PropertyUtil.getVsrApiLevel(getAndroidDevice()) >= 202504) {
             assertTrue(
                     "Missing Updatable VM support, have you declared Secretkeeper interface?",
                     isUpdatableVmSupported());