Merge changes from topic "crosvm-backcompat-dt" into main

* changes:
  Add golden device tree test for backwards compatibility check
  Add option to dump device tree blob in VM config
diff --git a/OWNERS b/OWNERS
index 717a4db..afd2555 100644
--- a/OWNERS
+++ b/OWNERS
@@ -35,3 +35,4 @@
 per-file android/TerminalApp/**=jiyong@google.com,jeongik@google.com
 per-file android/VmLauncherApp/**=jiyong@google.com,jeongik@google.com
 per-file libs/vm_launcher_lib/**=jiyong@google.com,jeongik@google.com
+per-file build/debian/**=jiyong@google.com,jeongik@google.com
diff --git a/android/FerrochromeApp/Android.bp b/android/FerrochromeApp/Android.bp
deleted file mode 100644
index 3e4ad14..0000000
--- a/android/FerrochromeApp/Android.bp
+++ /dev/null
@@ -1,42 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_defaults {
-    name: "VmPayloadInstaller",
-    init_rc: [":custom_vm_setup.rc"],
-    required: ["custom_vm_setup"],
-    // TODO(b/348113995): move this app to product partition
-    system_ext_specific: true,
-    platform_apis: true,
-    privileged: true,
-}
-
-android_app {
-    name: "FerrochromeApp",
-    srcs: ["java/**/*.java"],
-    resource_dirs: ["res"],
-    defaults: ["VmPayloadInstaller"],
-    required: [
-        "privapp-permissions-ferrochrome.xml",
-    ],
-}
-
-prebuilt_etc {
-    name: "privapp-permissions-ferrochrome.xml",
-    src: "privapp-permissions-ferrochrome.xml",
-    sub_dir: "permissions",
-    system_ext_specific: true,
-}
-
-filegroup {
-    name: "custom_vm_setup.rc",
-    srcs: ["custom_vm_setup.rc"],
-}
-
-sh_binary {
-    name: "custom_vm_setup",
-    src: "custom_vm_setup.sh",
-    system_ext_specific: true,
-    host_supported: false,
-}
diff --git a/android/FerrochromeApp/AndroidManifest.xml b/android/FerrochromeApp/AndroidManifest.xml
deleted file mode 100644
index f6d3f6a..0000000
--- a/android/FerrochromeApp/AndroidManifest.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.ferrochrome" >
-
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER" />
-
-    <queries>
-        <intent>
-            <action android:name="android.virtualization.VM_LAUNCHER" />
-        </intent>
-        <intent>
-            <action android:name="android.virtualization.FERROCHROME_DOWNLOADER" />
-        </intent>
-    </queries>
-    <application
-        android:label="Ferrochrome">
-        <activity android:name=".FerrochromeActivity"
-                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
-                  android:screenOrientation="landscape"
-                  android:resizeableActivity="false"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-        <activity android:name=".OpenUrlActivity"
-                  android:theme="@android:style/Theme.NoDisplay"
-                  android:launchMode="singleTask"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/*" />
-            </intent-filter>
-        </activity>
-    </application>
-
-</manifest>
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
deleted file mode 100644
index dba0078..0000000
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.virtualization.ferrochrome;
-
-import android.annotation.WorkerThread;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.SystemProperties;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class FerrochromeActivity extends Activity {
-    private static final String TAG = FerrochromeActivity.class.getName();
-    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
-    private static final String ACTION_FERROCHROME_DOWNLOAD =
-            "android.virtualization.FERROCHROME_DOWNLOADER";
-    private static final String EXTRA_FERROCHROME_DEST_DIR = "dest_dir";
-    private static final String EXTRA_FERROCHROME_UPDATE_NEEDED = "update_needed";
-
-    private static final Path DEST_DIR =
-            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
-    private static final String ASSET_DIR = "ferrochrome";
-    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
-
-    private static final int REQUEST_CODE_VMLAUNCHER = 1;
-    private static final int REQUEST_CODE_FERROCHROME_DOWNLOADER = 2;
-
-    private ResolvedActivity mVmLauncher;
-
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        if (!isTaskRoot()) {
-            // In case we launched this activity multiple times, only start one instance of this
-            // activity by only starting this as the root activity in task.
-            finish();
-            Log.w(TAG, "Not starting because not task root");
-            return;
-        }
-        setContentView(R.layout.activity_ferrochrome);
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
-        // Find VM Launcher
-        mVmLauncher = ResolvedActivity.resolve(getPackageManager(), ACTION_VM_LAUNCHER);
-        if (mVmLauncher == null) {
-            updateStatus("Failed to resolve VM Launcher");
-            return;
-        }
-
-        // Clean up the existing vm launcher process if there is
-        ActivityManager am = getSystemService(ActivityManager.class);
-        am.killBackgroundProcesses(mVmLauncher.activityInfo.packageName);
-
-        executorService.execute(
-                () -> {
-                    if (hasLocalAssets()) {
-                        if (updateImageIfNeeded()) {
-                            updateStatus("Starting Ferrochrome...");
-                            runOnUiThread(
-                                    () ->
-                                            startActivityForResult(
-                                                    mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
-                        }
-                    } else {
-                        tryLaunchDownloader();
-                    }
-                });
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == REQUEST_CODE_VMLAUNCHER) {
-            finishAndRemoveTask();
-        } else if (requestCode == REQUEST_CODE_FERROCHROME_DOWNLOADER) {
-            String destDir = data.getStringExtra(EXTRA_FERROCHROME_DEST_DIR);
-            boolean updateNeeded =
-                    data.getBooleanExtra(EXTRA_FERROCHROME_UPDATE_NEEDED, /* default= */ true);
-
-            if (resultCode != RESULT_OK || TextUtils.isEmpty(destDir)) {
-                Log.w(
-                        TAG,
-                        "Ferrochrome downloader returned error, code="
-                                + resultCode
-                                + ", dest="
-                                + destDir);
-                updateStatus("User didn't accepted ferrochrome download..");
-                return;
-            }
-
-            Log.w(TAG, "Ferrochrome downloader returned OK");
-
-            if (!updateNeeded) {
-                updateStatus("Starting Ferrochrome...");
-                startActivityForResult(mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER);
-            }
-
-            executorService.execute(
-                    () -> {
-                        if (!extractImages(destDir)) {
-                            updateStatus("Images from downloader looks bad..");
-                            return;
-                        }
-                        updateStatus("Starting Ferrochrome...");
-                        runOnUiThread(
-                                () ->
-                                        startActivityForResult(
-                                                mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
-                    });
-        }
-    }
-
-    @WorkerThread
-    private boolean hasLocalAssets() {
-        try {
-            String[] files = getAssets().list(ASSET_DIR);
-            return files != null && files.length > 0;
-        } catch (IOException e) {
-            return false;
-        }
-    }
-
-    @WorkerThread
-    private boolean updateImageIfNeeded() {
-        if (!isUpdateNeeded()) {
-            Log.d(TAG, "No update needed.");
-            return true;
-        }
-
-        try {
-            if (Files.notExists(DEST_DIR)) {
-                Files.createDirectory(DEST_DIR);
-            }
-
-            updateStatus("Copying images...");
-            String[] files = getAssets().list("ferrochrome");
-            for (String file : files) {
-                updateStatus(file);
-                Path dst = Path.of(DEST_DIR.toString(), file);
-                updateFile(getAssets().open("ferrochrome/" + file), dst);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error while updating image: " + e);
-            updateStatus("Failed.");
-            return false;
-        }
-        updateStatus("Done.");
-
-        return extractImages(DEST_DIR.toAbsolutePath().toString());
-    }
-
-    @WorkerThread
-    private void tryLaunchDownloader() {
-        // TODO(jaewan): Add safeguard to check whether ferrochrome downloader is valid.
-        Log.w(TAG, "No built-in assets found. Try again with ferrochrome downloader");
-
-        ResolvedActivity downloader =
-                ResolvedActivity.resolve(getPackageManager(), ACTION_FERROCHROME_DOWNLOAD);
-        if (downloader == null) {
-            Log.d(TAG, "Ferrochrome downloader doesn't exist");
-            updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
-            return;
-        }
-        String pkgName = downloader.activityInfo.packageName;
-        Log.d(TAG, "Resolved Ferrochrome Downloader, pkgName=" + pkgName);
-        updateStatus("Launching Ferrochrome downloader for update");
-
-        // onActivityResult() will handle downloader result.
-        startActivityForResult(downloader.intent, REQUEST_CODE_FERROCHROME_DOWNLOADER);
-    }
-
-    @WorkerThread
-    private boolean extractImages(String destDir) {
-        updateStatus("Extracting images...");
-
-        if (TextUtils.isEmpty(destDir)) {
-            throw new RuntimeException("Internal error: destDir shouldn't be null");
-        }
-
-        SystemProperties.set("debug.custom_vm_setup.path", destDir);
-        SystemProperties.set("debug.custom_vm_setup.done", "false");
-        SystemProperties.set("debug.custom_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-            try {
-                Thread.sleep(1000);
-            } catch (Exception e) {
-                Log.e(TAG, "Error while extracting image: " + e);
-                updateStatus("Failed.");
-                return false;
-            }
-        }
-
-        updateStatus("Done.");
-        return true;
-    }
-
-    @WorkerThread
-    private boolean isUpdateNeeded() {
-        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
-        for (Path p : pathsToCheck) {
-            if (Files.notExists(p)) {
-                Log.d(TAG, p.toString() + " does not exist.");
-                return true;
-            }
-        }
-
-        try {
-            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
-            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
-            if (installedVer.equals(updatedVer)) {
-                return false;
-            }
-            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
-        } catch (IOException e) {
-            Log.e(TAG, "Error while checking version: " + e);
-        }
-        return true;
-    }
-
-    private static String readLine(InputStream input) throws IOException {
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
-            return reader.readLine();
-        } catch (IOException e) {
-            throw e;
-        }
-    }
-
-    private static void updateFile(InputStream input, Path path) throws IOException {
-        try {
-            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
-        } finally {
-            input.close();
-        }
-    }
-
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
-    }
-
-    private static final class ResolvedActivity {
-        public final ActivityInfo activityInfo;
-        public final Intent intent;
-
-        private ResolvedActivity(ActivityInfo activityInfo, Intent intent) {
-            this.activityInfo = activityInfo;
-            this.intent = intent;
-        }
-
-        /* synthetic access */
-        static ResolvedActivity resolve(PackageManager pm, String action) {
-            Intent intent = new Intent(action).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
-            List<ResolveInfo> resolveInfos =
-                    pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
-            if (resolveInfos == null || resolveInfos.size() != 1) {
-                Log.w(
-                        TAG,
-                        "Failed to resolve activity, action="
-                                + action
-                                + ", resolved="
-                                + resolveInfos);
-                return null;
-            }
-            ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
-            intent.setClassName(activityInfo.packageName, activityInfo.name);
-            return new ResolvedActivity(activityInfo, intent);
-        }
-    }
-}
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
deleted file mode 100644
index 433e89c..0000000
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.virtualization.ferrochrome;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.util.Log;
-
-public class OpenUrlActivity extends Activity {
-    private static final String TAG = OpenUrlActivity.class.getSimpleName();
-
-    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        finish();
-
-        if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
-            return;
-        }
-        String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
-        if (text == null) {
-            return;
-        }
-        Uri uri = Uri.parse(text);
-        if (uri == null) {
-            return;
-        }
-        String scheme = uri.getScheme();
-        if (!("http".equals(scheme) || "https".equals(scheme) || "mailto".equals(scheme))) {
-            Log.e(TAG, "Unsupported URL scheme: " + scheme);
-            return;
-        }
-        Log.i(TAG, "Sending " + scheme + " URL to VM");
-        startActivity(
-                new Intent(ACTION_VM_OPEN_URL)
-                        .setFlags(
-                                Intent.FLAG_ACTIVITY_SINGLE_TOP
-                                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
-                        .putExtra(Intent.EXTRA_TEXT, text));
-    }
-}
diff --git a/android/FerrochromeApp/privapp-permissions-ferrochrome.xml b/android/FerrochromeApp/privapp-permissions-ferrochrome.xml
deleted file mode 100644
index 987db9c..0000000
--- a/android/FerrochromeApp/privapp-permissions-ferrochrome.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<permissions>
-    <privapp-permissions package="com.android.virtualization.ferrochrome">
-        <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/>
-    </privapp-permissions>
-</permissions>
\ No newline at end of file
diff --git a/android/FerrochromeApp/repack.sh b/android/FerrochromeApp/repack.sh
deleted file mode 100755
index b2a96dd..0000000
--- a/android/FerrochromeApp/repack.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-# Repacks chromiumos_*.bin into the assets of FerrochromeApp
-
-usage() {
-	echo "Usage: $0 CHROME_OS_DISK_IMAGE"
-	exit 1
-}
-
-if [ "$#" -ne 1 ]; then
-	usage
-fi
-
-disk=$1
-
-loop=$(sudo losetup --show -f -P ${disk})
-kern=$(sudo fdisk -x ${loop} | grep KERN-A | awk "{print\$1}")
-root=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$1}")
-efi=$(sudo fdisk -x ${loop} | grep EFI-SYSTEM | awk "{print\$1}")
-state=$(sudo fdisk -x ${loop} | grep STATE | awk "{print\$1}")
-root_guid=$(sudo fdisk -x ${loop} | grep ROOT-A | awk "{print\$6}")
-
-tempdir=$(mktemp -d)
-pushd ${tempdir} > /dev/null
-echo Extracting partition images...
-sudo cp --sparse=always ${kern} kernel.img
-sudo cp --sparse=always ${root} root.img
-sudo cp --sparse=always ${efi} efi.img
-sudo cp --sparse=always ${state} state.img
-sudo chmod 777 *.img
-
-echo Archiving. This can take long...
-tar czvS -f images.tar.gz *.img
-
-echo Splitting...
-split -b 100M -d images.tar.gz images.tar.gz.part
-
-popd > /dev/null
-asset_dir=$(dirname $0)/assets/ferrochrome
-echo Updating ${asset_dir}...
-vm_config_template=$(dirname $0)/vm_config.json.template
-mkdir -p ${asset_dir}
-rm ${asset_dir}/images.tar.gz.part*
-mv ${tempdir}/images.tar.gz.part* ${asset_dir}
-sed -E s/GUID/${root_guid}/ ${vm_config_template} > ${asset_dir}/vm_config.json
-
-echo Calculating hash...
-hash=$(cat ${tempdir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
-echo ${hash} > ${asset_dir}/version
-
-echo Cleanup...
-sudo losetup -d ${loop}
-rm -rf ${tempdir}
-echo Done.
diff --git a/android/FerrochromeApp/res/layout/activity_ferrochrome.xml b/android/FerrochromeApp/res/layout/activity_ferrochrome.xml
deleted file mode 100644
index 3967167..0000000
--- a/android/FerrochromeApp/res/layout/activity_ferrochrome.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
-    android:paddingLeft="16dp"
-    android:paddingRight="16dp">
-  <TextView
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:textSize="28sp"
-      android:id="@+id/status_txt_view"/>
-
-</RelativeLayout>
diff --git a/android/FerrochromeApp/vm_config.json.template b/android/FerrochromeApp/vm_config.json.template
deleted file mode 100644
index 380f016..0000000
--- a/android/FerrochromeApp/vm_config.json.template
+++ /dev/null
@@ -1,56 +0,0 @@
-{
-    "name": "cros",
-    "disks": [
-        {
-            "writable": true,
-            "partitions": [
-                {
-                    "label": "STATE",
-                    "path": "/data/local/tmp/state.img",
-                    "writable": true
-                },
-                {
-                    "label": "KERN-A",
-                    "path": "/data/local/tmp/kernel.img"
-                },
-                {
-                    "label": "ROOT-A",
-                    "path": "/data/local/tmp/root.img",
-                    "guid": "GUID"
-                },
-                {
-                    "label": "EFI-SYSTEM",
-                    "path": "/data/local/tmp/efi.img"
-                }
-            ]
-        }
-    ],
-    "protected": false,
-    "cpu_topology": "match_host",
-    "platform_version": "~1.0",
-    "memory_mib": 8096,
-    "debuggable": true,
-    "console_out": true,
-    "connect_console": true,
-    "console_input_device": "hvc0",
-    "network": true,
-    "input": {
-        "touchscreen": true,
-        "keyboard": true,
-        "mouse": true,
-        "trackpad": true,
-        "switches": true
-    },
-    "audio": {
-        "speaker": true,
-        "microphone": true
-    },
-    "gpu": {
-        "backend": "virglrenderer",
-        "context_types": ["virgl2"]
-    },
-    "display": {
-        "scale": "0.77",
-        "refresh_rate": "30"
-    }
-}
diff --git a/android/LinuxInstaller/Android.bp b/android/LinuxInstaller/Android.bp
index f70452d..f7994ef 100644
--- a/android/LinuxInstaller/Android.bp
+++ b/android/LinuxInstaller/Android.bp
@@ -2,13 +2,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+java_defaults {
+    name: "LinuxVmPayloadInstaller",
+    init_rc: [":linux_vm_setup.rc"],
+    required: ["linux_vm_setup"],
+    system_ext_specific: true,
+    platform_apis: true,
+    privileged: true,
+}
+
 android_app {
     name: "LinuxInstallerApp",
     srcs: ["java/**/*.java"],
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
     manifest: "AndroidManifest.xml",
-    defaults: ["VmPayloadInstaller"],
+    defaults: ["LinuxVmPayloadInstaller"],
     overrides: ["LinuxInstallerAppStub"],
     required: [
         "privapp-permissions-linuxinstaller.xml",
@@ -21,7 +30,7 @@
     srcs: ["java/**/*.java"],
     resource_dirs: ["res"],
     manifest: "AndroidManifest_stub.xml",
-    defaults: ["VmPayloadInstaller"],
+    defaults: ["LinuxVmPayloadInstaller"],
     required: [
         "privapp-permissions-linuxinstaller.xml",
     ],
@@ -39,3 +48,15 @@
     name: "com.android.virtualization.linuxinstaller_certificate",
     certificate: "com_android_virtualization_linuxinstaller",
 }
+
+filegroup {
+    name: "linux_vm_setup.rc",
+    srcs: ["linux_vm_setup.rc"],
+}
+
+sh_binary {
+    name: "linux_vm_setup",
+    src: "linux_vm_setup.sh",
+    system_ext_specific: true,
+    host_supported: false,
+}
diff --git a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
index 1d875cb..0351f97 100644
--- a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
+++ b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
@@ -136,10 +136,10 @@
             throw new RuntimeException("Internal error: destDir shouldn't be null");
         }
 
-        SystemProperties.set("debug.custom_vm_setup.path", destDir);
-        SystemProperties.set("debug.custom_vm_setup.done", "false");
-        SystemProperties.set("debug.custom_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+        SystemProperties.set("debug.linux_vm_setup.path", destDir);
+        SystemProperties.set("debug.linux_vm_setup.done", "false");
+        SystemProperties.set("debug.linux_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.linux_vm_setup.done", false)) {
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
diff --git a/android/FerrochromeApp/custom_vm_setup.rc b/android/LinuxInstaller/linux_vm_setup.rc
similarity index 77%
rename from android/FerrochromeApp/custom_vm_setup.rc
rename to android/LinuxInstaller/linux_vm_setup.rc
index 68f370e..9264d96 100644
--- a/android/FerrochromeApp/custom_vm_setup.rc
+++ b/android/LinuxInstaller/linux_vm_setup.rc
@@ -1,4 +1,4 @@
-# Copyright (C) 2024 The Android Open Source Project
+# Copyright 2024 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,12 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-service custom_vm_setup /system_ext/bin/custom_vm_setup
+service linux_vm_setup /system_ext/bin/linux_vm_setup
     user shell
     group shell media_rw
     disabled
     oneshot
     seclabel u:r:shell:s0
 
-on property:debug.custom_vm_setup.start=true
-    start custom_vm_setup
+on property:debug.linux_vm_setup.start=true
+    start linux_vm_setup
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/LinuxInstaller/linux_vm_setup.sh
similarity index 82%
rename from android/FerrochromeApp/custom_vm_setup.sh
rename to android/LinuxInstaller/linux_vm_setup.sh
index df1a3a6..6a93f6f 100644
--- a/android/FerrochromeApp/custom_vm_setup.sh
+++ b/android/LinuxInstaller/linux_vm_setup.sh
@@ -7,7 +7,7 @@
 }
 
 function install() {
-  src_dir=$(getprop debug.custom_vm_setup.path)
+  src_dir=$(getprop debug.linux_vm_setup.path)
   src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
   dst_dir=/data/local/tmp/
 
@@ -26,7 +26,7 @@
   rm ${src_dir}/vm_config.json
 }
 
-setprop debug.custom_vm_setup.done false
+setprop debug.linux_vm_setup.done false
 install
-setprop debug.custom_vm_setup.start false
-setprop debug.custom_vm_setup.done true
+setprop debug.linux_vm_setup.start false
+setprop debug.linux_vm_setup.done true
diff --git a/build/apex/product_packages.mk b/build/apex/product_packages.mk
index b2a4ca2..c678693 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -26,11 +26,13 @@
     com.android.compos \
     features_com.android.virt.xml
 
-# TODO(b/207336449): Figure out how to get these off /system
+ifneq (true, $(RELEASE_INSTALL_APEX_SYSTEMSERVER_DEXPREOPT_SAME_PARTITION))
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
     system/framework/oat/%@service-compos.jar@classes.odex \
     system/framework/oat/%@service-compos.jar@classes.vdex \
 
+endif
+
 PRODUCT_APEX_SYSTEM_SERVER_JARS := com.android.compos:service-compos
 
 PRODUCT_SYSTEM_EXT_PROPERTIES := ro.config.isolated_compilation_enabled=true
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 3d3820a..c50e5e5 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -37,22 +37,36 @@
 	apt update
 	DEBIAN_FRONTEND=noninteractive \
 	apt install --no-install-recommends --assume-yes \
+		binfmt-support \
+		build-essential \
 		ca-certificates \
+		curl \
 		debsums \
 		dosfstools \
 		fai-server \
 		fai-setup-storage \
 		fdisk \
+		gcc-aarch64-linux-gnu \
+		libc6-dev-arm64-cross \
 		make \
 		python3 \
 		python3-libcloud \
 		python3-marshmallow \
 		python3-pytest \
 		python3-yaml \
-		qemu-utils \
-		udev \
 		qemu-system-arm \
-		qemu-user-static
+		qemu-user-static \
+		qemu-utils \
+		sudo \
+		udev \
+
+
+	if [ ! -f $HOME/.cargo/bin/cargo ]; then
+		curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+	fi
+
+        source $HOME/.cargo/env
+        rustup target add aarch64-unknown-linux-gnu
 
         sed -i s/losetup\ -f/losetup\ -P\ -f/g /usr/sbin/fai-diskimage
         sed -i 's/wget \$/wget -t 0 \$/g' /usr/share/debootstrap/functions
@@ -86,6 +100,15 @@
 	mkdir -p ${dst}/files/usr/local/bin/ttyd
 	wget ${url} -O ${dst}/files/usr/local/bin/ttyd/AVF
 	chmod 777 ${dst}/files/usr/local/bin/ttyd/AVF
+
+        pushd $(dirname $0)/forwarder_guest > /dev/null
+		RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc" cargo build \
+			--target aarch64-unknown-linux-gnu \
+			--target-dir ${workdir}/forwarder_guest
+		mkdir -p ${dst}/files/usr/local/bin/forwarder_guest
+		cp ${workdir}/forwarder_guest/aarch64-unknown-linux-gnu/debug/forwarder_guest ${dst}/files/usr/local/bin/forwarder_guest/AVF
+		chmod 777 ${dst}/files/usr/local/bin/forwarder_guest/AVF
+	popd > /dev/null
 }
 
 run_fai() {
diff --git a/build/debian/build_in_container.sh b/build/debian/build_in_container.sh
new file mode 100755
index 0000000..6bc366b
--- /dev/null
+++ b/build/debian/build_in_container.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+if [ -z $ANDROID_BUILD_TOP ]; then echo "forgot to source build/envsetup.sh?" && exit 1; fi
+
+docker run --privileged -it -v $ANDROID_BUILD_TOP/packages/modules/Virtualization:/root/Virtualization -v /dev:/dev ubuntu:22.04 /root/Virtualization/build/debian/build.sh
diff --git a/build/debian/fai_config/hooks/extrbase.BASE b/build/debian/fai_config/hooks/extrbase.BASE
deleted file mode 100755
index 05d1e96..0000000
--- a/build/debian/fai_config/hooks/extrbase.BASE
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-set -euE
-
-touch "${LOGDIR}/skip.extrbase"
-
-debootstrap --verbose --variant minbase --arch "$DEBOOTSTRAP_ARCH" "$SUITE" "$FAI_ROOT" "$DEBOOTSTRAP_MIRROR"
diff --git a/build/debian/fai_config/hooks/partition.ARM64 b/build/debian/fai_config/hooks/partition.ARM64
deleted file mode 100755
index b3b603b..0000000
--- a/build/debian/fai_config/hooks/partition.ARM64
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/sh
-set -eu
-touch $LOGDIR/skip.partition
-
-set -- $disklist
-device=/dev/$1
-
-wait_for_device() {
-  for s in $(seq 10); do
-    if [ -e "$1" ]; then
-      break
-    fi
-    sleep 1
-  done
-}
-
-sfdisk "$device" << EOF
-label: gpt
-unit: sectors
-
-# EFI system
-p15 : start=2048, size=260096, type="EFI System", uuid=${PARTUUID_ESP}
-# Linux
-p1 : start=262144, type="Linux root (ARM-64)", uuid=${PARTUUID_ROOT}
-EOF
-
-file=$(losetup -O BACK-FILE ${device} | tail -1)
-
-root_offset=$(parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $2 }')
-root_size=$(  parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $6 }')
-efi_offset=$( parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $2 }')
-efi_size=$(   parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $6 }')
-device_root=$(losetup -o ${root_offset} --sizelimit ${root_size} --show -f ${file})
-device_efi=$(losetup -o ${efi_offset} --sizelimit ${efi_size} --show -f ${file})
-rm -f ${device}p1
-rm -f ${device}p15
-ln -sf ${device_root} ${device}p1
-ln -sf ${device_efi} ${device}p15
-
-ls -al /dev/loop*
-losetup -a -l
-parted ${device} unit B print
-
-partprobe "$device"
-
-wait_for_device "$device_root"
-mkfs.ext4 -U "$FSUUID_ROOT" "$device_root"
-tune2fs -c 0 -i 0 "$device_root"
-
-wait_for_device "$device_efi"
-mkfs.vfat "$device_efi"
-
-parted ${device} unit B print
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index e04a562..d33b92a 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -1,5 +1,6 @@
 #!/bin/bash
 
+chmod +x $target/usr/local/bin/forwarder_guest
 chmod +x $target/usr/local/bin/ttyd
 chmod +x $target/usr/local/bin/vsock.py
 ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
diff --git a/build/debian/forwarder_guest/Cargo.toml b/build/debian/forwarder_guest/Cargo.toml
new file mode 100644
index 0000000..e70dcd4
--- /dev/null
+++ b/build/debian/forwarder_guest/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "forwarder_guest"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.5.19", features = ["derive"] }
+forwarder = { path = "../../../libs/libforwarder" }
+poll_token_derive = "0.1.0"
+remain = "0.2.14"
+vmm-sys-util = "0.12.1"
diff --git a/build/debian/forwarder_guest/src/main.rs b/build/debian/forwarder_guest/src/main.rs
new file mode 100644
index 0000000..6ebd4ef
--- /dev/null
+++ b/build/debian/forwarder_guest/src/main.rs
@@ -0,0 +1,123 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/chunnel/src/bin/chunnel.rs
+
+//! Guest-side stream socket forwarder
+
+use std::fmt;
+use std::result;
+
+use clap::Parser;
+use forwarder::forwarder::{ForwarderError, ForwarderSession};
+use forwarder::stream::{StreamSocket, StreamSocketError};
+use poll_token_derive::PollToken;
+use vmm_sys_util::poll::{PollContext, PollToken};
+
+#[remain::sorted]
+#[derive(Debug)]
+enum Error {
+    ConnectSocket(StreamSocketError),
+    Forward(ForwarderError),
+    PollContextAdd(vmm_sys_util::errno::Error),
+    PollContextDelete(vmm_sys_util::errno::Error),
+    PollContextNew(vmm_sys_util::errno::Error),
+    PollWait(vmm_sys_util::errno::Error),
+}
+
+type Result<T> = result::Result<T, Error>;
+
+impl fmt::Display for Error {
+    #[remain::check]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::Error::*;
+
+        #[remain::sorted]
+        match self {
+            ConnectSocket(e) => write!(f, "failed to connect socket: {}", e),
+            Forward(e) => write!(f, "failed to forward traffic: {}", e),
+            PollContextAdd(e) => write!(f, "failed to add fd to poll context: {}", e),
+            PollContextDelete(e) => write!(f, "failed to delete fd from poll context: {}", e),
+            PollContextNew(e) => write!(f, "failed to create poll context: {}", e),
+            PollWait(e) => write!(f, "failed to wait for poll: {}", e),
+        }
+    }
+}
+
+fn run_forwarder(local_stream: StreamSocket, remote_stream: StreamSocket) -> Result<()> {
+    #[derive(PollToken)]
+    enum Token {
+        LocalStreamReadable,
+        RemoteStreamReadable,
+    }
+    let poll_ctx: PollContext<Token> = PollContext::new().map_err(Error::PollContextNew)?;
+    poll_ctx.add(&local_stream, Token::LocalStreamReadable).map_err(Error::PollContextAdd)?;
+    poll_ctx.add(&remote_stream, Token::RemoteStreamReadable).map_err(Error::PollContextAdd)?;
+
+    let mut forwarder = ForwarderSession::new(local_stream, remote_stream);
+
+    loop {
+        let events = poll_ctx.wait().map_err(Error::PollWait)?;
+
+        for event in events.iter_readable() {
+            match event.token() {
+                Token::LocalStreamReadable => {
+                    let shutdown = forwarder.forward_from_local().map_err(Error::Forward)?;
+                    if shutdown {
+                        poll_ctx
+                            .delete(forwarder.local_stream())
+                            .map_err(Error::PollContextDelete)?;
+                    }
+                }
+                Token::RemoteStreamReadable => {
+                    let shutdown = forwarder.forward_from_remote().map_err(Error::Forward)?;
+                    if shutdown {
+                        poll_ctx
+                            .delete(forwarder.remote_stream())
+                            .map_err(Error::PollContextDelete)?;
+                    }
+                }
+            }
+        }
+        if forwarder.is_shut_down() {
+            return Ok(());
+        }
+    }
+}
+
+#[derive(Parser)]
+/// Flags for running command
+pub struct Args {
+    /// Local socket address
+    #[arg(long)]
+    #[arg(alias = "local")]
+    local_sockaddr: String,
+
+    /// Remote socket address
+    #[arg(long)]
+    #[arg(alias = "remote")]
+    remote_sockaddr: String,
+}
+
+// TODO(b/370897694): Support forwarding for datagram socket
+fn main() -> Result<()> {
+    let args = Args::parse();
+
+    let local_stream = StreamSocket::connect(&args.local_sockaddr).map_err(Error::ConnectSocket)?;
+    let remote_stream =
+        StreamSocket::connect(&args.remote_sockaddr).map_err(Error::ConnectSocket)?;
+
+    run_forwarder(local_stream, remote_stream)
+}
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/build.sh
index fb2a1a3..4cc4769 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/build.sh
@@ -4,4 +4,6 @@
 
 cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
 sudo losetup -D
+grep vmx /proc/cpuinfo || true
 sudo ./build.sh
+tar czvS -f ${KOKORO_ARTIFACTS_DIR}/image.tar.gz image.raw
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
index d92031e..97ebd5d 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
@@ -5,3 +5,9 @@
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
 build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+
+action {
+  define_artifacts {
+    regex: "image.tar.gz"
+  }
+}
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg b/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
similarity index 84%
rename from build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg
rename to build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
index d92031e..97ebd5d 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg
+++ b/build/debian/kokoro/gcp_ubuntu_docker/hourly.cfg
@@ -5,3 +5,9 @@
 # Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
 # git_on_borg_scm.name is specified in the job configuration (next section).
 build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
+
+action {
+  define_artifacts {
+    regex: "image.tar.gz"
+  }
+}
diff --git a/build/debian/port_listener/build.sh b/build/debian/port_listener/build.sh
new file mode 100755
index 0000000..a1d0205
--- /dev/null
+++ b/build/debian/port_listener/build.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+set -e
+
+check_sudo() {
+	if [ "$EUID" -ne 0 ]; then
+		echo "Please run as root."
+		exit
+	fi
+}
+
+install_prerequisites() {
+    apt update
+    apt install --no-install-recommends --assume-yes \
+        bpftool \
+        clang \
+        libbpf-dev \
+        libgoogle-glog-dev \
+        libstdc++-14-dev
+}
+
+build_port_listener() {
+    cp $(dirname $0)/src/* ${workdir}
+    out_dir=${PWD}
+    pushd ${workdir}
+        bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
+        clang \
+            -O2 \
+            -Wall \
+            -target bpf \
+            -g \
+            -c listen_tracker.ebpf.c \
+            -o listen_tracker.ebpf.o
+        bpftool gen skeleton listen_tracker.ebpf.o > listen_tracker.skel.h
+        clang++ \
+            -O2 \
+            -Wall \
+            -lbpf \
+            -lglog \
+            -o port_listener \
+            main.cc
+        cp port_listener ${out_dir}
+    popd
+}
+
+clean_up() {
+	rm -rf ${workdir}
+}
+trap clean_up EXIT
+workdir=$(mktemp -d)
+
+check_sudo
+install_prerequisites
+build_port_listener
diff --git a/build/debian/port_listener/src/common.h b/build/debian/port_listener/src/common.h
new file mode 100644
index 0000000..d6e507c
--- /dev/null
+++ b/build/debian/port_listener/src/common.h
@@ -0,0 +1,31 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/common.h
+
+#ifndef VM_TOOLS_PORT_LISTENER_COMMON_H_
+#define VM_TOOLS_PORT_LISTENER_COMMON_H_
+
+enum State {
+    kPortListenerUp,
+    kPortListenerDown,
+};
+
+struct event {
+    enum State state;
+    uint16_t port;
+};
+
+#endif // VM_TOOLS_PORT_LISTENER_COMMON_H_
diff --git a/build/debian/port_listener/src/listen_tracker.ebpf.c b/build/debian/port_listener/src/listen_tracker.ebpf.c
new file mode 100644
index 0000000..030ded0
--- /dev/null
+++ b/build/debian/port_listener/src/listen_tracker.ebpf.c
@@ -0,0 +1,81 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/listen_tracker.ebpf.c
+
+// bpf_helpers.h uses types defined here
+#include "vmlinux.h"
+
+#include <bpf/bpf_helpers.h>
+
+#include "common.h"
+
+// For some reason 6.1 doesn't include these symbols in the debug build
+// so they don't get included in vmlinux.h. These features have existed since
+// well before 6.1.
+#define BPF_F_NO_PREALLOC (1U << 0)
+#define BPF_ANY 0
+
+struct {
+    __uint(type, BPF_MAP_TYPE_RINGBUF);
+    __uint(max_entries, 1 << 24);
+} events SEC(".maps");
+
+struct {
+    __uint(type, BPF_MAP_TYPE_HASH);
+    __type(key, struct sock*);
+    __type(value, __u8);
+    __uint(max_entries, 65535);
+    __uint(map_flags, BPF_F_NO_PREALLOC);
+} sockmap SEC(".maps");
+
+const __u8 set_value = 0;
+
+SEC("tp/sock/inet_sock_set_state")
+int tracepoint_inet_sock_set_state(struct trace_event_raw_inet_sock_set_state* ctx) {
+    // We don't support anything other than TCP.
+    if (ctx->protocol != IPPROTO_TCP) {
+        return 0;
+    }
+    struct sock* sk = (struct sock*)ctx->skaddr;
+    // If we're transitioning away from LISTEN but we don't know about this
+    // socket yet then don't report anything.
+    if (ctx->oldstate == BPF_TCP_LISTEN && bpf_map_lookup_elem(&sockmap, &sk) == NULL) {
+        return 0;
+    }
+    // If we aren't transitioning to or from TCP_LISTEN then we don't care.
+    if (ctx->newstate != BPF_TCP_LISTEN && ctx->oldstate != BPF_TCP_LISTEN) {
+        return 0;
+    }
+
+    struct event* ev;
+    ev = bpf_ringbuf_reserve(&events, sizeof(*ev), 0);
+    if (!ev) {
+        return 0;
+    }
+    ev->port = ctx->sport;
+
+    if (ctx->newstate == BPF_TCP_LISTEN) {
+        bpf_map_update_elem(&sockmap, &sk, &set_value, BPF_ANY);
+        ev->state = kPortListenerUp;
+    }
+    if (ctx->oldstate == BPF_TCP_LISTEN) {
+        bpf_map_delete_elem(&sockmap, &sk);
+        ev->state = kPortListenerDown;
+    }
+    bpf_ringbuf_submit(ev, 0);
+
+    return 0;
+}
diff --git a/build/debian/port_listener/src/main.cc b/build/debian/port_listener/src/main.cc
new file mode 100644
index 0000000..b0b0979
--- /dev/null
+++ b/build/debian/port_listener/src/main.cc
@@ -0,0 +1,168 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/port_listener/main.cc
+
+#include <bpf/libbpf.h>
+#include <bpf/libbpf_legacy.h>
+#include <glog/logging.h>
+#include <sys/socket.h>
+
+#include <linux/vm_sockets.h> // Needs to come after sys/socket.h
+
+#include <memory>
+#include <unordered_map>
+
+#include "common.h"
+#include "listen_tracker.skel.h"
+
+typedef std::unordered_map<int, int> port_usage_map;
+
+namespace port_listener {
+namespace {
+
+int HandleEvent(void* ctx, void* const data, size_t size) {
+    port_usage_map* map = reinterpret_cast<port_usage_map*>(ctx);
+    const struct event* ev = (struct event*)data;
+
+    switch (ev->state) {
+        case kPortListenerUp:
+            (*map)[ev->port]++;
+            break;
+
+        case kPortListenerDown:
+            if ((*map)[ev->port] > 0) {
+                (*map)[ev->port]--;
+            } else {
+                LOG(INFO) << "Received down event while port count was 0; ignoring";
+            }
+
+            break;
+
+        default:
+            LOG(ERROR) << "Unknown event state " << ev->state;
+    }
+
+    LOG(INFO) << "Listen event: port=" << ev->port << " state=" << ev->state;
+
+    return 0;
+}
+
+typedef std::unique_ptr<struct ring_buffer, decltype(&ring_buffer__free)> ring_buffer_ptr;
+typedef std::unique_ptr<listen_tracker_ebpf, decltype(&listen_tracker_ebpf__destroy)>
+        listen_tracker_ptr;
+
+// BPFProgram tracks the state and resources of the listen_tracker BPF program.
+class BPFProgram {
+public:
+    // Default movable but not copyable.
+    BPFProgram(BPFProgram&& other) = default;
+    BPFProgram(const BPFProgram& other) = delete;
+    BPFProgram& operator=(BPFProgram&& other) = default;
+    BPFProgram& operator=(const BPFProgram& other) = delete;
+
+    // Load loads the listen_tracker BPF program and prepares it for polling. On
+    // error nullptr is returned.
+    static std::unique_ptr<BPFProgram> Load() {
+        auto* skel = listen_tracker_ebpf__open();
+        if (!skel) {
+            PLOG(ERROR) << "Failed to open listen_tracker BPF skeleton";
+            return nullptr;
+        }
+        listen_tracker_ptr skeleton(skel, listen_tracker_ebpf__destroy);
+
+        int err = listen_tracker_ebpf__load(skeleton.get());
+        if (err) {
+            PLOG(ERROR) << "Failed to load listen_tracker BPF program";
+            return nullptr;
+        }
+
+        auto map = std::make_unique<port_usage_map>();
+        auto* rb = ring_buffer__new(bpf_map__fd(skel->maps.events), HandleEvent, map.get(), NULL);
+        if (!rb) {
+            PLOG(ERROR) << "Failed to open ring buffer for listen_tracker";
+            return nullptr;
+        }
+        ring_buffer_ptr ringbuf(rb, ring_buffer__free);
+
+        err = listen_tracker_ebpf__attach(skeleton.get());
+        if (err) {
+            PLOG(ERROR) << "Failed to attach listen_tracker";
+            return nullptr;
+        }
+
+        return std::unique_ptr<BPFProgram>(
+                new BPFProgram(std::move(skeleton), std::move(ringbuf), std::move(map)));
+    }
+
+    // Poll waits for the listen_tracker BPF program to post a new event to the
+    // ring buffer. BPFProgram handles integrating this new event into the
+    // port_usage map and callers should consult port_usage() after Poll returns
+    // for the latest data.
+    const bool Poll() {
+        int err = ring_buffer__poll(rb_.get(), -1);
+        if (err < 0) {
+            LOG(ERROR) << "Error polling ring buffer ret=" << err;
+            return false;
+        }
+
+        return true;
+    }
+
+    const port_usage_map& port_usage() { return *port_usage_; }
+
+private:
+    BPFProgram(listen_tracker_ptr&& skeleton, ring_buffer_ptr&& rb,
+               std::unique_ptr<port_usage_map>&& port_usage)
+          : skeleton_(std::move(skeleton)),
+            rb_(std::move(rb)),
+            port_usage_(std::move(port_usage)) {}
+
+    listen_tracker_ptr skeleton_;
+    ring_buffer_ptr rb_;
+    std::unique_ptr<port_usage_map> port_usage_;
+};
+
+} // namespace
+} // namespace port_listener
+
+int main(int argc, char** argv) {
+    google::InitGoogleLogging(argv[0]);
+    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
+
+    // Load our BPF program.
+    auto program = port_listener::BPFProgram::Load();
+    if (program == nullptr) {
+        LOG(ERROR) << "Failed to load BPF program";
+        return EXIT_FAILURE;
+    }
+
+    // main loop: poll for listen updates
+    for (;;) {
+        if (!program->Poll()) {
+            LOG(ERROR) << "Failure while polling BPF program";
+            return EXIT_FAILURE;
+        }
+        // port_usage will be updated with the latest usage data
+
+        for (auto it : program->port_usage()) {
+            if (it.second <= 0) {
+                continue;
+            }
+            // TODO(b/340126051): Add listening TCP4 ports.
+        }
+        // TODO(b/340126051): Notify port information to the guest agent.
+    }
+}
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index b502af6..d0d309b 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -19,7 +19,7 @@
         "libcstr",
         "libdiced_open_dice_nostd",
         "libfdtpci",
-        "liblibfdt",
+        "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libonce_cell_nostd",
         "libpvmfw_avb_nostd",
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 58ba10c..3ffa3f0 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -233,10 +233,10 @@
 
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/main/Documentation/dt-object-internal.txt
-[debug_policy]: ../docs/debug/README.md#debug-policy
-[device_assignment]: ../docs/device_assignment.md
+[debug_policy]: ../../docs/debug/README.md#debug-policy
+[device_assignment]: ../../docs/device_assignment.md
 [secretkeeper_key]: https://android.googlesource.com/platform/system/secretkeeper/+/refs/heads/main/README.md#secretkeeper-public-key
-[vendor_hashtree_digest]: ../microdroid/README.md#verification-of-vendor-image
+[vendor_hashtree_digest]: ../../build/microdroid/README.md#verification-of-vendor-image
 
 #### Virtual Platform DICE Chain Handover
 
@@ -402,7 +402,7 @@
 };
 ```
 
-[dt.md]: ../docs/device_trees.md#avf_specific-properties-and-nodes
+[dt.md]: ../../docs/device_trees.md#avf_specific-properties-and-nodes
 
 ### Guest Image Signing
 
diff --git a/guest/pvmfw/avb/Android.bp b/guest/pvmfw/avb/Android.bp
index 558152d..f97a713 100644
--- a/guest/pvmfw/avb/Android.bp
+++ b/guest/pvmfw/avb/Android.bp
@@ -43,6 +43,7 @@
         ":test_image_with_duplicated_capability",
         ":test_image_with_rollback_index_5",
         ":test_image_with_multiple_capabilities",
+        ":test_image_with_all_capabilities",
         ":unsigned_test_image",
     ],
     prefer_rlib: true,
@@ -218,3 +219,17 @@
         },
     ],
 }
+
+avb_add_hash_footer {
+    name: "test_image_with_all_capabilities",
+    src: ":unsigned_test_image",
+    partition_name: "boot",
+    private_key: ":pvmfw_sign_key",
+    salt: "4231",
+    props: [
+        {
+            name: "com.android.virt.cap",
+            value: "remote_attest|secretkeeper_protection|supports_uefi_boot",
+        },
+    ],
+}
diff --git a/guest/pvmfw/avb/src/verify.rs b/guest/pvmfw/avb/src/verify.rs
index 038b1d6..bd700ce 100644
--- a/guest/pvmfw/avb/src/verify.rs
+++ b/guest/pvmfw/avb/src/verify.rs
@@ -70,6 +70,11 @@
     RemoteAttest,
     /// Secretkeeper protected secrets.
     SecretkeeperProtection,
+    /// UEFI support for booting guest kernel.
+    SupportsUefiBoot,
+    /// (internal)
+    #[allow(non_camel_case_types)] // TODO: Use mem::variant_count once stable.
+    _VARIANT_COUNT,
 }
 
 impl Capability {
@@ -77,6 +82,9 @@
     const REMOTE_ATTEST: &'static [u8] = b"remote_attest";
     const SECRETKEEPER_PROTECTION: &'static [u8] = b"secretkeeper_protection";
     const SEPARATOR: u8 = b'|';
+    const SUPPORTS_UEFI_BOOT: &'static [u8] = b"supports_uefi_boot";
+    /// Number of supported capabilites.
+    pub const COUNT: usize = Self::_VARIANT_COUNT as usize;
 
     /// Returns the capabilities indicated in `descriptor`, or error if the descriptor has
     /// unexpected contents.
@@ -91,6 +99,7 @@
             let cap = match v {
                 Self::REMOTE_ATTEST => Self::RemoteAttest,
                 Self::SECRETKEEPER_PROTECTION => Self::SecretkeeperProtection,
+                Self::SUPPORTS_UEFI_BOOT => Self::SupportsUefiBoot,
                 _ => return Err(PvmfwVerifyError::UnknownVbmetaProperty),
             };
             if res.contains(&cap) {
diff --git a/guest/pvmfw/avb/tests/api_test.rs b/guest/pvmfw/avb/tests/api_test.rs
index 8683e69..01c13d4 100644
--- a/guest/pvmfw/avb/tests/api_test.rs
+++ b/guest/pvmfw/avb/tests/api_test.rs
@@ -38,6 +38,7 @@
 const TEST_IMG_WITH_INITRD_AND_NON_INITRD_DESC_PATH: &str =
     "test_image_with_initrd_and_non_initrd_desc.img";
 const TEST_IMG_WITH_MULTIPLE_CAPABILITIES: &str = "test_image_with_multiple_capabilities.img";
+const TEST_IMG_WITH_ALL_CAPABILITIES: &str = "test_image_with_all_capabilities.img";
 const UNSIGNED_TEST_IMG_PATH: &str = "unsigned_test.img";
 
 const RANDOM_FOOTER_POS: usize = 30;
@@ -418,3 +419,22 @@
     assert!(verified_boot_data.has_capability(Capability::SecretkeeperProtection));
     Ok(())
 }
+
+#[test]
+fn payload_with_all_capabilities() -> Result<()> {
+    let public_key = load_trusted_public_key()?;
+    let verified_boot_data = verify_payload(
+        &fs::read(TEST_IMG_WITH_ALL_CAPABILITIES)?,
+        /* initrd= */ None,
+        &public_key,
+    )
+    .map_err(|e| anyhow!("Verification failed. Error: {}", e))?;
+
+    assert!(verified_boot_data.has_capability(Capability::RemoteAttest));
+    assert!(verified_boot_data.has_capability(Capability::SecretkeeperProtection));
+    assert!(verified_boot_data.has_capability(Capability::SupportsUefiBoot));
+    // Fail if this test doesn't actually cover all supported capabilities.
+    assert_eq!(Capability::COUNT, 3);
+
+    Ok(())
+}
diff --git a/guest/rialto/Android.bp b/guest/rialto/Android.bp
index 4c18bf9..eeb5b2d 100644
--- a/guest/rialto/Android.bp
+++ b/guest/rialto/Android.bp
@@ -15,7 +15,7 @@
         "libcstr",
         "libdiced_open_dice_nostd",
         "libfdtpci",
-        "liblibfdt",
+        "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libservice_vm_comm_nostd",
         "libservice_vm_fake_chain_nostd",
diff --git a/guest/vmbase_example/Android.bp b/guest/vmbase_example/Android.bp
index ff7bd83..49a6d69 100644
--- a/guest/vmbase_example/Android.bp
+++ b/guest/vmbase_example/Android.bp
@@ -12,7 +12,7 @@
         "libcstr",
         "libdiced_open_dice_nostd",
         "libfdtpci",
-        "liblibfdt",
+        "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libvirtio_drivers",
         "libvmbase",
diff --git a/libs/fdtpci/Android.bp b/libs/fdtpci/Android.bp
index e12c24f..d7a5da3 100644
--- a/libs/fdtpci/Android.bp
+++ b/libs/fdtpci/Android.bp
@@ -11,7 +11,7 @@
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
     rustlibs: [
-        "liblibfdt",
+        "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libvirtio_drivers",
     ],
diff --git a/libs/framework-virtualization/README.md b/libs/framework-virtualization/README.md
index 0dd7e64..a742ccc 100644
--- a/libs/framework-virtualization/README.md
+++ b/libs/framework-virtualization/README.md
@@ -1,12 +1,12 @@
 # Android Virtualization Framework API
 
 These Java APIs allow an app to configure and run a Virtual Machine running
-[Microdroid](../build/microdroid/README.md) and execute native code from the app (the
+[Microdroid](../../build/microdroid/README.md) and execute native code from the app (the
 payload) within it.
 
-There is more information on AVF [here](../README.md). To see how to package the
+There is more information on AVF [here](../../README.md). To see how to package the
 payload code that is to run inside a VM, and the native API available to it, see
-the [VM Payload API](../libs/libvm_payload/README.md)
+the [VM Payload API](../libvm_payload/README.md)
 
 The API classes are all in the
 [`android.system.virtualmachine`](src/android/system/virtualmachine) package.
diff --git a/libs/libfdt/Android.bp b/libs/libfdt/Android.bp
index b2e7b2b..09f288d 100644
--- a/libs/libfdt/Android.bp
+++ b/libs/libfdt/Android.bp
@@ -19,14 +19,14 @@
     dylib: {
         enabled: false,
     },
-    static_libs: [
+    header_libs: [
         "libfdt",
     ],
     apex_available: ["com.android.virt"],
 }
 
-rust_library_rlib {
-    name: "liblibfdt",
+rust_defaults {
+    name: "liblibfdt_defaults",
     crate_name: "libfdt",
     defaults: ["avf_build_flags_rust"],
     srcs: [
@@ -34,23 +34,36 @@
         ":liblibfdt_bindgen",
     ],
     edition: "2021",
-    no_stdlibs: true,
-    prefer_rlib: true,
-    stdlibs: [
-        "libcore.rust_sysroot",
-    ],
     rustlibs: [
         "libcstr",
         "liblibfdt_bindgen",
         "libstatic_assertions",
         "libzerocopy_nostd",
     ],
+}
+
+rust_library_rlib {
+    name: "liblibfdt",
+    defaults: ["liblibfdt_defaults"],
     whole_static_libs: [
         "libfdt",
     ],
     apex_available: ["com.android.virt"],
 }
 
+rust_library_rlib {
+    name: "liblibfdt_nostd",
+    defaults: ["liblibfdt_defaults"],
+    no_stdlibs: true,
+    prefer_rlib: true,
+    stdlibs: [
+        "libcore.rust_sysroot",
+    ],
+    whole_static_libs: [
+        "libfdt_baremetal",
+    ],
+}
+
 rust_test {
     name: "liblibfdt.integration_test",
     crate_name: "libfdt_test",
diff --git a/libs/libforwarder/Android.bp b/libs/libforwarder/Android.bp
new file mode 100644
index 0000000..48307e7
--- /dev/null
+++ b/libs/libforwarder/Android.bp
@@ -0,0 +1,15 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+    name: "libforwarder",
+    crate_name: "forwarder",
+    edition: "2021",
+    srcs: ["src/lib.rs"],
+    rustlibs: [
+        "liblibc",
+        "libvsock",
+    ],
+    proc_macros: ["libremain"],
+}
diff --git a/libs/libforwarder/Cargo.toml b/libs/libforwarder/Cargo.toml
new file mode 100644
index 0000000..9f3f341
--- /dev/null
+++ b/libs/libforwarder/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "forwarder"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+libc = "0.2.159"
+remain = "0.2.14"
+vsock = "0.5.1"
diff --git a/libs/libforwarder/src/forwarder.rs b/libs/libforwarder/src/forwarder.rs
new file mode 100644
index 0000000..3600ab2
--- /dev/null
+++ b/libs/libforwarder/src/forwarder.rs
@@ -0,0 +1,170 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/chunnel/src/forwarder.rs
+
+//! This module contains forwarding mechanism between stream sockets.
+
+use std::fmt;
+use std::io::{self, Read, Write};
+use std::result;
+
+use crate::stream::StreamSocket;
+
+// This was picked arbitrarily. crosvm doesn't yet use VIRTIO_NET_F_MTU, so there's no reason to
+// opt for massive 65535 byte frames.
+const MAX_FRAME_SIZE: usize = 8192;
+
+/// Errors that can be encountered by a ForwarderSession.
+#[remain::sorted]
+#[derive(Debug)]
+pub enum ForwarderError {
+    /// An io::Error was encountered while reading from a stream.
+    ReadFromStream(io::Error),
+    /// An io::Error was encountered while shutting down writes on a stream.
+    ShutDownStream(io::Error),
+    /// An io::Error was encountered while writing to a stream.
+    WriteToStream(io::Error),
+}
+
+type Result<T> = result::Result<T, ForwarderError>;
+
+impl fmt::Display for ForwarderError {
+    #[remain::check]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::ForwarderError::*;
+
+        #[remain::sorted]
+        match self {
+            ReadFromStream(e) => write!(f, "failed to read from stream: {}", e),
+            ShutDownStream(e) => write!(f, "failed to shut down stream: {}", e),
+            WriteToStream(e) => write!(f, "failed to write to stream: {}", e),
+        }
+    }
+}
+
+/// A ForwarderSession owns stream sockets that it forwards traffic between.
+pub struct ForwarderSession {
+    local: StreamSocket,
+    remote: StreamSocket,
+}
+
+fn forward(from_stream: &mut StreamSocket, to_stream: &mut StreamSocket) -> Result<bool> {
+    let mut buf = [0u8; MAX_FRAME_SIZE];
+
+    let count = from_stream.read(&mut buf).map_err(ForwarderError::ReadFromStream)?;
+    if count == 0 {
+        to_stream.shut_down_write().map_err(ForwarderError::ShutDownStream)?;
+        return Ok(true);
+    }
+
+    to_stream.write_all(&buf[..count]).map_err(ForwarderError::WriteToStream)?;
+    Ok(false)
+}
+
+impl ForwarderSession {
+    /// Creates a forwarder session from a local and remote stream socket.
+    pub fn new(local: StreamSocket, remote: StreamSocket) -> Self {
+        ForwarderSession { local, remote }
+    }
+
+    /// Forwards traffic from the local socket to the remote socket.
+    /// Returns true if the local socket has reached EOF and the
+    /// remote socket has been shut down for further writes.
+    pub fn forward_from_local(&mut self) -> Result<bool> {
+        forward(&mut self.local, &mut self.remote)
+    }
+
+    /// Forwards traffic from the remote socket to the local socket.
+    /// Returns true if the remote socket has reached EOF and the
+    /// local socket has been shut down for further writes.
+    pub fn forward_from_remote(&mut self) -> Result<bool> {
+        forward(&mut self.remote, &mut self.local)
+    }
+
+    /// Returns a reference to the local stream socket.
+    pub fn local_stream(&self) -> &StreamSocket {
+        &self.local
+    }
+
+    /// Returns a reference to the remote stream socket.
+    pub fn remote_stream(&self) -> &StreamSocket {
+        &self.remote
+    }
+
+    /// Returns true if both sockets are completely shut down and the session can be dropped.
+    pub fn is_shut_down(&self) -> bool {
+        self.local.is_shut_down() && self.remote.is_shut_down()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{Read, Write};
+    use std::net::Shutdown;
+    use std::os::unix::net::UnixStream;
+
+    #[test]
+    fn forward_unix() {
+        // Local streams.
+        let (mut london, folkestone) = UnixStream::pair().unwrap();
+        // Remote streams.
+        let (coquelles, mut paris) = UnixStream::pair().unwrap();
+
+        // Connect the local and remote sockets via the chunnel.
+        let mut forwarder = ForwarderSession::new(folkestone.into(), coquelles.into());
+
+        // Put some traffic in from London.
+        let greeting = b"hello";
+        london.write_all(greeting).unwrap();
+
+        // Expect forwarding from the local end not to have reached EOF.
+        assert!(!forwarder.forward_from_local().unwrap());
+        let mut salutation = [0u8; 8];
+        let count = paris.read(&mut salutation).unwrap();
+        assert_eq!(greeting.len(), count);
+        assert_eq!(greeting, &salutation[..count]);
+
+        // Shut the local socket down. The forwarder should detect this and perform a shutdown,
+        // which will manifest as an EOF when reading.
+        london.shutdown(Shutdown::Write).unwrap();
+        assert!(forwarder.forward_from_local().unwrap());
+        assert_eq!(paris.read(&mut salutation).unwrap(), 0);
+
+        // Don't consider the forwarder shut down until both ends are.
+        assert!(!forwarder.is_shut_down());
+
+        // Forward traffic from the remote end.
+        let salutation = b"bonjour";
+        paris.write_all(salutation).unwrap();
+
+        // Expect forwarding from the remote end not to have reached EOF.
+        assert!(!forwarder.forward_from_remote().unwrap());
+        let mut greeting = [0u8; 8];
+        let count = london.read(&mut greeting).unwrap();
+        assert_eq!(salutation.len(), count);
+        assert_eq!(salutation, &greeting[..count]);
+
+        // Shut the remote socket down. The forwarder should detect this and perform a shutdown,
+        // which will manifest as an EOF when reading.
+        paris.shutdown(Shutdown::Write).unwrap();
+        assert!(forwarder.forward_from_remote().unwrap());
+        assert_eq!(london.read(&mut greeting).unwrap(), 0);
+
+        // The forwarder should now be considered shut down.
+        assert!(forwarder.is_shut_down());
+    }
+}
diff --git a/libs/libforwarder/src/lib.rs b/libs/libforwarder/src/lib.rs
new file mode 100644
index 0000000..bcce689
--- /dev/null
+++ b/libs/libforwarder/src/lib.rs
@@ -0,0 +1,21 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/chunnel/src/lib.rs
+
+//! Library for stream socket forwarding.
+
+pub mod forwarder;
+pub mod stream;
diff --git a/libs/libforwarder/src/stream.rs b/libs/libforwarder/src/stream.rs
new file mode 100644
index 0000000..d8c7f51
--- /dev/null
+++ b/libs/libforwarder/src/stream.rs
@@ -0,0 +1,263 @@
+// Copyright 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/chunnel/src/stream.rs
+
+//! This module provides abstraction of various stream socket type.
+
+use std::fmt;
+use std::io;
+use std::net::TcpStream;
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
+use std::os::unix::net::UnixStream;
+use std::result;
+
+use libc::{self, c_void, shutdown, EPIPE, SHUT_WR};
+use vsock::VsockAddr;
+use vsock::VsockStream;
+
+/// Parse a vsock SocketAddr from a string. vsock socket addresses are of the form
+/// "vsock:cid:port".
+pub fn parse_vsock_addr(addr: &str) -> result::Result<VsockAddr, io::Error> {
+    let components: Vec<&str> = addr.split(':').collect();
+    if components.len() != 3 || components[0] != "vsock" {
+        return Err(io::Error::from_raw_os_error(libc::EINVAL));
+    }
+
+    Ok(VsockAddr::new(
+        components[1].parse().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?,
+        components[2].parse().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?,
+    ))
+}
+
+/// StreamSocket provides a generic abstraction around any connection-oriented stream socket.
+/// The socket will be closed when StreamSocket is dropped, but writes to the socket can also
+/// be shut down manually.
+pub struct StreamSocket {
+    fd: RawFd,
+    shut_down: bool,
+}
+
+impl StreamSocket {
+    /// Connects to the given socket address. Supported socket types are vsock, unix, and TCP.
+    pub fn connect(sockaddr: &str) -> result::Result<StreamSocket, StreamSocketError> {
+        const UNIX_PREFIX: &str = "unix:";
+        const VSOCK_PREFIX: &str = "vsock:";
+
+        if sockaddr.starts_with(VSOCK_PREFIX) {
+            let addr = parse_vsock_addr(sockaddr)
+                .map_err(|e| StreamSocketError::ConnectVsock(sockaddr.to_string(), e))?;
+            let vsock_stream = VsockStream::connect(&addr)
+                .map_err(|e| StreamSocketError::ConnectVsock(sockaddr.to_string(), e))?;
+            Ok(vsock_stream.into())
+        } else if sockaddr.starts_with(UNIX_PREFIX) {
+            let (_prefix, sock_path) = sockaddr.split_at(UNIX_PREFIX.len());
+            let unix_stream = UnixStream::connect(sock_path)
+                .map_err(|e| StreamSocketError::ConnectUnix(sockaddr.to_string(), e))?;
+            Ok(unix_stream.into())
+        } else {
+            // Assume this is a TCP stream.
+            let tcp_stream = TcpStream::connect(sockaddr)
+                .map_err(|e| StreamSocketError::ConnectTcp(sockaddr.to_string(), e))?;
+            Ok(tcp_stream.into())
+        }
+    }
+
+    /// Shuts down writes to the socket using shutdown(2).
+    pub fn shut_down_write(&mut self) -> io::Result<()> {
+        // SAFETY:
+        // Safe because no memory is modified and the return value is checked.
+        let ret = unsafe { shutdown(self.fd, SHUT_WR) };
+        if ret < 0 {
+            return Err(io::Error::last_os_error());
+        }
+
+        self.shut_down = true;
+        Ok(())
+    }
+
+    /// Returns true if the socket has been shut down for writes, false otherwise.
+    pub fn is_shut_down(&self) -> bool {
+        self.shut_down
+    }
+}
+
+impl io::Read for StreamSocket {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        // SAFETY:
+        // Safe because this will only modify the contents of |buf| and we check the return value.
+        let ret = unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut c_void, buf.len()) };
+        if ret < 0 {
+            return Err(io::Error::last_os_error());
+        }
+
+        Ok(ret as usize)
+    }
+}
+
+impl io::Write for StreamSocket {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        // SAFETY:
+        // Safe because this doesn't modify any memory and we check the return value.
+        let ret = unsafe { libc::write(self.fd, buf.as_ptr() as *const c_void, buf.len()) };
+        if ret < 0 {
+            // If a write causes EPIPE then the socket is shut down for writes.
+            let err = io::Error::last_os_error();
+            if let Some(errno) = err.raw_os_error() {
+                if errno == EPIPE {
+                    self.shut_down = true
+                }
+            }
+
+            return Err(err);
+        }
+
+        Ok(ret as usize)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        // No buffered data so nothing to do.
+        Ok(())
+    }
+}
+
+impl AsRawFd for StreamSocket {
+    fn as_raw_fd(&self) -> RawFd {
+        self.fd
+    }
+}
+
+impl From<TcpStream> for StreamSocket {
+    fn from(stream: TcpStream) -> Self {
+        StreamSocket { fd: stream.into_raw_fd(), shut_down: false }
+    }
+}
+
+impl From<UnixStream> for StreamSocket {
+    fn from(stream: UnixStream) -> Self {
+        StreamSocket { fd: stream.into_raw_fd(), shut_down: false }
+    }
+}
+
+impl From<VsockStream> for StreamSocket {
+    fn from(stream: VsockStream) -> Self {
+        StreamSocket { fd: stream.into_raw_fd(), shut_down: false }
+    }
+}
+
+impl FromRawFd for StreamSocket {
+    unsafe fn from_raw_fd(fd: RawFd) -> Self {
+        StreamSocket { fd, shut_down: false }
+    }
+}
+
+impl Drop for StreamSocket {
+    fn drop(&mut self) {
+        // SAFETY:
+        // Safe because this doesn't modify any memory and we are the only
+        // owner of the file descriptor.
+        unsafe { libc::close(self.fd) };
+    }
+}
+
+/// Error enums for StreamSocket.
+#[remain::sorted]
+#[derive(Debug)]
+pub enum StreamSocketError {
+    /// Error on connecting TCP socket.
+    ConnectTcp(String, io::Error),
+    /// Error on connecting unix socket.
+    ConnectUnix(String, io::Error),
+    /// Error on connecting vsock socket.
+    ConnectVsock(String, io::Error),
+}
+
+impl fmt::Display for StreamSocketError {
+    #[remain::check]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::StreamSocketError::*;
+
+        #[remain::sorted]
+        match self {
+            ConnectTcp(sockaddr, e) => {
+                write!(f, "failed to connect to TCP sockaddr {}: {}", sockaddr, e)
+            }
+            ConnectUnix(sockaddr, e) => {
+                write!(f, "failed to connect to unix sockaddr {}: {}", sockaddr, e)
+            }
+            ConnectVsock(sockaddr, e) => {
+                write!(f, "failed to connect to vsock sockaddr {}: {}", sockaddr, e)
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{Read, Write};
+    use std::net::TcpListener;
+    use std::os::unix::net::{UnixListener, UnixStream};
+    use tempfile::TempDir;
+
+    #[test]
+    fn sock_connect_tcp() {
+        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+        let sockaddr = format!("127.0.0.1:{}", listener.local_addr().unwrap().port());
+
+        let _stream = StreamSocket::connect(&sockaddr).unwrap();
+    }
+
+    #[test]
+    fn sock_connect_unix() {
+        let tempdir = TempDir::new().unwrap();
+        let path = tempdir.path().to_owned().join("test.sock");
+        let _listener = UnixListener::bind(&path).unwrap();
+
+        let unix_addr = format!("unix:{}", path.to_str().unwrap());
+        let _stream = StreamSocket::connect(&unix_addr).unwrap();
+    }
+
+    #[test]
+    fn invalid_sockaddr() {
+        assert!(StreamSocket::connect("this is not a valid sockaddr").is_err());
+    }
+
+    #[test]
+    fn shut_down_write() {
+        let (unix_stream, _dummy) = UnixStream::pair().unwrap();
+        let mut stream: StreamSocket = unix_stream.into();
+
+        stream.write_all(b"hello").unwrap();
+
+        stream.shut_down_write().unwrap();
+
+        assert!(stream.is_shut_down());
+        assert!(stream.write(b"goodbye").is_err());
+    }
+
+    #[test]
+    fn read_from_shut_down_sock() {
+        let (unix_stream1, unix_stream2) = UnixStream::pair().unwrap();
+        let mut stream1: StreamSocket = unix_stream1.into();
+        let mut stream2: StreamSocket = unix_stream2.into();
+
+        stream1.shut_down_write().unwrap();
+
+        // Reads from the other end of the socket should now return EOF.
+        let mut buf = Vec::new();
+        assert_eq!(stream2.read_to_end(&mut buf).unwrap(), 0);
+    }
+}
diff --git a/libs/libvm_payload/README.md b/libs/libvm_payload/README.md
index 8ef1bac..bbcfc61 100644
--- a/libs/libvm_payload/README.md
+++ b/libs/libvm_payload/README.md
@@ -9,7 +9,7 @@
 available in the VM, and only 64 bit code is supported.
 
 To create a VM and run the payload from Android see the [AVF Java
-APIs](../libs/framework-virtualization/README.md).
+APIs](../framework-virtualization/README.md).
 
 ## Entry point
 
diff --git a/libs/libvmbase/Android.bp b/libs/libvmbase/Android.bp
index ee12e85..e634c18 100644
--- a/libs/libvmbase/Android.bp
+++ b/libs/libvmbase/Android.bp
@@ -81,7 +81,7 @@
         "libbuddy_system_allocator",
         "libcstr",
         "libfdtpci",
-        "liblibfdt",
+        "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libonce_cell_nostd",
         "libsmccc",
diff --git a/libs/service-compos/Android.bp b/libs/service-compos/Android.bp
index 3dcf8be..053680c 100644
--- a/libs/service-compos/Android.bp
+++ b/libs/service-compos/Android.bp
@@ -37,4 +37,7 @@
     libs: ["services"],
     sdk_version: "",
     installable: true,
+
+    // The dexpreopt artifacts of service-compos will be installed in /system_ext
+    system_ext_specific: true,
 }
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index 18728ad..f77def3 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -28,4 +28,8 @@
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="MicrodroidHostTestCases.jar" />
     </test>
+
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
 </configuration>
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 22cd0dc..e490da4 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -39,4 +39,8 @@
         <option name="shell-timeout" value="300000" />
         <option name="test-timeout" value="300000" />
     </test>
+
+    <!-- Controller that will skip the module if a native bridge situation is detected -->
+    <!-- For example: module wants to run arm and device is x86 -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.NativeBridgeModuleController" />
 </configuration>