Merge "Use libhex to encode hex string in libs/apkverify/"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 8376c1f..dd81738 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -30,6 +30,11 @@
       "name": "AVFHostTestCases"
     }
   ],
+  "postsubmit": [
+    {
+      "name": "CtsMicrodroidDisabledTestCases"
+    }
+  ],
   "imports": [
     {
       "path": "packages/modules/Virtualization/apkdmverity"
diff --git a/apex/Android.bp b/apex/Android.bp
index 4e64e50..596493a 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -104,6 +104,9 @@
     host_required: [
         "vm_shell",
     ],
+    apps: [
+        "EmptyPayloadApp",
+    ],
 }
 
 apex_defaults {
diff --git a/apex/empty-payload-apk/Android.bp b/apex/empty-payload-apk/Android.bp
new file mode 100644
index 0000000..70e6754
--- /dev/null
+++ b/apex/empty-payload-apk/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "EmptyPayloadApp",
+    installable: true,
+    jni_libs: ["MicrodroidEmptyPayloadJniLib"],
+    apex_available: ["com.android.virt"],
+    sdk_version: "system_current",
+    jni_uses_platform_apis: true,
+    min_sdk_version: "UpsideDownCake",
+    target_sdk_version: "UpsideDownCake",
+    compile_multilib: "first",
+    stl: "none",
+}
+
+cc_library {
+    name: "MicrodroidEmptyPayloadJniLib",
+    srcs: ["empty_binary.cpp"],
+    shared_libs: ["libvm_payload#current"],
+    installable: true,
+    apex_available: ["com.android.virt"],
+    compile_multilib: "first",
+    stl: "none",
+}
diff --git a/apex/empty-payload-apk/AndroidManifest.xml b/apex/empty-payload-apk/AndroidManifest.xml
new file mode 100644
index 0000000..e649744
--- /dev/null
+++ b/apex/empty-payload-apk/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.microdroid.empty_payload">
+
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+    <application android:testOnly="true" android:hasCode="false" />
+
+</manifest>
diff --git a/apex/empty-payload-apk/empty_binary.cpp b/apex/empty-payload-apk/empty_binary.cpp
new file mode 100644
index 0000000..4308954
--- /dev/null
+++ b/apex/empty-payload-apk/empty_binary.cpp
@@ -0,0 +1,22 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <vm_main.h>
+#include <vm_payload.h>
+
+extern "C" int AVmPayload_main() {
+    // disable buffering to communicate seamlessly
+    setvbuf(stdin, nullptr, _IONBF, 0);
+    setvbuf(stdout, nullptr, _IONBF, 0);
+    setvbuf(stderr, nullptr, _IONBF, 0);
+
+    printf("Hello Microdroid\n");
+
+    AVmPayload_notifyPayloadReady();
+
+    // Wait forever to allow developer to interact with Microdroid shell
+    for (;;) {
+        pause();
+    }
+
+    return 0;
+}
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index e67a309..32eafb8 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -140,6 +140,7 @@
 
             String rate = mAuthFsTestRule.getMicrodroid().run(cmd);
             rates.add(Double.parseDouble(rate));
+            mAuthFsTestRule.killFdServerOnAndroid();
         }
         reportMetrics(rates, mode + "_read", "mb_per_sec");
     }
@@ -152,11 +153,14 @@
         List<Double> rates = new ArrayList<>(TRIAL_COUNT);
         for (int i = 0; i < TRIAL_COUNT + 1; ++i) {
             mAuthFsTestRule.runFdServerOnAndroid(
-                    "--open-rw 5:" + mAuthFsTestRule.TEST_OUTPUT_DIR + "/out.file", "--rw-fds 5");
+                    "--open-rw 5:" + AuthFsTestRule.TEST_OUTPUT_DIR + "/out.file", "--rw-fds 5");
             mAuthFsTestRule.runAuthFsOnMicrodroid("--remote-new-rw-file 5");
 
             String rate = mAuthFsTestRule.getMicrodroid().run(cmd);
             rates.add(Double.parseDouble(rate));
+            mAuthFsTestRule.killFdServerOnAndroid();
+            AuthFsTestRule.getAndroid()
+                    .runForResult("rm", "-rf", AuthFsTestRule.TEST_OUTPUT_DIR + "/out.file");
         }
         reportMetrics(rates, mode + "_write", "mb_per_sec");
     }
diff --git a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
index 6087eef..357edea 100644
--- a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
+++ b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
@@ -181,6 +181,10 @@
         Future<?> unusedFuture = mThreadPool.submit(() -> runForResult(sAndroid, cmd, "fd_server"));
     }
 
+    public void killFdServerOnAndroid() throws DeviceNotAvailableException {
+        sAndroid.tryRun("killall fd_server");
+    }
+
     public void runAuthFsOnMicrodroid(String flags) {
         String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags + " --cid " + VMADDR_CID_HOST;
 
@@ -250,7 +254,7 @@
         }
 
         assertNotNull(sAndroid);
-        sAndroid.tryRun("killall fd_server");
+        killFdServerOnAndroid();
 
         // Even though we only run one VM for the whole class, and could have collect the VM log
         // after all tests are done, TestLogData doesn't seem to work at class level. Hence,
diff --git a/compos/compos_key_helper/Android.bp b/compos/compos_key_helper/Android.bp
index cffa1e3..f8dc783 100644
--- a/compos/compos_key_helper/Android.bp
+++ b/compos/compos_key_helper/Android.bp
@@ -29,7 +29,7 @@
         "libcompos_key",
     ],
     shared_libs: [
-        "libvm_payload",
+        "libvm_payload#current",
         "libbinder_ndk",
     ],
 }
diff --git a/compos/composd/src/service.rs b/compos/composd/src/service.rs
index 27c31e3..49cfd3a 100644
--- a/compos/composd/src/service.rs
+++ b/compos/composd/src/service.rs
@@ -67,10 +67,7 @@
             ApexSource::PreferStaged => true,
             _ => unreachable!("Invalid ApexSource {:?}", apex_source),
         };
-        // b/250929504 failure here intentionally crashes composd to trigger a bugreport
-        Ok(self
-            .do_start_test_compile(prefer_staged, callback)
-            .expect("Failed to start the test compile"))
+        to_binder_result(self.do_start_test_compile(prefer_staged, callback))
     }
 }
 
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
index be56430..479ae7f 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -23,7 +23,6 @@
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
 import android.content.ComponentName;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -45,28 +44,10 @@
  */
 public class IsolatedCompilationJobService extends JobService {
     private static final String TAG = IsolatedCompilationJobService.class.getName();
-    private static final int DAILY_JOB_ID = 5132250;
     private static final int STAGED_APEX_JOB_ID = 5132251;
 
     private final AtomicReference<CompilationJob> mCurrentJob = new AtomicReference<>();
 
-    static void scheduleDailyJob(JobScheduler scheduler) {
-        // This daily job is only for dogfooders (userdebug/eng)
-        if (Build.IS_USER) return;
-
-        ComponentName serviceName =
-                new ComponentName("android", IsolatedCompilationJobService.class.getName());
-
-        int result = scheduler.schedule(new JobInfo.Builder(DAILY_JOB_ID, serviceName)
-                .setRequiresDeviceIdle(true)
-                .setRequiresCharging(true)
-                .setPeriodic(TimeUnit.DAYS.toMillis(1))
-                .build());
-        if (result != JobScheduler.RESULT_SUCCESS) {
-            Log.e(TAG, "Failed to schedule daily job");
-        }
-    }
-
     static void scheduleStagedApexJob(JobScheduler scheduler) {
         ComponentName serviceName =
                 new ComponentName("android", IsolatedCompilationJobService.class.getName());
@@ -87,7 +68,6 @@
                     IsolatedCompilationMetrics.SCHEDULING_FAILURE);
             Log.e(TAG, "Failed to schedule staged APEX job");
         }
-
     }
 
     static boolean isStagedApexJobScheduled(JobScheduler scheduler) {
@@ -96,9 +76,7 @@
 
     @Override
     public boolean onStartJob(JobParameters params) {
-        int jobId = params.getJobId();
-
-        Log.i(TAG, "Starting job " + jobId);
+        Log.i(TAG, "Starting job");
 
         // This function (and onStopJob) are only ever called on the main thread, so we don't have
         // to worry about two starts at once, or start and stop happening at once. But onCompletion
@@ -112,9 +90,6 @@
         }
 
         IsolatedCompilationMetrics metrics = new IsolatedCompilationMetrics();
-        if (jobId != STAGED_APEX_JOB_ID) {
-            metrics.disable();
-        }
 
         CompilationJob newJob = new CompilationJob(IsolatedCompilationJobService.this::onCompletion,
                 params, metrics);
@@ -127,7 +102,7 @@
             @Override
             public void run() {
                 try {
-                    newJob.start(jobId);
+                    newJob.start();
                 } catch (RuntimeException e) {
                     Log.e(TAG, "Starting CompilationJob failed", e);
                     metrics.onCompilationEnded(IsolatedCompilationMetrics.RESULT_FAILED_TO_START);
@@ -184,7 +159,7 @@
             mMetrics = requireNonNull(metrics);
         }
 
-        void start(int jobId) {
+        void start() {
             IBinder binder = ServiceManager.waitForService("android.system.composd");
             IIsolatedCompilationService composd =
                     IIsolatedCompilationService.Stub.asInterface(binder);
@@ -194,13 +169,7 @@
             }
 
             try {
-                ICompilationTask composTask;
-                if (jobId == DAILY_JOB_ID) {
-                    composTask = composd.startTestCompile(
-                            IIsolatedCompilationService.ApexSource.NoStaged, this);
-                } else {
-                    composTask = composd.startStagedApexCompile(this);
-                }
+                ICompilationTask composTask = composd.startStagedApexCompile(this);
                 mMetrics.onCompilationStarted();
                 mTask.set(composTask);
                 composTask.asBinder().linkToDeath(this, 0);
@@ -222,16 +191,24 @@
 
         private void cancelTask() {
             ICompilationTask task = mTask.getAndSet(null);
-            if (task != null) {
-                Log.i(TAG, "Cancelling task");
-                try {
-                    task.cancel();
-                    mMetrics.onCompilationJobCanceled(mParams.getStopReason());
-                } catch (RuntimeException | RemoteException e) {
-                    // If canceling failed we'll assume it means that the task has already failed;
-                    // there's nothing else we can do anyway.
-                    Log.w(TAG, "Failed to cancel CompilationTask", e);
-                }
+            if (task == null) {
+                return;
+            }
+
+            Log.i(TAG, "Cancelling task");
+            try {
+                task.cancel();
+            } catch (RuntimeException | RemoteException e) {
+                // If canceling failed we'll assume it means that the task has already failed;
+                // there's nothing else we can do anyway.
+                Log.w(TAG, "Failed to cancel CompilationTask", e);
+            }
+
+            mMetrics.onCompilationJobCanceled(mParams.getStopReason());
+            try {
+                task.asBinder().unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                // Harmless
             }
         }
 
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java b/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
index df590f3..e333198 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
@@ -75,21 +75,14 @@
             ArtStatsLog.ISOLATED_COMPILATION_SCHEDULED__SCHEDULING_RESULT__SCHEDULING_SUCCESS;
 
     private long mCompilationStartTimeMs = 0;
-    private boolean mEnabled = true; // TODO(b/205296305) Remove this
 
     public static void onCompilationScheduled(@ScheduleJobResult int result) {
         ArtStatsLog.write(ArtStatsLog.ISOLATED_COMPILATION_SCHEDULED, result);
         Log.i(TAG, "ISOLATED_COMPILATION_SCHEDULED: " + result);
     }
 
-    public void disable() {
-        mEnabled = false;
-    }
-
     public void onCompilationStarted() {
-        if (mEnabled) {
-            mCompilationStartTimeMs = SystemClock.elapsedRealtime();
-        }
+        mCompilationStartTimeMs = SystemClock.elapsedRealtime();
     }
 
     public void onCompilationJobCanceled(@JobParameters.StopReason int jobStopReason) {
@@ -102,9 +95,6 @@
 
     private void statsLogPostCompilation(@CompilationResult int result,
                 @JobParameters.StopReason int jobStopReason) {
-        if (!mEnabled) {
-            return;
-        }
 
         long compilationTime = mCompilationStartTimeMs == 0 ? -1
                 : SystemClock.elapsedRealtime() - mCompilationStartTimeMs;
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
index 11e3743..b2fcbe0 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
@@ -67,7 +67,6 @@
             return;
         }
 
-        IsolatedCompilationJobService.scheduleDailyJob(scheduler);
         StagedApexObserver.registerForStagedApexUpdates(scheduler);
     }
 
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 8e870ea..77f2ee7 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -256,7 +256,8 @@
                     builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
                 }
                 VirtualMachineConfig config = builder.build();
-                VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
+                VirtualMachineManager vmm =
+                        getApplication().getSystemService(VirtualMachineManager.class);
                 mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
                 try {
                     mVirtualMachine.setConfig(config);
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
index 5f552f9..f184862 100644
--- a/docs/getting_started/index.md
+++ b/docs/getting_started/index.md
@@ -97,6 +97,15 @@
 If you run into problems, inspect the logs produced by `atest`. Their location is printed at the
 end. The `host_log_*.zip` file should contain the output of individual commands as well as VM logs.
 
+### Custom pvmfw
+
+Hostside tests, which run on the PC and extends `MicrodroidHostTestCaseBase`, can be run with
+a custom `pvmfw`. Use `--module-arg` to push `pvmfw` for individual test methods.
+
+```shell
+atest com.android.microdroid.test.MicrodroidHostTests -- --module-arg MicrodroidHostTestCases:set-option:pvmfw:pvmfw.img
+```
+
 ## Spawning your own VMs with custom kernel
 
 You can spawn your own VMs by passing a JSON config file to the VirtualizationService via the `vm`
@@ -120,10 +129,30 @@
 The `vm` command also has other subcommands for debugging; run `/apex/com.android.virt/bin/vm help`
 for details.
 
+## Spawning your own VMs with custom pvmfw
+
+Set system property `hypervisor.pvmfw.path` to custom `pvmfw` on the device before using `vm` tool.
+`virtualizationservice` will pass the specified `pvmfw` to `crosvm` for protected VMs.
+
+```shell
+adb push pvmfw.img /data/local/tmp/pvmfw.img
+adb root  # required for setprop
+adb shell setprop hypervisor.pvmfw.path /data/local/tmp/pvmfw.img
+```
+
 ## Spawning your own VMs with Microdroid
 
 [Microdroid](../../microdroid/README.md) is a lightweight version of Android that is intended to run
-on pVM. You can manually run the demo app on top of Microdroid as follows:
+on pVM. You can run a Microdroid with empty payload using the following command:
+
+```shell
+adb shell /apex/com.android.virt/bin/vm run-microdroid --debug full
+```
+
+The `instance.img` and `apk.idsig` files will be stored in a subdirectory under
+`/data/local/tmp/microdroid`, that `vm` will create.
+
+Atlernatively, you can manually run the demo app on top of Microdroid as follows:
 
 ```shell
 UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true TARGET_BUILD_APPS=MicrodroidDemoApp m apps_only dist
diff --git a/encryptedstore/src/main.rs b/encryptedstore/src/main.rs
index d7d2382..9c8311d 100644
--- a/encryptedstore/src/main.rs
+++ b/encryptedstore/src/main.rs
@@ -20,7 +20,8 @@
 
 use anyhow::{ensure, Context, Result};
 use clap::{arg, App};
-use dm::{crypt::CipherType, util};
+use dm::crypt::CipherType;
+use dm::util;
 use log::info;
 use std::ffi::CString;
 use std::fs::{create_dir_all, OpenOptions};
@@ -45,7 +46,7 @@
         .args(&[
             arg!(--blkdevice <FILE> "the block device backing the encrypted storage")
                 .required(true),
-            arg!(--key <KEY> "key (in hex) equivalent to 64 bytes)").required(true),
+            arg!(--key <KEY> "key (in hex) equivalent to 32 bytes)").required(true),
             arg!(--mountpoint <MOUNTPOINT> "mount point for the storage").required(true),
         ])
         .get_matches();
@@ -87,12 +88,11 @@
 fn enable_crypt(data_device: &Path, key: &str, name: &str) -> Result<PathBuf> {
     let dev_size = util::blkgetsize64(data_device)?;
     let key = hex::decode(key).context("Unable to decode hex key")?;
-    ensure!(key.len() == 64, "We need 64 bytes' key for aes-xts cipher for block encryption");
 
     // Create the dm-crypt spec
     let target = dm::crypt::DmCryptTargetBuilder::default()
         .data_device(data_device, dev_size)
-        .cipher(CipherType::AES256XTS) // TODO(b/259253336) Move to HCTR2 based encryption.
+        .cipher(CipherType::AES256HCTR2)
         .key(&key)
         .build()
         .context("Couldn't build the DMCrypt target")?;
diff --git a/javalib/Android.bp b/javalib/Android.bp
index 8421231..71287f2 100644
--- a/javalib/Android.bp
+++ b/javalib/Android.bp
@@ -38,9 +38,6 @@
         javacflags: [
             // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
             "-Xep:GuardedBy:ERROR",
-            // JavaApiUsedByMainlineModule is quite spammy, and since we com.android.virt is not
-            // an updatable module we don't need it.
-            "-Xep:JavaApiUsedByMainlineModule:OFF",
         ],
     },
 
diff --git a/javalib/api/module-lib-current.txt b/javalib/api/module-lib-current.txt
index d802177..4d59764 100644
--- a/javalib/api/module-lib-current.txt
+++ b/javalib/api/module-lib-current.txt
@@ -1 +1,9 @@
 // Signature format: 2.0
+package android.system.virtualmachine {
+
+  public class VirtualizationFrameworkInitializer {
+    method public static void registerServiceWrappers();
+  }
+
+}
+
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index ea2d23e..f38d8fd 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -3,7 +3,7 @@
 
   public class VirtualMachine implements java.lang.AutoCloseable {
     method public void clearCallback();
-    method public void close() throws android.system.virtualmachine.VirtualMachineException;
+    method public void close();
     method @NonNull public android.os.IBinder connectToVsockServer(int) throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public android.os.ParcelFileDescriptor connectVsock(int) throws android.system.virtualmachine.VirtualMachineException;
     method public int getCid() throws android.system.virtualmachine.VirtualMachineException;
@@ -61,7 +61,6 @@
     method @IntRange(from=0) public int getMemoryMib();
     method @IntRange(from=1) public int getNumCpus();
     method @Nullable public String getPayloadBinaryPath();
-    method @Nullable public String getPayloadConfigPath();
     method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
     method public boolean isProtectedVm();
     field public static final int DEBUG_LEVEL_APP_ONLY = 1; // 0x1
@@ -77,7 +76,6 @@
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryMib(@IntRange(from=0) int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryPath(@NonNull String);
-    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
   }
 
@@ -98,7 +96,6 @@
     method public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
     method @Nullable public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
     method public int getCapabilities();
-    method @NonNull public static android.system.virtualmachine.VirtualMachineManager getInstance(@NonNull android.content.Context);
     method @NonNull public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
     field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index d802177..42ad060 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -1 +1,13 @@
 // Signature format: 2.0
+package android.system.virtualmachine {
+
+  public final class VirtualMachineConfig {
+    method @Nullable public String getPayloadConfigPath();
+  }
+
+  public static final class VirtualMachineConfig.Builder {
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+  }
+
+}
+
diff --git a/javalib/jarjar-rules.txt b/javalib/jarjar-rules.txt
index dd8ad2d..726f9aa 100644
--- a/javalib/jarjar-rules.txt
+++ b/javalib/jarjar-rules.txt
@@ -1,9 +1,10 @@
 # Rules for the android.system.virtualmachine java_sdk_library.
 
-# This is the root of the API, everything we care about should be
-# reachable from here.
-# (This gets rid of all the android.sysprop classes we don't use.)
+# Keep the API surface, most of it is accessible from VirtualMachineManager
 keep android.system.virtualmachine.VirtualMachineManager
+# VirtualizationModuleFrameworkInitializer is not accessible from
+# VirtualMachineManager, we need to explicitly keep it.
+keep android.system.virtualmachine.VirtualizationFrameworkInitializer
 
 # We statically link PlatformProperties, rename to avoid clashes.
 rule android.sysprop.** com.android.system.virtualmachine.sysprop.@1
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 193d213..63b5628 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -49,7 +49,9 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.content.ComponentCallbacks2;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
@@ -61,10 +63,12 @@
 import android.system.virtualizationservice.IVirtualMachine;
 import android.system.virtualizationservice.IVirtualMachineCallback;
 import android.system.virtualizationservice.IVirtualizationService;
+import android.system.virtualizationservice.MemoryTrimLevel;
 import android.system.virtualizationservice.PartitionType;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
+import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -105,24 +109,6 @@
  */
 @SystemApi
 public class VirtualMachine implements AutoCloseable {
-    /** Name of the directory under the files directory where all VMs created for the app exist. */
-    private static final String VM_DIR = "vm";
-
-    /** Name of the persisted config file for a VM. */
-    private static final String CONFIG_FILE = "config.xml";
-
-    /** Name of the instance image file for a VM. (Not implemented) */
-    private static final String INSTANCE_IMAGE_FILE = "instance.img";
-
-    /** Name of the idsig file for a VM */
-    private static final String IDSIG_FILE = "idsig";
-
-    /** Name of the idsig files for extra APKs. */
-    private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";
-
-    /** Name of the virtualization service. */
-    private static final String SERVICE_NAME = "android.system.virtualizationservice";
-
     /** The permission needed to create or run a virtual machine. */
     public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
             "android.permission.MANAGE_VIRTUAL_MACHINE";
@@ -159,6 +145,29 @@
      */
     public static final int STATUS_DELETED = 2;
 
+    private static final String TAG = "VirtualMachine";
+
+    /** Name of the directory under the files directory where all VMs created for the app exist. */
+    private static final String VM_DIR = "vm";
+
+    /** Name of the persisted config file for a VM. */
+    private static final String CONFIG_FILE = "config.xml";
+
+    /** Name of the instance image file for a VM. (Not implemented) */
+    private static final String INSTANCE_IMAGE_FILE = "instance.img";
+
+    /** Name of the idsig file for a VM */
+    private static final String IDSIG_FILE = "idsig";
+
+    /** Name of the idsig files for extra APKs. */
+    private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";
+
+    /** Name of the virtualization service. */
+    private static final String SERVICE_NAME = "android.system.virtualizationservice";
+
+    /** Size of the instance image. 10 MB. */
+    private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
+
     /** The package which owns this VM. */
     @NonNull private final String mPackageName;
 
@@ -181,24 +190,61 @@
     /** Path to the idsig file for this VM. */
     @NonNull private final File mIdsigFilePath;
 
-    private static class ExtraApkSpec {
-        public final File apk;
-        public final File idsig;
-
-        ExtraApkSpec(File apk, File idsig) {
-            this.apk = apk;
-            this.idsig = idsig;
-        }
-    }
-
     /**
      * Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
      * idsigs are to be generated.
      */
     @NonNull private final List<ExtraApkSpec> mExtraApks;
 
-    /** Size of the instance image. 10 MB. */
-    private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
+    private class MemoryManagementCallbacks implements ComponentCallbacks2 {
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {}
+
+        @Override
+        public void onLowMemory() {}
+
+        @Override
+        public void onTrimMemory(int level) {
+            @MemoryTrimLevel int vmTrimLevel;
+
+            switch (level) {
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
+                    vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_CRITICAL;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
+                    vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_LOW;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
+                    vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_MODERATE;
+                    break;
+                case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
+                case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
+                case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
+                    /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
+                    vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_CRITICAL;
+                    break;
+                default:
+                    /* Treat unrecognised messages as generic low-memory warnings. */
+                    vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_LOW;
+                    break;
+            }
+
+            synchronized (mLock) {
+                try {
+                    if (mVirtualMachine != null) {
+                        mVirtualMachine.onTrimMemory(vmTrimLevel);
+                    }
+                } catch (Exception e) {
+                    /* Caller doesn't want our exceptions. Log them instead. */
+                    Log.w(TAG, "TrimMemory failed: ", e);
+                }
+            }
+        }
+    }
+
+    @NonNull private final MemoryManagementCallbacks mMemoryManagementCallbacks;
+
+    @NonNull private final Context mContext;
 
     // A note on lock ordering:
     // You can take mLock while holding VirtualMachineManager.sCreateLock, but not vice versa.
@@ -248,6 +294,16 @@
     @Nullable
     private Executor mCallbackExecutor;
 
+    private static class ExtraApkSpec {
+        public final File apk;
+        public final File idsig;
+
+        ExtraApkSpec(File apk, File idsig) {
+            this.apk = apk;
+            this.idsig = idsig;
+        }
+    }
+
     static {
         System.loadLibrary("virtualmachine_jni");
     }
@@ -265,6 +321,8 @@
         mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
         mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
         mExtraApks = setupExtraApks(context, config, thisVmDir);
+        mMemoryManagementCallbacks = new MemoryManagementCallbacks();
+        mContext = context;
     }
 
     /**
@@ -415,7 +473,10 @@
     }
 
     @NonNull
-    private static File getVmDir(Context context, String name) {
+    private static File getVmDir(@NonNull Context context, @NonNull String name) {
+        if (name.contains(File.separator) || name.equals(".") || name.equals("..")) {
+            throw new IllegalArgumentException("Invalid VM name: " + name);
+        }
         File vmRoot = new File(context.getDataDir(), VM_DIR);
         return new File(vmRoot, name);
     }
@@ -690,6 +751,7 @@
                                 executeCallback((cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
                             }
                         });
+                mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 service.asBinder().linkToDeath(deathRecipient, 0);
                 mVirtualMachine.start();
             } catch (IOException | IllegalStateException | ServiceSpecificException e) {
@@ -754,7 +816,8 @@
      * computer; the machine halts immediately. Software running on the virtual machine is not
      * notified of the event. A stopped virtual machine can be re-started by calling {@link #run()}.
      *
-     * @throws VirtualMachineException if the virtual machine could not be stopped.
+     * @throws VirtualMachineException if the virtual machine is not running or could not be
+     *     stopped.
      * @hide
      */
     @SystemApi
@@ -765,6 +828,7 @@
             }
             try {
                 mVirtualMachine.stop();
+                mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine = null;
             } catch (RemoteException e) {
                 throw e.rethrowAsRuntimeException();
@@ -775,15 +839,32 @@
     }
 
     /**
-     * Stops this virtual machine. See {@link #stop()}.
+     * Stops this virtual machine, if it is running.
      *
-     * @throws VirtualMachineException if the virtual machine could not be stopped.
+     * @see #stop()
      * @hide
      */
     @SystemApi
     @Override
-    public void close() throws VirtualMachineException {
-        stop();
+    public void close() {
+        synchronized (mLock) {
+            if (mVirtualMachine == null) {
+                return;
+            }
+            try {
+                if (stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
+                    mVirtualMachine.stop();
+                    mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
+                    mVirtualMachine = null;
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            } catch (ServiceSpecificException e) {
+                // Deliberately ignored; this almost certainly means the VM exited just as
+                // we tried to stop it.
+                Log.i(TAG, "Ignoring error on close()", e);
+            }
+        }
     }
 
     private static void deleteRecursively(File dir) throws IOException {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 8678b99..f9f29a1 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -27,6 +27,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -65,9 +66,6 @@
     private static final String KEY_MEMORY_MIB = "memoryMib";
     private static final String KEY_NUM_CPUS = "numCpus";
 
-    // Absolute path to the APK file containing the VM payload.
-    @NonNull private final String mApkPath;
-
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "DEBUG_LEVEL_", value = {
@@ -101,6 +99,9 @@
      */
     @SystemApi public static final int DEBUG_LEVEL_FULL = 2;
 
+    /** Absolute path to the APK file containing the VM payload. */
+    @NonNull private final String mApkPath;
+
     @DebugLevel private final int mDebugLevel;
 
     /**
@@ -284,7 +285,7 @@
      *
      * @hide
      */
-    @SystemApi // TODO(b/243512115): Switch back to @TestApi
+    @TestApi
     @Nullable
     public String getPayloadConfigPath() {
         return mPayloadConfigPath;
@@ -471,7 +472,7 @@
          * @hide
          */
         @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
-        @SystemApi // TODO(b/243512115): Switch to @TestApi
+        @TestApi
         @NonNull
         public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
             mPayloadConfigPath = requireNonNull(payloadConfigPath);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index a520ab4..ea0a305 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -21,10 +21,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
-import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.sysprop.HypervisorProperties;
 import android.util.ArrayMap;
 
@@ -34,7 +35,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.util.Map;
-import java.util.WeakHashMap;
 
 /**
  * Manages {@link VirtualMachine virtual machine} instances created by an app. Each instance is
@@ -43,12 +43,17 @@
  *
  * <p>Each virtual machine instance is named; the configuration and related state of each is
  * persisted in the app's private data directory and an instance can be retrieved given the name.
+ * The name must be a valid directory name and must not contain '/'.
  *
  * <p>The app can then start, stop and otherwise interact with the VM.
  *
+ * <p>An instance of {@link VirtualMachineManager} can be obtained by calling {@link
+ * Context#getSystemService(Class)}.
+ *
  * @hide
  */
 @SystemApi
+@RequiresFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)
 public class VirtualMachineManager {
     /**
      * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
@@ -59,14 +64,11 @@
 
     @NonNull private final Context mContext;
 
-    private VirtualMachineManager(@NonNull Context context) {
-        mContext = context;
+    /** @hide */
+    public VirtualMachineManager(@NonNull Context context) {
+        mContext = requireNonNull(context);
     }
 
-    @GuardedBy("sInstances")
-    private static final Map<Context, WeakReference<VirtualMachineManager>> sInstances =
-            new WeakHashMap<>();
-
     @GuardedBy("sCreateLock")
     private final Map<String, WeakReference<VirtualMachine>> mVmsByName = new ArrayMap<>();
 
@@ -93,27 +95,6 @@
     public static final int CAPABILITY_NON_PROTECTED_VM = 2;
 
     /**
-     * Returns the per-context instance.
-     *
-     * @hide
-     */
-    @SystemApi
-    @NonNull
-    @SuppressLint("ManagerLookup") // TODO(b/249093790): remove
-    public static VirtualMachineManager getInstance(@NonNull Context context) {
-        requireNonNull(context, "context must not be null");
-        synchronized (sInstances) {
-            VirtualMachineManager vmm =
-                    sInstances.containsKey(context) ? sInstances.get(context).get() : null;
-            if (vmm == null) {
-                vmm = new VirtualMachineManager(context);
-                sInstances.put(context, new WeakReference<>(vmm));
-            }
-            return vmm;
-        }
-    }
-
-    /**
      * Returns a set of flags indicating what this implementation of virtualization is capable of.
      *
      * @see #CAPABILITY_PROTECTED_VM
diff --git a/javalib/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java b/javalib/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
new file mode 100644
index 0000000..30ac425
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Holds initialization code for virtualization module
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class VirtualizationFrameworkInitializer {
+
+    private VirtualizationFrameworkInitializer() {}
+
+    /**
+     * Called by the static initializer in the {@link SystemServiceRegistry}, and registers {@link
+     * VirtualMachineManager} to the {@link Context}. so that it's accessible from {@link
+     * Context#getSystemService(String)}.
+     */
+    public static void registerServiceWrappers() {
+        // Note: it's important that the getPackageManager().hasSystemFeature() check is executed
+        // in the lambda, and not directly in the registerServiceWrappers method. The
+        // registerServiceWrappers is called during Zygote static initialization, and at that
+        // point the PackageManager is not available yet.
+        //
+        // On the other hand, the lambda is executed after the app calls Context.getSystemService
+        // (VirtualMachineManager.class), at which point the PackageManager is available. The
+        // result of the lambda is cached on per-context basis.
+        SystemServiceRegistry.registerContextAwareService(
+                Context.VIRTUALIZATION_SERVICE,
+                VirtualMachineManager.class,
+                ctx ->
+                        ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK)
+                                ? new VirtualMachineManager(ctx)
+                                : null);
+    }
+}
diff --git a/launcher/main.cpp b/launcher/main.cpp
index 18a768d..ae55be9 100644
--- a/launcher/main.cpp
+++ b/launcher/main.cpp
@@ -34,12 +34,21 @@
         const char* name, const char* ld_library_path, const char* default_library_path,
         uint64_t type, const char* permitted_when_isolated_path,
         struct android_namespace_t* parent);
+
+extern bool android_link_namespaces(struct android_namespace_t* from,
+                                    struct android_namespace_t* to,
+                                    const char* shared_libs_sonames);
 } // extern "C"
 
 static void* load(const std::string& libname);
 
 constexpr char entrypoint_name[] = "AVmPayload_main";
 
+static constexpr const char* kAllowedLibs[] = {
+        "libc.so",   "libm.so",          "libdl.so",         "libdl_android.so",
+        "liblog.so", "libvm_payload.so", "libbinder_ndk.so", "libbinder_rpc_unstable.so",
+};
+
 int main(int argc, char* argv[]) {
     if (argc != 2) {
         std::cout << "Usage:\n";
@@ -69,8 +78,8 @@
 void* load(const std::string& libname) {
     // Parent as nullptr means the default namespace
     android_namespace_t* parent = nullptr;
-    // The search paths of the new namespace are inherited from the parent namespace.
-    const uint64_t type = ANDROID_NAMESPACE_TYPE_SHARED;
+    // The search paths of the new namespace are isolated to restrict system private libraries.
+    const uint64_t type = ANDROID_NAMESPACE_TYPE_ISOLATED;
     // The directory of the library is appended to the search paths
     const std::string libdir = libname.substr(0, libname.find_last_of("/"));
     const char* ld_library_path = libdir.c_str();
@@ -84,6 +93,13 @@
         return nullptr;
     }
 
+    std::string libs;
+    for (const char* lib : kAllowedLibs) {
+        if (!libs.empty()) libs += ':';
+        libs += lib;
+    }
+    android_link_namespaces(new_ns, nullptr, libs.c_str());
+
     const android_dlextinfo info = {
             .flags = ANDROID_DLEXT_USE_NAMESPACE,
             .library_namespace = new_ns,
diff --git a/libs/avb_bindgen/Android.bp b/libs/avb/Android.bp
similarity index 67%
rename from libs/avb_bindgen/Android.bp
rename to libs/avb/Android.bp
index 80b96a6..28e969d 100644
--- a/libs/avb_bindgen/Android.bp
+++ b/libs/avb/Android.bp
@@ -13,6 +13,9 @@
     bindgen_flags: [
         "--size_t-is-usize",
         "--allowlist-function=.*",
+        "--use-core",
+        "--raw-line=#![no_std]",
+        "--ctypes-prefix=core::ffi",
     ],
     static_libs: [
         "libavb",
@@ -33,3 +36,18 @@
     clippy_lints: "none",
     lints: "none",
 }
+
+rust_library_rlib {
+    name: "libavb_nostd",
+    crate_name: "avb_nostd",
+    srcs: ["src/lib.rs"],
+    no_stdlibs: true,
+    prefer_rlib: true,
+    stdlibs: [
+        "libcore.rust_sysroot",
+    ],
+    rustlibs: [
+        "libavb_bindgen",
+        "liblog_rust_nostd",
+    ],
+}
diff --git a/libs/avb_bindgen/bindgen/avb.h b/libs/avb/bindgen/avb.h
similarity index 100%
rename from libs/avb_bindgen/bindgen/avb.h
rename to libs/avb/bindgen/avb.h
diff --git a/libs/avb/src/avb_ops.rs b/libs/avb/src/avb_ops.rs
new file mode 100644
index 0000000..900e152
--- /dev/null
+++ b/libs/avb/src/avb_ops.rs
@@ -0,0 +1,153 @@
+// Copyright 2022, 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.
+
+//! This module regroups methods related to AvbOps.
+
+#![warn(unsafe_op_in_unsafe_fn)]
+// TODO(b/256148034): Remove this when the feature is code complete.
+#![allow(dead_code)]
+#![allow(unused_imports)]
+
+extern crate alloc;
+
+use alloc::ffi::CString;
+use avb_bindgen::{
+    avb_slot_verify, AvbHashtreeErrorMode_AVB_HASHTREE_ERROR_MODE_EIO,
+    AvbSlotVerifyFlags_AVB_SLOT_VERIFY_FLAGS_NO_VBMETA_PARTITION,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_IO,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_OOM,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION,
+    AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_OK,
+};
+use core::fmt;
+use log::debug;
+
+/// Error code from AVB image verification.
+#[derive(Clone, Copy, Debug)]
+pub enum AvbImageVerifyError {
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT
+    InvalidArgument,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA
+    InvalidMetadata,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_IO
+    Io,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_OOM
+    Oom,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED
+    PublicKeyRejected,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX
+    RollbackIndex,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION
+    UnsupportedVersion,
+    /// AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION
+    Verification,
+    /// Unknown error.
+    Unknown(u32),
+}
+
+fn to_avb_verify_result(result: u32) -> Result<(), AvbImageVerifyError> {
+    #[allow(non_upper_case_globals)]
+    match result {
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_OK => Ok(()),
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT => {
+            Err(AvbImageVerifyError::InvalidArgument)
+        }
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA => {
+            Err(AvbImageVerifyError::InvalidMetadata)
+        }
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_IO => Err(AvbImageVerifyError::Io),
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_OOM => Err(AvbImageVerifyError::Oom),
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED => {
+            Err(AvbImageVerifyError::PublicKeyRejected)
+        }
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX => {
+            Err(AvbImageVerifyError::RollbackIndex)
+        }
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION => {
+            Err(AvbImageVerifyError::UnsupportedVersion)
+        }
+        AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION => {
+            Err(AvbImageVerifyError::Verification)
+        }
+        _ => Err(AvbImageVerifyError::Unknown(result)),
+    }
+}
+
+impl fmt::Display for AvbImageVerifyError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::InvalidArgument => write!(f, "Invalid parameters."),
+            Self::InvalidMetadata => write!(f, "Invalid metadata."),
+            Self::Io => write!(f, "I/O error while trying to load data or get a rollback index."),
+            Self::Oom => write!(f, "Unable to allocate memory."),
+            Self::PublicKeyRejected => write!(
+                f,
+                "Everything is verified correctly out but the public key is not accepted. \
+                This includes the case where integrity data is not signed."
+            ),
+            Self::RollbackIndex => write!(f, "Rollback index is less than its stored value."),
+            Self::UnsupportedVersion => write!(
+                f,
+                "Some of the metadata requires a newer version of libavb than what is in use."
+            ),
+            Self::Verification => write!(f, "Data does not verify."),
+            Self::Unknown(e) => write!(f, "Unknown avb_slot_verify error '{e}'"),
+        }
+    }
+}
+
+/// Verifies that for the given image:
+///  - The given public key is acceptable.
+///  - The VBMeta struct is valid.
+///  - The partitions of the image match the descriptors of the verified VBMeta struct.
+/// Returns Ok if everything is verified correctly and the public key is accepted.
+pub fn verify_image(image: &[u8], public_key: &[u8]) -> Result<(), AvbImageVerifyError> {
+    AvbOps::new().verify_image(image, public_key)
+}
+
+/// TODO(b/256148034): Make AvbOps a rust wrapper of avb_bindgen::AvbOps using foreign_types.
+struct AvbOps {}
+
+impl AvbOps {
+    fn new() -> Self {
+        AvbOps {}
+    }
+
+    fn verify_image(&self, image: &[u8], public_key: &[u8]) -> Result<(), AvbImageVerifyError> {
+        debug!("AVB image: addr={:?}, size={:#x} ({1})", image.as_ptr(), image.len());
+        debug!(
+            "AVB public key: addr={:?}, size={:#x} ({1})",
+            public_key.as_ptr(),
+            public_key.len()
+        );
+        // TODO(b/256148034): Verify the kernel image with avb_slot_verify()
+        // let result = unsafe {
+        //     avb_slot_verify(
+        //         self.as_ptr(),
+        //         requested_partitions.as_ptr(),
+        //         ab_suffix.as_ptr(),
+        //         AvbSlotVerifyFlags_AVB_SLOT_VERIFY_FLAGS_NO_VBMETA_PARTITION,
+        //         AvbHashtreeErrorMode_AVB_HASHTREE_ERROR_MODE_EIO,
+        //         &image.as_ptr(),
+        //     )
+        // };
+        let result = AvbSlotVerifyResult_AVB_SLOT_VERIFY_RESULT_OK;
+        to_avb_verify_result(result)
+    }
+}
diff --git a/libs/avb/src/lib.rs b/libs/avb/src/lib.rs
new file mode 100644
index 0000000..81b554d
--- /dev/null
+++ b/libs/avb/src/lib.rs
@@ -0,0 +1,21 @@
+// Copyright 2022, 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.
+
+//! This module regroups the rust API for libavb.
+
+#![no_std]
+
+mod avb_ops;
+
+pub use avb_ops::{verify_image, AvbImageVerifyError};
diff --git a/libs/devicemapper/Android.bp b/libs/devicemapper/Android.bp
index 783fa79..9fa010c 100644
--- a/libs/devicemapper/Android.bp
+++ b/libs/devicemapper/Android.bp
@@ -13,6 +13,7 @@
         "libbitflags",
         "liblibc",
         "libdata_model",
+        "libhex",
         "libnix",
         "libuuid",
     ],
@@ -33,6 +34,7 @@
     defaults: ["libdm_rust.defaults"],
     test_suites: ["general-tests"],
     rustlibs: [
+        "librustutils",
         "libscopeguard",
         "libtempfile",
     ],
diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs
index 9b715a5..b2e677a 100644
--- a/libs/devicemapper/src/crypt.rs
+++ b/libs/devicemapper/src/crypt.rs
@@ -17,10 +17,9 @@
 /// `crypt` module implements the "crypt" target in the device mapper framework. Specifically,
 /// it provides `DmCryptTargetBuilder` struct which is used to construct a `DmCryptTarget` struct
 /// which is then given to `DeviceMapper` to create a mapper device.
-use crate::util::*;
 use crate::DmTargetSpec;
 
-use anyhow::{bail, Context, Result};
+use anyhow::{ensure, Context, Result};
 use data_model::DataInit;
 use std::io::Write;
 use std::mem::size_of;
@@ -32,10 +31,34 @@
 // Documentation/admin-guide/device-mapper/dm-crypt.rst
 
 /// Supported ciphers
+#[derive(Clone, Copy, Debug)]
 pub enum CipherType {
-    // TODO(b/253394457) Include ciphers with authenticated modes as well
+    // AES-256-HCTR2 takes a 32-byte key
+    AES256HCTR2,
+    // XTS requires key of twice the length of the underlying block cipher i.e., 64B for AES256
     AES256XTS,
 }
+impl CipherType {
+    fn get_kernel_crypto_name(&self) -> &str {
+        match *self {
+            // We use "plain64" as the IV/nonce generation algorithm -
+            // which basically is the sector number.
+            CipherType::AES256HCTR2 => "aes-hctr2-plain64",
+            CipherType::AES256XTS => "aes-xts-plain64",
+        }
+    }
+
+    fn get_required_key_size(&self) -> usize {
+        match *self {
+            CipherType::AES256HCTR2 => 32,
+            CipherType::AES256XTS => 64,
+        }
+    }
+
+    fn validata_key_size(&self, key_size: usize) -> bool {
+        key_size == self.get_required_key_size()
+    }
+}
 
 pub struct DmCryptTarget(Box<[u8]>);
 
@@ -59,7 +82,7 @@
 impl<'a> Default for DmCryptTargetBuilder<'a> {
     fn default() -> Self {
         DmCryptTargetBuilder {
-            cipher: CipherType::AES256XTS,
+            cipher: CipherType::AES256HCTR2,
             key: None,
             iv_offset: 0,
             device_path: None,
@@ -112,8 +135,13 @@
             .to_str()
             .context("data device path is not encoded in utf8")?;
 
-        let key =
-            if let Some(key) = self.key { hexstring_from(key) } else { bail!("key is not set") };
+        ensure!(self.key.is_some(), "key is not set");
+        // Unwrap is safe because we already made sure key.is_some()
+        ensure!(
+            self.cipher.validata_key_size(self.key.unwrap().len()),
+            format!("Invalid key size for cipher:{}", self.cipher.get_kernel_crypto_name())
+        );
+        let key = hex::encode(self.key.unwrap());
 
         // Step2: serialize the information according to the spec, which is ...
         // DmTargetSpec{...}
@@ -121,7 +149,7 @@
         // <offset> [<#opt_params> <opt_params>]
         let mut body = String::new();
         use std::fmt::Write;
-        write!(&mut body, "{} ", get_kernel_crypto_name(&self.cipher))?;
+        write!(&mut body, "{} ", self.cipher.get_kernel_crypto_name())?;
         write!(&mut body, "{} ", key)?;
         write!(&mut body, "{} ", self.iv_offset)?;
         write!(&mut body, "{} ", device_path)?;
@@ -145,9 +173,3 @@
         Ok(DmCryptTarget(buf.into_boxed_slice()))
     }
 }
-
-fn get_kernel_crypto_name(cipher: &CipherType) -> &str {
-    match cipher {
-        CipherType::AES256XTS => "aes-xts-plain64",
-    }
-}
diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs
index ebe71e4..9069eb2 100644
--- a/libs/devicemapper/src/lib.rs
+++ b/libs/devicemapper/src/lib.rs
@@ -234,12 +234,28 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crypt::DmCryptTargetBuilder;
+    use crypt::{CipherType, DmCryptTargetBuilder};
+    use rustutils::system_properties;
     use std::fs::{read, File, OpenOptions};
     use std::io::Write;
 
-    const KEY: &[u8; 32] = b"thirtytwobyteslongreallylongword";
-    const DIFFERENT_KEY: &[u8; 32] = b"drowgnolyllaergnolsetybowtytriht";
+    // Just a logical set of keys to make testing easy. This has no real meaning.
+    struct KeySet<'a> {
+        cipher: CipherType,
+        key: &'a [u8],
+        different_key: &'a [u8],
+    }
+
+    const KEY_SET_XTS: KeySet = KeySet {
+        cipher: CipherType::AES256XTS,
+        key: b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard",
+        different_key: b"drahtahtebtnacyrtatievigsteltuberareraecnetnesgnolsetybruofytxis",
+    };
+    const KEY_SET_HCTR2: KeySet = KeySet {
+        cipher: CipherType::AES256HCTR2,
+        key: b"thirtytwobyteslongreallylongword",
+        different_key: b"drowgnolyllaergnolsetybowtytriht",
+    };
 
     // Create a file in given temp directory with given size
     fn prepare_tmpfile(test_dir: &Path, filename: &str, sz: u64) -> PathBuf {
@@ -254,14 +270,54 @@
         f.write_all(data).unwrap();
     }
 
+    // TODO(b/250880499): delete_device() doesn't really delete it even without DM_DEFERRED_REMOVE.
+    // Hence, we have to create a new device with a different name for each test. Retrying
+    // the test on same machine without reboot will also fail.
     fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> {
         dm.delete_device_deferred(name)?;
         wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(name))?;
         Ok(())
     }
 
+    // TODO(b/260692911): Find a better way to skip a test instead of silently passing it.
+    fn is_hctr2_supported() -> bool {
+        // hctr2 is NOT enabled in kernel 5.10 or lower. We run Microdroid tests on kernel versions
+        // 5.10 or above & therefore,  we don't really care to skip test on other versions.
+        if let Some(version) = system_properties::read("ro.kernel.version")
+            .expect("Unable to read system property ro.kernel.version")
+        {
+            version != "5.10"
+        } else {
+            panic!("Could not read property: kernel.version!!");
+        }
+    }
+
     #[test]
-    fn mapping_again_keeps_data() {
+    fn mapping_again_keeps_data_xts() {
+        mapping_again_keeps_data(&KEY_SET_XTS, "name1");
+    }
+
+    #[test]
+    fn mapping_again_keeps_data_hctr2() {
+        if !is_hctr2_supported() {
+            return;
+        }
+        mapping_again_keeps_data(&KEY_SET_HCTR2, "name2");
+    }
+    #[test]
+    fn data_inaccessible_with_diff_key_xts() {
+        data_inaccessible_with_diff_key(&KEY_SET_XTS, "name3");
+    }
+
+    #[test]
+    fn data_inaccessible_with_diff_key_hctr2() {
+        if !is_hctr2_supported() {
+            return;
+        }
+        data_inaccessible_with_diff_key(&KEY_SET_HCTR2, "name4");
+    }
+
+    fn mapping_again_keeps_data(keyset: &KeySet, device: &str) {
         // This test creates 2 different crypt devices using same key backed by same data_device
         // -> Write data on dev1 -> Check the data is visible & same on dev2
         let dm = DeviceMapper::new().unwrap();
@@ -278,28 +334,33 @@
             /*writable*/ true,
         )
         .unwrap();
+        let device_diff = device.to_owned() + "_diff";
+
         scopeguard::defer! {
             loopdevice::detach(&data_device).unwrap();
-            _ = delete_device(&dm, "crypt1");
-            _ = delete_device(&dm, "crypt2");
+            _ = delete_device(&dm, device);
+            _ = delete_device(&dm, &device_diff);
         }
 
-        let target =
-            DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap();
+        let target = DmCryptTargetBuilder::default()
+            .data_device(&data_device, sz)
+            .cipher(keyset.cipher)
+            .key(keyset.key)
+            .build()
+            .unwrap();
 
-        let mut crypt_device = dm.create_crypt_device("crypt1", &target).unwrap();
+        let mut crypt_device = dm.create_crypt_device(device, &target).unwrap();
         write_to_dev(&crypt_device, inputimg);
 
         // Recreate another device using same target spec & check if the content is the same
-        crypt_device = dm.create_crypt_device("crypt2", &target).unwrap();
+        crypt_device = dm.create_crypt_device(&device_diff, &target).unwrap();
 
         let crypt = read(crypt_device).unwrap();
         assert_eq!(inputimg.len(), crypt.len()); // fail early if the size doesn't match
         assert_eq!(inputimg, crypt.as_slice());
     }
 
-    #[test]
-    fn data_inaccessible_with_diff_key() {
+    fn data_inaccessible_with_diff_key(keyset: &KeySet, device: &str) {
         // This test creates 2 different crypt devices using different keys backed
         // by same data_device -> Write data on dev1 -> Check the data is visible but not the same on dev2
         let dm = DeviceMapper::new().unwrap();
@@ -316,26 +377,32 @@
             /*writable*/ true,
         )
         .unwrap();
+        let device_diff = device.to_owned() + "_diff";
         scopeguard::defer! {
             loopdevice::detach(&data_device).unwrap();
-            _ = delete_device(&dm, "crypt3");
-            _ = delete_device(&dm, "crypt4");
+            _ = delete_device(&dm, device);
+            _ = delete_device(&dm, &device_diff);
         }
 
-        let target =
-            DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap();
+        let target = DmCryptTargetBuilder::default()
+            .data_device(&data_device, sz)
+            .cipher(keyset.cipher)
+            .key(keyset.key)
+            .build()
+            .unwrap();
         let target2 = DmCryptTargetBuilder::default()
             .data_device(&data_device, sz)
-            .key(DIFFERENT_KEY)
+            .cipher(keyset.cipher)
+            .key(keyset.different_key)
             .build()
             .unwrap();
 
-        let mut crypt_device = dm.create_crypt_device("crypt3", &target).unwrap();
+        let mut crypt_device = dm.create_crypt_device(device, &target).unwrap();
 
         write_to_dev(&crypt_device, inputimg);
 
         // Recreate the crypt device again diff key & check if the content is changed
-        crypt_device = dm.create_crypt_device("crypt4", &target2).unwrap();
+        crypt_device = dm.create_crypt_device(&device_diff, &target2).unwrap();
         let crypt = read(crypt_device).unwrap();
         assert_ne!(inputimg, crypt.as_slice());
     }
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 01f7b36..ff1db63 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -16,6 +16,7 @@
 //! to a bare-metal environment.
 
 #![no_std]
+#![feature(let_else)] // Stabilized in 1.65.0
 
 use core::ffi::{c_int, c_void, CStr};
 use core::fmt;
@@ -136,6 +137,14 @@
     }
 }
 
+fn fdt_err_or_option(val: c_int) -> Result<Option<c_int>> {
+    match fdt_err(val) {
+        Ok(val) => Ok(Some(val)),
+        Err(FdtError::NotFound) => Ok(None),
+        Err(e) => Err(e),
+    }
+}
+
 /// Value of a #address-cells property.
 #[derive(Copy, Clone, Debug)]
 enum AddrCells {
@@ -251,8 +260,8 @@
 }
 
 impl<'a> MemRegIterator<'a> {
-    fn new(reg: RegIterator<'a>) -> Result<Self> {
-        Ok(Self { reg })
+    fn new(reg: RegIterator<'a>) -> Self {
+        Self { reg }
     }
 }
 
@@ -285,45 +294,67 @@
     }
 
     /// Retrieve the standard (deprecated) device_type <string> property.
-    pub fn device_type(&self) -> Result<&CStr> {
+    pub fn device_type(&self) -> Result<Option<&CStr>> {
         self.getprop_str(CStr::from_bytes_with_nul(b"device_type\0").unwrap())
     }
 
     /// Retrieve the standard reg <prop-encoded-array> property.
-    pub fn reg(&self) -> Result<RegIterator<'a>> {
-        let parent = self.parent()?;
+    pub fn reg(&self) -> Result<Option<RegIterator<'a>>> {
+        let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
 
-        let addr_cells = parent.address_cells()?;
-        let size_cells = parent.size_cells()?;
-        let cells = self.getprop_cells(CStr::from_bytes_with_nul(b"reg\0").unwrap())?;
+        if let Some(cells) = self.getprop_cells(reg)? {
+            let parent = self.parent()?;
 
-        Ok(RegIterator::new(cells, addr_cells, size_cells))
+            let addr_cells = parent.address_cells()?;
+            let size_cells = parent.size_cells()?;
+
+            Ok(Some(RegIterator::new(cells, addr_cells, size_cells)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the value of a given <string> property.
-    pub fn getprop_str(&self, name: &CStr) -> Result<&CStr> {
-        CStr::from_bytes_with_nul(self.getprop(name)?).map_err(|_| FdtError::BadValue)
+    pub fn getprop_str(&self, name: &CStr) -> Result<Option<&CStr>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(CStr::from_bytes_with_nul(bytes).map_err(|_| FdtError::BadValue)?)
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given property as an array of cells.
-    pub fn getprop_cells(&self, name: &CStr) -> Result<CellIterator<'a>> {
-        Ok(CellIterator::new(self.getprop(name)?))
+    pub fn getprop_cells(&self, name: &CStr) -> Result<Option<CellIterator<'a>>> {
+        if let Some(cells) = self.getprop(name)? {
+            Ok(Some(CellIterator::new(cells)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the value of a given <u32> property.
-    pub fn getprop_u32(&self, name: &CStr) -> Result<u32> {
-        let prop = self.getprop(name)?.try_into().map_err(|_| FdtError::BadValue)?;
-        Ok(u32::from_be_bytes(prop))
+    pub fn getprop_u32(&self, name: &CStr) -> Result<Option<u32>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(u32::from_be_bytes(bytes.try_into().map_err(|_| FdtError::BadValue)?))
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given <u64> property.
-    pub fn getprop_u64(&self, name: &CStr) -> Result<u64> {
-        let prop = self.getprop(name)?.try_into().map_err(|_| FdtError::BadValue)?;
-        Ok(u64::from_be_bytes(prop))
+    pub fn getprop_u64(&self, name: &CStr) -> Result<Option<u64>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(u64::from_be_bytes(bytes.try_into().map_err(|_| FdtError::BadValue)?))
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given property.
-    pub fn getprop(&self, name: &CStr) -> Result<&'a [u8]> {
+    pub fn getprop(&self, name: &CStr) -> Result<Option<&'a [u8]>> {
         let mut len: i32 = 0;
         // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
@@ -337,14 +368,21 @@
                 &mut len as *mut i32,
             )
         } as *const u8;
+
+        let Some(len) = fdt_err_or_option(len)? else {
+            return Ok(None); // Property was not found.
+        };
+        let len = usize::try_from(len).map_err(|_| FdtError::Internal)?;
+
         if prop.is_null() {
-            return fdt_err(len).and(Err(FdtError::Internal));
+            // We expected an error code in len but still received a valid value?!
+            return Err(FdtError::Internal);
         }
-        let len = usize::try_from(fdt_err(len)?).map_err(|_| FdtError::Internal)?;
-        let base =
+
+        let offset =
             (prop as usize).checked_sub(self.fdt.as_ptr() as usize).ok_or(FdtError::Internal)?;
 
-        self.fdt.bytes.get(base..(base + len)).ok_or(FdtError::Internal)
+        Ok(Some(self.fdt.buffer.get(offset..(offset + len)).ok_or(FdtError::Internal)?))
     }
 
     /// Get reference to the containing device tree.
@@ -362,11 +400,7 @@
             )
         };
 
-        match fdt_err(ret) {
-            Ok(offset) => Ok(Some(Self { fdt: self.fdt, offset })),
-            Err(FdtError::NotFound) => Ok(None),
-            Err(e) => Err(e),
-        }
+        Ok(fdt_err_or_option(ret)?.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     fn address_cells(&self) -> Result<AddrCells> {
@@ -384,6 +418,69 @@
     }
 }
 
+/// Mutable FDT node.
+pub struct FdtNodeMut<'a> {
+    fdt: &'a mut Fdt,
+    offset: c_int,
+}
+
+impl<'a> FdtNodeMut<'a> {
+    /// Append a property name-value (possibly empty) pair to the given node.
+    pub fn appendprop<T: AsRef<[u8]>>(&mut self, name: &CStr, value: &T) -> Result<()> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_appendprop(
+                self.fdt.as_mut_ptr(),
+                self.offset,
+                name.as_ptr(),
+                value.as_ref().as_ptr().cast::<c_void>(),
+                value.as_ref().len().try_into().map_err(|_| FdtError::BadValue)?,
+            )
+        };
+
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Append a (address, size) pair property to the given node.
+    pub fn appendprop_addrrange(&mut self, name: &CStr, addr: u64, size: u64) -> Result<()> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_appendprop_addrrange(
+                self.fdt.as_mut_ptr(),
+                self.parent()?.offset,
+                self.offset,
+                name.as_ptr(),
+                addr,
+                size,
+            )
+        };
+
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Get reference to the containing device tree.
+    pub fn fdt(&mut self) -> &mut Fdt {
+        self.fdt
+    }
+
+    /// Add a new subnode to the given node and return it as a FdtNodeMut on success.
+    pub fn add_subnode(&'a mut self, name: &CStr) -> Result<Self> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_add_subnode(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
+        };
+
+        Ok(Self { fdt: self.fdt, offset: fdt_err(ret)? })
+    }
+
+    fn parent(&'a self) -> Result<FdtNode<'a>> {
+        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_parent_offset(self.fdt.as_ptr(), self.offset) };
+
+        Ok(FdtNode { fdt: &*self.fdt, offset: fdt_err(ret)? })
+    }
+}
+
 /// Iterator over nodes sharing a same compatible string.
 pub struct CompatibleIterator<'a> {
     node: FdtNode<'a>,
@@ -411,10 +508,10 @@
     }
 }
 
-/// Wrapper around low-level read-only libfdt functions.
+/// Wrapper around low-level libfdt functions.
 #[repr(transparent)]
 pub struct Fdt {
-    bytes: [u8],
+    buffer: [u8],
 }
 
 impl Fdt {
@@ -428,6 +525,16 @@
         Ok(fdt)
     }
 
+    /// Wraps a mutable slice containing a Flattened Device Tree.
+    ///
+    /// Fails if the FDT does not pass validation.
+    pub fn from_mut_slice(fdt: &mut [u8]) -> Result<&mut Self> {
+        // SAFETY - The FDT will be validated before it is returned.
+        let fdt = unsafe { Self::unchecked_from_mut_slice(fdt) };
+        fdt.check_full()?;
+        Ok(fdt)
+    }
+
     /// Wraps a slice containing a Flattened Device Tree.
     ///
     /// # Safety
@@ -437,35 +544,71 @@
         mem::transmute::<&[u8], &Self>(fdt)
     }
 
+    /// Wraps a mutable slice containing a Flattened Device Tree.
+    ///
+    /// # Safety
+    ///
+    /// The returned FDT might be invalid, only use on slices containing a valid DT.
+    pub unsafe fn unchecked_from_mut_slice(fdt: &mut [u8]) -> &mut Self {
+        mem::transmute::<&mut [u8], &mut Self>(fdt)
+    }
+
+    /// Make the whole slice containing the DT available to libfdt.
+    pub fn unpack(&mut self) -> Result<()> {
+        // SAFETY - "Opens" the DT in-place (supported use-case) by updating its header and
+        // internal structures to make use of the whole self.fdt slice but performs no accesses
+        // outside of it and leaves the DT in a state that will be detected by other functions.
+        let ret = unsafe {
+            libfdt_bindgen::fdt_open_into(
+                self.as_ptr(),
+                self.as_mut_ptr(),
+                self.capacity().try_into().map_err(|_| FdtError::Internal)?,
+            )
+        };
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Pack the DT to take a minimum amount of memory.
+    ///
+    /// Doesn't shrink the underlying memory slice.
+    pub fn pack(&mut self) -> Result<()> {
+        // SAFETY - "Closes" the DT in-place by updating its header and relocating its structs.
+        let ret = unsafe { libfdt_bindgen::fdt_pack(self.as_mut_ptr()) };
+        fdt_err_expect_zero(ret)
+    }
+
     /// Return an iterator of memory banks specified the "/memory" node.
     ///
     /// NOTE: This does not support individual "/memory@XXXX" banks.
-    pub fn memory(&self) -> Result<MemRegIterator> {
+    pub fn memory(&self) -> Result<Option<MemRegIterator>> {
         let memory = CStr::from_bytes_with_nul(b"/memory\0").unwrap();
         let device_type = CStr::from_bytes_with_nul(b"memory\0").unwrap();
 
-        let node = self.node(memory)?;
-        if node.device_type()? != device_type {
-            return Err(FdtError::BadValue);
-        }
+        if let Some(node) = self.node(memory)? {
+            if node.device_type()? != Some(device_type) {
+                return Err(FdtError::BadValue);
+            }
+            let reg = node.reg()?.ok_or(FdtError::BadValue)?;
 
-        MemRegIterator::new(node.reg()?)
+            Ok(Some(MemRegIterator::new(reg)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the standard /chosen node.
-    pub fn chosen(&self) -> Result<FdtNode> {
+    pub fn chosen(&self) -> Result<Option<FdtNode>> {
         self.node(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
     }
 
     /// Get the root node of the tree.
     pub fn root(&self) -> Result<FdtNode> {
-        self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())
+        self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
     }
 
     /// Find a tree node by its full path.
-    pub fn node(&self, path: &CStr) -> Result<FdtNode> {
-        let offset = self.path_offset(path)?;
-        Ok(FdtNode { fdt: self, offset })
+    pub fn node(&self, path: &CStr) -> Result<Option<FdtNode>> {
+        Ok(self.path_offset(path)?.map(|offset| FdtNode { fdt: self, offset }))
     }
 
     /// Iterate over nodes with a given compatible string.
@@ -473,7 +616,17 @@
         CompatibleIterator::new(self, compatible)
     }
 
-    fn path_offset(&self, path: &CStr) -> Result<c_int> {
+    /// Get the mutable root node of the tree.
+    pub fn root_mut(&mut self) -> Result<FdtNodeMut> {
+        self.node_mut(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
+    }
+
+    /// Find a mutable tree node by its full path.
+    pub fn node_mut(&mut self, path: &CStr) -> Result<Option<FdtNodeMut>> {
+        Ok(self.path_offset(path)?.map(|offset| FdtNodeMut { fdt: self, offset }))
+    }
+
+    fn path_offset(&self, path: &CStr) -> Result<Option<c_int>> {
         let len = path.to_bytes().len().try_into().map_err(|_| FdtError::BadPath)?;
         // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
@@ -482,11 +635,11 @@
             libfdt_bindgen::fdt_path_offset_namelen(self.as_ptr(), path.as_ptr(), len)
         };
 
-        fdt_err(ret)
+        fdt_err_or_option(ret)
     }
 
     fn check_full(&self) -> Result<()> {
-        let len = self.bytes.len();
+        let len = self.buffer.len();
         // SAFETY - Only performs read accesses within the limits of the slice. If successful, this
         // call guarantees to other unsafe calls that the header contains a valid totalsize (w.r.t.
         // 'len' i.e. the self.fdt slice) that those C functions can use to perform bounds
@@ -499,4 +652,12 @@
     fn as_ptr(&self) -> *const c_void {
         self as *const _ as *const c_void
     }
+
+    fn as_mut_ptr(&mut self) -> *mut c_void {
+        self as *mut _ as *mut c_void
+    }
+
+    fn capacity(&self) -> usize {
+        self.buffer.len()
+    }
 }
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
index c5078c2..a487097 100644
--- a/libs/vbmeta/Android.bp
+++ b/libs/vbmeta/Android.bp
@@ -27,7 +27,11 @@
         "libanyhow",
         "libtempfile",
     ],
-    data: ["tests/data/*"],
+    data: [
+        ":avb_testkey_rsa2048",
+        ":avb_testkey_rsa4096",
+        ":avb_testkey_rsa8192",
+    ],
     required: ["avbtool"],
     test_suites: ["general-tests"],
     test_options: {
diff --git a/libs/vbmeta/src/lib.rs b/libs/vbmeta/src/lib.rs
index 887844c..8e81ea4 100644
--- a/libs/vbmeta/src/lib.rs
+++ b/libs/vbmeta/src/lib.rs
@@ -238,7 +238,7 @@
         Ok(())
     }
 
-    fn test_signed_image(algorithm: &str, key: &str) -> Result<()> {
+    fn signed_image_has_valid_vbmeta(algorithm: &str, key: &str) -> Result<()> {
         let test_dir = TempDir::new().unwrap();
         let test_file = test_dir.path().join("test.img");
         let mut cmd = Command::new("./avbtool");
@@ -289,16 +289,16 @@
 
     #[test]
     fn test_rsa2048_signed_image() -> Result<()> {
-        test_signed_image("SHA256_RSA2048", "tests/data/testkey_rsa2048.pem")
+        signed_image_has_valid_vbmeta("SHA256_RSA2048", "data/testkey_rsa2048.pem")
     }
 
     #[test]
     fn test_rsa4096_signed_image() -> Result<()> {
-        test_signed_image("SHA256_RSA4096", "tests/data/testkey_rsa4096.pem")
+        signed_image_has_valid_vbmeta("SHA256_RSA4096", "data/testkey_rsa4096.pem")
     }
 
     #[test]
     fn test_rsa8192_signed_image() -> Result<()> {
-        test_signed_image("SHA256_RSA8192", "tests/data/testkey_rsa8192.pem")
+        signed_image_has_valid_vbmeta("SHA256_RSA8192", "data/testkey_rsa8192.pem")
     }
 }
diff --git a/libs/vbmeta/tests/data/testkey_rsa2048.pem b/libs/vbmeta/tests/data/testkey_rsa2048.pem
deleted file mode 100644
index 867dcff..0000000
--- a/libs/vbmeta/tests/data/testkey_rsa2048.pem
+++ /dev/null
@@ -1,27 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIEowIBAAKCAQEAxlVR3TIkouAOvH79vaJTgFhpfvVKQIeVkFRZPVXK/zY0Gvrh
-4JAqGjJoW/PfrQv5sdD36qtHH3a+G5hLZ6Ni+t/mtfjucxZfuLGC3kmJ1T3XqEKZ
-gXXI2IR7vVSoImREvDQGEDyJwtHzLANlkbGg0cghVhWZSCAndO8BenalC2v94/rt
-DfkPekH6dgU3Sf40T0sBSeSY94mOzTaqOR2pfV1rWlLRdWmo33zeHBv52Rlbt0dM
-uXAureXWiHztkm5GCBC1dgM+CaxNtizNEgC91KcD0xuRCCM2WxH+r1lpszyIJDct
-YbrFmVEYl/kjQpafhy7Nsk1fqSTyRdriZSYmTQIDAQABAoIBAQC+kJgaCuX8wYAn
-SXWQ0fmdZlXnMNRpcF0a0pD0SAzGb1RdYBXMaXiqtyhiwc53PPxsCDdNecjayIMd
-jJVXPTwLhTruOgMS/bp3gcgWwV34UHV4LJXGOGAE+jbS0hbDBMiudOYmj6RmVshp
-z9G1zZCSQNMXHaWsEYkX59XpzzoB384nRul2QgEtwzUNR9XlpzgtJBLk3SACkvsN
-mQ/DW8IWHXLg8vLn1LzVJ2e3B16H4MoE2TCHxqfMgr03IDRRJogkenQuQsFhevYT
-o/mJyHSWavVgzMHG9I5m+eepF4Wyhj1Y4WyKAuMI+9dHAX/h7Lt8XFCQCh5DbkVG
-zGr34sWBAoGBAOs7n7YZqNaaguovfIdRRsxxZr1yJAyDsr6w3yGImDZYju4c4WY9
-5esO2kP3FA4p0c7FhQF5oOb1rBuHEPp36cpL4aGeK87caqTfq63WZAujoTZpr9Lp
-BRbkL7w/xG7jpQ/clpA8sHzHGQs/nelxoOtC7E118FiRgvD/jdhlMyL9AoGBANfX
-vyoN1pplfT2xR8QOjSZ+Q35S/+SAtMuBnHx3l0qH2bbBjcvM1MNDWjnRDyaYhiRu
-i+KA7tqfib09+XpB3g5D6Ov7ls/Ldx0S/VcmVWtia2HK8y8iLGtokoBZKQ5AaFX2
-iQU8+tC4h69GnJYQKqNwgCUzh8+gHX5Y46oDiTmRAoGAYpOx8lX+czB8/Da6MNrW
-mIZNT8atZLEsDs2ANEVRxDSIcTCZJId7+m1W+nRoaycLTWNowZ1+2ErLvR10+AGY
-b7Ys79Wg9idYaY9yGn9lnZsMzAiuLeyIvXcSqgjvAKlVWrhOQFOughvNWvFl85Yy
-oWSCMlPiTLtt7CCsCKsgKuECgYBgdIp6GZsIfkgclKe0hqgvRoeU4TR3gcjJlM9A
-lBTo+pKhaBectplx9RxR8AnsPobbqwcaHnIfAuKDzjk5mEvKZjClnFXF4HAHbyAF
-nRzZEy9XkWFhc80T5rRpZO7C7qdxmu2aiKixM3V3L3/0U58qULEDbubHMw9bEhAT
-PudI8QKBgHEEiMm/hr9T41hbQi/LYanWnlFw1ue+osKuF8bXQuxnnHNuFT/c+9/A
-vWhgqG6bOEHu+p/IPrYm4tBMYlwsyh4nXCyGgDJLbLIfzKwKAWCtH9LwnyDVhOow
-GH9shdR+sW3Ew97xef02KAH4VlNANEmBV4sQNqWWvsYrcFm2rOdL
------END RSA PRIVATE KEY-----
diff --git a/libs/vbmeta/tests/data/testkey_rsa4096.pem b/libs/vbmeta/tests/data/testkey_rsa4096.pem
deleted file mode 100644
index 26db5c3..0000000
--- a/libs/vbmeta/tests/data/testkey_rsa4096.pem
+++ /dev/null
@@ -1,51 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIJKQIBAAKCAgEA2ASv49OEbH4NiT3CjNMSVeliyfEPXswWcqtEfCxlSpS1FisA
-uwbvEwdTTPlkuSh6G4SYiNhnpCP5p0vcSg/3OhiuVKgV/rCtrDXaO60nvK/o0y83
-NNZRK2xaJ9eWBq9ruIDK+jC0sYWzTaqqwxY0Grjnx/r5CXerl5PrRK7PILzwgBHb
-IwxHcblt1ntgR4cWVpO3wiqasEwBDDDYk4fw7W6LvjBb9qav3YB8RV6PkZNeRP64
-ggfuecq/MXNiWOPNxLzCER2hSr/+J32h9jWjXsrcVy8+8Mldhmr4r2an7c247aFf
-upuFGtUJrpROO8/LXMl5gPfMpkqoatjTMRH59gJjKhot0RpmGxZBvb33TcBK5SdJ
-X39Y4yct5clmDlI4Fjj7FutTP+b96aJeJVnYeUX/A0wmogBajsJRoRX5e/RcgZsY
-RzXYLQXprQ81dBWjjovMJ9p8XeT6BNMFC7o6sklFL0fHDUE/l4BNP8G1u3Bfpzev
-SCISRS71D4eS4oQB+RIPFBUkzomZ7rnEF3BwFeq+xmwfYrP0LRaH+1YeRauuMuRe
-ke1TZl697a3mEjkNg8noa2wtpe7EWmaujJfXDWxJx/XEkjGLCe4z2qk3tkkY+A5g
-Rcgzke8gVxC+eC2DJtbKYfkv4L8FMFJaEhwAp13MfC7FlYujO/BDLl7dANsCAwEA
-AQKCAgAWoL8P/WsktjuSwb5sY/vKtgzcHH1Ar942GsysuTXPDy686LpF3R8T/jNy
-n7k2UBAia8xSoWCR6BbRuHeV5oA+PLGeOpE7QaSfonB+yc+cy0x3Or3ssfqEsu/q
-toGHp75/8DXS6WE0K04x94u1rdC9b9sPrrGBlWCLGzqM0kbuJfyHXdd3n2SofAUO
-b5QRSgxD+2tHUpEroHqHnWJCaf4J0QegX45yktlfOYNK/PHLDQXV8ly/ejc32M4Y
-Tv7hUtOOJTuq8VCg9OWZm2Zo1QuM9XEJTPCp5l3+o5vzO6yhk2gotDvD32CdA+3k
-tLJRP54M1Sn+IXb1gGKN9rKAtGJbenWIPlNObhQgkbwG89Qd+5rfMXsiPv1Hl1tK
-+tqwjD82/H3/ElaaMnwHCpeoGSp95OblAoBjzjMP2KsbvKSdL8O/rf1c3uOw9+DF
-cth0SA8y3ZzI11gJtb2QMGUrCny5n4sPGGbc3x38NdLhwbkPKZy60OiT4g2kNpdY
-dIitmAML2otttiF4AJM6AraPk8YVzkPLTksoL3azPBya5lIoDI2H3QvTtSvpXkXP
-yKchsDSWYbdqfplqC/X0Djp2/Zd8jpN5I6+1aSmpTmbwx/JTllY1N89FRZLIdxoh
-2k81LPiXhE6uRbjioJUlbnEWIpY2y2N2Clmxpjh0/IcXd1XImQKCAQEA7Zai+yjj
-8xit24aO9Tf3mZBXBjSaDodjC2KS1yCcAIXp6S7aH0wZipyZpQjys3zaBQyMRYFG
-bQqIfVAa6inWyDoofbAJHMu5BVcHFBPZvSS5YhDjc8XZ5dqSCxzIz9opIqAbm+b4
-aEV/3A3Jki5Dy8y/5j21GAK4Y4mqQOYzne7bDGi3Hyu041MGM4qfIcIkS5N1eHW4
-sDZJh6+K5tuxN5TX3nDZSpm9luNH8mLGgKAZ15b1LqXAtM5ycoBY9Hv082suPPom
-O+r0ybdRX6nDSH8+11y2KiP2kdVIUHCGkwlqgrux5YZyjCZPwOvEPhzSoOS+vBiF
-UVXA8idnxNLk1QKCAQEA6MIihDSXx+350fWqhQ/3Qc6gA/t2C15JwJ9+uFWA+gjd
-c/hn5HcmnmBJN4R04nLG/aU9SQur87a4mnC/Mp9JIARjHlZ/WNT4U0sJyPEVRg5U
-Z9VajAucWwi0JyJYCO1EMMy68Jp8qlTriK/L7nbD86JJ5ASxjojiN/0psK/Pk60F
-Rr+shKPi3jRQ1BDjDtAxOfo4ctf/nFbUM4bY0FNPQMP7WesoSKU0NBCRR6d0d2tq
-YflMjIQHx+N74P5jEdSCHTVGQm+dj47pUt3lLPLWc0bX1G/GekwXP4NUsR/70Hsi
-bwxkNnK2TSGzkt2rcOnutP125rJu6WpV7SNrq9rm7wKCAQAfMROcnbWviKHqnDPQ
-hdR/2K9UJTvEhInASOS2UZWpi+s1rez9BuSjigOx4wbaAZ4t44PW7C3uyt84dHfU
-HkIQb3I5bg8ENMrJpK9NN33ykwuzkDwMSwFcZ+Gci97hSubzoMl/IkeiiN1MapL4
-GhLUgsD+3UMVL+Y9SymK8637IgyoCGdiND6/SXsa8SwLJo3VTjqx4eKpX7cvlSBL
-RrRxc50TmwUsAhsd4CDl9YnSATLjVvJBeYlfM2tbFPaYwl1aR8v+PWkfnK0efm60
-fHki33HEnGteBPKuGq4vwVYpn6bYGwQz+f6335/A2DMfZHFSpjVURHPcRcHbCMla
-0cUxAoIBAQC25eYNkO478mo+bBbEXJlkoqLmvjAyGrNFo48F9lpVH6Y0vNuWkXJN
-PUgLUhAu6RYotjGENqG17rz8zt/PPY9Ok2P3sOx8t00y1mIn/hlDZXs55FM0fOMu
-PZaiscAPs7HDzvyOmDah+fzi+ZD8H2M3DS2W+YE0iaeJa2vZJS2t02W0BGXiDI33
-IZDqMyLYvwwPjOnShJydEzXID4xLl0tNjzLxo3GSNA7jYqlmbtV8CXIc7rMSL6WV
-ktIDKKJcnmpn3TcKeX6MEjaSIT82pNOS3fY3PmXuL+CMzfw8+u77Eecq78fHaTiL
-P5JGM93F6mzi19EY0tmInUBMCWtQLcENAoIBAQCg0KaOkb8T36qzPrtgbfou0E2D
-ufdpL1ugmD4edOFKQB5fDFQhLnSEVSJq3KUg4kWsXapQdsBd6kLdxS+K6MQrLBzr
-4tf0c7UCF1AzWk6wXMExZ8mRb2RkGZYQB2DdyhFB3TPmnq9CW8JCq+6kxg/wkU4s
-vM4JXzgcqVoSf42QJl+B9waeWhg0BTWx01lal4ds88HvEKmE0ik5GwiDbr7EvDDw
-E6UbZtQcIoSTIIZDgYqVFfR2DAho3wXJRsOXh433lEJ8X7cCDzrngFbQnlKrpwML
-Xgm0SIUc+Nf5poMM3rfLFK77t/ob4w+5PwRKcoSniyAxrHd6bwykYA8Vuydv
------END RSA PRIVATE KEY-----
diff --git a/libs/vbmeta/tests/data/testkey_rsa8192.pem b/libs/vbmeta/tests/data/testkey_rsa8192.pem
deleted file mode 100644
index a383428..0000000
--- a/libs/vbmeta/tests/data/testkey_rsa8192.pem
+++ /dev/null
@@ -1,99 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIISKgIBAAKCBAEA0D3T+dISsmCHm797wsX0vVfqUWDJ/3mvDYozlCabDhnGLlSE
-pAQbf1Z8Ts+OM4pVRHOJUJL0WebNdmPPGjsyWQz6zZE96lQZL3avCEXqYVQR66V5
-3wdK/ohaMSRnGyEMBrqkVVbF3gCr+/irxD3YK+VowO2WKs/6GrMdqTA8Y5CTF/Je
-ptwsSg5MMjr6UaK4qDcrej3hkgBVGvRV3cj1snK6Br8HuYdFnpGGTS0d7UJlHFgl
-trGHU/CBO923hkHgJaWEjC0giSGjhKKtLzrVcpDV2y/lWQP9T/T4djEAIaHqQ++P
-SdOSR6psIGR6hVgSigt7HCnE7nW711/rfV5Ur9EiVpB040mDImKZcy8//TMnXydN
-1KYTVd/34fdpzMpSw5iblErbwOLXVTUmOztYnpl41feHSv/jPesHstPlfklIF2vo
-GZEohf9scQvcuM7wEBfC/aTA9K39zMmkBbcvSZjLyhmcSZWMPPOZyIcl3zY53QhW
-QC/abmIcBfI1S4+r7mC4i2Jn++oEvuGNVGr2SY2Z0ZZxXGL1HI/08D/3+Tcumrcn
-4YjPK/DMFi0F+e+1x41lipuf+cx/2qRNQX/m02STrLYdM6e0g33KvlnFdi2b752y
-/OIaMwxDaJvunMh6EMDWKM1AHbY/ioAoK7eS26HeJLEDllqO4+SWP37c8lMvSEWy
-1GiErR0HcsOj/QwWGPFseoVroMiA2sUQ0Ic/tgVjCTlXg+12XpUnouIweCi8KcL/
-ad2zJkju9hBhJLBQ/2GnivJi3lFgF4Gd//TSJ6rgWuXFfMKt/9z2Sz35ohEX4yA0
-flqlCeLInFEoevbz+XT9aRfDe65MZ79yw3TfP9CrV74hf1RRzveD4zpi3F+hcY2i
-JWsH7gROZeCm6fAX5Trecd3hOxJOfA4N4rvSSCq6BwCvebT8FY25Z/VF7cQrHYDS
-ij5w6lqhMzXHeUEY90Ga9AK4XzaWwGgezq+R7Zs00YSKqFv9qYNKdR7tz3cjijWf
-9q/3R1uh6EQKTMZKo4SEClJiGyjOBvmPK09jMFZTJv00hDxagDPZBl7XpLDJ5/Ln
-1uppvLCNWWY1zeJfaElMyq3/PqKZLidF9rVoA1SIwk2lpdUvPote2oFiwCZoXlwZ
-J2ncjmXgQNs76/8unDJA0rj4JPqccw4M5GxQ7okbgm3F4rmzriCuv8BeMSCkr2ry
-0mY3UhpohX4wCMq0G4x5sEUAz9FVVPZKjxnYBmLDzrJAR+4+G7gZsct01XDJYgDd
-JVYInFP22/cIre8VrFWYtHbgOFdNqUiVq58de6PdZG/E+uaWmEThSlRrgEjTxupi
-OXfgdKW/20j1qAtjOlqFwsY094Q5rqULQ6wPxQIDAQABAoIEAQChmkmlhrRBv42d
-fYUiyxK52b8ath0saJdDz6tlXmxYDgJxM9/XlORt9oTzeDknoEO5olu+rrx4BBgQ
-tzYiaiwRVXRREVTWQ7tjzRvaNL/GFkLt93XTccpuKwyrNE/bitLVagRbwcI+HZFa
-MknCOihHMHoRto8h3FKAY94xzSAgODMek1WG8jhgpCXXmVNnBPt+d4oDDIDAGAfz
-qgf03J5nhIb+80KgZOzPOKnbvJaL6EmlLHbgB3c42dzAw7hHtVmofYGWcvLb2MIY
-DVKO435/sQx1U/8NDH6JjVdACZjLgObXH9K3/Tt46DWPEcrPLmD8xhoc6gFM+Qr0
-AhkzKoBYDNk0CljbhdIBXjktXU6wRQFZ45uP2e4JZ4zrzGBLr/t4lTavZ0SQtLld
-A6kOsGh+dCWFDtnshxYnl/xad/yR+3a5zmDJbo/fJTBXrlf1B4rfQkFtK20etOPQ
-B++FC/rjh3Mm/Kb/p9Gz/2upZdArH97ZvD2LBFfj77lFmAhqAi3wCRlN+ekuYxaZ
-t1pBV9yXig8Dyldg1d7X8pOn2kyrF3rQUDDf4pa7x9vpnbkUlEUifoV9gnYsmdni
-qDzYBtTv2g6MKqwQySXaIUW0YOBPbOellWEwxJqGYQ7y4IfVHfM0iyHnehk2tZcr
-+XazLnwGe+Bz4vcguFhJXLyIu//lAOhZtbk6r1QJEUuxaOOQX3wzyceE6nkDsgmr
-P5dj3Zpd7fS2VV2vyGHIFnBJ88LRxreVvgr6Q28UT27SB82zMb7mRZTVE2zeuubT
-5D2D1XbZ0wBo6WiK6eRRrDQ2Haeetkj/uoRy6PWXwnAaTmmIrrXwLqaoJh/U1e+D
-tfsDLWd6IxLjfXvGglrHsrtAz0oprpixUTeVhgTrGk9IQRd5rvxuGUYhFujVaYI6
-+QUf+33AFdtncb8y9C9jZmgx8AKbJk+e73SLhB5JVos+WteU7b8d/Mim5mALjnO6
-Z1n/uimsT79sSDqy3XSymtKWXo/22UlrvGCpoEuELPMb6dSFWR7vwrsvhFngY4/K
-UnitnvxboEflQnaIQ4IfRLRzZsX+sC5Esqw9U5tHt4oI+91Dv3KbdbcERgV73K6B
-ZQgC4lkAQquFXiZ5AICkxjiMyZwTtU9KJ7xv17Xu6oywF/3AtbVGETW1D+3maHsD
-y3DASWojyqZdLj+WGzKQRa+swgCDAYKeek2fIAXFSdF63zxJ2RxOJ4GijSaoh+mr
-4HVvcpDaTj+A8T1+QdByM4s98gu4GD7kVtVQGBZdWjutyHvh0hWv1gtVmbhQ/413
-gDMFFDzHIjLTYGYes4hHL22169jVR9sZ1eQxwvTIg3N4pD5cFm0rRuZZTS+oJToF
-G27aBFihAoICAQDyVB62ZDnbxQthk+zITKIzRUrJbLoXrUcANcSHfaN7inF87Ova
-ze7ejT9DNSEhbtfZFJ1G6diOYoSw+2MzFXv0gEkLKY0dETydKgHEu6nVq5eivMgv
-D4hc9YkJMHDSlmv2FDkpL3AXCAmnW9rKp+ddttBZECnmlPEpHLoj6xgBw3pNa1Xs
-IcLVfdugH86Hexj6o0oKgYfcqrX8UUHtUI2/XQqgFrIj8ksjf1fFVWJRJFWmBXqp
-nMEsYarzATeM1kQ/kDeT1ZUpoGPQt02/XqXT4B5A3ATiEtpM2u+l48xtogWWg2Ry
-G9l938StAmhUiW1m7GnKE6EIFvQY85WvbzxOR0JYVUSr7MrasF6nnQlhYxFuIJoJ
-2h/KJQao5GCTvG4+GtbJJm4c2nyZgwyhizMsdgsdcls79aXiMkrZZkamLVUZWOtE
-3pA/oBuz2qnO9HwjbH1HGOccq0TXfmpFScEV3CQGYJdno6Fy7cbmupaL4U9agQ4e
-w+ygL18nq5HV++LStFnVrgs5YijjskfRdE9GUMVDh5pCsd9Y23Fymaad4O/2SRCC
-YkSsyH5OvyDOLpoyUJ6g6Q+45Hqm/3lG4YjNpzFUiMcnp7+3xU35qC0LK8xEfeei
-Ms1mTVEiHNIp6xH/TqRdX73WD7+YuKZSLIfRG7dgrirU6w+mhhvxD51uHQKCAgEA
-2/1mBCR5qm3/0Lt++RQbeyE3tiw40UeyQqucG/+VvY77sSLkI/Lx8iwRlywXcLBn
-+A4TvgukmAdWzCs8ndgKNxPA+gfohvBsMOGN9KOB1Ug5vvg2J2kiI64vwYCwzhdZ
-NTUUmL+GMFHUqSsWYg6i7iBFcZmznr4W2T3bBxyTMZki7JStB86e35KXrzc2/W/b
-+/p5U2HCSazDHI5mMyuClHc6GmUSVJ7f7LHjL94jviNqobp0Vj603tScHISmNrZw
-TBavkvZGYXsoWKvqavk7jBB9QzaBL+unaFRslg5jTaiKnISj44Us1fjFKu84xifL
-nJaEzjDPt7PBxko7LPgEY7wF39nM9VpoetI7bwR6NwDLSX8UU97MGd+HY+MO1Wi1
-pd2Lapwrx/EK7Oxz335VRK4Je0aZna4j2TyQdMJac9fsGPXv4ZsLfDLj/wD6l1j+
-lLLbBv3ImdSj32LBbhsgF4iCGeXO8HpPO+Q/h9XVsnY52Um2XdNMn03PCGm6ZvtM
-7DXiS+lPF90HjolJVHZTBNtdVRrLr53zLuWEfqT4FeKrDaxdtiXkxLjrB+5/VYu7
-ntyk01ZQ63VNfEwS1irmKl9+qZkTHk3HHV9jNV5RzWViwmJI7Wpr1YzBwmcKCB1O
-oGUADDs8QpnkCz0xkMVtYwHj9qKZlqfbHzrFDUUcF8kCggIAdYvUcgjf//ju8mA8
-5VQ3AcPE6TvycPW+kR2DvW12VcDsF/sc1UA7dHzziPhGn98SmNxlBjb8suSbFPZ8
-QhVT0WBBDkcTilwIGPx9ax7U3S6lGW2VdS6FqQH5fRmgQKZyrCVXLOEz8BgYBrSJ
-xu/3TQAWxH0QtibdbGHg8Pdi58gYlWFRhn9B8Slh1aRYHGPb1AhNLBd0/ddY+5G2
-9xSyDXdmZg1cUA+B3zAwNSqbzFxhp2zU+V1uXsbpk4KtnYV6CZM9QlrCRjTk9iNU
-dVXF/qaiRjfzrm4SsmEpCkEbsrp7F22Y1bkooORglMOsNAWNqfVXw4wN+syXj1ro
-6vZ8PERYrFyAOR1dsQMIhymnmTPjCpaJ4emKrhWTy20sY71thHakZWJc22YoNpbZ
-E6tgIVsJPTlxg/4+fyCCKj5wWr92nhsB1KBZPGO/zFhvMlJpvQ0tH8W2pbN2a0mI
-5x9FqALm/qjwCHfZItSwPM+ZozSht3cOkGHdcD5KXAXfcfsDJc4SHZKVIzq4NusN
-504R/jvD1GP8sglyG7omp75ckgzAmakLdxOP2HhQvIX9tcXpSirNJ6Sl2bwKuuMF
-wxo3r/o/9Y97e4LlfpEYp9eqMdcG+NpR993IwK0UhAWS9H5wdnWBSUHd5e4xtDUt
-iILNRuO46g7R/AIhz1cSSraWWQkCggIBAMhhPP5C9yt9PIm1b0eTwCBctnFSQIKo
-KsA9rll2ab+bMLk9jc8M6MLszy0CtWso09sHf4YY9tifvrkEHRethEh8zscwUuYu
-sm2n1fTixk0ul6LSVgl54uXbMJayENn4PIKRkew8cA8tSma43497w37hmD+MgCb1
-ALzqcco9hfmkgkI6fo1g8Ce3UEECKy2YKSmREdgYcK9JFQO61W6AkFWJcDxAmfzI
-JjFkKwsb7TSw79zWiEdSoM9jm7sCPKATd6Bm/ZAAkUUTuEFkfobn9Ax1rJN/Xxb2
-MKuAUtQv0NYY0gEVdG62jItuKLId6nncH8PG+rsRjPLIYpWqYdJpKx5pUnR+4AkQ
-S6CsRASwcF4PdBvDDBIFG6XpjFo4pPdQhDzL2sTF8b8SWSBLlJQbb7G6UNqgCSau
-SusCFpazvU5NfDmUMuctob2EYVaSXq9jGaj6bTUmDwXHwWilfIk9XfLxnYfXYrJ6
-xhdIpXGmHhuLQtAgK2O1JtLoPc9s9qP8/SkfP7xjjG6xHsP/WvL7QE1pPs9ZM/UI
-C01JNHFi9LKCn8o5mbZjN8jUowi7ffK+76wZUG1L7zM5ytWQOYwo0TQBfc8fpmFw
-+RBRJX2kJyDO27ExczoGOKjwqEDaODIB9+9zcCK0BgSoRibSm4ZBvoxzWWD65Kls
-xdPhZUHcFGW5AoICAQC8iG27aD8aRUt94Oek66gFOJx84QVZehWPqtZjWyVenDuc
-T8dink8oejGjcK2UJuQDa83azv90ocVqE0n0ronYyszt9Ib1jlYC+CK1Ar9TYGFg
-WU5OWEDyCzCpqW/w/aG68U8qhKm0MvkLJR+G6evan9TwEhFEVAm3iWllNXs9x29s
-BucwyMMC23zsimxYlS7dA4DtyvVA+zL1omLpSWHbU/qtuI3HV1NeJzsy+gC4mwPh
-j52tdl669fyWLzHzBRLeq6dVOedjnCo+jlU3dL20DEk9SaW08D1CPuZekV1jVPMw
-JoaDcIRh4KLtQ0BYZ7UJeFUTsx1CS/+UqzqYSPOi57a5kvr0Y8YwRnSB8dHVFttX
-JTv83wTQXHPFSBgfnHNe7lsRTfIQfuIkr2bpiU7h85UQ7LsqcI6YHaC07URcsGFF
-FrLWGh91qzAd1diSHla2RnY3n8PPuMnCkguNhLUrYdmyMol7FfWFa9lwplsuTzBq
-B6yj8iaiE3LL+Q/eulJ7S6QPfAI2bU0UJO23Y4koeoIibEEDMSCQ6KYZ2NClRRRT
-ga5fS1YfkDFEcHUQ1/KIkdYHGBKBjoKGExzi8+CgiSySVSYDZl6wIOhLjH2OZ3ol
-ldPN7iNAHirrxg9v8QO6OQlpLUk5Lhp/1dSlZ6sy3UjFqvax3tw6ZjrL88YP5g==
------END RSA PRIVATE KEY-----
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 94ef940..310cf2b 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -45,7 +45,7 @@
 
     setprop ro.debuggable ${ro.boot.microdroid.debuggable:-0}
 
-on property:dev.bootcomplete=1
+on property:microdroid_manager.init_done=1
     # Stop ueventd to save memory
     stop ueventd
 
@@ -74,6 +74,12 @@
     # some services can be started.
     trigger late-fs
 
+    # Wait for microdroid_manager to finish setting up sysprops from the payload config.
+    # Some further actions in the boot sequence might depend on the sysprops from the payloag,
+    # e.g. microdroid.config.enable_authfs configures whether to run authfs_service after
+    # /data is mounted.
+    wait_for_prop microdroid_manager.config_done 1
+
     trigger post-fs-data
 
     # Load persist properties and override properties (if enabled) from /data.
@@ -132,6 +138,13 @@
     mkdir /data/local 0751 root root
     mkdir /data/local/tmp 0771 shell shell
 
+on post-fs-data && property:microdroid_manager.authfs.enabled=1
+    start authfs_service
+
+on boot
+    # Mark boot completed. This will notify microdroid_manager to run payload.
+    setprop dev.bootcomplete 1
+
 service tombstone_transmit /system/bin/tombstone_transmit.microdroid -cid 2 -port 2000 -remove_tombstones_after_transmitting
     user system
     group system
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index f8e7d34..3859785 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -27,6 +27,12 @@
     /** Path to the APK contents path. */
     const String VM_APK_CONTENTS_PATH = "/mnt/apk";
 
+    /**
+     * Path to the encrypted storage. Note the path will not exist if encrypted storage
+     * is not enabled.
+     */
+    const String ENCRYPTEDSTORE_MOUNTPOINT = "/mnt/encryptedstore";
+
     /** Notifies that the payload is ready to serve. */
     void notifyPayloadReady();
 
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index a706dbe..0e45461 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -29,6 +29,7 @@
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     VM_APK_CONTENTS_PATH,
     VM_PAYLOAD_SERVICE_SOCKET_NAME,
+    ENCRYPTEDSTORE_MOUNTPOINT,
 };
 use anyhow::{anyhow, bail, ensure, Context, Error, Result};
 use apkverify::{get_public_key_der, verify, V4Signature};
@@ -85,8 +86,7 @@
 const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
 const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
 const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
-const ENCRYPTEDSTORE_KEYSIZE: u32 = 64;
-const ENCRYPTEDSTORE_MOUNTPOINT: &str = "/mnt/encryptedstore";
+const ENCRYPTEDSTORE_KEYSIZE: u32 = 32;
 
 #[derive(thiserror::Error, Debug)]
 enum MicrodroidError {
@@ -417,9 +417,10 @@
     mount_extra_apks(&config)?;
 
     // Wait until apex config is done. (e.g. linker configuration for apexes)
-    // TODO(jooyung): wait until sys.boot_completed?
     wait_for_apex_config_done()?;
 
+    setup_config_sysprops(&config)?;
+
     // Start tombstone_transmit if enabled
     if config.export_tombstones {
         control_service("start", "tombstone_transmit")?;
@@ -427,22 +428,23 @@
         control_service("stop", "tombstoned")?;
     }
 
-    // Start authfs if enabled
-    if config.enable_authfs {
-        control_service("start", "authfs_service")?;
-    }
-
     // Wait until zipfuse has mounted the APK so we can access the payload
     wait_for_property_true(APK_MOUNT_DONE_PROP).context("Failed waiting for APK mount done")?;
 
     register_vm_payload_service(allow_restricted_apis, service.clone(), dice_context)?;
 
+    // Wait for encryptedstore to finish mounting the storage (if enabled) before setting
+    // microdroid_manager.init_done. Reason is init stops uneventd after that.
+    // Encryptedstore, however requires ueventd
     if let Some(mut child) = encryptedstore_child {
         let exitcode = child.wait().context("Wait for encryptedstore child")?;
         ensure!(exitcode.success(), "Unable to prepare encrypted storage. Exitcode={}", exitcode);
     }
 
-    system_properties::write("dev.bootcomplete", "1").context("set dev.bootcomplete")?;
+    wait_for_property_true("dev.bootcomplete").context("failed waiting for dev.bootcomplete")?;
+    system_properties::write("microdroid_manager.init_done", "1")
+        .context("set microdroid_manager.init_done")?;
+    info!("boot completed, time to run payload");
     exec_task(task, service).context("Failed to run payload")
 }
 
@@ -682,6 +684,16 @@
     Ok(())
 }
 
+fn setup_config_sysprops(config: &VmPayloadConfig) -> Result<()> {
+    if config.enable_authfs {
+        system_properties::write("microdroid_manager.authfs.enabled", "1")
+            .context("failed to write microdroid_manager.authfs.enabled")?;
+    }
+    system_properties::write("microdroid_manager.config_done", "1")
+        .context("failed to write microdroid_manager.config_done")?;
+    Ok(())
+}
+
 // Waits until linker config is generated
 fn wait_for_apex_config_done() -> Result<()> {
     wait_for_property_true(APEX_CONFIG_DONE_PROP).context("Failed waiting for apex config done")
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 77de696..0da24c7 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -12,11 +12,18 @@
         "legacy",
     ],
     rustlibs: [
+        "libaarch64_paging",
+        "libavb_nostd",
         "libbuddy_system_allocator",
+        "liblibfdt",
         "liblog_rust_nostd",
         "libpvmfw_embedded_key",
+        "libtinyvec_nostd",
         "libvmbase",
     ],
+    static_libs: [
+        "libarm-optimized-routines-mem",
+    ],
     apex_available: ["com.android.virt"],
 }
 
diff --git a/pvmfw/idmap.S b/pvmfw/idmap.S
index ec3ceaf..2ef0d42 100644
--- a/pvmfw/idmap.S
+++ b/pvmfw/idmap.S
@@ -40,13 +40,9 @@
 	/* level 1 */
 	.quad		.L_BLOCK_DEV | 0x0		// 1 GB of device mappings
 	.quad		.L_TT_TYPE_TABLE + 0f		// Unmapped device memory, and pVM firmware
-	.quad		.L_TT_TYPE_TABLE + 1f		// up to 1 GB of DRAM
-	.fill		509, 8, 0x0			// 509 GB of remaining VA space
+	.fill		510, 8, 0x0			// 510 GB of remaining VA space
 
 	/* level 2 */
 0:	.fill		510, 8, 0x0
 	.quad		.L_BLOCK_MEM_XIP | 0x7fc00000	// pVM firmware image
 	.quad		.L_BLOCK_MEM	 | 0x7fe00000	// Writable memory for stack, heap &c.
-1:	.quad		.L_BLOCK_RO	 | 0x80000000	// DT provided by VMM
-	.quad		.L_BLOCK_RO	 | 0x80200000	// 2 MB of DRAM containing payload image
-	.fill		510, 8, 0x0
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
new file mode 100644
index 0000000..0f2a39c
--- /dev/null
+++ b/pvmfw/src/config.rs
@@ -0,0 +1,200 @@
+// Copyright 2022, 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.
+
+//! Support for the pvmfw configuration data format.
+
+use crate::helpers;
+use core::fmt;
+use core::mem;
+use core::num::NonZeroUsize;
+use core::ops;
+use core::result;
+
+#[repr(C, packed)]
+#[derive(Clone, Copy, Debug)]
+struct Header {
+    magic: u32,
+    version: u32,
+    total_size: u32,
+    flags: u32,
+    entries: [HeaderEntry; Entry::COUNT],
+}
+
+#[derive(Debug)]
+pub enum Error {
+    /// Reserved region can't fit configuration header.
+    BufferTooSmall,
+    /// Header doesn't contain the expect magic value.
+    InvalidMagic,
+    /// Version of the header isn't supported.
+    UnsupportedVersion(u16, u16),
+    /// Header sets flags incorrectly or uses reserved flags.
+    InvalidFlags(u32),
+    /// Header describes configuration data that doesn't fit in the expected buffer.
+    InvalidSize(usize),
+    /// Header entry is invalid.
+    InvalidEntry(Entry),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::BufferTooSmall => write!(f, "Reserved region is smaller than config header"),
+            Self::InvalidMagic => write!(f, "Wrong magic number"),
+            Self::UnsupportedVersion(x, y) => write!(f, "Version {x}.{y} not supported"),
+            Self::InvalidFlags(v) => write!(f, "Flags value {v:#x} is incorrect or reserved"),
+            Self::InvalidSize(sz) => write!(f, "Total size ({sz:#x}) overflows reserved region"),
+            Self::InvalidEntry(e) => write!(f, "Entry {e:?} is invalid"),
+        }
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+impl Header {
+    const MAGIC: u32 = u32::from_ne_bytes(*b"pvmf");
+    const PADDED_SIZE: usize =
+        helpers::unchecked_align_up(mem::size_of::<Self>(), mem::size_of::<u64>());
+
+    pub const fn version(major: u16, minor: u16) -> u32 {
+        ((major as u32) << 16) | (minor as u32)
+    }
+
+    pub const fn version_tuple(&self) -> (u16, u16) {
+        ((self.version >> 16) as u16, self.version as u16)
+    }
+
+    pub fn total_size(&self) -> usize {
+        self.total_size as usize
+    }
+
+    pub fn body_size(&self) -> usize {
+        self.total_size() - Self::PADDED_SIZE
+    }
+
+    fn get(&self, entry: Entry) -> HeaderEntry {
+        self.entries[entry as usize]
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum Entry {
+    Bcc = 0,
+    DebugPolicy = 1,
+}
+
+impl Entry {
+    const COUNT: usize = 2;
+}
+
+#[repr(packed)]
+#[derive(Clone, Copy, Debug)]
+struct HeaderEntry {
+    offset: u32,
+    size: u32,
+}
+
+impl HeaderEntry {
+    pub fn is_empty(&self) -> bool {
+        self.offset() == 0 && self.size() == 0
+    }
+
+    pub fn fits_in(&self, max_size: usize) -> bool {
+        (Header::PADDED_SIZE..max_size).contains(&self.offset())
+            && NonZeroUsize::new(self.size())
+                .and_then(|s| s.checked_add(self.offset()))
+                .filter(|&x| x.get() <= max_size)
+                .is_some()
+    }
+
+    pub fn as_body_range(&self) -> ops::Range<usize> {
+        let start = self.offset() - Header::PADDED_SIZE;
+
+        start..(start + self.size())
+    }
+
+    pub fn offset(&self) -> usize {
+        self.offset as usize
+    }
+
+    pub fn size(&self) -> usize {
+        self.size as usize
+    }
+}
+
+#[derive(Debug)]
+pub struct Config<'a> {
+    header: &'a Header,
+    body: &'a mut [u8],
+}
+
+impl<'a> Config<'a> {
+    /// Take ownership of a pvmfw configuration consisting of its header and following entries.
+    ///
+    /// SAFETY - 'data' should respect the alignment of Header.
+    pub unsafe fn new(data: &'a mut [u8]) -> Result<Self> {
+        let header = data.get(..Header::PADDED_SIZE).ok_or(Error::BufferTooSmall)?;
+
+        let header = &*(header.as_ptr() as *const Header);
+
+        if header.magic != Header::MAGIC {
+            return Err(Error::InvalidMagic);
+        }
+
+        if header.version != Header::version(1, 0) {
+            let (major, minor) = header.version_tuple();
+            return Err(Error::UnsupportedVersion(major, minor));
+        }
+
+        if header.flags != 0 {
+            return Err(Error::InvalidFlags(header.flags));
+        }
+
+        let total_size = header.total_size();
+
+        // BCC is a mandatory entry of the configuration data.
+        if !header.get(Entry::Bcc).fits_in(total_size) {
+            return Err(Error::InvalidEntry(Entry::Bcc));
+        }
+
+        // Debug policy is optional.
+        let dp = header.get(Entry::DebugPolicy);
+        if !dp.is_empty() && !dp.fits_in(total_size) {
+            return Err(Error::InvalidEntry(Entry::DebugPolicy));
+        }
+
+        let body = data
+            .get_mut(Header::PADDED_SIZE..)
+            .ok_or(Error::BufferTooSmall)?
+            .get_mut(..header.body_size())
+            .ok_or(Error::InvalidSize(total_size))?;
+
+        Ok(Self { header, body })
+    }
+
+    /// Get slice containing the platform BCC.
+    pub fn get_bcc_mut(&mut self) -> &mut [u8] {
+        &mut self.body[self.header.get(Entry::Bcc).as_body_range()]
+    }
+
+    /// Get slice containing the platform debug policy.
+    pub fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+        let entry = self.header.get(Entry::DebugPolicy);
+        if entry.is_empty() {
+            None
+        } else {
+            Some(&mut self.body[entry.as_body_range()])
+        }
+    }
+}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index c0ad878..ee32509 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -14,22 +14,39 @@
 
 //! Low-level entry and exit points of pvmfw.
 
+use crate::config;
+use crate::fdt;
 use crate::heap;
 use crate::helpers;
+use crate::memory::MemoryTracker;
 use crate::mmio_guard;
+use crate::mmu;
 use core::arch::asm;
+use core::num::NonZeroUsize;
 use core::slice;
 use log::debug;
 use log::error;
+use log::info;
+use log::warn;
 use log::LevelFilter;
 use vmbase::{console, layout, logger, main, power::reboot};
 
 #[derive(Debug, Clone)]
-enum RebootReason {
+pub(crate) enum RebootReason {
     /// A malformed BCC was received.
     InvalidBcc,
+    /// An invalid configuration was appended to pvmfw.
+    InvalidConfig,
     /// An unexpected internal error happened.
     InternalError,
+    /// The provided FDT was invalid.
+    InvalidFdt,
+    /// The provided payload was invalid.
+    InvalidPayload,
+    /// The provided ramdisk was invalid.
+    InvalidRamdisk,
+    /// Failed to verify the payload.
+    PayloadVerificationError,
 }
 
 main!(start);
@@ -42,12 +59,104 @@
 
     match main_wrapper(fdt_address as usize, payload_start as usize, payload_size as usize) {
         Ok(_) => jump_to_payload(fdt_address, payload_start),
-        Err(_) => reboot(),
+        Err(_) => reboot(), // TODO(b/220071963) propagate the reason back to the host.
     }
 
     // if we reach this point and return, vmbase::entry::rust_entry() will call power::shutdown().
 }
 
+struct MemorySlices<'a> {
+    fdt: &'a mut libfdt::Fdt,
+    kernel: &'a [u8],
+    ramdisk: Option<&'a [u8]>,
+}
+
+impl<'a> MemorySlices<'a> {
+    fn new(
+        fdt: usize,
+        payload: usize,
+        payload_size: usize,
+        memory: &mut MemoryTracker,
+    ) -> Result<Self, RebootReason> {
+        // SAFETY - SIZE_2MB is non-zero.
+        const FDT_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(helpers::SIZE_2MB) };
+        // TODO - Only map the FDT as read-only, until we modify it right before jump_to_payload()
+        // e.g. by generating a DTBO for a template DT in main() and, on return, re-map DT as RW,
+        // overwrite with the template DT and apply the DTBO.
+        let range = memory.alloc_mut(fdt, FDT_SIZE).map_err(|e| {
+            error!("Failed to allocate the FDT range: {e}");
+            RebootReason::InternalError
+        })?;
+
+        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
+        let fdt = unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) };
+        let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
+            error!("Failed to spawn the FDT wrapper: {e}");
+            RebootReason::InvalidFdt
+        })?;
+
+        debug!("Fdt passed validation!");
+
+        let memory_range = fdt
+            .memory()
+            .map_err(|e| {
+                error!("Failed to get /memory from the DT: {e}");
+                RebootReason::InvalidFdt
+            })?
+            .ok_or_else(|| {
+                error!("Node /memory was found empty");
+                RebootReason::InvalidFdt
+            })?
+            .next()
+            .ok_or_else(|| {
+                error!("Failed to read the memory size from the FDT");
+                RebootReason::InternalError
+            })?;
+
+        debug!("Resizing MemoryTracker to range {memory_range:#x?}");
+
+        memory.shrink(&memory_range).map_err(|_| {
+            error!("Failed to use memory range value from DT: {memory_range:#x?}");
+            RebootReason::InvalidFdt
+        })?;
+
+        let payload_size = NonZeroUsize::new(payload_size).ok_or_else(|| {
+            error!("Invalid payload size: {payload_size:#x}");
+            RebootReason::InvalidPayload
+        })?;
+
+        let payload_range = memory.alloc(payload, payload_size).map_err(|e| {
+            error!("Failed to obtain the payload range: {e}");
+            RebootReason::InternalError
+        })?;
+        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
+        let kernel =
+            unsafe { slice::from_raw_parts(payload_range.start as *const u8, payload_range.len()) };
+
+        let ramdisk_range = fdt::initrd_range(fdt).map_err(|e| {
+            error!("An error occurred while locating the ramdisk in the device tree: {e}");
+            RebootReason::InternalError
+        })?;
+
+        let ramdisk = if let Some(r) = ramdisk_range {
+            debug!("Located ramdisk at {r:?}");
+            let r = memory.alloc_range(&r).map_err(|e| {
+                error!("Failed to obtain the initrd range: {e}");
+                RebootReason::InvalidRamdisk
+            })?;
+
+            // SAFETY - The region was validated by memory to be in main memory, mapped, and
+            // not overlap.
+            Some(unsafe { slice::from_raw_parts(r.start as *const u8, r.len()) })
+        } else {
+            info!("Couldn't locate the ramdisk from the device tree");
+            None
+        };
+
+        Ok(Self { fdt, kernel, ramdisk })
+    }
+}
+
 /// Sets up the environment for main() and wraps its result for start().
 ///
 /// Provide the abstractions necessary for start() to abort the pVM boot and for main() to run with
@@ -63,14 +172,6 @@
 
     logger::init(LevelFilter::Info).map_err(|_| RebootReason::InternalError)?;
 
-    const FDT_MAX_SIZE: usize = helpers::SIZE_2MB;
-    // TODO: Check that the FDT is fully contained in RAM.
-    // SAFETY - We trust the VMM, for now.
-    let fdt = unsafe { slice::from_raw_parts_mut(fdt as *mut u8, FDT_MAX_SIZE) };
-    // TODO: Check that the payload is fully contained in RAM and doesn't overlap with the FDT.
-    // SAFETY - We trust the VMM, for now.
-    let payload = unsafe { slice::from_raw_parts(payload as *const u8, payload_size) };
-
     // Use debug!() to avoid printing to the UART if we failed to configure it as only local
     // builds that have tweaked the logger::init() call will actually attempt to log the message.
 
@@ -86,13 +187,45 @@
 
     // SAFETY - We only get the appended payload from here, once. It is mapped and the linker
     // script prevents it from overlapping with other objects.
-    let bcc = as_bcc(unsafe { get_appended_data_slice() }).ok_or_else(|| {
+    let appended_data = unsafe { get_appended_data_slice() };
+
+    // Up to this point, we were using the built-in static (from .rodata) page tables.
+
+    let mut page_table = mmu::PageTable::from_static_layout().map_err(|e| {
+        error!("Failed to set up the dynamic page tables: {e}");
+        RebootReason::InternalError
+    })?;
+
+    const CONSOLE_LEN: usize = 1; // vmbase::uart::Uart only uses one u8 register.
+    let uart_range = console::BASE_ADDRESS..(console::BASE_ADDRESS + CONSOLE_LEN);
+    page_table.map_device(&uart_range).map_err(|e| {
+        error!("Failed to remap the UART as a dynamic page table entry: {e}");
+        RebootReason::InternalError
+    })?;
+
+    // SAFETY - We only get the appended payload from here, once. It is statically mapped and the
+    // linker script prevents it from overlapping with other objects.
+    let mut appended = unsafe { AppendedPayload::new(appended_data) }.ok_or_else(|| {
+        error!("No valid configuration found");
+        RebootReason::InvalidConfig
+    })?;
+
+    let bcc = appended.get_bcc_mut().ok_or_else(|| {
         error!("Invalid BCC");
         RebootReason::InvalidBcc
     })?;
 
+    debug!("Activating dynamic page table...");
+    // SAFETY - page_table duplicates the static mappings for everything that the Rust code is
+    // aware of so activating it shouldn't have any visible effect.
+    unsafe { page_table.activate() };
+    debug!("... Success!");
+
+    let mut memory = MemoryTracker::new(page_table);
+    let slices = MemorySlices::new(fdt, payload, payload_size, &mut memory)?;
+
     // This wrapper allows main() to be blissfully ignorant of platform details.
-    crate::main(fdt, payload, bcc);
+    crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc)?;
 
     // TODO: Overwrite BCC before jumping to payload to avoid leaking our sealing key.
 
@@ -105,16 +238,19 @@
 }
 
 fn jump_to_payload(fdt_address: u64, payload_start: u64) -> ! {
-    const SCTLR_EL1_RES1: usize = (0b11 << 28) | (0b101 << 20) | (0b1 << 11);
+    const SCTLR_EL1_RES1: u64 = (0b11 << 28) | (0b101 << 20) | (0b1 << 11);
     // Stage 1 instruction access cacheability is unaffected.
-    const SCTLR_EL1_I: usize = 0b1 << 12;
+    const SCTLR_EL1_I: u64 = 0b1 << 12;
     // SETEND instruction disabled at EL0 in aarch32 mode.
-    const SCTLR_EL1_SED: usize = 0b1 << 8;
+    const SCTLR_EL1_SED: u64 = 0b1 << 8;
     // Various IT instructions are disabled at EL0 in aarch32 mode.
-    const SCTLR_EL1_ITD: usize = 0b1 << 7;
+    const SCTLR_EL1_ITD: u64 = 0b1 << 7;
 
-    const SCTLR_EL1_VAL: usize = SCTLR_EL1_RES1 | SCTLR_EL1_ITD | SCTLR_EL1_SED | SCTLR_EL1_I;
+    const SCTLR_EL1_VAL: u64 = SCTLR_EL1_RES1 | SCTLR_EL1_ITD | SCTLR_EL1_SED | SCTLR_EL1_I;
 
+    // Disable the exception vector, caches and page table and then jump to the payload at the
+    // given address, passing it the given FDT pointer.
+    //
     // SAFETY - We're exiting pvmfw by passing the register values we need to a noreturn asm!().
     unsafe {
         asm!(
@@ -169,13 +305,50 @@
     slice::from_raw_parts_mut(base as *mut u8, size)
 }
 
-fn as_bcc(data: &mut [u8]) -> Option<&mut [u8]> {
-    const BCC_SIZE: usize = helpers::SIZE_4KB;
+enum AppendedPayload<'a> {
+    /// Configuration data.
+    Config(config::Config<'a>),
+    /// Deprecated raw BCC, as used in Android T.
+    LegacyBcc(&'a mut [u8]),
+}
 
-    if cfg!(feature = "legacy") {
+impl<'a> AppendedPayload<'a> {
+    /// SAFETY - 'data' should respect the alignment of config::Header.
+    unsafe fn new(data: &'a mut [u8]) -> Option<Self> {
+        if Self::is_valid_config(data) {
+            Some(Self::Config(config::Config::new(data).unwrap()))
+        } else if cfg!(feature = "legacy") {
+            const BCC_SIZE: usize = helpers::SIZE_4KB;
+            warn!("Assuming the appended data at {:?} to be a raw BCC", data.as_ptr());
+            Some(Self::LegacyBcc(&mut data[..BCC_SIZE]))
+        } else {
+            None
+        }
+    }
+
+    unsafe fn is_valid_config(data: &mut [u8]) -> bool {
+        // This function is necessary to prevent the borrow checker from getting confused
+        // about the ownership of data in new(); see https://users.rust-lang.org/t/78467.
+        let addr = data.as_ptr();
+        config::Config::new(data)
+            .map_err(|e| warn!("Invalid configuration data at {addr:?}: {e}"))
+            .is_ok()
+    }
+
+    #[allow(dead_code)] // TODO(b/232900974)
+    fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+        match self {
+            Self::Config(ref mut cfg) => cfg.get_debug_policy(),
+            Self::LegacyBcc(_) => None,
+        }
+    }
+
+    fn get_bcc_mut(&mut self) -> Option<&mut [u8]> {
+        let bcc = match self {
+            Self::LegacyBcc(ref mut bcc) => bcc,
+            Self::Config(ref mut cfg) => cfg.get_bcc_mut(),
+        };
         // TODO(b/256148034): return None if BccHandoverParse(bcc) != kDiceResultOk.
-        Some(&mut data[..BCC_SIZE])
-    } else {
-        None
+        Some(bcc)
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
new file mode 100644
index 0000000..5b9efd2
--- /dev/null
+++ b/pvmfw/src/fdt.rs
@@ -0,0 +1,32 @@
+// Copyright 2022, 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.
+
+//! High-level FDT functions.
+
+use core::ffi::CStr;
+use core::ops::Range;
+
+/// Extract from /chosen the address range containing the pre-loaded ramdisk.
+pub fn initrd_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
+    let start = CStr::from_bytes_with_nul(b"linux,initrd-start\0").unwrap();
+    let end = CStr::from_bytes_with_nul(b"linux,initrd-end\0").unwrap();
+
+    if let Some(chosen) = fdt.chosen()? {
+        if let (Some(start), Some(end)) = (chosen.getprop_u32(start)?, chosen.getprop_u32(end)?) {
+            return Ok(Some((start as usize)..(end as usize)));
+        }
+    }
+
+    Ok(None)
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index 59cf9f3..f1ff36d 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -14,6 +14,8 @@
 
 //! Miscellaneous helper functions.
 
+use core::arch::asm;
+
 pub const SIZE_4KB: usize = 4 << 10;
 pub const SIZE_2MB: usize = 2 << 20;
 
@@ -24,6 +26,13 @@
     addr & !(alignment - 1)
 }
 
+/// Computes the smallest multiple of the provided alignment larger or equal to the address.
+///
+/// Note: the result is undefined if alignment isn't a power of two and may wrap to 0.
+pub const fn unchecked_align_up(addr: usize, alignment: usize) -> usize {
+    unchecked_align_down(addr + alignment - 1, alignment)
+}
+
 /// Safe wrapper around unchecked_align_up() that validates its assumptions and doesn't wrap.
 pub const fn align_up(addr: usize, alignment: usize) -> Option<usize> {
     if !alignment.is_power_of_two() {
@@ -39,3 +48,30 @@
 pub const fn page_4kb_of(addr: usize) -> usize {
     unchecked_align_down(addr, SIZE_4KB)
 }
+
+#[inline]
+fn min_dcache_line_size() -> usize {
+    const DMINLINE_SHIFT: usize = 16;
+    const DMINLINE_MASK: usize = 0xf;
+    let ctr_el0: usize;
+
+    unsafe { asm!("mrs {x}, ctr_el0", x = out(reg) ctr_el0) }
+
+    // DminLine: log2 of the number of words in the smallest cache line of all the data caches.
+    let dminline = (ctr_el0 >> DMINLINE_SHIFT) & DMINLINE_MASK;
+
+    1 << dminline
+}
+
+/// Flush `size` bytes of data cache by virtual address.
+#[inline]
+pub fn flush_region(start: usize, size: usize) {
+    let line_size = min_dcache_line_size();
+    let end = start + size;
+    let start = unchecked_align_down(start, line_size);
+
+    for line in (start..end).step_by(line_size) {
+        // SAFETY - Clearing cache lines shouldn't have Rust-visible side effects.
+        unsafe { asm!("dc cvau, {x}", x = in(reg) line) }
+    }
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 8178d0b..cf7e90a 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -17,27 +17,44 @@
 #![no_main]
 #![no_std]
 #![feature(default_alloc_error_handler)]
+#![feature(ptr_const_cast)] // Stabilized in 1.65.0
 
 mod avb;
+mod config;
 mod entry;
 mod exceptions;
+mod fdt;
 mod heap;
 mod helpers;
+mod memory;
 mod mmio_guard;
+mod mmu;
 mod smccc;
 
+use crate::entry::RebootReason;
 use avb::PUBLIC_KEY;
-use log::{debug, info};
+use avb_nostd::verify_image;
+use log::{debug, error, info};
 
-fn main(fdt: &mut [u8], payload: &[u8], bcc: &[u8]) {
+fn main(
+    fdt: &libfdt::Fdt,
+    signed_kernel: &[u8],
+    ramdisk: Option<&[u8]>,
+    bcc: &[u8],
+) -> Result<(), RebootReason> {
     info!("pVM firmware");
-    debug!(
-        "fdt_address={:#018x}, payload_start={:#018x}, payload_size={:#018x}",
-        fdt.as_ptr() as usize,
-        payload.as_ptr() as usize,
-        payload.len(),
-    );
+    debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
+    debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
+    if let Some(rd) = ramdisk {
+        debug!("Ramdisk: {:?} ({:#x} bytes)", rd.as_ptr(), rd.len());
+    } else {
+        debug!("Ramdisk: None");
+    }
     debug!("BCC: {:?} ({:#x} bytes)", bcc.as_ptr(), bcc.len());
-    debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
-    info!("Starting payload...");
+    verify_image(signed_kernel, PUBLIC_KEY).map_err(|e| {
+        error!("Failed to verify the payload: {e}");
+        RebootReason::PayloadVerificationError
+    })?;
+    info!("Payload verified. Starting payload...");
+    Ok(())
 }
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
new file mode 100644
index 0000000..ca0b886
--- /dev/null
+++ b/pvmfw/src/memory.rs
@@ -0,0 +1,190 @@
+// Copyright 2022, 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.
+
+//! Low-level allocation and tracking of main memory.
+
+use crate::helpers;
+use crate::mmu;
+use core::cmp::max;
+use core::cmp::min;
+use core::fmt;
+use core::num::NonZeroUsize;
+use core::ops::Range;
+use core::result;
+use log::error;
+use tinyvec::ArrayVec;
+
+type MemoryRange = Range<usize>;
+
+#[derive(Clone, Copy, Debug, Default)]
+enum MemoryType {
+    #[default]
+    ReadOnly,
+    ReadWrite,
+}
+
+#[derive(Clone, Debug, Default)]
+struct MemoryRegion {
+    range: MemoryRange,
+    mem_type: MemoryType,
+}
+
+impl MemoryRegion {
+    /// True if the instance overlaps with the passed range.
+    pub fn overlaps(&self, range: &MemoryRange) -> bool {
+        let our: &MemoryRange = self.as_ref();
+        max(our.start, range.start) < min(our.end, range.end)
+    }
+
+    /// True if the instance is fully contained within the passed range.
+    pub fn is_within(&self, range: &MemoryRange) -> bool {
+        let our: &MemoryRange = self.as_ref();
+        self.as_ref() == &(max(our.start, range.start)..min(our.end, range.end))
+    }
+}
+
+impl AsRef<MemoryRange> for MemoryRegion {
+    fn as_ref(&self) -> &MemoryRange {
+        &self.range
+    }
+}
+
+/// Tracks non-overlapping slices of main memory.
+pub struct MemoryTracker {
+    regions: ArrayVec<[MemoryRegion; MemoryTracker::CAPACITY]>,
+    total: MemoryRange,
+    page_table: mmu::PageTable,
+}
+
+/// Errors for MemoryTracker operations.
+#[derive(Debug, Clone)]
+pub enum MemoryTrackerError {
+    /// Tried to modify the memory base address.
+    DifferentBaseAddress,
+    /// Tried to shrink to a larger memory size.
+    SizeTooLarge,
+    /// Tracked regions would not fit in memory size.
+    SizeTooSmall,
+    /// Reached limit number of tracked regions.
+    Full,
+    /// Region is out of the tracked memory address space.
+    OutOfRange,
+    /// New region overlaps with tracked regions.
+    Overlaps,
+    /// Region couldn't be mapped.
+    FailedToMap,
+}
+
+impl fmt::Display for MemoryTrackerError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::DifferentBaseAddress => write!(f, "Received different base address"),
+            Self::SizeTooLarge => write!(f, "Tried to shrink to a larger memory size"),
+            Self::SizeTooSmall => write!(f, "Tracked regions would not fit in memory size"),
+            Self::Full => write!(f, "Reached limit number of tracked regions"),
+            Self::OutOfRange => write!(f, "Region is out of the tracked memory address space"),
+            Self::Overlaps => write!(f, "New region overlaps with tracked regions"),
+            Self::FailedToMap => write!(f, "Failed to map the new region"),
+        }
+    }
+}
+
+type Result<T> = result::Result<T, MemoryTrackerError>;
+
+impl MemoryTracker {
+    const CAPACITY: usize = 5;
+    /// Base of the system's contiguous "main" memory.
+    const BASE: usize = 0x8000_0000;
+    /// First address that can't be translated by a level 1 TTBR0_EL1.
+    const MAX_ADDR: usize = 1 << 39;
+
+    /// Create a new instance from an active page table, covering the maximum RAM size.
+    pub fn new(page_table: mmu::PageTable) -> Self {
+        Self { total: Self::BASE..Self::MAX_ADDR, page_table, regions: ArrayVec::new() }
+    }
+
+    /// Resize the total RAM size.
+    ///
+    /// This function fails if it contains regions that are not included within the new size.
+    pub fn shrink(&mut self, range: &MemoryRange) -> Result<()> {
+        if range.start != self.total.start {
+            return Err(MemoryTrackerError::DifferentBaseAddress);
+        }
+        if self.total.end < range.end {
+            return Err(MemoryTrackerError::SizeTooLarge);
+        }
+        if !self.regions.iter().all(|r| r.is_within(range)) {
+            return Err(MemoryTrackerError::SizeTooSmall);
+        }
+
+        self.total = range.clone();
+        Ok(())
+    }
+
+    /// Allocate the address range for a const slice; returns None if failed.
+    pub fn alloc_range(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+        self.page_table.map_rodata(range).map_err(|e| {
+            error!("Error during range allocation: {e}");
+            MemoryTrackerError::FailedToMap
+        })?;
+        self.add(MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadOnly })
+    }
+
+    /// Allocate the address range for a mutable slice; returns None if failed.
+    pub fn alloc_range_mut(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+        self.page_table.map_data(range).map_err(|e| {
+            error!("Error during mutable range allocation: {e}");
+            MemoryTrackerError::FailedToMap
+        })?;
+        self.add(MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadWrite })
+    }
+
+    /// Allocate the address range for a const slice; returns None if failed.
+    pub fn alloc(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
+        self.alloc_range(&(base..(base + size.get())))
+    }
+
+    /// Allocate the address range for a mutable slice; returns None if failed.
+    pub fn alloc_mut(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
+        self.alloc_range_mut(&(base..(base + size.get())))
+    }
+
+    fn add(&mut self, region: MemoryRegion) -> Result<MemoryRange> {
+        if !region.is_within(&self.total) {
+            return Err(MemoryTrackerError::OutOfRange);
+        }
+        if self.regions.iter().any(|r| r.overlaps(region.as_ref())) {
+            return Err(MemoryTrackerError::Overlaps);
+        }
+        if self.regions.try_push(region).is_some() {
+            return Err(MemoryTrackerError::Full);
+        }
+
+        Ok(self.regions.last().unwrap().as_ref().clone())
+    }
+}
+
+impl Drop for MemoryTracker {
+    fn drop(&mut self) {
+        for region in self.regions.iter() {
+            match region.mem_type {
+                MemoryType::ReadWrite => {
+                    // TODO: Use page table's dirty bit to only flush pages that were touched.
+                    helpers::flush_region(region.range.start, region.range.len())
+                }
+                MemoryType::ReadOnly => {}
+            }
+        }
+    }
+}
diff --git a/pvmfw/src/mmu.rs b/pvmfw/src/mmu.rs
new file mode 100644
index 0000000..fa94e85
--- /dev/null
+++ b/pvmfw/src/mmu.rs
@@ -0,0 +1,86 @@
+// Copyright 2022, 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.
+
+//! Memory management.
+
+use crate::helpers;
+use aarch64_paging::idmap::IdMap;
+use aarch64_paging::paging::Attributes;
+use aarch64_paging::paging::MemoryRegion;
+use aarch64_paging::MapError;
+use core::ops::Range;
+use vmbase::layout;
+
+// We assume that:
+// - MAIR_EL1.Attr0 = "Device-nGnRE memory" (0b0000_0100)
+// - MAIR_EL1.Attr1 = "Normal memory, Outer & Inner WB Non-transient, R/W-Allocate" (0b1111_1111)
+const MEMORY: Attributes = Attributes::NORMAL.union(Attributes::NON_GLOBAL);
+const DEVICE: Attributes = Attributes::DEVICE_NGNRE.union(Attributes::EXECUTE_NEVER);
+const CODE: Attributes = MEMORY.union(Attributes::READ_ONLY);
+const DATA: Attributes = MEMORY.union(Attributes::EXECUTE_NEVER);
+const RODATA: Attributes = DATA.union(Attributes::READ_ONLY);
+
+/// High-level API for managing MMU mappings.
+pub struct PageTable {
+    idmap: IdMap,
+}
+
+fn appended_payload_range() -> Range<usize> {
+    let start = helpers::align_up(layout::binary_end(), helpers::SIZE_4KB).unwrap();
+    // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
+    let end = helpers::align_up(start, helpers::SIZE_2MB).unwrap();
+
+    start..end
+}
+
+impl PageTable {
+    const ASID: usize = 1;
+    const ROOT_LEVEL: usize = 1;
+
+    /// Creates an instance pre-populated with pvmfw's binary layout.
+    pub fn from_static_layout() -> Result<Self, MapError> {
+        let mut page_table = Self { idmap: IdMap::new(Self::ASID, Self::ROOT_LEVEL) };
+
+        page_table.map_code(&layout::text_range())?;
+        page_table.map_data(&layout::writable_region())?;
+        page_table.map_rodata(&layout::rodata_range())?;
+        page_table.map_data(&appended_payload_range())?;
+
+        Ok(page_table)
+    }
+
+    pub unsafe fn activate(&mut self) {
+        self.idmap.activate()
+    }
+
+    pub fn map_device(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, DEVICE)
+    }
+
+    pub fn map_data(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, DATA)
+    }
+
+    pub fn map_code(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, CODE)
+    }
+
+    pub fn map_rodata(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, RODATA)
+    }
+
+    fn map_range(&mut self, range: &Range<usize>, attr: Attributes) -> Result<(), MapError> {
+        self.idmap.map_range(&MemoryRegion::new(range.start, range.end), attr)
+    }
+}
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index 16e4893..c8c8660 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -30,6 +30,9 @@
     /** Returns an entry from /proc/meminfo. */
     long getMemInfoEntry(String name);
 
+    /** Allocates anonymous memory and returns the raw pointer. */
+    long allocAnonMemory(long mb);
+
     /**
      * Initializes the vsock server on VM.
      * @return the server socket file descriptor.
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index eda4f75..077c74f 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -19,6 +19,8 @@
 interface ITestService {
     const int SERVICE_PORT = 5678;
 
+    const int ECHO_REVERSE_PORT = 6789;
+
     /* add two integers. */
     int addInteger(int a, int b);
 
@@ -39,4 +41,9 @@
 
     /* get the encrypted storage path. */
     String getEncryptedStoragePath();
+
+    /* start a simple vsock server on ECHO_REVERSE_PORT that reads a line at a time and echoes
+     * each line reverse.
+     */
+    void runEchoReverseServer();
 }
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 10cdac5..9d2b6c7 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -38,6 +38,6 @@
         "libbase",
         "libbinder_ndk",
         "liblog",
-        "libvm_payload",
+        "libvm_payload#current",
     ],
 }
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index 28852e8..3c3faf2 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -27,6 +27,7 @@
 import android.app.Instrumentation;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
@@ -88,16 +89,17 @@
 
     private boolean canBootMicrodroidWithMemory(int mem)
             throws VirtualMachineException, InterruptedException, IOException {
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
-                .setDebugLevel(DEBUG_LEVEL_NONE)
-                .setMemoryMib(mem)
-                .build();
+        VirtualMachineConfig normalConfig =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setMemoryMib(mem)
+                        .build();
 
         // returns true if succeeded at least once.
         final int trialCount = 5;
         for (int i = 0; i < trialCount; i++) {
-            mInner.forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig);
+            forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig);
 
             if (tryBootVm(TAG, "test_vm_minimum_memory").payloadStarted) return true;
         }
@@ -147,12 +149,13 @@
         for (int i = 0; i < trialCount; i++) {
 
             // To grab boot events from log, set debug mode to FULL
-            VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                    .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
-                    .setDebugLevel(DEBUG_LEVEL_FULL)
-                    .setMemoryMib(256)
-                    .build();
-            mInner.forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
+            VirtualMachineConfig normalConfig =
+                    newVmConfigBuilder()
+                            .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
+                            .setDebugLevel(DEBUG_LEVEL_FULL)
+                            .setMemoryMib(256)
+                            .build();
+            forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
 
             BootResult result = tryBootVm(TAG, "test_vm_boot_time");
             assertThat(result.payloadStarted).isTrue();
@@ -195,17 +198,17 @@
 
     @Test
     public void testVsockTransferFromHostToVM() throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config_io.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
         for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
             int port = (mProtectedVm ? 5666 : 6666) + i;
             String vmName = "test_vm_io_" + i;
-            mInner.forceCreateNewVirtualMachine(vmName, config);
-            VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+            VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
             BenchmarkVmListener.create(new VsockListener(transferRates, port)).runToFinish(TAG, vm);
         }
         reportMetrics(transferRates, "vsock/transfer_host_to_vm", "mb_per_sec");
@@ -222,10 +225,11 @@
     }
 
     private void testVirtioBlkReadRate(boolean isRand) throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config_io.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
         for (int i = 0; i < IO_TEST_TRIAL_COUNT + 1; ++i) {
@@ -236,8 +240,7 @@
                 readRates.clear();
             }
             String vmName = "test_vm_io_" + i;
-            mInner.forceCreateNewVirtualMachine(vmName, config);
-            VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+            VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
             BenchmarkVmListener.create(new VirtioBlkListener(readRates, isRand))
                     .runToFinish(TAG, vm);
         }
@@ -277,16 +280,68 @@
         return runInShell(TAG, mInstrumentation.getUiAutomation(), command);
     }
 
+    private static class CrosvmStats {
+        public final long mHostRss;
+        public final long mHostPss;
+        public final long mGuestRss;
+        public final long mGuestPss;
+
+        CrosvmStats(Function<String, String> shellExecutor) {
+            try {
+                List<Integer> crosvmPids =
+                        ProcessUtil.getProcessMap(shellExecutor).entrySet().stream()
+                                .filter(e -> e.getValue().contains("crosvm"))
+                                .map(e -> e.getKey())
+                                .collect(java.util.stream.Collectors.toList());
+                if (crosvmPids.size() != 1) {
+                    throw new IllegalStateException(
+                            "expected to find exactly one crosvm processes, found "
+                                    + crosvmPids.size());
+                }
+
+                long hostRss = 0;
+                long hostPss = 0;
+                long guestRss = 0;
+                long guestPss = 0;
+                boolean hasGuestMaps = false;
+                for (ProcessUtil.SMapEntry entry :
+                        ProcessUtil.getProcessSmaps(crosvmPids.get(0), shellExecutor)) {
+                    long rss = entry.metrics.get("Rss");
+                    long pss = entry.metrics.get("Pss");
+                    if (entry.name.contains("crosvm_guest")) {
+                        guestRss += rss;
+                        guestPss += pss;
+                        hasGuestMaps = true;
+                    } else {
+                        hostRss += rss;
+                        hostPss += pss;
+                    }
+                }
+                if (!hasGuestMaps) {
+                    throw new IllegalStateException(
+                            "found no crosvm_guest smap entry in crosvm process");
+                }
+                mHostRss = hostRss;
+                mHostPss = hostPss;
+                mGuestRss = guestRss;
+                mGuestPss = guestPss;
+            } catch (Exception e) {
+                Log.e(TAG, "Error inside onPayloadReady():" + e);
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     @Test
     public void testMemoryUsage() throws Exception {
         final String vmName = "test_vm_mem_usage";
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config_io.json")
-                .setDebugLevel(DEBUG_LEVEL_NONE)
-                .setMemoryMib(256)
-                .build();
-        mInner.forceCreateNewVirtualMachine(vmName, config);
-        VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setMemoryMib(256)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
         MemoryUsageListener listener = new MemoryUsageListener(this::executeCommand);
         BenchmarkVmListener.create(listener).runToFinish(TAG, vm);
 
@@ -297,10 +352,10 @@
         double mem_buffers = (double) listener.mBuffers / 1024.0;
         double mem_cached = (double) listener.mCached / 1024.0;
         double mem_slab = (double) listener.mSlab / 1024.0;
-        double mem_crosvm_host_rss = (double) listener.mCrosvmHostRss / 1024.0;
-        double mem_crosvm_host_pss = (double) listener.mCrosvmHostPss / 1024.0;
-        double mem_crosvm_guest_rss = (double) listener.mCrosvmGuestRss / 1024.0;
-        double mem_crosvm_guest_pss = (double) listener.mCrosvmGuestPss / 1024.0;
+        double mem_crosvm_host_rss = (double) listener.mCrosvm.mHostRss / 1024.0;
+        double mem_crosvm_host_pss = (double) listener.mCrosvm.mHostPss / 1024.0;
+        double mem_crosvm_guest_rss = (double) listener.mCrosvm.mGuestRss / 1024.0;
+        double mem_crosvm_guest_pss = (double) listener.mCrosvm.mGuestPss / 1024.0;
 
         double mem_kernel = mem_overall - mem_total;
         double mem_used = mem_total - mem_free - mem_buffers - mem_cached - mem_slab;
@@ -325,7 +380,7 @@
             mShellExecutor = shellExecutor;
         }
 
-        public Function<String, String> mShellExecutor;
+        public final Function<String, String> mShellExecutor;
 
         public long mMemTotal;
         public long mMemFree;
@@ -334,10 +389,7 @@
         public long mCached;
         public long mSlab;
 
-        public long mCrosvmHostRss;
-        public long mCrosvmHostPss;
-        public long mCrosvmGuestRss;
-        public long mCrosvmGuestPss;
+        public CrosvmStats mCrosvm;
 
         @Override
         public void onPayloadReady(VirtualMachine vm, IBenchmarkService service)
@@ -348,39 +400,80 @@
             mBuffers = service.getMemInfoEntry("Buffers");
             mCached = service.getMemInfoEntry("Cached");
             mSlab = service.getMemInfoEntry("Slab");
+            mCrosvm = new CrosvmStats(mShellExecutor);
+        }
+    }
 
+    @Test
+    public void testMemoryReclaim() throws Exception {
+        final String vmName = "test_vm_mem_reclaim";
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setMemoryMib(256)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
+        MemoryReclaimListener listener = new MemoryReclaimListener(this::executeCommand);
+        BenchmarkVmListener.create(listener).runToFinish(TAG, vm);
+
+        double mem_pre_crosvm_host_rss = (double) listener.mPreCrosvm.mHostRss / 1024.0;
+        double mem_pre_crosvm_host_pss = (double) listener.mPreCrosvm.mHostPss / 1024.0;
+        double mem_pre_crosvm_guest_rss = (double) listener.mPreCrosvm.mGuestRss / 1024.0;
+        double mem_pre_crosvm_guest_pss = (double) listener.mPreCrosvm.mGuestPss / 1024.0;
+        double mem_post_crosvm_host_rss = (double) listener.mPostCrosvm.mHostRss / 1024.0;
+        double mem_post_crosvm_host_pss = (double) listener.mPostCrosvm.mHostPss / 1024.0;
+        double mem_post_crosvm_guest_rss = (double) listener.mPostCrosvm.mGuestRss / 1024.0;
+        double mem_post_crosvm_guest_pss = (double) listener.mPostCrosvm.mGuestPss / 1024.0;
+
+        Bundle bundle = new Bundle();
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_pre_crosvm_host_rss_MB", mem_pre_crosvm_host_rss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_pre_crosvm_host_pss_MB", mem_pre_crosvm_host_pss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_rss_MB", mem_pre_crosvm_guest_rss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_pss_MB", mem_pre_crosvm_guest_pss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_post_crosvm_host_rss_MB", mem_post_crosvm_host_rss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_post_crosvm_host_pss_MB", mem_post_crosvm_host_pss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_post_crosvm_guest_rss_MB", mem_post_crosvm_guest_rss);
+        bundle.putDouble(
+                METRIC_NAME_PREFIX + "mem_post_crosvm_guest_pss_MB", mem_post_crosvm_guest_pss);
+        mInstrumentation.sendStatus(0, bundle);
+    }
+
+    private static class MemoryReclaimListener implements BenchmarkVmListener.InnerListener {
+        MemoryReclaimListener(Function<String, String> shellExecutor) {
+            mShellExecutor = shellExecutor;
+        }
+
+        public final Function<String, String> mShellExecutor;
+
+        public CrosvmStats mPreCrosvm;
+        public CrosvmStats mPostCrosvm;
+
+        @Override
+        @SuppressWarnings("ReturnValueIgnored")
+        public void onPayloadReady(VirtualMachine vm, IBenchmarkService service)
+                throws RemoteException {
+            // Allocate 256MB of anonymous memory. This will fill all guest
+            // memory and cause swapping to start.
+            service.allocAnonMemory(256);
+            mPreCrosvm = new CrosvmStats(mShellExecutor);
+            // Send a memory trim hint to cause memory reclaim.
+            mShellExecutor.apply("am send-trim-memory " + Process.myPid() + " RUNNING_CRITICAL");
+            // Give time for the memory reclaim to do its work.
             try {
-                List<Integer> crosvmPids =
-                        ProcessUtil.getProcessMap(mShellExecutor).entrySet().stream()
-                                .filter(e -> e.getValue().contains("crosvm"))
-                                .map(e -> e.getKey())
-                                .collect(java.util.stream.Collectors.toList());
-                if (crosvmPids.size() != 1) {
-                    throw new RuntimeException(
-                            "expected to find exactly one crosvm processes, found "
-                                    + crosvmPids.size());
-                }
-
-                mCrosvmHostRss = 0;
-                mCrosvmHostPss = 0;
-                mCrosvmGuestRss = 0;
-                mCrosvmGuestPss = 0;
-                for (ProcessUtil.SMapEntry entry :
-                        ProcessUtil.getProcessSmaps(crosvmPids.get(0), mShellExecutor)) {
-                    long rss = entry.metrics.get("Rss");
-                    long pss = entry.metrics.get("Pss");
-                    if (entry.name.contains("crosvm_guest")) {
-                        mCrosvmGuestRss += rss;
-                        mCrosvmGuestPss += pss;
-                    } else {
-                        mCrosvmHostRss += rss;
-                        mCrosvmHostPss += pss;
-                    }
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "Error inside onPayloadReady():" + e);
-                throw new RuntimeException(e);
+                Thread.sleep(isCuttlefish() ? 10000 : 5000);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Interrupted sleep:" + e);
+                Thread.currentThread().interrupt();
             }
+            mPostCrosvm = new CrosvmStats(mShellExecutor);
         }
     }
 
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 70ec7db..56963e6 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -77,6 +77,11 @@
         return ndk::ScopedAStatus::ok();
     }
 
+    ndk::ScopedAStatus allocAnonMemory(int64_t mb, int64_t* out) override {
+        *out = (int64_t)(long)alloc_anon_memory((long)mb);
+        return ndk::ScopedAStatus::ok();
+    }
+
     ndk::ScopedAStatus initVsockServer(int32_t port, int32_t* out) override {
         auto res = io_vsock::init_vsock_server(port);
         if (res.ok()) {
@@ -131,6 +136,17 @@
         return {file_size_mb / elapsed_seconds};
     }
 
+    void* alloc_anon_memory(long mb) {
+        long bytes = mb << 20;
+        void* p = malloc(bytes);
+        /*
+         * Heap memory is demand allocated. Dirty all pages to ensure
+         * all are allocated.
+         */
+        memset(p, 0x55, bytes);
+        return p;
+    }
+
     Result<size_t> read_meminfo_entry(const std::string& stat) {
         std::ifstream fs("/proc/meminfo");
         if (!fs.is_open()) {
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
index d85929d..940ec9c 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
@@ -99,12 +99,14 @@
             if (line.length() == 0) {
                 continue;
             }
-            if (line.contains(": ")) {
+            // Each line is '<metrics>:        <number> kB'.
+            // EX : Pss_Anon:        70712 kB
+            // EX : Active(file):     5792 kB
+            // EX : ProtectionKey:       0
+            if (line.matches("[\\w()]+:\\s+.*")) {
                 if (entries.size() == 0) {
                     throw new RuntimeException("unexpected line: " + line);
                 }
-                // Each line is '<metrics>:        <number> kB'.
-                // EX : Pss_Anon:        70712 kB
                 if (line.endsWith(" kB")) line = line.substring(0, line.length() - 3);
                 String[] elems = line.split(":");
                 String name = elems[0].trim();
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 24e2049..bd5b180 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -73,59 +73,48 @@
                 permission);
     }
 
-    // TODO(b/220920264): remove Inner class; this is a hack to hide virt APEX types
-    protected static class Inner {
-        private final boolean mProtectedVm;
-        private final Context mContext;
-        private final VirtualMachineManager mVmm;
-
-        public Inner(Context context, boolean protectedVm, VirtualMachineManager vmm) {
-            mProtectedVm = protectedVm;
-            mVmm = vmm;
-            mContext = context;
-        }
-
-        public VirtualMachineManager getVirtualMachineManager() {
-            return mVmm;
-        }
-
-        public Context getContext() {
-            return mContext;
-        }
-
-        public VirtualMachineConfig.Builder newVmConfigBuilder() {
-            return new VirtualMachineConfig.Builder(mContext).setProtectedVm(mProtectedVm);
-        }
-
-        /**
-         * Creates a new virtual machine, potentially removing an existing virtual machine with
-         * given name.
-         */
-        public VirtualMachine forceCreateNewVirtualMachine(String name, VirtualMachineConfig config)
-                throws VirtualMachineException {
-            VirtualMachine existingVm = mVmm.get(name);
-            if (existingVm != null) {
-                mVmm.delete(name);
-            }
-            return mVmm.create(name, config);
-        }
-    }
-
-    protected Inner mInner;
+    private Context mCtx;
+    private boolean mProtectedVm;
 
     protected Context getContext() {
-        return mInner.getContext();
+        return mCtx;
+    }
+
+    public VirtualMachineManager getVirtualMachineManager() {
+        return mCtx.getSystemService(VirtualMachineManager.class);
+    }
+
+    public VirtualMachineConfig.Builder newVmConfigBuilder() {
+        return new VirtualMachineConfig.Builder(mCtx).setProtectedVm(mProtectedVm);
+    }
+
+    protected final boolean isProtectedVm() {
+        return mProtectedVm;
+    }
+
+    /**
+     * Creates a new virtual machine, potentially removing an existing virtual machine with given
+     * name.
+     */
+    public VirtualMachine forceCreateNewVirtualMachine(String name, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        final VirtualMachineManager vmm = getVirtualMachineManager();
+        VirtualMachine existingVm = vmm.get(name);
+        if (existingVm != null) {
+            vmm.delete(name);
+        }
+        return vmm.create(name, config);
     }
 
     public void prepareTestSetup(boolean protectedVm) {
-        Context ctx = ApplicationProvider.getApplicationContext();
+        mCtx = ApplicationProvider.getApplicationContext();
         assume().withMessage("Device doesn't support AVF")
-                .that(ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
+                .that(mCtx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
                 .isTrue();
 
-        mInner = new Inner(ctx, protectedVm, VirtualMachineManager.getInstance(ctx));
+        mProtectedVm = protectedVm;
 
-        int capabilities = mInner.getVirtualMachineManager().getCapabilities();
+        int capabilities = getVirtualMachineManager().getCapabilities();
         if (protectedVm) {
             assume().withMessage("Skip where protected VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_PROTECTED_VM)
@@ -314,7 +303,7 @@
 
     public BootResult tryBootVm(String logTag, String vmName)
             throws VirtualMachineException, InterruptedException {
-        VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+        VirtualMachine vm = getVirtualMachineManager().get(vmName);
         final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
         final CompletableFuture<Integer> deathReason = new CompletableFuture<>();
         final CompletableFuture<Long> endTime = new CompletableFuture<>();
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index e5aa908..8816dbd 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -29,6 +29,7 @@
 import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
@@ -41,12 +42,15 @@
 import java.util.Arrays;
 
 public abstract class MicrodroidHostTestCaseBase extends BaseHostJUnit4Test {
+
     protected static final String TEST_ROOT = "/data/local/tmp/virt/";
     protected static final String LOG_PATH = TEST_ROOT + "log.txt";
     protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt";
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
+    private static final String PVMFW_IMG_PATH = TEST_ROOT + "pvmfw.img";
+    private static final String PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
 
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
     protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
@@ -55,6 +59,19 @@
             (int) (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000
                 / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
 
+    @Option(
+            name = "pvmfw",
+            description =
+                    "Custom pvmfw.img path on host device."
+                            + " If present, it will be pushed to "
+                            + PVMFW_IMG_PATH,
+            mandatory = false)
+    private static String sCustomPvmfwPathOnHost = "";
+
+    private static boolean isEmptyText(String str) {
+        return str == null || str.length() == 0;
+    }
+
     public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
             throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(androidDevice);
@@ -67,6 +84,13 @@
 
         // remove any leftover files under test root
         android.tryRun("rm", "-rf", TEST_ROOT + "*");
+
+        // prepare custom pvmfw.img if necessary
+        if (!isEmptyText(sCustomPvmfwPathOnHost)) {
+            runOnHost("adb", "root");
+            runOnHost("adb", "push", sCustomPvmfwPathOnHost, PVMFW_IMG_PATH);
+            runOnHost("adb", "shell", "setprop", PVMFW_IMG_PATH_PROP, PVMFW_IMG_PATH);
+        }
     }
 
     public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice)
@@ -80,6 +104,10 @@
         android.tryRun("killall", "crosvm");
         android.tryRun("stop", "virtualizationservice");
         android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
+
+        if (!isEmptyText(sCustomPvmfwPathOnHost)) {
+            runOnHost("adb", "shell", "setprop", PVMFW_IMG_PATH_PROP, "\"\"");
+        }
     }
 
     protected boolean isCuttlefish() {
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 795d7b3..bf2d411 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -146,23 +146,16 @@
                                 .collect(toList())));
         FileUtil.writeToFile(config.toString(), configFile);
 
-        File mkPayload = findTestFile("mk_payload");
         RunUtil runUtil = new RunUtil();
-        // Set the parent dir on the PATH (e.g. <workdir>/bin)
-        String separator = System.getProperty("path.separator");
-        String path = mkPayload.getParentFile().getPath() + separator + System.getenv("PATH");
-        runUtil.setEnvVariable("PATH", path);
-
-        List<String> command = new ArrayList<>();
-        command.add("mk_payload");
-        command.add("--metadata-only");
-        command.add(configFile.toString());
-        command.add(payloadMetadata.toString());
-
-        CommandResult result =
-                runUtil.runTimedCmd(
-                        // mk_payload should run fast enough
-                        5 * 1000, "/bin/bash", "-c", String.join(" ", command));
+        String command =
+                String.join(
+                        " ",
+                        findTestFile("mk_payload").getAbsolutePath(),
+                        "--metadata-only",
+                        configFile.getAbsolutePath(),
+                        payloadMetadata.getAbsolutePath());
+        // mk_payload should run fast enough
+        CommandResult result = runUtil.runTimedCmd(5000, "/bin/bash", "-c", command);
         String out = result.getStdout();
         String err = result.getStderr();
         assertWithMessage(
@@ -186,7 +179,7 @@
         runUtil.setEnvVariable("PATH", path);
 
         List<String> command = new ArrayList<>();
-        command.add("sign_virt_apex");
+        command.add(signVirtApex.getAbsolutePath());
         keyOverrides.forEach(
                 (filename, keyFile) ->
                         command.add("--key_override " + filename + "=" + keyFile.getPath()));
@@ -211,18 +204,11 @@
             long timeoutMillis, Callable<T> callable, org.hamcrest.Matcher<T> matcher)
             throws Exception {
         long start = System.currentTimeMillis();
-        while (true) {
-            try {
-                assertThat(callable.call(), matcher);
-                return;
-            } catch (Throwable e) {
-                if (System.currentTimeMillis() - start < timeoutMillis) {
-                    Thread.sleep(500);
-                } else {
-                    throw e;
-                }
-            }
+        while ((System.currentTimeMillis() - start < timeoutMillis)
+                && !matcher.matches(callable.call())) {
+            Thread.sleep(500);
         }
+        assertThat(callable.call(), matcher);
     }
 
     static class ActiveApexInfo {
@@ -421,20 +407,51 @@
     }
 
     @Test
-    @Ignore("b/245081929")
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
-    public void testBootFailsWhenProtectedVmStartsWithImagesSignedWithDifferentKey()
-            throws Exception {
+    public void protectedVmWithValidKernelImageRunsPvmfw() throws Exception {
+        // Arrange
         boolean protectedVm = true;
         assumeTrue(
                 "Skip if protected VMs are not supported",
                 getAndroidDevice().supportsMicrodroid(protectedVm));
-
         File key = findTestFile("test.com.android.virt.pem");
-        Map<String, File> keyOverrides = Map.of();
-        VmInfo vmInfo = runMicrodroidWithResignedImages(key, keyOverrides, protectedVm);
+
+        // Act
+        // TODO(b/256148034): Do not resign kernel image
+        VmInfo vmInfo =
+                runMicrodroidWithResignedImages(key, /*keyOverrides=*/ Map.of(), protectedVm);
+
+        // Assert
         vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
-        assertThat(getDevice().pullFileContents(CONSOLE_PATH), containsString("pvmfw boot failed"));
+        String consoleLog = getDevice().pullFileContents(CONSOLE_PATH);
+        assertWithMessage("pvmfw should start").that(consoleLog).contains("pVM firmware");
+        assertWithMessage("pvmfw should start payload")
+                .that(consoleLog)
+                .contains("Payload verified. Starting payload...");
+        vmInfo.mProcess.destroy();
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
+    public void protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() throws Exception {
+        // Arrange
+        boolean protectedVm = true;
+        assumeTrue(
+                "Skip if protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(protectedVm));
+        File key = findTestFile("test.com.android.virt.pem");
+
+        // Act
+        VmInfo vmInfo =
+                runMicrodroidWithResignedImages(key, /*keyOverrides=*/ Map.of(), protectedVm);
+
+        // Assert
+        vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
+        String consoleLog = getDevice().pullFileContents(CONSOLE_PATH);
+        assertWithMessage("pvmfw should start").that(consoleLog).contains("pVM firmware");
+        // TODO(b/256148034): Asserts that pvmfw run fails when this verification is implemented.
+        // Also rename the test.
+        vmInfo.mProcess.destroy();
     }
 
     // TODO(b/245277660): Resigning the system/vendor image changes the vbmeta hash.
diff --git a/tests/no_avf/Android.bp b/tests/no_avf/Android.bp
new file mode 100644
index 0000000..fd0d5e2
--- /dev/null
+++ b/tests/no_avf/Android.bp
@@ -0,0 +1,21 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CtsMicrodroidDisabledTestCases",
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "compatibility-common-util-devicesidelib",
+        "truth-prebuilt",
+    ],
+    sdk_version: "test_current",
+    compile_multilib: "both",
+    min_sdk_version: "UpsideDownCake",
+}
diff --git a/tests/no_avf/AndroidManifest.xml b/tests/no_avf/AndroidManifest.xml
new file mode 100644
index 0000000..4a1304e
--- /dev/null
+++ b/tests/no_avf/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.nomicrodroid.test">
+    <application />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.nomicrodroid.test"
+        android:label="CTS test for devices without Microdroid support" />
+</manifest>
diff --git a/tests/no_avf/AndroidTest.xml b/tests/no_avf/AndroidTest.xml
new file mode 100644
index 0000000..1e93887
--- /dev/null
+++ b/tests/no_avf/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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 Microdroid device-side tests.">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="security" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="CtsMicrodroidDisabledTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.nomicrodroid.test" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/no_avf/README.md b/tests/no_avf/README.md
new file mode 100644
index 0000000..b96dc97
--- /dev/null
+++ b/tests/no_avf/README.md
@@ -0,0 +1 @@
+CTS tests for devices that don't support AVF.
\ No newline at end of file
diff --git a/tests/no_avf/src/com/android/nomicrodroid/test/NoMicrodroidTest.java b/tests/no_avf/src/com/android/nomicrodroid/test/NoMicrodroidTest.java
new file mode 100644
index 0000000..0982e35
--- /dev/null
+++ b/tests/no_avf/src/com/android/nomicrodroid/test/NoMicrodroidTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 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.nomicrodroid.test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.system.virtualmachine.VirtualMachineManager;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests to validate that devices without support for AVF (Android Virtualization Framework) are set
+ * up correctly.
+ */
+@RunWith(JUnit4.class)
+public class NoMicrodroidTest {
+
+    @Before
+    public void setUp() {
+        final PackageManager pm =
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+        assume().withMessage("Device supports AVF")
+                .that(pm.hasSystemFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK))
+                .isFalse();
+    }
+
+    @CddTest(requirements = {"9.17/C-1-1"})
+    @Test
+    public void testVirtualMachineManagerLookupReturnsNull() {
+        final Context ctx = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertThat(ctx.getSystemService(VirtualMachineManager.class)).isNull();
+    }
+}
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index df7c6c0..4dc9489 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -39,7 +39,7 @@
     shared_libs: [
         "libbinder_ndk",
         "MicrodroidTestNativeLibSub",
-        "libvm_payload",
+        "libvm_payload#current",
     ],
     static_libs: [
         "com.android.microdroid.testservice-ndk",
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 71a9e3b..35b9e61 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -15,6 +15,9 @@
  */
 package com.android.microdroid.test;
 
+import static android.system.virtualmachine.VirtualMachine.STATUS_DELETED;
+import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING;
+import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_APP_ONLY;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
@@ -28,6 +31,9 @@
 
 import android.content.Context;
 import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.SystemProperties;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
@@ -52,11 +58,17 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.RandomAccessFile;
+import java.io.Writer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -65,6 +77,7 @@
 import java.util.OptionalLong;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
 
 import co.nstant.in.cbor.CborDecoder;
 import co.nstant.in.cbor.model.Array;
@@ -102,18 +115,17 @@
     private static final int MIN_MEM_X86_64 = 196;
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-1"
-    })
-    public void connectToVmService() throws Exception {
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void createAndConnectToVm() throws Exception {
         assumeSupportedKernel();
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setMemoryMib(minMemoryRequired())
-                .build();
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
 
         TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mException).isNull();
@@ -125,28 +137,194 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-1-2",
-            "9.17/C-1-4",
-    })
-    public void createVmRequiresPermission() throws Exception {
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void createAndRunNoDebugVm() throws Exception {
+        assumeSupportedKernel();
+
+        // For most of our tests we use a debug VM so failures can be diagnosed.
+        // But we do need non-debug VMs to work, so run one.
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+
+        TestResults testResults = runVmTestService(vm);
+        assertThat(testResults.mException).isNull();
+    }
+
+    @Test
+    @CddTest(
+            requirements = {
+                "9.17/C-1-1",
+                "9.17/C-1-2",
+                "9.17/C-1-4",
+            })
+    public void createVmRequiresPermission() {
         assumeSupportedKernel();
 
         revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setMemoryMib(minMemoryRequired())
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .build();
 
-        SecurityException e = assertThrows(SecurityException.class,
-                () -> mInner.forceCreateNewVirtualMachine("test_vm_requires_permission", config));
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class,
+                        () -> forceCreateNewVirtualMachine("test_vm_requires_permission", config));
         assertThat(e).hasMessageThat()
                 .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
     }
 
     @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void autoCloseVm() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        try (VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config)) {
+            assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED);
+            // close() implicitly called on stopped VM.
+        }
+
+        try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) {
+            vm.run();
+            assertThat(vm.getStatus()).isEqualTo(STATUS_RUNNING);
+            // close() implicitly called on running VM.
+        }
+
+        try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) {
+            assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED);
+            getVirtualMachineManager().delete("test_vm");
+            assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED);
+            // close() implicitly called on deleted VM.
+        }
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void connectVsock() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_vsock", config);
+
+        AtomicReference<Exception> exception = new AtomicReference<>();
+        AtomicReference<String> response = new AtomicReference<>();
+        String request = "Look not into the abyss";
+
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        try (vm) {
+                            ITestService testService =
+                                    ITestService.Stub.asInterface(
+                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            testService.runEchoReverseServer();
+
+                            ParcelFileDescriptor pfd =
+                                    vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
+                            try (InputStream input = new AutoCloseInputStream(pfd);
+                                    OutputStream output = new AutoCloseOutputStream(pfd)) {
+                                BufferedReader reader =
+                                        new BufferedReader(new InputStreamReader(input));
+                                Writer writer = new OutputStreamWriter(output);
+                                writer.write(request + "\n");
+                                writer.flush();
+                                response.set(reader.readLine());
+                            }
+                        } catch (Exception e) {
+                            exception.set(e);
+                        }
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+        if (exception.get() != null) {
+            throw new RuntimeException(exception.get());
+        }
+        assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void vmConfigUnitTests() {
+        VirtualMachineConfig minimal =
+                newVmConfigBuilder().setPayloadBinaryPath("binary/path").build();
+
+        assertThat(minimal.getApkPath()).isEqualTo(getContext().getPackageCodePath());
+        assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
+        assertThat(minimal.getMemoryMib()).isEqualTo(0);
+        assertThat(minimal.getNumCpus()).isEqualTo(1);
+        assertThat(minimal.getPayloadBinaryPath()).isEqualTo("binary/path");
+        assertThat(minimal.getPayloadConfigPath()).isNull();
+        assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
+
+        int maxCpus = Runtime.getRuntime().availableProcessors();
+        VirtualMachineConfig.Builder maximalBuilder =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("config/path")
+                        .setApkPath("/apk/path")
+                        .setNumCpus(maxCpus)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setMemoryMib(42);
+        VirtualMachineConfig maximal = maximalBuilder.build();
+
+        assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
+        assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
+        assertThat(maximal.getMemoryMib()).isEqualTo(42);
+        assertThat(maximal.getNumCpus()).isEqualTo(maxCpus);
+        assertThat(maximal.getPayloadBinaryPath()).isNull();
+        assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
+        assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
+
+        assertThat(minimal.isCompatibleWith(maximal)).isFalse();
+        assertThat(minimal.isCompatibleWith(minimal)).isTrue();
+        assertThat(maximal.isCompatibleWith(maximal)).isTrue();
+
+        VirtualMachineConfig compatible = maximalBuilder.setNumCpus(1).setMemoryMib(99).build();
+        assertThat(compatible.isCompatibleWith(maximal)).isTrue();
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void vmUnitTests() throws Exception {
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder().setPayloadBinaryPath("binary/path");
+        VirtualMachineConfig config = builder.build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("vm_name", config);
+
+        assertThat(vm.getName()).isEqualTo("vm_name");
+        assertThat(vm.getConfig().getPayloadBinaryPath()).isEqualTo("binary/path");
+        assertThat(vm.getConfig().getMemoryMib()).isEqualTo(0);
+
+        VirtualMachineConfig compatibleConfig = builder.setMemoryMib(42).build();
+        vm.setConfig(compatibleConfig);
+
+        assertThat(vm.getName()).isEqualTo("vm_name");
+        assertThat(vm.getConfig().getPayloadBinaryPath()).isEqualTo("binary/path");
+        assertThat(vm.getConfig().getMemoryMib()).isEqualTo(42);
+
+        assertThat(getVirtualMachineManager().get("vm_name")).isSameInstanceAs(vm);
+    }
+
+    @Test
     @CddTest(requirements = {
             "9.17/C-1-1",
             "9.17/C-1-2",
@@ -155,13 +333,14 @@
     public void createVmWithConfigRequiresPermission() throws Exception {
         assumeSupportedKernel();
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config.json")
-                .setMemoryMib(minMemoryRequired())
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config.json")
+                        .setMemoryMib(minMemoryRequired())
+                        .build();
 
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine(
-                "test_vm_config_requires_permission", config);
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("test_vm_config_requires_permission", config);
 
         SecurityException e = assertThrows(SecurityException.class, () -> runVmTestService(vm));
         assertThat(e).hasMessageThat()
@@ -175,14 +354,14 @@
     public void deleteVm() throws Exception {
         assumeSupportedKernel();
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setMemoryMib(minMemoryRequired())
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .build();
 
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_delete",
-                config);
-        VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_delete", config);
+        VirtualMachineManager vmm = getVirtualMachineManager();
         vmm.delete("test_vm_delete");
 
         // VM should no longer exist
@@ -202,14 +381,15 @@
     public void validApkPathIsAccepted() throws Exception {
         assumeSupportedKernel();
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setApkPath(getContext().getPackageCodePath())
-                .setMemoryMib(minMemoryRequired())
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setApkPath(getContext().getPackageCodePath())
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
 
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine(
-                "test_vm_explicit_apk_path", config);
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_explicit_apk_path", config);
 
         TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mException).isNull();
@@ -220,16 +400,24 @@
             "9.17/C-1-1",
     })
     public void invalidApkPathIsRejected() {
-        assumeSupportedKernel();
-
-        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setApkPath("relative/path/to.apk")
-                .setMemoryMib(minMemoryRequired());
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setApkPath("relative/path/to.apk")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setMemoryMib(minMemoryRequired());
         assertThrows(IllegalArgumentException.class, () -> builder.build());
     }
 
     @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void invalidVmNameIsRejected() {
+        VirtualMachineManager vmm = getVirtualMachineManager();
+        assertThrows(IllegalArgumentException.class, () -> vmm.get("../foo"));
+        assertThrows(IllegalArgumentException.class, () -> vmm.get(".."));
+    }
+
+    @Test
     @CddTest(requirements = {
             "9.17/C-1-1",
             "9.17/C-2-1"
@@ -238,11 +426,13 @@
         assumeSupportedKernel();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config_extra_apk.json")
-                .setMemoryMib(minMemoryRequired())
-                .build();
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_extra_apk.json")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_extra_apk", config);
 
         TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
@@ -251,12 +441,13 @@
     @Test
     public void bootFailsWhenLowMem() throws Exception {
         for (int memMib : new int[]{ 10, 20, 40 }) {
-            VirtualMachineConfig lowMemConfig = mInner.newVmConfigBuilder()
-                    .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                    .setMemoryMib(memMib)
-                    .setDebugLevel(DEBUG_LEVEL_NONE)
-                    .build();
-            VirtualMachine vm = mInner.forceCreateNewVirtualMachine("low_mem", lowMemConfig);
+            VirtualMachineConfig lowMemConfig =
+                    newVmConfigBuilder()
+                            .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                            .setMemoryMib(memMib)
+                            .setDebugLevel(DEBUG_LEVEL_NONE)
+                            .build();
+            VirtualMachine vm = forceCreateNewVirtualMachine("low_mem", lowMemConfig);
             final CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
             final CompletableFuture<Boolean> onStoppedExecuted = new CompletableFuture<>();
             VmEventListener listener =
@@ -298,11 +489,11 @@
         assumeSupportedKernel();
 
         VirtualMachineConfig.Builder builder =
-                mInner.newVmConfigBuilder()
+                newVmConfigBuilder()
                         .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
                         .setDebugLevel(fromLevel);
         VirtualMachineConfig normalConfig = builder.build();
-        mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
+        forceCreateNewVirtualMachine("test_vm", normalConfig);
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
 
         // Try to run the VM again with the previous instance.img
@@ -311,7 +502,7 @@
         File vmInstance = getVmFile("test_vm", "instance.img");
         File vmInstanceBackup = File.createTempFile("instance", ".img");
         Files.copy(vmInstance.toPath(), vmInstanceBackup.toPath(), REPLACE_EXISTING);
-        mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
+        forceCreateNewVirtualMachine("test_vm", normalConfig);
         Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
 
@@ -320,7 +511,7 @@
         // For testing, we do that by creating a new VM with debug level, and copy the old instance
         // image to the new VM instance image.
         VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build();
-        mInner.forceCreateNewVirtualMachine("test_vm", debugConfig);
+        forceCreateNewVirtualMachine("test_vm", debugConfig);
         Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
     }
@@ -331,7 +522,7 @@
     }
 
     private VmCdis launchVmAndGetCdis(String instanceName) throws Exception {
-        VirtualMachine vm = mInner.getVirtualMachineManager().get(instanceName);
+        VirtualMachine vm = getVirtualMachineManager().get(instanceName);
         final VmCdis vmCdis = new VmCdis();
         final CompletableFuture<Exception> exception = new CompletableFuture<>();
         VmEventListener listener =
@@ -367,12 +558,13 @@
         assumeSupportedKernel();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
-        mInner.forceCreateNewVirtualMachine("test_vm_a", normalConfig);
-        mInner.forceCreateNewVirtualMachine("test_vm_b", normalConfig);
+        VirtualMachineConfig normalConfig =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        forceCreateNewVirtualMachine("test_vm_a", normalConfig);
+        forceCreateNewVirtualMachine("test_vm_b", normalConfig);
         VmCdis vm_a_cdis = launchVmAndGetCdis("test_vm_a");
         VmCdis vm_b_cdis = launchVmAndGetCdis("test_vm_b");
         assertThat(vm_a_cdis.cdiAttest).isNotNull();
@@ -390,13 +582,15 @@
     })
     public void sameInstanceKeepsSameCdis() throws Exception {
         assumeSupportedKernel();
+        assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
-        mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
+        VirtualMachineConfig normalConfig =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        forceCreateNewVirtualMachine("test_vm", normalConfig);
 
         VmCdis first_boot_cdis = launchVmAndGetCdis("test_vm");
         VmCdis second_boot_cdis = launchVmAndGetCdis("test_vm");
@@ -415,11 +609,12 @@
         assumeSupportedKernel();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("bcc_vm", normalConfig);
+        VirtualMachineConfig normalConfig =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
         final CompletableFuture<byte[]> bcc = new CompletableFuture<>();
         final CompletableFuture<Exception> exception = new CompletableFuture<>();
         VmEventListener listener =
@@ -463,11 +658,12 @@
     public void accessToCdisIsRestricted() throws Exception {
         assumeSupportedKernel();
 
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
-        mInner.forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        forceCreateNewVirtualMachine("test_vm", config);
 
         assertThrows(Exception.class, () -> launchVmAndGetCdis("test_vm"));
     }
@@ -509,12 +705,13 @@
     }
 
     private RandomAccessFile prepareInstanceImage(String vmName) throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
 
-        mInner.forceCreateNewVirtualMachine(vmName, config);
+        forceCreateNewVirtualMachine(vmName, config);
         assertThat(tryBootVm(TAG, vmName).payloadStarted).isTrue();
         File instanceImgPath = getVmFile(vmName, "instance.img");
         return new RandomAccessFile(instanceImgPath, "rw");
@@ -569,11 +766,12 @@
     @Test
     public void bootFailsWhenConfigIsInvalid() throws Exception {
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
-                .setPayloadConfigPath("assets/vm_config_no_task.json")
-                .setDebugLevel(DEBUG_LEVEL_FULL)
-                .build();
-        mInner.forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig);
+        VirtualMachineConfig normalConfig =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_no_task.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig);
 
         BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_config");
         assertThat(bootResult.payloadStarted).isFalse();
@@ -583,10 +781,10 @@
 
     @Test
     public void bootFailsWhenBinaryPathIsInvalid() throws Exception {
-        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("DoesNotExist.so");
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder().setPayloadBinaryPath("DoesNotExist.so");
         VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
-        mInner.forceCreateNewVirtualMachine("test_vm_invalid_binary_path", normalConfig);
+        forceCreateNewVirtualMachine("test_vm_invalid_binary_path", normalConfig);
 
         BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_binary_path");
         assertThat(bootResult.payloadStarted).isFalse();
@@ -596,17 +794,17 @@
 
     @Test
     public void sameInstancesShareTheSameVmObject() throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder()
-                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                .setDebugLevel(DEBUG_LEVEL_NONE)
-                .build();
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .build();
 
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm", config);
-        VirtualMachine vm2 = mInner.getVirtualMachineManager().get("test_vm");
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachine vm2 = getVirtualMachineManager().get("test_vm");
         assertThat(vm).isEqualTo(vm2);
 
-        VirtualMachine newVm = mInner.forceCreateNewVirtualMachine("test_vm", config);
-        VirtualMachine newVm2 = mInner.getVirtualMachineManager().get("test_vm");
+        VirtualMachine newVm = forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachine newVm2 = getVirtualMachineManager().get("test_vm");
         assertThat(newVm).isEqualTo(newVm2);
 
         assertThat(vm).isNotEqualTo(newVm);
@@ -618,17 +816,17 @@
         // Arrange
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
-                mInner.newVmConfigBuilder()
+                newVmConfigBuilder()
                         .setPayloadConfigPath("assets/vm_config.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         String vmNameOrig = "test_vm_orig";
         String vmNameImport = "test_vm_import";
-        VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+        VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
         VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
         assertThat(origCdis.instanceSecret).isNotNull();
         VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
-        VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+        VirtualMachineManager vmm = getVirtualMachineManager();
         if (vmm.get(vmNameImport) != null) {
             vmm.delete(vmNameImport);
         }
@@ -646,19 +844,19 @@
     public void importedVmIsEqualToTheOriginalVm() throws Exception {
         // Arrange
         VirtualMachineConfig config =
-                mInner.newVmConfigBuilder()
+                newVmConfigBuilder()
                         .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
-                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         String vmNameOrig = "test_vm_orig";
         String vmNameImport = "test_vm_import";
-        VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+        VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
         // Run something to make the instance.img different with the initialized one.
         TestResults origTestResults = runVmTestService(vmOrig);
         assertThat(origTestResults.mException).isNull();
         assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
         VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
-        VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+        VirtualMachineManager vmm = getVirtualMachineManager();
         if (vmm.get(vmNameImport) != null) {
             vmm.delete(vmNameImport);
         }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index c0a8c0e..8a0019d 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -13,33 +13,40 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 #include <aidl/com/android/microdroid/testservice/BnTestService.h>
 #include <android-base/file.h>
 #include <android-base/properties.h>
 #include <android-base/result.h>
-#include <android/binder_auto_utils.h>
-#include <android/binder_manager.h>
+#include <android/log.h>
 #include <fcntl.h>
 #include <fsverity_digests.pb.h>
 #include <linux/vm_sockets.h>
 #include <stdint.h>
 #include <stdio.h>
-#include <sys/ioctl.h>
 #include <sys/system_properties.h>
 #include <unistd.h>
 #include <vm_main.h>
 #include <vm_payload_restricted.h>
 
 #include <string>
+#include <thread>
 
+using android::base::borrowed_fd;
 using android::base::ErrnoError;
 using android::base::Error;
 using android::base::Result;
+using android::base::unique_fd;
+
+using aidl::com::android::microdroid::testservice::BnTestService;
+using ndk::ScopedAStatus;
 
 extern void testlib_sub();
 
 namespace {
 
+constexpr char TAG[] = "testbinary";
+
 template <typename T>
 Result<T> report_test(std::string name, Result<T> result) {
     auto property = "debug.microdroid.test." + name;
@@ -48,72 +55,156 @@
         outcome << "PASS";
     } else {
         outcome << "FAIL: " << result.error();
-        // Pollute stderr with the error in case the property is truncated.
-        std::cerr << "[" << name << "] test failed: " << result.error() << "\n";
+        // Log the error in case the property is truncated.
+        std::string message = name + ": " + outcome.str();
+        __android_log_write(ANDROID_LOG_WARN, TAG, message.c_str());
     }
     __system_property_set(property.c_str(), outcome.str().c_str());
     return result;
 }
 
+Result<void> run_echo_reverse_server(borrowed_fd listening_fd) {
+    struct sockaddr_vm client_sa = {};
+    socklen_t client_sa_len = sizeof(client_sa);
+    unique_fd connect_fd{accept4(listening_fd.get(), (struct sockaddr*)&client_sa, &client_sa_len,
+                                 SOCK_CLOEXEC)};
+    if (!connect_fd.ok()) {
+        return ErrnoError() << "Failed to accept vsock connection";
+    }
+
+    unique_fd input_fd{fcntl(connect_fd, F_DUPFD_CLOEXEC, 0)};
+    if (!input_fd.ok()) {
+        return ErrnoError() << "Failed to dup";
+    }
+    FILE* input = fdopen(input_fd.release(), "r");
+    if (!input) {
+        return ErrnoError() << "Failed to fdopen";
+    }
+
+    char* line = nullptr;
+    size_t size = 0;
+    if (getline(&line, &size, input) < 0) {
+        return ErrnoError() << "Failed to read";
+    }
+
+    if (fclose(input) != 0) {
+        return ErrnoError() << "Failed to fclose";
+    }
+
+    std::string_view original = line;
+    if (!original.empty() && original.back() == '\n') {
+        original = original.substr(0, original.size() - 1);
+    }
+
+    std::string reversed(original.rbegin(), original.rend());
+
+    if (write(connect_fd, reversed.data(), reversed.size()) < 0) {
+        return ErrnoError() << "Failed to write";
+    }
+
+    return {};
+}
+
+Result<void> start_echo_reverse_server() {
+    unique_fd server_fd{TEMP_FAILURE_RETRY(socket(AF_VSOCK, SOCK_STREAM | SOCK_CLOEXEC, 0))};
+    if (!server_fd.ok()) {
+        return ErrnoError() << "Failed to create vsock socket";
+    }
+    struct sockaddr_vm server_sa = (struct sockaddr_vm){
+            .svm_family = AF_VSOCK,
+            .svm_port = BnTestService::ECHO_REVERSE_PORT,
+            .svm_cid = VMADDR_CID_ANY,
+    };
+    int ret = TEMP_FAILURE_RETRY(bind(server_fd, (struct sockaddr*)&server_sa, sizeof(server_sa)));
+    if (ret < 0) {
+        return ErrnoError() << "Failed to bind vsock socket";
+    }
+    ret = TEMP_FAILURE_RETRY(listen(server_fd, /*backlog=*/1));
+    if (ret < 0) {
+        return ErrnoError() << "Failed to listen";
+    }
+
+    std::thread accept_thread{[listening_fd = std::move(server_fd)] {
+        auto result = run_echo_reverse_server(listening_fd);
+        if (!result.ok()) {
+            __android_log_write(ANDROID_LOG_ERROR, TAG, result.error().message().c_str());
+            // Make sure the VM exits so the test will fail solidly
+            exit(1);
+        }
+    }};
+    accept_thread.detach();
+
+    return {};
+}
+
 Result<void> start_test_service() {
-    class TestService : public aidl::com::android::microdroid::testservice::BnTestService {
-        ndk::ScopedAStatus addInteger(int32_t a, int32_t b, int32_t* out) override {
+    class TestService : public BnTestService {
+        ScopedAStatus addInteger(int32_t a, int32_t b, int32_t* out) override {
             *out = a + b;
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus readProperty(const std::string& prop, std::string* out) override {
+        ScopedAStatus readProperty(const std::string& prop, std::string* out) override {
             *out = android::base::GetProperty(prop, "");
             if (out->empty()) {
                 std::string msg = "cannot find property " + prop;
-                return ndk::ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
-                                                                        msg.c_str());
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   msg.c_str());
             }
 
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus insecurelyExposeVmInstanceSecret(std::vector<uint8_t>* out) override {
+        ScopedAStatus insecurelyExposeVmInstanceSecret(std::vector<uint8_t>* out) override {
             const uint8_t identifier[] = {1, 2, 3, 4};
             out->resize(32);
             AVmPayload_getVmInstanceSecret(identifier, sizeof(identifier), out->data(),
                                            out->size());
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus insecurelyExposeAttestationCdi(std::vector<uint8_t>* out) override {
+        ScopedAStatus insecurelyExposeAttestationCdi(std::vector<uint8_t>* out) override {
             size_t cdi_size = AVmPayload_getDiceAttestationCdi(nullptr, 0);
             out->resize(cdi_size);
             AVmPayload_getDiceAttestationCdi(out->data(), out->size());
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus getBcc(std::vector<uint8_t>* out) override {
+        ScopedAStatus getBcc(std::vector<uint8_t>* out) override {
             size_t bcc_size = AVmPayload_getDiceAttestationChain(nullptr, 0);
             out->resize(bcc_size);
             AVmPayload_getDiceAttestationChain(out->data(), out->size());
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus getApkContentsPath(std::string* out) override {
+        ScopedAStatus getApkContentsPath(std::string* out) override {
             const char* path_c = AVmPayload_getApkContentsPath();
             if (path_c == nullptr) {
-                return ndk::ScopedAStatus::
+                return ScopedAStatus::
                         fromServiceSpecificErrorWithMessage(0, "Failed to get APK contents path");
             }
-            std::string path(path_c);
-            *out = path;
-            return ndk::ScopedAStatus::ok();
+            *out = path_c;
+            return ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus getEncryptedStoragePath(std::string* out) override {
+        ScopedAStatus getEncryptedStoragePath(std::string* out) override {
             const char* path_c = AVmPayload_getEncryptedStoragePath();
             if (path_c == nullptr) {
                 out->clear();
             } else {
                 *out = path_c;
             }
-            return ndk::ScopedAStatus::ok();
+            return ScopedAStatus::ok();
+        }
+
+        virtual ::ScopedAStatus runEchoReverseServer() override {
+            auto result = start_echo_reverse_server();
+            if (result.ok()) {
+                return ScopedAStatus::ok();
+            } else {
+                std::string message = result.error().message();
+                return ScopedAStatus::fromServiceSpecificErrorWithMessage(-1, message.c_str());
+            }
         }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
@@ -143,14 +234,10 @@
 } // Anonymous namespace
 
 extern "C" int AVmPayload_main() {
-    // disable buffering to communicate seamlessly
-    setvbuf(stdin, nullptr, _IONBF, 0);
-    setvbuf(stdout, nullptr, _IONBF, 0);
-    setvbuf(stderr, nullptr, _IONBF, 0);
+    __android_log_write(ANDROID_LOG_INFO, TAG, "Hello Microdroid");
 
-    printf("Hello Microdroid");
+    // Make sure we can call into other shared libraries.
     testlib_sub();
-    printf("\n");
 
     // Extra apks may be missing; this is not a fatal error
     report_test("extra_apk", verify_apk());
@@ -160,7 +247,7 @@
     if (auto res = start_test_service(); res.ok()) {
         return 0;
     } else {
-        std::cerr << "starting service failed: " << res.error() << "\n";
+        __android_log_write(ANDROID_LOG_ERROR, TAG, res.error().message().c_str());
         return 1;
     }
 }
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index b767013..da56f76 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -51,6 +51,7 @@
         "libshared_child",
         "libstatslog_virtualization_rust",
         "libtombstoned_client_rust",
+        "libvm_control",
         "libvmconfig",
         "libzip",
         "libvsock",
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index d9d9a61..d76b586 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -16,6 +16,7 @@
 package android.system.virtualizationservice;
 
 import android.system.virtualizationservice.IVirtualMachineCallback;
+import android.system.virtualizationservice.MemoryTrimLevel;
 import android.system.virtualizationservice.VirtualMachineState;
 
 interface IVirtualMachine {
@@ -41,6 +42,9 @@
      */
     void stop();
 
+    /** Communicate app low-memory notifications to the VM. */
+    void onTrimMemory(MemoryTrimLevel level);
+
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/MemoryTrimLevel.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/MemoryTrimLevel.aidl
new file mode 100644
index 0000000..9ed9e99
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/MemoryTrimLevel.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.virtualizationservice;
+
+/**
+ * Memory trim levels propagated from the app to the VM.
+ */
+@Backing(type="int")
+enum MemoryTrimLevel {
+    /* Same meaning as in ComponentCallbacks2 */
+    TRIM_MEMORY_RUNNING_CRITICAL = 0,
+    TRIM_MEMORY_RUNNING_LOW = 1,
+    TRIM_MEMORY_RUNNING_MODERATE = 2,
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 040c0d8..7d24a32 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -27,6 +27,7 @@
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
     IVirtualizationService::IVirtualizationService,
+    MemoryTrimLevel::MemoryTrimLevel,
     Partition::Partition,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::{Payload::Payload, VirtualMachineAppConfig},
@@ -961,6 +962,13 @@
         })
     }
 
+    fn onTrimMemory(&self, level: MemoryTrimLevel) -> binder::Result<()> {
+        self.instance.trim_memory(level).map_err(|e| {
+            error!("Error trimming VM with CID {}: {:?}", self.instance.cid, e);
+            Status::new_service_specific_error_str(-1, Some(e.to_string()))
+        })
+    }
+
     fn connectVsock(&self, port: i32) -> binder::Result<ParcelFileDescriptor> {
         if !matches!(&*self.instance.vm_state.lock().unwrap(), VmState::Running { .. }) {
             return Err(Status::new_service_specific_error_str(-1, Some("VM is not running")));
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 85a57c9..0fdc293 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -22,12 +22,13 @@
 use libc::{sysconf, _SC_CLK_TCK};
 use log::{debug, error, info};
 use semver::{Version, VersionReq};
-use nix::{fcntl::OFlag, unistd::pipe2};
+use nix::{fcntl::OFlag, unistd::pipe2, unistd::Uid, unistd::User};
 use regex::{Captures, Regex};
 use rustutils::system_properties;
 use shared_child::SharedChild;
 use std::borrow::Cow;
 use std::cmp::max;
+use std::fmt;
 use std::fs::{read_to_string, remove_dir_all, File};
 use std::io::{self, Read};
 use std::mem;
@@ -38,7 +39,10 @@
 use std::sync::{Arc, Condvar, Mutex};
 use std::time::{Duration, SystemTime};
 use std::thread;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DeathReason::DeathReason;
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    DeathReason::DeathReason,
+    MemoryTrimLevel::MemoryTrimLevel,
+};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
@@ -47,6 +51,7 @@
 
 /// external/crosvm
 use base::UnixSeqpacketListener;
+use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
 
@@ -67,6 +72,8 @@
 
 const MILLIS_PER_SEC: i64 = 1000;
 
+const SYSPROP_CUSTOM_PVMFW_PATH: &str = "hypervisor.pvmfw.path";
+
 lazy_static! {
     /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
     /// triggered.
@@ -169,7 +176,7 @@
 
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
-                Arc::new(run_vm(config, &instance.temporary_directory, failure_pipe_write)?);
+                Arc::new(run_vm(config, &instance.crosvm_control_socket_path, failure_pipe_write)?);
 
             let instance_monitor_status = instance.clone();
             let child_monitor_status = child.clone();
@@ -226,6 +233,8 @@
     vm_context: VmContext,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
+    /// Path to crosvm control socket
+    crosvm_control_socket_path: PathBuf,
     /// The name of the VM.
     pub name: String,
     /// Whether the VM is a protected VM.
@@ -247,6 +256,19 @@
     payload_state: Mutex<PayloadState>,
     /// Represents the condition that payload_state was updated
     payload_state_updated: Condvar,
+    /// The human readable name of requester_uid
+    requester_uid_name: String,
+}
+
+impl fmt::Display for VmInstance {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let adj = if self.protected { "Protected" } else { "Non-protected" };
+        write!(
+            f,
+            "{} virtual machine \"{}\" (owner: {}, cid: {})",
+            adj, self.name, self.requester_uid_name, self.cid
+        )
+    }
 }
 
 impl VmInstance {
@@ -262,10 +284,15 @@
         let cid = config.cid;
         let name = config.name.clone();
         let protected = config.protected;
-        Ok(VmInstance {
+        let requester_uid_name = User::from_uid(Uid::from_raw(requester_uid))
+            .ok()
+            .flatten()
+            .map_or_else(|| format!("{}", requester_uid), |u| u.name);
+        let instance = VmInstance {
             vm_state: Mutex::new(VmState::NotStarted { config }),
             vm_context,
             cid,
+            crosvm_control_socket_path: temporary_directory.join("crosvm.sock"),
             name,
             protected,
             temporary_directory,
@@ -276,7 +303,10 @@
             vm_metric: Mutex::new(Default::default()),
             payload_state: Mutex::new(PayloadState::Starting),
             payload_state_updated: Condvar::new(),
-        })
+            requester_uid_name,
+        };
+        info!("{} created", &instance);
+        Ok(instance)
     }
 
     /// Starts an instance of `crosvm` to manage the VM. The `crosvm` instance will be killed when
@@ -284,7 +314,11 @@
     pub fn start(self: &Arc<Self>) -> Result<(), Error> {
         let mut vm_metric = self.vm_metric.lock().unwrap();
         vm_metric.start_timestamp = Some(SystemTime::now());
-        self.vm_state.lock().unwrap().start(self.clone())
+        let ret = self.vm_state.lock().unwrap().start(self.clone());
+        if ret.is_ok() {
+            info!("{} started", &self);
+        }
+        ret.with_context(|| format!("{} failed to start", &self))
     }
 
     /// Monitors the exit of the VM (i.e. termination of the `child` process). When that happens,
@@ -308,6 +342,7 @@
         *vm_state = VmState::Dead;
         // Ensure that the mutex is released before calling the callbacks.
         drop(vm_state);
+        info!("{} exited", &self);
 
         // Read the pipe to see if any failure reason is written
         let mut failure_reason = String::new();
@@ -437,6 +472,46 @@
         }
     }
 
+    /// Responds to memory-trimming notifications by inflating the virtio
+    /// balloon to reclaim guest memory.
+    pub fn trim_memory(&self, level: MemoryTrimLevel) -> Result<(), Error> {
+        let request = VmRequest::BalloonCommand(BalloonControlCommand::Stats {});
+        match vm_control::client::handle_request(&request, &self.crosvm_control_socket_path) {
+            Ok(VmResponse::BalloonStats { stats, balloon_actual: _ }) => {
+                if let Some(total_memory) = stats.total_memory {
+                    // Reclaim up to 50% of total memory assuming worst case
+                    // most memory is anonymous and must be swapped to zram
+                    // with an approximate 2:1 compression ratio.
+                    let pct = match level {
+                        MemoryTrimLevel::TRIM_MEMORY_RUNNING_CRITICAL => 50,
+                        MemoryTrimLevel::TRIM_MEMORY_RUNNING_LOW => 30,
+                        MemoryTrimLevel::TRIM_MEMORY_RUNNING_MODERATE => 10,
+                        _ => bail!("Invalid memory trim level {:?}", level),
+                    };
+                    let command =
+                        BalloonControlCommand::Adjust { num_bytes: total_memory * pct / 100 };
+                    if let Err(e) = vm_control::client::handle_request(
+                        &VmRequest::BalloonCommand(command),
+                        &self.crosvm_control_socket_path,
+                    ) {
+                        bail!("Error sending balloon adjustment: {:?}", e);
+                    }
+                }
+            }
+            Ok(VmResponse::Err(e)) => {
+                // ENOTSUP is returned when the balloon protocol is not initialised. This
+                // can occur for numerous reasons: Guest is still booting, guest doesn't
+                // support ballooning, host doesn't support ballooning. We don't log or
+                // raise an error in this case: trim is just a hint and we can ignore it.
+                if e.errno() != libc::ENOTSUP {
+                    bail!("Errno return when requesting balloon stats: {}", e.errno())
+                }
+            }
+            e => bail!("Error requesting balloon stats: {:?}", e),
+        }
+        Ok(())
+    }
+
     /// Checks if ramdump has been created. If so, send a notification to the user with the handle
     /// to read the ramdump.
     fn handle_ramdump(&self) -> Result<(), Error> {
@@ -576,7 +651,7 @@
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
-    temporary_directory: &Path,
+    crosvm_control_socket_path: &Path,
     failure_pipe_write: File,
 ) -> Result<SharedChild, Error> {
     validate_config(&config)?;
@@ -601,7 +676,12 @@
     }
 
     if config.protected {
-        command.arg("--protected-vm");
+        match system_properties::read(SYSPROP_CUSTOM_PVMFW_PATH)? {
+            Some(pvmfw_path) if !pvmfw_path.is_empty() => {
+                command.arg("--protected-vm-with-firmware").arg(pvmfw_path)
+            }
+            _ => command.arg("--protected-vm"),
+        };
 
         // 3 virtio-console devices + vsock = 4.
         let virtio_pci_device_count = 4 + config.disks.len();
@@ -677,9 +757,8 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
-    let control_server_socket =
-        UnixSeqpacketListener::bind(temporary_directory.join("crosvm.sock"))
-            .context("failed to create control server")?;
+    let control_server_socket = UnixSeqpacketListener::bind(crosvm_control_socket_path)
+        .context("failed to create control server")?;
     command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, &control_server_socket));
 
     debug!("Preserving FDs {:?}", preserved_fds);
diff --git a/vm/Android.bp b/vm/Android.bp
index 7b016d4..b95dca3 100644
--- a/vm/Android.bp
+++ b/vm/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_binary {
-    name: "vm",
+rust_defaults {
+    name: "vm.defaults",
     crate_name: "vm",
     srcs: ["src/main.rs"],
     edition: "2021",
@@ -17,6 +17,7 @@
         "liblibc",
         "liblog_rust",
         "libmicrodroid_payload_config",
+        "librand",
         "librustutils",
         "libserde_json",
         "libserde",
@@ -24,11 +25,23 @@
         "libvmclient",
         "libzip",
     ],
+}
+
+rust_binary {
+    name: "vm",
+    defaults: ["vm.defaults"],
     apex_available: [
         "com.android.virt",
     ],
 }
 
+rust_test {
+    name: "vm.test",
+    defaults: ["vm.defaults"],
+    test_suites: ["general-tests"],
+    compile_multilib: "first",
+}
+
 sh_binary_host {
     name: "vm_shell",
     src: "vm_shell.sh",
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 89d56d4..bc18fae 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -27,7 +27,7 @@
 use clap::Parser;
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
-use run::{command_run, command_run_app};
+use run::{command_run, command_run_app, command_run_microdroid};
 use rustutils::system_properties;
 use std::path::{Path, PathBuf};
 
@@ -110,6 +110,65 @@
         #[clap(long = "extra-idsig")]
         extra_idsigs: Vec<PathBuf>,
     },
+    /// Run a virtual machine with Microdroid inside
+    RunMicrodroid {
+        /// Path to the directory where VM-related files (e.g. instance.img, apk.idsig, etc.) will
+        /// be stored. If not specified a random directory under /data/local/tmp/microdroid will be
+        /// created and used.
+        #[clap(long)]
+        work_dir: Option<PathBuf>,
+
+        /// Name of VM
+        #[clap(long)]
+        name: Option<String>,
+
+        /// Detach VM from the terminal and run in the background
+        #[clap(short, long)]
+        daemonize: bool,
+
+        /// Path to the file backing the storage.
+        /// Created if the option is used but the path does not exist in the device.
+        #[clap(long)]
+        storage: Option<PathBuf>,
+
+        /// Size of the storage. Used only if --storage is supplied but path does not exist
+        /// Default size is 10*1024*1024
+        #[clap(long)]
+        storage_size: Option<u64>,
+
+        /// Path to file for VM console output.
+        #[clap(long)]
+        console: Option<PathBuf>,
+
+        /// Path to file for VM log output.
+        #[clap(long)]
+        log: Option<PathBuf>,
+
+        /// Path to file where ramdump is recorded on kernel panic
+        #[clap(long)]
+        ramdump: Option<PathBuf>,
+
+        /// Debug level of the VM. Supported values: "none" (default), "app_only", and "full".
+        #[clap(long, default_value = "full", value_parser = parse_debug_level)]
+        debug: DebugLevel,
+
+        /// Run VM in protected mode.
+        #[clap(short, long)]
+        protected: bool,
+
+        /// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
+        /// in the VM config file.
+        #[clap(short, long)]
+        mem: Option<u32>,
+
+        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
+        #[clap(long)]
+        cpus: Option<u32>,
+
+        /// Comma separated list of task profile names to apply to the VM
+        #[clap(long)]
+        task_profiles: Vec<String>,
+    },
     /// Run a virtual machine
     Run {
         /// Path to VM config JSON
@@ -238,6 +297,36 @@
             task_profiles,
             &extra_idsigs,
         ),
+        Opt::RunMicrodroid {
+            name,
+            work_dir,
+            storage,
+            storage_size,
+            daemonize,
+            console,
+            log,
+            ramdump,
+            debug,
+            protected,
+            mem,
+            cpus,
+            task_profiles,
+        } => command_run_microdroid(
+            name,
+            service.as_ref(),
+            work_dir,
+            storage.as_deref(),
+            storage_size,
+            daemonize,
+            console.as_deref(),
+            log.as_deref(),
+            ramdump.as_deref(),
+            debug,
+            protected,
+            mem,
+            cpus,
+            task_profiles,
+        ),
         Opt::Run { name, config, daemonize, cpus, task_profiles, console, log } => {
             command_run(
                 name,
@@ -304,3 +393,14 @@
 
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use clap::IntoApp;
+
+    #[test]
+    fn verify_app() {
+        Opt::into_app().debug_assert();
+    }
+}
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 01b916b..3f25bba 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -23,13 +23,16 @@
     VirtualMachinePayloadConfig::VirtualMachinePayloadConfig,
     VirtualMachineState::VirtualMachineState,
 };
-use anyhow::{bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
 use binder::ParcelFileDescriptor;
 use microdroid_payload_config::VmPayloadConfig;
+use rand::{distributions::Alphanumeric, Rng};
+use std::fs;
 use std::fs::File;
 use std::io;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
+use std::process::Command;
 use vmclient::{ErrorCode, VmInstance};
 use vmconfig::{open_parcel_file, VmConfig};
 use zip::ZipArchive;
@@ -144,6 +147,83 @@
     run(service, &config, &payload_config_str, daemonize, console_path, log_path, ramdump_path)
 }
 
+const EMPTY_PAYLOAD_APK: &str = "com.android.microdroid.empty_payload";
+
+fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
+    let output = Command::new("/system/bin/pm")
+        .arg("path")
+        .arg(EMPTY_PAYLOAD_APK)
+        .output()
+        .context("failed to execute pm path")?;
+    let output_str = String::from_utf8(output.stdout).context("failed to parse output")?;
+    match output_str.strip_prefix("package:") {
+        None => Err(anyhow!("Unexpected output {}", output_str)),
+        Some(apk_path) => Ok(PathBuf::from(apk_path.trim())),
+    }
+}
+
+fn create_work_dir() -> Result<PathBuf, Error> {
+    let s: String =
+        rand::thread_rng().sample_iter(&Alphanumeric).take(17).map(char::from).collect();
+    let work_dir = PathBuf::from("/data/local/tmp/microdroid").join(s);
+    println!("creating work dir {}", work_dir.display());
+    fs::create_dir_all(&work_dir).context("failed to mkdir")?;
+    Ok(work_dir)
+}
+
+/// Run a VM with Microdroid
+#[allow(clippy::too_many_arguments)]
+pub fn command_run_microdroid(
+    name: Option<String>,
+    service: &dyn IVirtualizationService,
+    work_dir: Option<PathBuf>,
+    storage: Option<&Path>,
+    storage_size: Option<u64>,
+    daemonize: bool,
+    console_path: Option<&Path>,
+    log_path: Option<&Path>,
+    ramdump_path: Option<&Path>,
+    debug_level: DebugLevel,
+    protected: bool,
+    mem: Option<u32>,
+    cpus: Option<u32>,
+    task_profiles: Vec<String>,
+) -> Result<(), Error> {
+    let apk = find_empty_payload_apk_path()
+        .context(anyhow!("failed to find path for {} apk", EMPTY_PAYLOAD_APK))?;
+    println!("found path for {} apk: {}", EMPTY_PAYLOAD_APK, apk.display());
+
+    let work_dir = work_dir.unwrap_or(create_work_dir()?);
+    let idsig = work_dir.join("apk.idsig");
+    println!("apk.idsig path: {}", idsig.display());
+    let instance_img = work_dir.join("instance.img");
+    println!("instance.img path: {}", instance_img.display());
+
+    let payload_path = "MicrodroidEmptyPayloadJniLib.so";
+    let extra_sig = [];
+    command_run_app(
+        name,
+        service,
+        &apk,
+        &idsig,
+        &instance_img,
+        storage,
+        storage_size,
+        /* config_path= */ None,
+        Some(payload_path.to_owned()),
+        daemonize,
+        console_path,
+        log_path,
+        ramdump_path,
+        debug_level,
+        protected,
+        mem,
+        cpus,
+        task_profiles,
+        &extra_sig,
+    )
+}
+
 /// Run a VM from the given configuration file.
 #[allow(clippy::too_many_arguments)]
 pub fn command_run(
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index 6be6f22..967d1cf 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -2,9 +2,11 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_ffi_shared {
-    name: "libvm_payload",
+// The Rust implementation of the C API.
+rust_ffi_static {
+    name: "libvm_payload_impl",
     crate_name: "vm_payload",
+    visibility: ["//visibility:private"],
     srcs: ["src/*.rs"],
     include_dirs: ["include"],
     prefer_rlib: true,
@@ -19,9 +21,6 @@
         "librpcbinder_rs",
         "libvsock",
     ],
-    apex_available: [
-        "com.android.compos",
-    ],
     // The sanitize section below fixes the fuzzer build in b/256166339.
     // TODO(b/250854486): Remove the sanitize section once the bug is fixed.
     sanitize: {
@@ -29,6 +28,8 @@
     },
 }
 
+// Rust wrappers round the C API for Rust clients.
+// (Yes, this involves going Rust -> C -> Rust.)
 rust_bindgen {
     name: "libvm_payload_bindgen",
     wrapper_src: "include-restricted/vm_payload_restricted.h",
@@ -37,16 +38,38 @@
     apex_available: ["com.android.compos"],
     visibility: ["//packages/modules/Virtualization/compos"],
     shared_libs: [
-        "libvm_payload",
+        "libvm_payload#current",
     ],
 }
 
+// Shared library for clients to link against.
+cc_library_shared {
+    name: "libvm_payload",
+    shared_libs: [
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+    ],
+    whole_static_libs: ["libvm_payload_impl"],
+    export_static_lib_headers: ["libvm_payload_impl"],
+    installable: false,
+    version_script: "libvm_payload.map.txt",
+    stubs: {
+        symbol_file: "libvm_payload.map.txt",
+        // Implementation is available inside a Microdroid VM.
+        implementation_installable: false,
+    },
+}
+
+// Just the headers. Mostly useful for clients that only want the
+// declaration of AVmPayload_main().
 cc_library_headers {
     name: "vm_payload_headers",
     apex_available: ["com.android.compos"],
     export_include_dirs: ["include"],
 }
 
+// Restricted headers for use by internal clients & associated tests.
 cc_library_headers {
     name: "vm_payload_restricted_headers",
     header_libs: ["vm_payload_headers"],
diff --git a/vm_payload/libvm_payload.map.txt b/vm_payload/libvm_payload.map.txt
new file mode 100644
index 0000000..a2402d1
--- /dev/null
+++ b/vm_payload/libvm_payload.map.txt
@@ -0,0 +1,12 @@
+LIBVM_PAYLOAD {
+  global:
+    AVmPayload_notifyPayloadReady;       # systemapi
+    AVmPayload_runVsockRpcServer;        # systemapi
+    AVmPayload_getVmInstanceSecret;      # systemapi
+    AVmPayload_getDiceAttestationChain;  # systemapi
+    AVmPayload_getDiceAttestationCdi;    # systemapi
+    AVmPayload_getApkContentsPath;       # systemapi
+    AVmPayload_getEncryptedStoragePath;  # systemapi
+  local:
+    *;
+};
diff --git a/vm_payload/src/api.rs b/vm_payload/src/api.rs
index a79c0bb..28b440e 100644
--- a/vm_payload/src/api.rs
+++ b/vm_payload/src/api.rs
@@ -18,7 +18,7 @@
 #![warn(unsafe_op_in_unsafe_fn)]
 
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
-    IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME, VM_APK_CONTENTS_PATH};
+    ENCRYPTEDSTORE_MOUNTPOINT, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME, VM_APK_CONTENTS_PATH};
 use anyhow::{ensure, bail, Context, Result};
 use binder::{Strong, unstable_api::{AIBinder, new_spibinder}};
 use lazy_static::lazy_static;
@@ -28,6 +28,7 @@
 use std::ffi::CString;
 use std::fmt::Debug;
 use std::os::raw::{c_char, c_void};
+use std::path::Path;
 use std::ptr;
 use std::sync::{Mutex, atomic::{AtomicBool, Ordering}};
 
@@ -35,6 +36,8 @@
     static ref VM_APK_CONTENTS_PATH_C: CString =
         CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed");
     static ref PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::default();
+    static ref VM_ENCRYPTED_STORAGE_PATH_C: CString =
+        CString::new(ENCRYPTEDSTORE_MOUNTPOINT).expect("CString::new failed");
 }
 
 static ALREADY_NOTIFIED: AtomicBool = AtomicBool::new(false);
@@ -249,12 +252,15 @@
 /// Gets the path to the APK contents.
 #[no_mangle]
 pub extern "C" fn AVmPayload_getApkContentsPath() -> *const c_char {
-    (*VM_APK_CONTENTS_PATH_C).as_ptr()
+    VM_APK_CONTENTS_PATH_C.as_ptr()
 }
 
 /// Gets the path to the VM's encrypted storage.
 #[no_mangle]
 pub extern "C" fn AVmPayload_getEncryptedStoragePath() -> *const c_char {
-    // TODO(b/254454578): Return a real path if storage is present
-    ptr::null()
+    if Path::new(ENCRYPTEDSTORE_MOUNTPOINT).exists() {
+        VM_ENCRYPTED_STORAGE_PATH_C.as_ptr()
+    } else {
+        ptr::null()
+    }
 }
diff --git a/vmbase/example/idmap.S b/vmbase/example/idmap.S
index 7fc5d5e..71a6ade 100644
--- a/vmbase/example/idmap.S
+++ b/vmbase/example/idmap.S
@@ -44,7 +44,7 @@
 	.fill		509, 8, 0x0			// 509 GiB of remaining VA space
 
 	/* level 2 */
-0:	.quad		.L_BLOCK_RO  | 0x80000000	// DT provided by VMM
+0:	.quad		.L_BLOCK_MEM | 0x80000000	// DT provided by VMM
 	.quad		.L_BLOCK_MEM_XIP | 0x80200000	// 2 MiB of DRAM containing image
 	.quad		.L_BLOCK_MEM | 0x80400000	// 2 MiB of writable DRAM
 	.fill		509, 8, 0x0
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index dcff6e1..bb64651 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -151,17 +151,41 @@
 
     let reader = Fdt::from_slice(fdt).unwrap();
     info!("FDT passed verification.");
-    for reg in reader.memory().unwrap() {
+    for reg in reader.memory().unwrap().unwrap() {
         info!("memory @ {reg:#x?}");
     }
 
     let compatible = CStr::from_bytes_with_nul(b"ns16550a\0").unwrap();
 
     for c in reader.compatible_nodes(compatible).unwrap() {
-        let reg = c.reg().unwrap().next().unwrap();
+        let reg = c.reg().unwrap().unwrap().next().unwrap();
         info!("node compatible with '{}' at {reg:?}", compatible.to_str().unwrap());
     }
 
+    let writer = Fdt::from_mut_slice(fdt).unwrap();
+    writer.unpack().unwrap();
+    info!("FDT successfully unpacked.");
+
+    let path = CStr::from_bytes_with_nul(b"/memory\0").unwrap();
+    let mut node = writer.node_mut(path).unwrap().unwrap();
+    let name = CStr::from_bytes_with_nul(b"child\0").unwrap();
+    let mut child = node.add_subnode(name).unwrap();
+    info!("Created subnode '{}/{}'.", path.to_str().unwrap(), name.to_str().unwrap());
+
+    let name = CStr::from_bytes_with_nul(b"str-property\0").unwrap();
+    child.appendprop(name, b"property-value\0").unwrap();
+    info!("Appended property '{}'.", name.to_str().unwrap());
+
+    let name = CStr::from_bytes_with_nul(b"pair-property\0").unwrap();
+    let addr = 0x0123_4567u64;
+    let size = 0x89ab_cdefu64;
+    child.appendprop_addrrange(name, addr, size).unwrap();
+    info!("Appended property '{}'.", name.to_str().unwrap());
+
+    let writer = child.fdt();
+    writer.pack().unwrap();
+    info!("FDT successfully packed.");
+
     info!("FDT checks done.");
 }