Merge "pvmfw: Make use of Rust 1.65 let-else"
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 59fdd9f..92c9a3c 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -139,6 +139,7 @@
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             cpuTopology: cpu_topology,
             taskProfiles: parameters.task_profiles.clone(),
+            gdbPort: 0, // Don't start gdb-server
         });
 
         let callback = Box::new(Callback {});
diff --git a/docs/debug/gdb.md b/docs/debug/gdb.md
new file mode 100644
index 0000000..316faad
--- /dev/null
+++ b/docs/debug/gdb.md
@@ -0,0 +1,46 @@
+# Debugging guest kernels with gdb
+
+Note: this feature is only available on android14-5.15 and newer host kernels.
+
+Starting with Android U it is possible to attach a gdb to the guest kernel, when
+starting a debuggable and non-protected guest VM.
+
+You can do this by passing `--gdb <port>` argument to the `vm run`, `vm run-app`
+and `vm run-microdroid` commands. The `crosvm` will start the gdb server on the
+provided port. It will wait for the gdb client to connect to it before
+proceeding with the VM boot.
+
+Here is an example invocation:
+
+```shell
+adb forward tcp:3456 tcp:3456
+adb shell /apex/com.android.virt/bin/vm run-microdroid --gdb 3456
+```
+
+Then in another shell:
+
+```shell
+gdb vmlinux
+(gdb) target remote :3456
+(gdb) hbreak start_kernel
+(gdb) c
+```
+
+The [kernel documentation](
+https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html) has
+some general techniques on how to debug kernel with gdb.
+
+## Obtaining vmlinux for Microdroid kernels
+
+If you are debugging Microdroid kernel that you have built [locally](
+../../microdroid/kernel/README.md), then look for `out/dist/vmlinux` in your
+kernel repository.
+
+If you are debugging Microdroid kernel bundled with the `com.android.virt` APEX,
+then you need to obtain the build ID of this kernel. You can do this by
+checking the prebuilt-info.txt file in the
+`packages/modules/Virtualization/microdroid/kernel/arm64` or
+`packages/modules/Virtualization/microdroid/kernel/x86_64` directories.
+
+Using that build ID you can download the vmlinux from the build server via:
+https://ci.android.com/builds/submitted/${BUILD_ID}/kernel_microdroid_aarch64/latest/vmlinux
diff --git a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
index 9281e73..b3354cc 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -58,9 +58,10 @@
 
     RpcSessionHandle session;
     // We need a thread pool to be able to support linkToDeath, or callbacks
-    // (b/268335700). This if a fairly arbitrary number, although it happens to
-    // match the default max outgoing threads.
-    ARpcSession_setMaxIncomingThreads(session.get(), 10);
+    // (b/268335700). These threads are currently created eagerly, so we don't
+    // want too many. The number 1 is chosen after some discussion, and to match
+    // the server-side default (mMaxThreads on RpcServer).
+    ARpcSession_setMaxIncomingThreads(session.get(), 1);
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 29d7abe..927bf50 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -614,7 +614,8 @@
         fdt_err_expect_zero(ret)
     }
 
-    fn as_ptr(&self) -> *const c_void {
+    /// Return a shared pointer to the device tree.
+    pub fn as_ptr(&self) -> *const c_void {
         self as *const _ as *const c_void
     }
 
diff --git a/microdroid_manager/src/dice.rs b/microdroid_manager/src/dice.rs
index fd22198..c3136e8 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -17,8 +17,8 @@
 use anyhow::{anyhow, bail, Context, Error, Result};
 use byteorder::{NativeEndian, ReadBytesExt};
 use diced_open_dice::{
-    bcc_handover_parse, retry_bcc_main_flow, BccHandover, Cdi, Config, DiceMode, Hash, Hidden,
-    InputValues, OwnedDiceArtifacts,
+    bcc_handover_parse, retry_bcc_main_flow, BccHandover, Config, DiceArtifacts, DiceMode, Hash,
+    Hidden, InputValues, OwnedDiceArtifacts,
 };
 use keystore2_crypto::ZVec;
 use libc::{c_void, mmap, munmap, MAP_FAILED, MAP_PRIVATE, PROT_READ};
@@ -32,14 +32,12 @@
 
 /// Derives a sealing key from the DICE sealing CDI.
 pub fn derive_sealing_key(
-    cdi_seal: &Cdi,
+    dice_artifacts: &dyn DiceArtifacts,
     salt: &[u8],
     info: &[u8],
-    keysize: usize,
-) -> Result<ZVec> {
-    let mut key = ZVec::new(keysize)?;
-    hkdf(&mut key, Md::sha256(), cdi_seal, salt, info)?;
-    Ok(key)
+    key: &mut [u8],
+) -> Result<()> {
+    Ok(hkdf(key, Md::sha256(), dice_artifacts.cdi_seal(), salt, info)?)
 }
 
 /// Artifacts that are mapped into the process address space from the driver.
@@ -54,6 +52,13 @@
 }
 
 impl DiceDriver<'_> {
+    fn dice_artifacts(&self) -> &dyn DiceArtifacts {
+        match self {
+            Self::Real { bcc_handover, .. } => bcc_handover,
+            Self::Fake(owned_dice_artifacts) => owned_dice_artifacts,
+        }
+    }
+
     pub fn new(driver_path: &Path) -> Result<Self> {
         if driver_path.exists() {
             log::info!("Using DICE values from driver");
@@ -95,16 +100,15 @@
         })
     }
 
-    pub fn get_sealing_key(&self, identifier: &[u8]) -> Result<ZVec> {
+    /// Derives a sealing key of `key_length` bytes from the DICE sealing CDI.
+    pub fn get_sealing_key(&self, identifier: &[u8], key_length: usize) -> Result<ZVec> {
         // Deterministically derive a key to use for sealing data, rather than using the CDI
         // directly, so we have the chance to rotate the key if needed. A salt isn't needed as the
         // input key material is already cryptographically strong.
-        let cdi_seal = match self {
-            Self::Real { bcc_handover, .. } => bcc_handover.cdi_seal,
-            Self::Fake(fake) => &fake.cdi_values.cdi_seal,
-        };
+        let mut key = ZVec::new(key_length)?;
         let salt = &[];
-        derive_sealing_key(cdi_seal, salt, identifier, 32)
+        derive_sealing_key(self.dice_artifacts(), salt, identifier, &mut key)?;
+        Ok(key)
     }
 
     pub fn derive(
@@ -122,25 +126,21 @@
             if debug { DiceMode::kDiceModeDebug } else { DiceMode::kDiceModeNormal },
             hidden,
         );
-        let (cdi_attest, cdi_seal, bcc) = match &self {
-            Self::Real { bcc_handover, .. } => (
-                bcc_handover.cdi_attest,
-                bcc_handover.cdi_seal,
-                bcc_handover.bcc.ok_or_else(|| anyhow!("bcc is none"))?,
-            ),
-            Self::Fake(fake) => {
-                (&fake.cdi_values.cdi_attest, &fake.cdi_values.cdi_seal, fake.bcc.as_slice())
-            }
-        };
-        let dice_artifacts = retry_bcc_main_flow(cdi_attest, cdi_seal, bcc, &input_values)
-            .context("DICE derive from driver")?;
+        let current_dice_artifacts = self.dice_artifacts();
+        let next_dice_artifacts = retry_bcc_main_flow(
+            current_dice_artifacts.cdi_attest(),
+            current_dice_artifacts.cdi_seal(),
+            current_dice_artifacts.bcc().ok_or_else(|| anyhow!("bcc is none"))?,
+            &input_values,
+        )
+        .context("DICE derive from driver")?;
         if let Self::Real { driver_path, .. } = &self {
             // Writing to the device wipes the artifacts. The string is ignored by the driver but
             // included for documentation.
             fs::write(driver_path, "wipe")
                 .map_err(|err| Error::new(err).context("Wiping driver"))?;
         }
-        Ok(dice_artifacts)
+        Ok(next_dice_artifacts)
     }
 }
 
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 96e9360..6900ea5 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -142,9 +142,9 @@
         self.file.read_exact(&mut header)?;
 
         // Decrypt and authenticate the data (along with the header).
-        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
-        let plaintext =
-            decrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &tag)?;
+        let cipher = Cipher::aes_256_gcm();
+        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
+        let plaintext = decrypt_aead(cipher, &key, Some(&nonce), &header, &data, &tag)?;
 
         let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
         Ok(Some(microdroid_data))
@@ -188,10 +188,10 @@
         self.file.write_all(nonce.as_ref())?;
 
         // Then encrypt and sign the data.
-        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
+        let cipher = Cipher::aes_256_gcm();
+        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
         let mut tag = [0; AES_256_GCM_TAG_LENGTH];
-        let ciphertext =
-            encrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &mut tag)?;
+        let ciphertext = encrypt_aead(cipher, &key, Some(&nonce), &header, &data, &mut tag)?;
 
         // Persist the encrypted payload data and the tag.
         self.file.write_all(&ciphertext)?;
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 7ca0d3c..1148c31 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -40,6 +40,7 @@
 use itertools::sorted;
 use libc::VMADDR_CID_HOST;
 use log::{error, info, warn};
+use keystore2_crypto::ZVec;
 use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::fcntl::{fcntl, F_SETFD, FdFlag};
@@ -917,12 +918,8 @@
         0x6F, 0xB3, 0xF9, 0x40, 0xCE, 0xDD, 0x99, 0x40, 0xAA, 0xA7, 0x0E, 0x92, 0x73, 0x90, 0x86,
         0x4A, 0x75,
     ];
-    let key = derive_sealing_key(
-        &dice_artifacts.cdi_values.cdi_seal,
-        &salt,
-        ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(),
-        ENCRYPTEDSTORE_KEYSIZE,
-    )?;
+    let mut key = ZVec::new(ENCRYPTEDSTORE_KEYSIZE)?;
+    derive_sealing_key(dice_artifacts, &salt, ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(), &mut key)?;
 
     let mut cmd = Command::new(ENCRYPTEDSTORE_BIN);
     cmd.arg("--blkdevice")
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index ac8f60a..96f51f0 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -14,15 +14,14 @@
 
 //! Implementation of the AIDL interface `IVmPayloadService`.
 
+use crate::dice::derive_sealing_key;
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use anyhow::Result;
 use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong};
-use diced_open_dice::OwnedDiceArtifacts;
+use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
 use log::{error, info};
-use openssl::hkdf::hkdf;
-use openssl::md::Md;
 use rpcbinder::RpcServer;
 
 /// Implementation of `IVmPayloadService`.
@@ -48,22 +47,25 @@
             0xB7, 0xA8, 0x43, 0x92,
         ];
         let mut secret = vec![0; size.try_into().unwrap()];
-        hkdf(&mut secret, Md::sha256(), &self.dice.cdi_values.cdi_seal, &salt, identifier)
-            .map_err(|e| {
-                error!("Failed to derive VM instance secret: {:?}", e);
-                Status::new_service_specific_error(-1, None)
-            })?;
+        derive_sealing_key(&self.dice, &salt, identifier, &mut secret).map_err(|e| {
+            error!("Failed to derive VM instance secret: {:?}", e);
+            Status::new_service_specific_error(-1, None)
+        })?;
         Ok(secret)
     }
 
     fn getDiceAttestationChain(&self) -> binder::Result<Vec<u8>> {
         self.check_restricted_apis_allowed()?;
-        Ok(self.dice.bcc.clone())
+        if let Some(bcc) = self.dice.bcc() {
+            Ok(bcc.to_vec())
+        } else {
+            Err(Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some("bcc is none")))
+        }
     }
 
     fn getDiceAttestationCdi(&self) -> binder::Result<Vec<u8>> {
         self.check_restricted_apis_allowed()?;
-        Ok(self.dice.cdi_values.cdi_attest.to_vec())
+        Ok(self.dice.cdi_attest().to_vec())
     }
 }
 
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index ba26114..2e56597 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -63,7 +63,7 @@
     memory: &mut MemoryTracker,
 ) -> Result<(), RebootReason> {
     info!("pVM firmware");
-    debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
+    debug!("FDT: {:?}", fdt.as_ptr());
     debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
     debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
     if let Some(rd) = ramdisk {
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
index ec2a687..86fcd00 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -267,7 +267,7 @@
         for region in &self.regions {
             match region.mem_type {
                 MemoryType::ReadWrite => {
-                    // TODO: Use page table's dirty bit to only flush pages that were touched.
+                    // TODO(b/269738062): Use PT's dirty bit to only flush pages that were touched.
                     helpers::flush_region(region.range.start, region.range.len())
                 }
                 MemoryType::ReadOnly => {}
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 04a87f3..be5f118 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -68,6 +68,7 @@
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
+        gdbPort: 0, // No gdb
     });
     let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
         .context("Failed to create VM")?;
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 89f74d6..aceb319 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -63,7 +63,7 @@
 use std::ffi::CStr;
 use std::fs::{read_dir, remove_file, File, OpenOptions};
 use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
@@ -299,14 +299,24 @@
                 // Some features are reserved for platform apps only, even when using
                 // VirtualMachineAppConfig:
                 // - controlling CPUs;
-                // - specifying a config file in the APK.
-                !config.taskProfiles.is_empty() || matches!(config.payload, Payload::ConfigPath(_))
+                // - specifying a config file in the APK;
+                // - gdbPort is set, meaning that crosvm will start a gdb server.
+                !config.taskProfiles.is_empty()
+                    || matches!(config.payload, Payload::ConfigPath(_))
+                    || config.gdbPort > 0
             }
         };
         if is_custom {
             check_use_custom_virtual_machine()?;
         }
 
+        let gdb_port = extract_gdb_port(config);
+
+        // Additional permission checks if caller request gdb.
+        if gdb_port.is_some() {
+            check_gdb_allowed(config)?;
+        }
+
         let state = &mut *self.state.lock().unwrap();
         let console_fd =
             clone_or_prepare_logger_fd(config, console_fd, format!("Console({})", cid))?;
@@ -430,6 +440,7 @@
             indirect_files,
             platform_version: parse_platform_version_req(&config.platformVersion)?,
             detect_hangup: is_app_config,
+            gdb_port,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -583,6 +594,7 @@
     vm_config.protectedVm = config.protectedVm;
     vm_config.cpuTopology = config.cpuTopology;
     vm_config.taskProfiles = config.taskProfiles.clone();
+    vm_config.gdbPort = config.gdbPort;
 
     // Microdroid takes additional init ramdisk & (optionally) storage image
     add_microdroid_system_images(config, instance_file, storage_image, &mut vm_config)?;
@@ -973,6 +985,43 @@
     }
 }
 
+fn is_protected(config: &VirtualMachineConfig) -> bool {
+    match config {
+        VirtualMachineConfig::RawConfig(config) => config.protectedVm,
+        VirtualMachineConfig::AppConfig(config) => config.protectedVm,
+    }
+}
+
+fn check_gdb_allowed(config: &VirtualMachineConfig) -> binder::Result<()> {
+    if is_protected(config) {
+        return Err(Status::new_exception_str(
+            ExceptionCode::SECURITY,
+            Some("can't use gdb with protected VMs"),
+        ));
+    }
+
+    match config {
+        VirtualMachineConfig::RawConfig(_) => Ok(()),
+        VirtualMachineConfig::AppConfig(config) => {
+            if config.debugLevel != DebugLevel::FULL {
+                Err(Status::new_exception_str(
+                    ExceptionCode::SECURITY,
+                    Some("can't use gdb with non-debuggable VMs"),
+                ))
+            } else {
+                Ok(())
+            }
+        }
+    }
+}
+
+fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
+    match config {
+        VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
+        VirtualMachineConfig::AppConfig(config) => NonZeroU16::new(config.gdbPort as u16),
+    }
+}
+
 fn clone_or_prepare_logger_fd(
     config: &VirtualMachineConfig,
     fd: Option<&ParcelFileDescriptor>,
diff --git a/virtualizationmanager/src/atom.rs b/virtualizationmanager/src/atom.rs
index 5c1f394..567fce9 100644
--- a/virtualizationmanager/src/atom.rs
+++ b/virtualizationmanager/src/atom.rs
@@ -82,7 +82,7 @@
 // Returns the number of CPUs configured in the host system.
 // This matches how crosvm determines the number of logical cores.
 // For telemetry purposes only.
-fn get_num_cpus() -> Option<usize> {
+pub(crate) fn get_num_cpus() -> Option<usize> {
     // SAFETY - Only integer constants passed back and forth.
     let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) };
     if ret > 0 {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 66efba9..09605a4 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -15,7 +15,7 @@
 //! Functions for running instances of `crosvm`.
 
 use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
-use crate::atom::write_vm_exited_stats;
+use crate::atom::{get_num_cpus, write_vm_exited_stats};
 use anyhow::{anyhow, bail, Context, Error, Result};
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
@@ -32,7 +32,7 @@
 use std::fs::{read_to_string, File};
 use std::io::{self, Read};
 use std::mem;
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{AsRawFd, RawFd, FromRawFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
@@ -105,6 +105,7 @@
     pub indirect_files: Vec<File>,
     pub platform_version: VersionReq,
     pub detect_hangup: bool,
+    pub gdb_port: Option<NonZeroU16>,
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -734,13 +735,22 @@
     }
 
     if config.host_cpu_topology {
-        command.arg("--host-cpu-topology");
+        // TODO(b/266664564): replace with --host-cpu-topology once available
+        if let Some(cpus) = get_num_cpus() {
+            command.arg("--cpus").arg(cpus.to_string());
+        } else {
+            bail!("Could not determine the number of CPUs in the system");
+        }
     }
 
     if !config.task_profiles.is_empty() {
         command.arg("--task-profiles").arg(config.task_profiles.join(","));
     }
 
+    if let Some(gdb_port) = config.gdb_port {
+        command.arg("--gdb").arg(gdb_port.to_string());
+    }
+
     // Keep track of what file descriptors should be mapped to the crosvm process.
     let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 7f90ed6..c467c2f 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -70,6 +70,12 @@
     /** Debug level of the VM */
     DebugLevel debugLevel = DebugLevel.NONE;
 
+    /**
+     * Port at which crosvm will start a gdb server to debug guest kernel.
+     * If set to zero, then gdb server won't be started.
+     */
+    int gdbPort = 0;
+
     /** Whether the VM should be a protected VM. */
     boolean protectedVm;
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 1cda163..87d4ba2 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -63,4 +63,10 @@
      * List of task profile names to apply for the VM
      */
     String[] taskProfiles;
+
+    /**
+     * Port at which crosvm will start a gdb server to debug guest kernel.
+     * If set to zero, then gdb server won't be started.
+     */
+    int gdbPort = 0;
 }
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 6c08a19..1d9f50b 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -28,6 +28,7 @@
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
+use std::num::NonZeroU16;
 use std::path::{Path, PathBuf};
 
 #[derive(Debug)]
@@ -101,6 +102,11 @@
         /// Paths to extra idsig files.
         #[clap(long = "extra-idsig")]
         extra_idsigs: Vec<PathBuf>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
@@ -152,6 +158,11 @@
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
         task_profiles: Vec<String>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// Run a virtual machine
     Run {
@@ -177,6 +188,11 @@
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// List running virtual machines
     List,
@@ -260,6 +276,7 @@
             cpu_topology,
             task_profiles,
             extra_idsigs,
+            gdb,
         } => command_run_app(
             name,
             get_service()?.as_ref(),
@@ -278,6 +295,7 @@
             cpu_topology,
             task_profiles,
             &extra_idsigs,
+            gdb,
         ),
         Opt::RunMicrodroid {
             name,
@@ -291,6 +309,7 @@
             mem,
             cpu_topology,
             task_profiles,
+            gdb,
         } => command_run_microdroid(
             name,
             get_service()?.as_ref(),
@@ -304,8 +323,9 @@
             mem,
             cpu_topology,
             task_profiles,
+            gdb,
         ),
-        Opt::Run { name, config, cpu_topology, task_profiles, console, log } => {
+        Opt::Run { name, config, cpu_topology, task_profiles, console, log, gdb } => {
             command_run(
                 name,
                 get_service()?.as_ref(),
@@ -315,6 +335,7 @@
                 /* mem */ None,
                 cpu_topology,
                 task_profiles,
+                gdb,
             )
         }
         Opt::List => command_list(get_service()?.as_ref()),
diff --git a/vm/src/run.rs b/vm/src/run.rs
index fa84591..5d785de 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -32,6 +32,7 @@
 use std::fs;
 use std::fs::File;
 use std::io;
+use std::num::NonZeroU16;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
@@ -58,6 +59,7 @@
     cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
     extra_idsigs: &[PathBuf],
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let apk_file = File::open(apk).context("Failed to open APK file")?;
 
@@ -144,6 +146,7 @@
         memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
         cpuTopology: cpu_topology,
         taskProfiles: task_profiles,
+        gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
     });
     run(service, &config, &payload_config_str, console_path, log_path)
 }
@@ -185,6 +188,7 @@
     mem: Option<u32>,
     cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
@@ -215,6 +219,7 @@
         cpu_topology,
         task_profiles,
         &extra_sig,
+        gdb,
     )
 }
 
@@ -229,6 +234,7 @@
     mem: Option<u32>,
     cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let config_file = File::open(config_path).context("Failed to open config file")?;
     let mut config =
@@ -241,6 +247,9 @@
     } else {
         config.name = String::from("VmRun");
     }
+    if let Some(gdb) = gdb {
+        config.gdbPort = gdb.get() as i32;
+    }
     config.cpuTopology = cpu_topology;
     config.taskProfiles = task_profiles;
     run(
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index cfb0225..930e137 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -87,6 +87,7 @@
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
+        gdbPort: 0, // no gdb
     });
     let console = android_log_fd()?;
     let log = android_log_fd()?;