Merge "Better logging on failure"
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 84eb0f4..40643b8 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -65,3 +65,32 @@
         "testdata/input.4m.fsv_meta.bad_merkle",
     ],
 }
+
+java_genrule {
+    name: "authfs_test_apk_assets",
+    out: ["authfs_test_apk_assets.jar"],
+    tools: [
+        "fsverity_manifest_generator",
+        "fsverity",
+    ],
+    srcs: [
+        "testdata/input.4k",
+        "testdata/input.4k1",
+        "testdata/input.4m",
+    ],
+    /*
+     * Create a JAR file with an assets directory that can merge into the
+     * assets of an APK that depends on it in static_libs. Use this mechanism
+     * to load a generated fsverity manifest for the test input files into the
+     * test VM.
+     */
+    cmd: "out_dir=$$(dirname $(out))" +
+        "&& assets_dir=\"assets\" " +
+        "&& mkdir -p $$out_dir/$$assets_dir" +
+        "&& $(location fsverity_manifest_generator) " +
+        "    --fsverity-path $(location fsverity) " +
+        "    --base-dir $$(dirname $(in) | head -1) " +
+        "    --output $$out_dir/$$assets_dir/input_manifest.pb " +
+        "    $(in) " +
+        "&& jar cf $(out) -C $$out_dir $$assets_dir",
+}
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 749f3c1..f5254bc 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -76,7 +76,7 @@
     private static final String TEST_APK_NAME = "MicrodroidTestApp.apk";
 
     /** VM config entry path in the test APK */
-    private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config_extra_apk.json";
+    private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config.json";
 
     /** Path to open_then_run on Android */
     private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run";
@@ -90,11 +90,8 @@
     /** Path to authfs on Microdroid */
     private static final String AUTHFS_BIN = "/system/bin/authfs";
 
-    /** Idsig paths to be created for each APK in the "extra_apks" of vm_config_extra_apk.json. */
-    private static final String EXTRA_IDSIG_PATH = TEST_DIR + "BuildManifest.apk.idsig";
-
-    /** Build manifest path in the VM. 0 is the index of extra_apks in vm_config_extra_apk.json. */
-    private static final String BUILD_MANIFEST_PATH = "/mnt/extra-apk/0/assets/build_manifest.pb";
+    /** Input manifest path in the VM. */
+    private static final String INPUT_MANIFEST_PATH = "/mnt/apk/assets/input_manifest.pb";
 
     /** Plenty of time for authfs to get ready */
     private static final int AUTHFS_INIT_TIMEOUT_MS = 3000;
@@ -145,7 +142,6 @@
                 MicrodroidBuilder
                         .fromFile(findTestApk(testInfo.getBuildInfo()), VM_CONFIG_PATH_IN_APK)
                         .debugLevel("full")
-                        .addExtraIdsigPath(EXTRA_IDSIG_PATH)
                         .build((TestDevice) androidDevice);
 
         // From this point on, we need to tear down the Microdroid instance
@@ -588,16 +584,15 @@
     public void testInputDirectory_CanReadFile() throws Exception {
         // Setup
         String authfsInputDir = MOUNT_DIR + "/3";
-        runFdServerOnAndroid("--open-dir 3:/system", "--ro-dirs 3");
-        runAuthFsOnMicrodroid("--remote-ro-dir 3:" + BUILD_MANIFEST_PATH + ":system/ --cid "
+        runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3");
+        runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ": --cid "
                 + VMADDR_CID_HOST);
 
         // Action
-        String actualHash =
-                computeFileHash(sMicrodroid, authfsInputDir + "/system/framework/framework.jar");
+        String actualHash = computeFileHash(sMicrodroid, authfsInputDir + "/input.4m");
 
         // Verify
-        String expectedHash = computeFileHash(sAndroid, "/system/framework/framework.jar");
+        String expectedHash = computeFileHash(sAndroid, TEST_DIR + "/input.4m");
         assertEquals("Expect consistent hash through /authfs/3: ", expectedHash, actualHash);
     }
 
@@ -605,13 +600,13 @@
     public void testInputDirectory_OnlyAllowlistedFilesExist() throws Exception {
         // Setup
         String authfsInputDir = MOUNT_DIR + "/3";
-        runFdServerOnAndroid("--open-dir 3:/system", "--ro-dirs 3");
-        runAuthFsOnMicrodroid("--remote-ro-dir 3:" + BUILD_MANIFEST_PATH + ":system/ --cid "
+        runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3");
+        runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ": --cid "
                 + VMADDR_CID_HOST);
 
         // Verify
-        sMicrodroid.run("test -f " + authfsInputDir + "/system/framework/services.jar");
-        assertThat(sMicrodroid.runForResult("test -f " + authfsInputDir + "/system/bin/sh"))
+        sMicrodroid.run("test -f " + authfsInputDir + "/input.4k");
+        assertThat(sMicrodroid.runForResult("test -f " + authfsInputDir + "/input.4k.fsv_meta"))
                 .isFailed();
     }
 
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 1fd939d..23cd505 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -235,6 +235,11 @@
         log::warn!("VM error, cid = {}, error code = {}, message = {}", cid, error_code, message,);
         Ok(())
     }
+
+    fn onRamdump(&self, _cid: i32, _ramdump: &ParcelFileDescriptor) -> BinderResult<()> {
+        // TODO(b/238295267) send this to tombstone?
+        Ok(())
+    }
 }
 
 fn start_logging(pfd: &ParcelFileDescriptor) -> Result<()> {
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index e53f95d..747e98c 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -278,6 +278,13 @@
                             mService.shutdownNow();
                             mStatus.postValue(VirtualMachine.Status.STOPPED);
                         }
+
+                        @Override
+                        public void onRamdump(VirtualMachine vm, ParcelFileDescriptor ramdump) {
+                            if (!mService.isShutdown()) {
+                                mPayloadOutput.postValue("(Kernel panic. Ramdump created)");
+                            }
+                        }
                     };
 
             try {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index de44b63..8d74f5e 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -445,6 +445,11 @@
                                 executeCallback((cb) -> cb.onDied(VirtualMachine.this, reason));
                             }
                         }
+                        @Override
+                        public void onRamdump(int cid, ParcelFileDescriptor ramdump) {
+                            executeCallback(
+                                    (cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
+                        }
                     }
             );
             service.asBinder().linkToDeath(deathRecipient, 0);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 54d0701..a37c15b 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -142,4 +142,7 @@
 
     /** Called when the VM died. */
     void onDied(@NonNull VirtualMachine vm, @DeathReason int reason);
+
+    /** Called when kernel panic occurs and as a result ramdump is generated from the VM. */
+    void onRamdump(@NonNull VirtualMachine vm, @NonNull ParcelFileDescriptor ramdump);
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index ba3da08..3bbab13 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -102,6 +102,7 @@
 
                 "microdroid_plat_sepolicy.cil",
                 "microdroid_plat_mapping_file",
+                "microdroid_crashdump_initrd",
             ],
         },
         lib64: {
@@ -109,6 +110,8 @@
                 "apkdmverity",
                 "authfs",
                 "authfs_service",
+                "microdroid_crashdump_kernel",
+                "microdroid_kexec",
                 "microdroid_manager",
                 "zipfuse",
             ],
@@ -239,6 +242,8 @@
     "panic=-1",
     "bootconfig",
     "ioremap_guard",
+    // TODO(b/237381762) do this only when ramdump is enabled. (bootloader shall append this)
+    "crashkernel=17M",
 ]
 
 bootimg {
diff --git a/microdroid/kdump/Android.bp b/microdroid/kdump/Android.bp
new file mode 100644
index 0000000..390886b
--- /dev/null
+++ b/microdroid/kdump/Android.bp
@@ -0,0 +1,34 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_binary {
+    name: "microdroid_kexec",
+    stem: "kexec",
+    srcs: ["kexec.c"],
+    installable: false,
+    compile_multilib: "64",
+}
+
+cc_binary {
+    name: "microdroid_crashdump",
+    stem: "crashdump",
+    srcs: ["crashdump.c"],
+    static_executable: true,
+    installable: false,
+    compile_multilib: "64",
+}
+
+android_filesystem {
+    name: "microdroid_crashdump_initrd",
+    multilib: {
+        lib64: {
+            deps: ["microdroid_crashdump"],
+        },
+    },
+    dirs: [
+        "dev",
+        "proc",
+    ],
+    type: "cpio",
+}
diff --git a/microdroid/kdump/crashdump.c b/microdroid/kdump/crashdump.c
new file mode 100644
index 0000000..47d359a
--- /dev/null
+++ b/microdroid/kdump/crashdump.c
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+// This program runs as init in the crash kernel.
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/reboot.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <termios.h>
+#include <unistd.h>
+
+#define DUMP_SOURCE "/proc/vmcore"
+#define DUMP_TARGET "/dev/hvc1" // See virtualizationserice/crosvm.rs
+#define BUF_SIZE 4096
+
+#define FAIL(format, ...)                                                \
+    {                                                                    \
+        fprintf(stderr, format ":%s\n", ##__VA_ARGS__, strerror(errno)); \
+        goto fail;                                                       \
+    }
+
+// Why declare? __reboot() is the Bionic's system call stub for the reboot syscall. It is
+// automatically generated (and is part of API), but Bionic doesn't export this in its headers.
+extern int __reboot(int, int, int, void*);
+
+int main() {
+    printf("Crashdump started\n");
+
+    if (mount("proc", "/proc", "proc", 0, NULL) == -1) {
+        FAIL("Failed to mount /proc");
+    }
+
+    if (mount("devtmpfs", "/dev", "devtmpfs", 0, NULL) == -1) {
+        FAIL("Failed to mount /dev");
+    }
+
+    int vmcore = open(DUMP_SOURCE, O_RDONLY);
+    if (vmcore == -1) {
+        FAIL("Failed to open %s", DUMP_SOURCE);
+    }
+
+    int dest = open(DUMP_TARGET, O_WRONLY);
+    if (dest == -1) {
+        FAIL("Failed to open %s", DUMP_TARGET);
+    }
+
+    // We need to turn the line discipline off, otherwise the virtio-console will automatically
+    // append more data than what we have written because some will be recognized as a control
+    // sequence.
+    struct termios term;
+    if (tcgetattr(dest, &term) != 0) {
+        FAIL("Failed to get termios for %s", DUMP_TARGET);
+    }
+
+    cfmakeraw(&term); // Always successful. Returns void.
+
+    if (tcsetattr(dest, TCSAFLUSH, &term) != 0) {
+        FAIL("Failed to set terminal to the raw mode for %s", DUMP_TARGET);
+    }
+
+    struct stat statbuf;
+    if (fstat(vmcore, &statbuf) == -1) {
+        FAIL("Failed to stat %s", DUMP_SOURCE);
+    }
+    printf("Size is %ld bytes\n", statbuf.st_size);
+
+    // sendfile(2) is faster, can't be used because /proc/vmcore doesn't support splice_read
+    size_t dumped = 0;
+    char buf[BUF_SIZE];
+    int progress = 0; // percentage
+
+    // Disable buffering for better display of the progress
+    if (setvbuf(stdout, NULL, _IONBF, 0) != 0) {
+        fprintf(stderr, "Failed to disable buffering for stdout: %s\n", strerror(errno));
+        // This isn't a critical error. Continue.
+    }
+
+    while (dumped < statbuf.st_size) {
+        ssize_t read_bytes = read(vmcore, buf, BUF_SIZE);
+        if (read_bytes == -1) {
+            FAIL("Failed to read from %s", DUMP_SOURCE);
+        }
+        ssize_t written_bytes = write(dest, buf, read_bytes);
+        if (written_bytes == -1) {
+            FAIL("Failed to write to %s", DUMP_TARGET);
+        }
+        dumped += written_bytes;
+        int new_progress = dumped * 100 / statbuf.st_size;
+        if (new_progress > progress) {
+            progress = new_progress;
+            printf(".");
+        }
+    }
+    printf("done\n");
+
+    __reboot(LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART2, "kernel panic");
+    // Never reach here
+
+fail:
+    printf("Crashdump failed\n");
+    return 1;
+}
diff --git a/microdroid/kdump/kernel/Android.bp b/microdroid/kdump/kernel/Android.bp
new file mode 100644
index 0000000..0705875
--- /dev/null
+++ b/microdroid/kdump/kernel/Android.bp
@@ -0,0 +1,25 @@
+package {
+    default_applicable_licenses: ["microdroid_crashdump_kernel_license"],
+}
+
+license {
+    name: "microdroid_crashdump_kernel_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-GPL-2.0-only",
+    ],
+}
+
+prebuilt_etc {
+    name: "microdroid_crashdump_kernel",
+    compile_multilib: "64",
+    arch: {
+        arm64: {
+            src: "arm64/kernel-5.15",
+        },
+        x86_64: {
+            src: "x86_64/kernel-5.15",
+        },
+    },
+    installable: false,
+}
diff --git a/microdroid/kdump/kernel/arm64/kernel-5.15 b/microdroid/kdump/kernel/arm64/kernel-5.15
new file mode 100644
index 0000000..0f2172b
--- /dev/null
+++ b/microdroid/kdump/kernel/arm64/kernel-5.15
Binary files differ
diff --git a/microdroid/kdump/kernel/empty b/microdroid/kdump/kernel/empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/microdroid/kdump/kernel/empty
diff --git a/microdroid/kdump/kernel/x86_64/kernel-5.15 b/microdroid/kdump/kernel/x86_64/kernel-5.15
new file mode 100644
index 0000000..98088e9
--- /dev/null
+++ b/microdroid/kdump/kernel/x86_64/kernel-5.15
Binary files differ
diff --git a/microdroid/kdump/kexec.c b/microdroid/kdump/kexec.c
new file mode 100644
index 0000000..65ea0ea
--- /dev/null
+++ b/microdroid/kdump/kexec.c
@@ -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.
+ */
+
+// This program loads kernel and initrd which the system will boot into when
+// panic occurs.
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/kexec.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+static int open_checked(const char* path) {
+    int fd = open(path, O_RDONLY);
+    if (fd == -1) {
+        fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
+        exit(1);
+    }
+    return fd;
+}
+
+int main(int argc, const char* argv[]) {
+    if (argc != 4) {
+        fprintf(stderr, "Usage: %s <kernel> <initrd> <commandline>\n", argv[0]);
+        return 1;
+    }
+
+    // TODO(b/238272206): consider harding these
+    const char* kernel = argv[1];
+    const char* initrd = argv[2];
+    const char* cmdline = argv[3];
+    unsigned long cmdline_len = strlen(cmdline) + 1; // include null terminator, otherwise EINVAL
+
+    if (syscall(SYS_kexec_file_load, open_checked(kernel), open_checked(initrd), cmdline_len,
+                cmdline, KEXEC_FILE_ON_CRASH) == -1) {
+        fprintf(stderr, "Failed to load panic kernel: %s\n", strerror(errno));
+        return 1;
+    }
+    return 0;
+}
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index c90f262..4fcde74 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -38,7 +38,10 @@
     nocrt: true,
     system_shared_libs: ["libc"],
     stl: "none",
-    linker_scripts: ["image.ld"],
+    linker_scripts: [
+        "image.ld",
+        ":vmbase_sections",
+    ],
     installable: false,
     enabled: false,
     target: {
diff --git a/pvmfw/image.ld b/pvmfw/image.ld
index e122c72..aeb5046 100644
--- a/pvmfw/image.ld
+++ b/pvmfw/image.ld
@@ -20,101 +20,3 @@
 	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
 	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
 }
-
-/*
- * Code will start running at this symbol which is placed at the start of the
- * image.
- */
-ENTRY(entry)
-
-/*
- * The following would be useful to check that .init code is not called back
- * into once it has completed but it isn't supported by ld.lld.
- *
- * NOCROSSREFS_TO(.init .text)
- */
-
-SECTIONS
-{
-	.dtb (NOLOAD) : {
-		dtb_begin = .;
-		. += LENGTH(dtb_region);
-		dtb_end = .;
-	} >dtb_region
-
-	/*
-	 * Collect together the code. This is page aligned so it can be mapped
-	 * as executable-only.
-	 */
-	.init : ALIGN(4096) {
-		text_begin = .;
-		*(.init.entry)
-		*(.init.*)
-	} >image
-	.text : {
-		*(.text.*)
-	} >image
-	text_end = .;
-
-	/*
-	 * Collect together read-only data. This is page aligned so it can be
-	 * mapped as read-only and non-executable.
-	 */
-	.rodata : ALIGN(4096) {
-		rodata_begin = .;
-		*(.rodata.*)
-	} >image
-	.got : {
-		*(.got)
-	} >image
-	rodata_end = .;
-
-	/*
-	 * Collect together the read-write data including .bss at the end which
-	 * will be zero'd by the entry code. This is page aligned so it can be
-	 * mapped as non-executable.
-	 */
-	.data : ALIGN(4096) {
-		data_begin = .;
-		*(.data.*)
-		/*
-		 * The entry point code assumes that .data is a multiple of 32
-		 * bytes long.
-		 */
-		. = ALIGN(32);
-		data_end = .;
-	} >writable_data AT>image
-	data_lma = LOADADDR(.data);
-
-	/* Everything beyond this point will not be included in the binary. */
-	bin_end = .;
-
-	/* The entry point code assumes that .bss is 16-byte aligned. */
-	.bss : ALIGN(16)  {
-		bss_begin = .;
-		*(.bss.*)
-		*(COMMON)
-		. = ALIGN(16);
-		bss_end = .;
-	} >writable_data
-
-	.stack (NOLOAD) : ALIGN(4096) {
-		boot_stack_begin = .;
-		. += 40 * 4096;
-		. = ALIGN(4096);
-		boot_stack_end = .;
-	} >writable_data
-
-	/*
-	 * Remove unused sections from the image.
-	 */
-	/DISCARD/ : {
-		/* The image loads itself so doesn't need these sections. */
-		*(.gnu.hash)
-		*(.hash)
-		*(.interp)
-		*(.eh_frame_hdr)
-		*(.eh_frame)
-		*(.note.gnu.build-id)
-	}
-}
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 5999af7..864d2d5 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -201,6 +201,9 @@
         public void onDied(VirtualMachine vm, @DeathReason int reason) {
             mExecutorService.shutdown();
         }
+
+        @Override
+        public void onRamdump(VirtualMachine vm, ParcelFileDescriptor ramdump) {}
     }
 
     private static class BootResult {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 818c05a..b3b0808 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -12,6 +12,7 @@
     static_libs: [
         "androidx.test.runner",
         "androidx.test.ext.junit",
+        "authfs_test_apk_assets",
         "cbor-java",
         "com.android.microdroid.testservice-java",
         "truth-prebuilt",
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 1f0e107..3a874c4 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -204,6 +204,9 @@
         public void onDied(VirtualMachine vm, @DeathReason int reason) {
             mExecutorService.shutdown();
         }
+
+        @Override
+        public void onRamdump(VirtualMachine vm, ParcelFileDescriptor ramdump) {}
     }
 
     private static final int MIN_MEM_ARM64 = 150;
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 12a056c..6c8eb4a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -53,4 +53,9 @@
      * also use `link_to_death` to handle that.
      */
     void onDied(int cid, in DeathReason reason);
+
+    /**
+     * Called when kernel panic occurs and as a result ramdump is generated from the VM.
+     */
+    void onRamdump(int cid, in ParcelFileDescriptor ramdump);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 745110b..4135253 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -464,6 +464,19 @@
             })
             .collect::<Result<Vec<DiskFile>, _>>()?;
 
+        // Creating this ramdump file unconditionally is not harmful as ramdump will be created
+        // only when the VM is configured as such. `ramdump_write` is sent to crosvm and will
+        // be the backing store for the /dev/hvc1 where VM will emit ramdump to. `ramdump_read`
+        // will be sent back to the client (i.e. the VM owner) for readout.
+        let ramdump_path = temporary_directory.join("ramdump");
+        let ramdump = prepare_ramdump_file(&ramdump_path).map_err(|e| {
+            error!("Failed to prepare ramdump file: {}", e);
+            new_binder_exception(
+                ExceptionCode::SERVICE_SPECIFIC,
+                format!("Failed to prepare ramdump file: {}", e),
+            )
+        })?;
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -479,6 +492,7 @@
             task_profiles: config.taskProfiles.clone(),
             console_fd,
             log_fd,
+            ramdump: Some(ramdump),
             indirect_files,
             platform_version: parse_platform_version_req(&config.platformVersion)?,
             detect_hangup: is_app_config,
@@ -558,6 +572,11 @@
     part.flush()
 }
 
+fn prepare_ramdump_file(ramdump_path: &Path) -> Result<File> {
+    File::create(&ramdump_path)
+        .context(format!("Failed to create ramdump file {:?}", &ramdump_path))
+}
+
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
@@ -883,6 +902,17 @@
         }
     }
 
+    /// Call all registered callbacks to say that there was a ramdump to download.
+    pub fn callback_on_ramdump(&self, cid: Cid, ramdump: File) {
+        let callbacks = &*self.0.lock().unwrap();
+        let pfd = ParcelFileDescriptor::new(ramdump);
+        for callback in callbacks {
+            if let Err(e) = callback.onRamdump(cid as i32, &pfd) {
+                error!("Error notifying ramdump of VM CID {}: {}", cid, e);
+            }
+        }
+    }
+
     /// Add a new callback to the set.
     fn add(&self, callback: Strong<dyn IVirtualMachineCallback>) {
         self.0.lock().unwrap().push(callback);
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 23719a7..46ad6b3 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -16,7 +16,7 @@
 
 use crate::aidl::VirtualMachineCallbacks;
 use crate::Cid;
-use anyhow::{bail, Error};
+use anyhow::{bail, Context, Error};
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
 use log::{debug, error, info};
@@ -81,6 +81,7 @@
     pub task_profiles: Vec<String>,
     pub console_fd: Option<File>,
     pub log_fd: Option<File>,
+    pub ramdump: Option<File>,
     pub indirect_files: Vec<File>,
     pub platform_version: VersionReq,
     pub detect_hangup: bool,
@@ -272,6 +273,7 @@
             Cow::from(s)
         };
 
+        self.handle_ramdump().unwrap_or_else(|e| error!("Error handling ramdump: {}", e));
         self.callbacks.callback_on_died(self.cid, death_reason(&result, &failure_string));
 
         // Delete temporary files.
@@ -313,6 +315,18 @@
             }
         }
     }
+
+    /// 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> {
+        let ramdump_path = self.temporary_directory.join("ramdump");
+        if std::fs::metadata(&ramdump_path)?.len() > 0 {
+            let ramdump = File::open(&ramdump_path)
+                .context(format!("Failed to open ramdump {:?} for reading", &ramdump_path))?;
+            self.callbacks.callback_on_ramdump(self.cid, ramdump);
+        }
+        Ok(())
+    }
 }
 
 fn death_reason(result: &Result<ExitStatus, io::Error>, failure_reason: &str) -> DeathReason {
@@ -404,7 +418,7 @@
     // 1. uart device: used as the output device by bootloaders and as early console by linux
     // 2. uart device: used to report the reason for the VM failing.
     // 3. virtio-console device: used as the console device where kmsg is redirected to
-    // 4. virtio-console device: used as the androidboot.console device (not used currently)
+    // 4. virtio-console device: used as the ramdump output
     // 5. virtio-console device: used as the logcat output
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
@@ -412,6 +426,7 @@
     let console_arg = format_serial_arg(&mut preserved_fds, &config.console_fd);
     let log_arg = format_serial_arg(&mut preserved_fds, &config.log_fd);
     let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
+    let ramdump_arg = format_serial_arg(&mut preserved_fds, &config.ramdump);
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
@@ -423,8 +438,8 @@
     command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
     command.arg(format!("--serial={},hardware=virtio-console,num=1", &console_arg));
-    // /dev/hvc1 (not used currently)
-    command.arg("--serial=type=sink,hardware=virtio-console,num=2");
+    // /dev/hvc1
+    command.arg(format!("--serial={},hardware=virtio-console,num=2", &ramdump_arg));
     // /dev/hvc2
     command.arg(format!("--serial={},hardware=virtio-console,num=3", &log_arg));
 
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 8450b41..60786ac 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -67,6 +67,10 @@
         #[structopt(long)]
         log: Option<PathBuf>,
 
+        /// Path to file where ramdump is recorded on kernel panic
+        #[structopt(long)]
+        ramdump: Option<PathBuf>,
+
         /// Debug level of the VM. Supported values: "none" (default), "app_only", and "full".
         #[structopt(long, default_value = "none", parse(try_from_str=parse_debug_level))]
         debug: DebugLevel,
@@ -198,6 +202,7 @@
             daemonize,
             console,
             log,
+            ramdump,
             debug,
             protected,
             mem,
@@ -214,6 +219,7 @@
             daemonize,
             console.as_deref(),
             log.as_deref(),
+            ramdump.as_deref(),
             debug,
             protected,
             mem,
diff --git a/vm/src/run.rs b/vm/src/run.rs
index ca71665..44eb27a 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -49,6 +49,7 @@
     daemonize: bool,
     console_path: Option<&Path>,
     log_path: Option<&Path>,
+    ramdump_path: Option<&Path>,
     debug_level: DebugLevel,
     protected: bool,
     mem: Option<u32>,
@@ -115,6 +116,7 @@
         daemonize,
         console_path,
         log_path,
+        ramdump_path,
     )
 }
 
@@ -149,6 +151,7 @@
         daemonize,
         console_path,
         log_path,
+        /* ramdump_path */ None,
     )
 }
 
@@ -171,6 +174,7 @@
     daemonize: bool,
     console_path: Option<&Path>,
     log_path: Option<&Path>,
+    ramdump_path: Option<&Path>,
 ) -> Result<(), Error> {
     let console = if let Some(console_path) = console_path {
         Some(
@@ -214,12 +218,27 @@
         // Wait until the VM or VirtualizationService dies. If we just returned immediately then the
         // IVirtualMachine Binder object would be dropped and the VM would be killed.
         let death_reason = vm.wait_for_death();
+
+        if let Some(path) = ramdump_path {
+            save_ramdump_if_available(path, &vm)?;
+        }
         println!("{}", death_reason);
     }
 
     Ok(())
 }
 
+fn save_ramdump_if_available(path: &Path, vm: &VmInstance) -> Result<(), Error> {
+    if let Some(mut ramdump) = vm.get_ramdump() {
+        let mut file =
+            File::create(path).context(format!("Failed to create ramdump file {:?}", path))?;
+        let size = std::io::copy(&mut ramdump, &mut file)
+            .context(format!("Failed to save ramdump to file {:?}", path))?;
+        eprintln!("Ramdump ({} bytes) saved to {:?}", size, path);
+    }
+    Ok(())
+}
+
 fn parse_extra_apk_list(apk: &Path, config_path: &str) -> Result<Vec<String>, Error> {
     let mut archive = ZipArchive::new(File::open(apk)?)?;
     let config_file = archive.by_name(config_path)?;
@@ -268,6 +287,11 @@
         Ok(())
     }
 
+    fn onRamdump(&self, _cid: i32, _stream: &ParcelFileDescriptor) -> BinderResult<()> {
+        // Do nothing. We get ramdump from the vmclient library.
+        Ok(())
+    }
+
     fn onDied(&self, _cid: i32, _reason: DeathReason) -> BinderResult<()> {
         Ok(())
     }
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index 473103e..e88420e 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -39,3 +39,8 @@
     },
     apex_available: ["com.android.virt"],
 }
+
+filegroup {
+    name: "vmbase_sections",
+    srcs: ["sections.ld"],
+}
diff --git a/vmbase/README.md b/vmbase/README.md
index 8e804c0..3554ae6 100644
--- a/vmbase/README.md
+++ b/vmbase/README.md
@@ -152,7 +152,10 @@
     nocrt: true,
     system_shared_libs: ["libc"],
     stl: "none",
-    linker_scripts: ["image.ld"],
+    linker_scripts: [
+        "image.ld",
+        ":vmbase_sections",
+    ],
     installable: false,
     enabled: false,
     target: {
@@ -165,8 +168,9 @@
 
 This takes your Rust library (`libvmbase_example`), the vmbase library entry point and exception
 vector (`libvmbase_entry`) and your initial idmap (`idmap.S`) and builds a static binary with your
-linker script (`image.ld`). This is an ELF binary, but to run it as a VM bootloader you need to
-`objcopy` it to a raw binary image instead, which you can do with a `raw_binary` rule:
+linker script (`image.ld`) and the one provided by vmbase ([`sections.ld`](sections.ld)). This is an
+ELF binary, but to run it as a VM bootloader you need to `objcopy` it to a raw binary image instead,
+which you can do with a `raw_binary` rule:
 
 ```soong
 raw_binary {
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index d540f4d..c75820c 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -40,7 +40,10 @@
     nocrt: true,
     system_shared_libs: ["libc"],
     stl: "none",
-    linker_scripts: ["image.ld"],
+    linker_scripts: [
+        "image.ld",
+        ":vmbase_sections",
+    ],
     installable: false,
     enabled: false,
     target: {
diff --git a/vmbase/example/image.ld b/vmbase/example/image.ld
index 4655f68..368acbb 100644
--- a/vmbase/example/image.ld
+++ b/vmbase/example/image.ld
@@ -20,101 +20,3 @@
 	image		: ORIGIN = 0x80200000, LENGTH = 2M
 	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
 }
-
-/*
- * Code will start running at this symbol which is placed at the start of the
- * image.
- */
-ENTRY(entry)
-
-/*
- * The following would be useful to check that .init code is not called back
- * into once it has completed but it isn't supported by ld.lld.
- *
- * NOCROSSREFS_TO(.init .text)
- */
-
-SECTIONS
-{
-	.dtb (NOLOAD) : {
-		dtb_begin = .;
-		. += LENGTH(dtb_region);
-		dtb_end = .;
-	} >dtb_region
-
-	/*
-	 * Collect together the code. This is page aligned so it can be mapped
-	 * as executable-only.
-	 */
-	.init : ALIGN(4096) {
-		text_begin = .;
-		*(.init.entry)
-		*(.init.*)
-	} >image
-	.text : {
-		*(.text.*)
-	} >image
-	text_end = .;
-
-	/*
-	 * Collect together read-only data. This is page aligned so it can be
-	 * mapped as read-only and non-executable.
-	 */
-	.rodata : ALIGN(4096) {
-		rodata_begin = .;
-		*(.rodata.*)
-	} >image
-	.got : {
-		*(.got)
-	} >image
-	rodata_end = .;
-
-	/*
-	 * Collect together the read-write data including .bss at the end which
-	 * will be zero'd by the entry code. This is page aligned so it can be
-	 * mapped as non-executable.
-	 */
-	.data : ALIGN(4096) {
-		data_begin = .;
-		*(.data.*)
-		/*
-		 * The entry point code assumes that .data is a multiple of 32
-		 * bytes long.
-		 */
-		. = ALIGN(32);
-		data_end = .;
-	} >writable_data AT>image
-	data_lma = LOADADDR(.data);
-
-	/* Everything beyond this point will not be included in the binary. */
-	bin_end = .;
-
-	/* The entry point code assumes that .bss is 16-byte aligned. */
-	.bss : ALIGN(16)  {
-		bss_begin = .;
-		*(.bss.*)
-		*(COMMON)
-		. = ALIGN(16);
-		bss_end = .;
-	} >writable_data
-
-	.stack (NOLOAD) : ALIGN(4096) {
-		boot_stack_begin = .;
-		. += 40 * 4096;
-		. = ALIGN(4096);
-		boot_stack_end = .;
-	} >writable_data
-
-	/*
-	 * Remove unused sections from the image.
-	 */
-	/DISCARD/ : {
-		/* The image loads itself so doesn't need these sections. */
-		*(.gnu.hash)
-		*(.hash)
-		*(.interp)
-		*(.eh_frame_hdr)
-		*(.eh_frame)
-		*(.note.gnu.build-id)
-	}
-}
diff --git a/vmbase/sections.ld b/vmbase/sections.ld
new file mode 100644
index 0000000..88033e3
--- /dev/null
+++ b/vmbase/sections.ld
@@ -0,0 +1,113 @@
+/*
+ * 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
+ *
+ *     https://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.
+ */
+
+/*
+ * Code will start running at this symbol which is placed at the start of the
+ * image.
+ */
+ENTRY(entry)
+
+/*
+ * The following would be useful to check that .init code is not called back
+ * into once it has completed but it isn't supported by ld.lld.
+ *
+ * NOCROSSREFS_TO(.init .text)
+ */
+
+SECTIONS
+{
+	.dtb (NOLOAD) : {
+		dtb_begin = .;
+		. += LENGTH(dtb_region);
+		dtb_end = .;
+	} >dtb_region
+
+	/*
+	 * Collect together the code. This is page aligned so it can be mapped
+	 * as executable-only.
+	 */
+	.init : ALIGN(4096) {
+		text_begin = .;
+		*(.init.entry)
+		*(.init.*)
+	} >image
+	.text : {
+		*(.text.*)
+	} >image
+	text_end = .;
+
+	/*
+	 * Collect together read-only data. This is page aligned so it can be
+	 * mapped as read-only and non-executable.
+	 */
+	.rodata : ALIGN(4096) {
+		rodata_begin = .;
+		*(.rodata.*)
+	} >image
+	.got : {
+		*(.got)
+	} >image
+	rodata_end = .;
+
+	/*
+	 * Collect together the read-write data including .bss at the end which
+	 * will be zero'd by the entry code. This is page aligned so it can be
+	 * mapped as non-executable.
+	 */
+	.data : ALIGN(4096) {
+		data_begin = .;
+		*(.data.*)
+		/*
+		 * The entry point code assumes that .data is a multiple of 32
+		 * bytes long.
+		 */
+		. = ALIGN(32);
+		data_end = .;
+	} >writable_data AT>image
+	data_lma = LOADADDR(.data);
+
+	/* Everything beyond this point will not be included in the binary. */
+	bin_end = .;
+
+	/* The entry point code assumes that .bss is 16-byte aligned. */
+	.bss : ALIGN(16)  {
+		bss_begin = .;
+		*(.bss.*)
+		*(COMMON)
+		. = ALIGN(16);
+		bss_end = .;
+	} >writable_data
+
+	.stack (NOLOAD) : ALIGN(4096) {
+		boot_stack_begin = .;
+		. += 40 * 4096;
+		. = ALIGN(4096);
+		boot_stack_end = .;
+	} >writable_data
+
+	/*
+	 * Remove unused sections from the image.
+	 */
+	/DISCARD/ : {
+		/* The image loads itself so doesn't need these sections. */
+		*(.gnu.hash)
+		*(.hash)
+		*(.interp)
+		*(.eh_frame_hdr)
+		*(.eh_frame)
+		*(.note.gnu.build-id)
+	}
+}
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index d182b60..867c3a7 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -143,6 +143,11 @@
 
         FromIBinder::try_from(ibinder).map_err(GetServiceError::WrongServiceType)
     }
+
+    /// Get ramdump
+    pub fn get_ramdump(&self) -> Option<File> {
+        self.state.get_ramdump()
+    }
 }
 
 impl Debug for VmInstance {
@@ -170,6 +175,7 @@
 struct VmState {
     death_reason: Option<DeathReason>,
     reported_state: VirtualMachineState,
+    ramdump: Option<File>,
 }
 
 impl Monitor<VmState> {
@@ -186,6 +192,14 @@
         self.state.lock().unwrap().reported_state = state;
         self.cv.notify_all();
     }
+
+    fn set_ramdump(&self, ramdump: File) {
+        self.state.lock().unwrap().ramdump = Some(ramdump);
+    }
+
+    fn get_ramdump(&self) -> Option<File> {
+        self.state.lock().unwrap().ramdump.as_ref().and_then(|f| f.try_clone().ok())
+    }
 }
 
 #[derive(Debug)]
@@ -220,6 +234,12 @@
         Ok(())
     }
 
+    fn onRamdump(&self, _cid: i32, ramdump: &ParcelFileDescriptor) -> BinderResult<()> {
+        let ramdump: File = ramdump.as_ref().try_clone().unwrap();
+        self.state.set_ramdump(ramdump);
+        Ok(())
+    }
+
     fn onDied(&self, _cid: i32, reason: AidlDeathReason) -> BinderResult<()> {
         self.state.notify_death(reason.into());
         Ok(())