From 93fda7dade102c9491b387a987c8f61a9f3849d8 Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Wed, 27 May 2026 18:51:54 +0000 Subject: [PATCH 1/6] feat(ohcldiag-dev): add GUID-based VM identification and VTL2 settings trace - Add VmId::HyperVId variant for direct GUID-based VM connections - Parse GUID from CLI argument (with or without braces, with hyperv: prefix) - Route HyperVId through all match arms (serial, vsock, connect) - Add trace line logging VTL2 settings blob receipt with byte count in GET client --- Cargo.lock | 1 + openhcl/ohcldiag-dev/Cargo.toml | 1 + openhcl/ohcldiag-dev/src/main.rs | 39 +++++++++++++++++-- .../guest_emulation_transport/src/client.rs | 38 ++++++++++++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6efbbde80..38b83d1f0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5176,6 +5176,7 @@ dependencies = [ "fs-err", "futures", "futures-concurrency", + "guid", "inspect", "kmsg", "mesh", diff --git a/openhcl/ohcldiag-dev/Cargo.toml b/openhcl/ohcldiag-dev/Cargo.toml index d7b4f0f4db..f408653adf 100644 --- a/openhcl/ohcldiag-dev/Cargo.toml +++ b/openhcl/ohcldiag-dev/Cargo.toml @@ -27,6 +27,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } unicycle.workspace = true [target.'cfg(windows)'.dependencies] +guid.workspace = true pal.workspace = true socket2.workspace = true diff --git a/openhcl/ohcldiag-dev/src/main.rs b/openhcl/ohcldiag-dev/src/main.rs index 111007e2eb..d99ca2cbb2 100644 --- a/openhcl/ohcldiag-dev/src/main.rs +++ b/openhcl/ohcldiag-dev/src/main.rs @@ -342,6 +342,8 @@ pub struct VmArg { enum VmId { #[cfg(windows)] HyperV(String), + #[cfg(windows)] + HyperVId(guid::Guid), HybridVsock(PathBuf), } @@ -353,10 +355,18 @@ impl FromStr for VmId { Ok(Self::HybridVsock(Path::new(s).to_owned())) } else { #[cfg(windows)] - if let Some(s) = s.strip_prefix("hyperv:") { - return Ok(Self::HyperV(s.to_owned())); - } else if !pal::windows::fs::is_unix_socket(s.as_ref()).unwrap_or(false) { - return Ok(Self::HyperV(s.to_owned())); + { + // Strip optional "hyperv:" prefix for all Hyper-V variants. + let s = s.strip_prefix("hyperv:").unwrap_or(s); + + // Try parsing as a GUID first (with or without braces). + if let Ok(guid) = s.parse::() { + return Ok(Self::HyperVId(guid)); + } + + if !pal::windows::fs::is_unix_socket(s.as_ref()).unwrap_or(false) { + return Ok(Self::HyperV(s.to_owned())); + } } // Default to hybrid vsock since this is what OpenVMM supports for // Underhill. @@ -475,6 +485,8 @@ fn new_client(driver: impl Driver + Spawn + Clone, input: &VmArg) -> anyhow::Res let client = match &input.id { #[cfg(windows)] VmId::HyperV(name) => DiagClient::from_hyperv_name(driver, name)?, + #[cfg(windows)] + VmId::HyperVId(guid) => DiagClient::from_hyperv_id(driver, *guid), VmId::HybridVsock(path) => DiagClient::from_hybrid_vsock(driver, path), }; Ok(client) @@ -657,6 +669,12 @@ pub fn main() -> anyhow::Result<()> { let vm_name = match &vm.id { VmId::HyperV(name) => name, + #[cfg(windows)] + VmId::HyperVId(_) => { + anyhow::bail!( + "--serial requires a VM name, not a GUID. Use a named VM instead." + ) + } _ => anyhow::bail!("--serial is only supported for Hyper-V VMs"), }; @@ -762,6 +780,12 @@ pub fn main() -> anyhow::Result<()> { diag_client::hyperv::connect_vsock(&driver, vm_id, port).await?; PolledSocket::new(&driver, socket2::Socket::from(stream))? } + #[cfg(windows)] + VmId::HyperVId(vm_id) => { + let stream = + diag_client::hyperv::connect_vsock(&driver, vm_id, port).await?; + PolledSocket::new(&driver, socket2::Socket::from(stream))? + } }; let vsock = Arc::new(vsock.into_inner()); @@ -906,6 +930,13 @@ pub fn main() -> anyhow::Result<()> { .await?; PolledSocket::new(&driver, socket2::Socket::from(stream))? } + #[cfg(windows)] + VmId::HyperVId(ref vm_id) => { + let stream = + diag_client::hyperv::connect_vsock(&driver, *vm_id, vsock_port) + .await?; + PolledSocket::new(&driver, socket2::Socket::from(stream))? + } }; println!("VSOCK connect to port {:?}", vsock_port); diff --git a/vm/devices/get/guest_emulation_transport/src/client.rs b/vm/devices/get/guest_emulation_transport/src/client.rs index fc361818a6..898e4e1566 100644 --- a/vm/devices/get/guest_emulation_transport/src/client.rs +++ b/vm/devices/get/guest_emulation_transport/src/client.rs @@ -211,6 +211,44 @@ impl GuestEmulationTransportClient { None }; + // Log VTL2 settings receipt with byte count and hex preview for validation + match json.v2.r#static.vtl2_settings.as_ref() { + Some(settings) => { + let len = settings.len(); + let preview_len = len.min(64); + let hex_preview: String = settings[..preview_len] + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" "); + let content_preview = if settings.first() == Some(&b'{') { + let s = String::from_utf8_lossy(settings); + if s.len() > 256 { + format!("{}...", &s[..256]) + } else { + s.into_owned() + } + } else { + format!("(protobuf, {} bytes)", len) + }; + tracing::info!( + CVM_ALLOWED, + bytes = len, + hex_preview = %hex_preview, + content = %content_preview, + "VTL2_SETTINGS_RECEIVED: blob present, {} bytes, content: {}", + len, + content_preview, + ); + } + None => { + tracing::warn!( + CVM_ALLOWED, + "VTL2_SETTINGS_RECEIVED: no vtl2_settings blob in DPS response", + ); + } + } + Ok(platform_settings::DevicePlatformSettings { smbios: platform_settings::Smbios { serial_number: json.v1.serial_number.into(), From e8f0123955f2b52d699e608a5e8de839cd45912f Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Tue, 16 Jun 2026 17:04:24 +0000 Subject: [PATCH 2/6] remove vtl2 settings trace (moved to separate branch) --- .../guest_emulation_transport/src/client.rs | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/vm/devices/get/guest_emulation_transport/src/client.rs b/vm/devices/get/guest_emulation_transport/src/client.rs index 898e4e1566..fc361818a6 100644 --- a/vm/devices/get/guest_emulation_transport/src/client.rs +++ b/vm/devices/get/guest_emulation_transport/src/client.rs @@ -211,44 +211,6 @@ impl GuestEmulationTransportClient { None }; - // Log VTL2 settings receipt with byte count and hex preview for validation - match json.v2.r#static.vtl2_settings.as_ref() { - Some(settings) => { - let len = settings.len(); - let preview_len = len.min(64); - let hex_preview: String = settings[..preview_len] - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(" "); - let content_preview = if settings.first() == Some(&b'{') { - let s = String::from_utf8_lossy(settings); - if s.len() > 256 { - format!("{}...", &s[..256]) - } else { - s.into_owned() - } - } else { - format!("(protobuf, {} bytes)", len) - }; - tracing::info!( - CVM_ALLOWED, - bytes = len, - hex_preview = %hex_preview, - content = %content_preview, - "VTL2_SETTINGS_RECEIVED: blob present, {} bytes, content: {}", - len, - content_preview, - ); - } - None => { - tracing::warn!( - CVM_ALLOWED, - "VTL2_SETTINGS_RECEIVED: no vtl2_settings blob in DPS response", - ); - } - } - Ok(platform_settings::DevicePlatformSettings { smbios: platform_settings::Smbios { serial_number: json.v1.serial_number.into(), From a76a29c292b2106b4c9710d4279ee1db4ce69394 Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Tue, 16 Jun 2026 17:16:41 +0000 Subject: [PATCH 3/6] feat(ohcldiag-dev): support --serial with GUID-based VM identification Add IdAndPortNumber variant to ComPortAccessInfo that uses Get-VMComPort -VMId instead of -VMName, removing the bail for GUID-based VMs in the --serial path. --- openhcl/diag_client/src/lib.rs | 20 ++++++++++++++++++++ openhcl/ohcldiag-dev/src/main.rs | 22 ++++++++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/openhcl/diag_client/src/lib.rs b/openhcl/diag_client/src/lib.rs index afd2ec3dfb..2df18328d5 100644 --- a/openhcl/diag_client/src/lib.rs +++ b/openhcl/diag_client/src/lib.rs @@ -50,6 +50,8 @@ pub mod hyperv { pub enum ComPortAccessInfo<'a> { /// Access by number NameAndPortNumber(&'a str, u32), + /// Access by VM ID and port number + IdAndPortNumber(Guid, u32), /// Access through a named pipe PortPipePath(&'a str), } @@ -140,6 +142,24 @@ pub mod hyperv { } &String::from_utf8(output.stdout)? } + ComPortAccessInfo::IdAndPortNumber(id, num) => { + let output = Command::new("powershell.exe") + .arg("-NoProfile") + .arg(format!( + r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"#, + )) + .output() + .context("failed to query VM com port")?; + + if !output.status.success() { + let _ = std::io::stderr().write_all(&output.stderr); + anyhow::bail!( + "failed to query VM com port: exit status {}", + output.status.code().unwrap() + ); + } + &String::from_utf8(output.stdout)? + } ComPortAccessInfo::PortPipePath(path) => path, }; diff --git a/openhcl/ohcldiag-dev/src/main.rs b/openhcl/ohcldiag-dev/src/main.rs index d99ca2cbb2..edbcef2547 100644 --- a/openhcl/ohcldiag-dev/src/main.rs +++ b/openhcl/ohcldiag-dev/src/main.rs @@ -667,21 +667,19 @@ pub fn main() -> anyhow::Result<()> { use diag_client::hyperv::ComPortAccessInfo; use futures::AsyncBufReadExt; - let vm_name = match &vm.id { - VmId::HyperV(name) => name, - #[cfg(windows)] - VmId::HyperVId(_) => { - anyhow::bail!( - "--serial requires a VM name, not a GUID. Use a named VM instead." - ) - } - _ => anyhow::bail!("--serial is only supported for Hyper-V VMs"), - }; - let port_access_info = if let Some(pipe_path) = pipe_path.as_ref() { ComPortAccessInfo::PortPipePath(pipe_path) } else { - ComPortAccessInfo::NameAndPortNumber(vm_name, 3) + match &vm.id { + VmId::HyperV(name) => { + ComPortAccessInfo::NameAndPortNumber(name, 3) + } + #[cfg(windows)] + VmId::HyperVId(guid) => { + ComPortAccessInfo::IdAndPortNumber(*guid, 3) + } + _ => anyhow::bail!("--serial is only supported for Hyper-V VMs"), + } }; let pipe = From 4ac0e7cb39e11ce753a4a448d84816b963499cff Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Thu, 18 Jun 2026 21:24:12 +0000 Subject: [PATCH 4/6] fix(ohcldiag-dev): address PR review feedback - Update CLI help text for GUID support - Preserve hyperv: prefix disambiguation semantics - Refactor serial port query to reduce duplication Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- openhcl/diag_client/src/lib.rs | 61 +++++++++++++------------------- openhcl/ohcldiag-dev/src/main.rs | 31 +++++++++------- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/openhcl/diag_client/src/lib.rs b/openhcl/diag_client/src/lib.rs index 2df18328d5..d03b37abbd 100644 --- a/openhcl/diag_client/src/lib.rs +++ b/openhcl/diag_client/src/lib.rs @@ -111,6 +111,23 @@ pub mod hyperv { Ok(socket.convert().into_inner()) } + fn query_vm_com_port(script: &str) -> anyhow::Result { + let output = Command::new("powershell.exe") + .arg("-NoProfile") + .arg(script) + .output() + .context("failed to query VM com port")?; + + if !output.status.success() { + let _ = std::io::stderr().write_all(&output.stderr); + anyhow::bail!( + "failed to query VM com port: exit status {}", + output.status.code().unwrap() + ); + } + Ok(String::from_utf8(output.stdout)?) + } + /// Opens a serial port on a Hyper-V VM. /// /// If the VM is not running, it will periodically try to connect to the @@ -124,43 +141,13 @@ pub mod hyperv { port: ComPortAccessInfo<'_>, ) -> anyhow::Result { let path = match port { - ComPortAccessInfo::NameAndPortNumber(vm, num) => { - let output = Command::new("powershell.exe") - .arg("-NoProfile") - .arg(format!( - r#"$x = Get-VMComPort "{vm}" -Number {num} -ErrorAction Stop; $x.Path"#, - )) - .output() - .context("failed to query VM com port")?; - - if !output.status.success() { - let _ = std::io::stderr().write_all(&output.stderr); - anyhow::bail!( - "failed to query VM com port: exit status {}", - output.status.code().unwrap() - ); - } - &String::from_utf8(output.stdout)? - } - ComPortAccessInfo::IdAndPortNumber(id, num) => { - let output = Command::new("powershell.exe") - .arg("-NoProfile") - .arg(format!( - r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"#, - )) - .output() - .context("failed to query VM com port")?; - - if !output.status.success() { - let _ = std::io::stderr().write_all(&output.stderr); - anyhow::bail!( - "failed to query VM com port: exit status {}", - output.status.code().unwrap() - ); - } - &String::from_utf8(output.stdout)? - } - ComPortAccessInfo::PortPipePath(path) => path, + ComPortAccessInfo::NameAndPortNumber(vm, num) => query_vm_com_port(&format!( + r#"$x = Get-VMComPort "{vm}" -Number {num} -ErrorAction Stop; $x.Path"#, + ))?, + ComPortAccessInfo::IdAndPortNumber(id, num) => query_vm_com_port(&format!( + r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"#, + ))?, + ComPortAccessInfo::PortPipePath(path) => path.to_owned(), }; let path = path.trim(); diff --git a/openhcl/ohcldiag-dev/src/main.rs b/openhcl/ohcldiag-dev/src/main.rs index edbcef2547..53a81c748d 100644 --- a/openhcl/ohcldiag-dev/src/main.rs +++ b/openhcl/ohcldiag-dev/src/main.rs @@ -331,7 +331,13 @@ pub struct VmArg { )] #[cfg_attr( windows, - doc = "* NAME_OR_PATH - Either a Hyper-V VM name, or a path as in vsock:PATH>" + doc = "* hyperv:GUID - A Hyper-V VM ID (with or without braces) + + " + )] + #[cfg_attr( + windows, + doc = "* NAME_OR_GUID - Either a Hyper-V VM name or GUID, or a path as in vsock:PATH" )] #[cfg_attr(not(windows), doc = "* PATH - A path as in vsock:PATH")] #[clap(name = "VM")] @@ -356,16 +362,21 @@ impl FromStr for VmId { } else { #[cfg(windows)] { - // Strip optional "hyperv:" prefix for all Hyper-V variants. - let s = s.strip_prefix("hyperv:").unwrap_or(s); + let (value, had_prefix) = match s.strip_prefix("hyperv:") { + Some(rest) => (rest, true), + None => (s, false), + }; // Try parsing as a GUID first (with or without braces). - if let Ok(guid) = s.parse::() { + if let Ok(guid) = value.parse::() { return Ok(Self::HyperVId(guid)); } - if !pal::windows::fs::is_unix_socket(s.as_ref()).unwrap_or(false) { - return Ok(Self::HyperV(s.to_owned())); + // If the hyperv: prefix was explicitly provided, always treat as + // a Hyper-V name (preserving disambiguation semantics). + if had_prefix || !pal::windows::fs::is_unix_socket(value.as_ref()).unwrap_or(false) + { + return Ok(Self::HyperV(value.to_owned())); } } // Default to hybrid vsock since this is what OpenVMM supports for @@ -671,13 +682,9 @@ pub fn main() -> anyhow::Result<()> { ComPortAccessInfo::PortPipePath(pipe_path) } else { match &vm.id { - VmId::HyperV(name) => { - ComPortAccessInfo::NameAndPortNumber(name, 3) - } + VmId::HyperV(name) => ComPortAccessInfo::NameAndPortNumber(name, 3), #[cfg(windows)] - VmId::HyperVId(guid) => { - ComPortAccessInfo::IdAndPortNumber(*guid, 3) - } + VmId::HyperVId(guid) => ComPortAccessInfo::IdAndPortNumber(*guid, 3), _ => anyhow::bail!("--serial is only supported for Hyper-V VMs"), } }; From c77e412f573d3e01de7ba712a6c1bc8d877985a4 Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Tue, 23 Jun 2026 16:49:00 +0000 Subject: [PATCH 5/6] fix(ohcldiag-dev): address PR review feedback - Refactor query_vm_com_port to take ComPortAccessInfo directly instead of an arbitrary script string - Fix potential panic from output.status.code().unwrap() by formatting the status directly - Update CLI help text to NAME_OR_GUID_OR_PATH - Remove unnecessary comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- openhcl/diag_client/src/lib.rs | 28 ++++++++++++++++------------ openhcl/ohcldiag-dev/src/main.rs | 4 +--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/openhcl/diag_client/src/lib.rs b/openhcl/diag_client/src/lib.rs index d03b37abbd..a044766be0 100644 --- a/openhcl/diag_client/src/lib.rs +++ b/openhcl/diag_client/src/lib.rs @@ -111,19 +111,28 @@ pub mod hyperv { Ok(socket.convert().into_inner()) } - fn query_vm_com_port(script: &str) -> anyhow::Result { + fn query_vm_com_port(port: ComPortAccessInfo<'_>) -> anyhow::Result { + let script = match port { + ComPortAccessInfo::NameAndPortNumber(vm, num) => { + format!(r#"$x = Get-VMComPort "{vm}" -Number {num} -ErrorAction Stop; $x.Path"#) + } + ComPortAccessInfo::IdAndPortNumber(id, num) => { + format!( + r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"# + ) + } + ComPortAccessInfo::PortPipePath(_) => unreachable!(), + }; + let output = Command::new("powershell.exe") .arg("-NoProfile") - .arg(script) + .arg(&script) .output() .context("failed to query VM com port")?; if !output.status.success() { let _ = std::io::stderr().write_all(&output.stderr); - anyhow::bail!( - "failed to query VM com port: exit status {}", - output.status.code().unwrap() - ); + anyhow::bail!("failed to query VM com port: exit status {}", output.status); } Ok(String::from_utf8(output.stdout)?) } @@ -141,13 +150,8 @@ pub mod hyperv { port: ComPortAccessInfo<'_>, ) -> anyhow::Result { let path = match port { - ComPortAccessInfo::NameAndPortNumber(vm, num) => query_vm_com_port(&format!( - r#"$x = Get-VMComPort "{vm}" -Number {num} -ErrorAction Stop; $x.Path"#, - ))?, - ComPortAccessInfo::IdAndPortNumber(id, num) => query_vm_com_port(&format!( - r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"#, - ))?, ComPortAccessInfo::PortPipePath(path) => path.to_owned(), + port => query_vm_com_port(port)?, }; let path = path.trim(); diff --git a/openhcl/ohcldiag-dev/src/main.rs b/openhcl/ohcldiag-dev/src/main.rs index 53a81c748d..f0fbfc8f6c 100644 --- a/openhcl/ohcldiag-dev/src/main.rs +++ b/openhcl/ohcldiag-dev/src/main.rs @@ -337,7 +337,7 @@ pub struct VmArg { )] #[cfg_attr( windows, - doc = "* NAME_OR_GUID - Either a Hyper-V VM name or GUID, or a path as in vsock:PATH" + doc = "* NAME_OR_GUID_OR_PATH - Either a Hyper-V VM name or GUID, or a path as in vsock:PATH" )] #[cfg_attr(not(windows), doc = "* PATH - A path as in vsock:PATH")] #[clap(name = "VM")] @@ -372,8 +372,6 @@ impl FromStr for VmId { return Ok(Self::HyperVId(guid)); } - // If the hyperv: prefix was explicitly provided, always treat as - // a Hyper-V name (preserving disambiguation semantics). if had_prefix || !pal::windows::fs::is_unix_socket(value.as_ref()).unwrap_or(false) { return Ok(Self::HyperV(value.to_owned())); From 2cca12bf581605042460c688107264a09fa0003e Mon Sep 17 00:00:00 2001 From: Kyle Moorhead Date: Thu, 25 Jun 2026 20:02:39 +0000 Subject: [PATCH 6/6] fix(diag_client): handle PortPipePath inside query_vm_com_port Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- openhcl/diag_client/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openhcl/diag_client/src/lib.rs b/openhcl/diag_client/src/lib.rs index a044766be0..a801bcbbaf 100644 --- a/openhcl/diag_client/src/lib.rs +++ b/openhcl/diag_client/src/lib.rs @@ -121,7 +121,7 @@ pub mod hyperv { r#"$x = Get-VMComPort -VMId "{id}" -Number {num} -ErrorAction Stop; $x.Path"# ) } - ComPortAccessInfo::PortPipePath(_) => unreachable!(), + ComPortAccessInfo::PortPipePath(path) => return Ok(path.to_owned()), }; let output = Command::new("powershell.exe") @@ -149,10 +149,7 @@ pub mod hyperv { driver: &(impl Driver + ?Sized), port: ComPortAccessInfo<'_>, ) -> anyhow::Result { - let path = match port { - ComPortAccessInfo::PortPipePath(path) => path.to_owned(), - port => query_vm_com_port(port)?, - }; + let path = query_vm_com_port(port)?; let path = path.trim(); if path.is_empty() {