From 98a0edbd025cda99ddcc422b5046f0c8df7bca57 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 20 Sep 2026 05:36:27 -0700 Subject: [PATCH 1/5] fix(vmm): bound the event name a guest reports over the host API --- dstack/vmm/src/app.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index df3e99281..3e1d56132 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -1507,6 +1507,16 @@ impl App { } pub(crate) fn vm_event_report(&self, cid: u32, event: &str, body: String) -> Result<()> { + // A guest chooses both of these, over a host API request that may + // carry 10 MiB. The name only ever selects one of the four short + // branches below; an unrecognised one is logged and kept in the event + // ring for every later GetInfo, so cap it before it is logged, next to + // the cap the body already has. + const MAX_EVENT_NAME_LEN: usize = 64; + if event.len() > MAX_EVENT_NAME_LEN { + error!(cid, "event name too large, skipping"); + return Ok(()); + } info!(cid, event, "VM event"); if body.len() > 1024 * 4 { error!("Event body too large, skipping"); @@ -3624,6 +3634,41 @@ mod tests { .map_err(anyhow::Error::msg)?; Ok(()) } + + /// A guest names its own events. The body has a cap; the name had none, + /// so a guest could hand the host a multi-megabyte string to log and to + /// keep in the event ring for every later `GetInfo`. + #[tokio::test] + async fn a_guest_event_name_is_bounded() { + let dir = tempfile::tempdir().unwrap(); + let app = app_talking_to(&dir.path().join("netd.sock")); + let (config, _) = bridge_vm(&app, "vm-1"); + std::fs::create_dir_all(&config.workdir).unwrap(); + app.lock().add(VmState::new(config)); + + // What one 10 MiB host API request can carry. + let huge = "x".repeat(10 * 1024 * 1024); + app.vm_event_report(3, &huge, "body".into()).unwrap(); + + let kept: usize = app + .lock() + .get("vm-1") + .unwrap() + .state + .events + .iter() + .map(|event| event.event.len()) + .sum(); + assert_eq!( + kept, 0, + "a guest kept {kept} bytes of event name on the host" + ); + + // A name the VMM acts on still gets through. + app.vm_event_report(3, "boot.progress", "ready".into()) + .unwrap(); + assert_eq!(app.lock().get("vm-1").unwrap().state.boot_progress, "ready"); + } } /// CIDs that must survive a pool rebuild, mapped to the VM that owns each one. From c95ab5c96dd22c64d9adb87e309622a5a51e04a2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 02:13:09 -0700 Subject: [PATCH 2/5] refactor(vmm): trim event name bound comment and test --- dstack/vmm/src/app.rs | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 3e1d56132..2b879e140 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -1507,11 +1507,7 @@ impl App { } pub(crate) fn vm_event_report(&self, cid: u32, event: &str, body: String) -> Result<()> { - // A guest chooses both of these, over a host API request that may - // carry 10 MiB. The name only ever selects one of the four short - // branches below; an unrecognised one is logged and kept in the event - // ring for every later GetInfo, so cap it before it is logged, next to - // the cap the body already has. + // Guest-chosen and kept in the event ring, so bound it like the body. const MAX_EVENT_NAME_LEN: usize = 64; if event.len() > MAX_EVENT_NAME_LEN { error!(cid, "event name too large, skipping"); @@ -3635,39 +3631,17 @@ mod tests { Ok(()) } - /// A guest names its own events. The body has a cap; the name had none, - /// so a guest could hand the host a multi-megabyte string to log and to - /// keep in the event ring for every later `GetInfo`. #[tokio::test] - async fn a_guest_event_name_is_bounded() { + async fn guest_event_name_is_bounded() { let dir = tempfile::tempdir().unwrap(); let app = app_talking_to(&dir.path().join("netd.sock")); let (config, _) = bridge_vm(&app, "vm-1"); std::fs::create_dir_all(&config.workdir).unwrap(); app.lock().add(VmState::new(config)); - // What one 10 MiB host API request can carry. - let huge = "x".repeat(10 * 1024 * 1024); - app.vm_event_report(3, &huge, "body".into()).unwrap(); - - let kept: usize = app - .lock() - .get("vm-1") - .unwrap() - .state - .events - .iter() - .map(|event| event.event.len()) - .sum(); - assert_eq!( - kept, 0, - "a guest kept {kept} bytes of event name on the host" - ); - - // A name the VMM acts on still gets through. - app.vm_event_report(3, "boot.progress", "ready".into()) + app.vm_event_report(3, &"x".repeat(1024), "body".into()) .unwrap(); - assert_eq!(app.lock().get("vm-1").unwrap().state.boot_progress, "ready"); + assert!(app.lock().get("vm-1").unwrap().state.events.is_empty()); } } From e28fe01c8e101ff2a314b812fa132d7cabdfbb09 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 05:41:52 -0700 Subject: [PATCH 3/5] feat(vmm): make the guest event name bound configurable Raise the default from 64 bytes to 2 KiB and expose it as max_event_name_len in vmm.toml. --- docs/tutorials/vmm-configuration.md | 1 + dstack/vmm/src/app.rs | 8 +++----- dstack/vmm/src/config.rs | 3 +++ dstack/vmm/vmm.toml | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/vmm-configuration.md b/docs/tutorials/vmm-configuration.md index 07e06426a..013b9e731 100644 --- a/docs/tutorials/vmm-configuration.md +++ b/docs/tutorials/vmm-configuration.md @@ -97,6 +97,7 @@ address = "127.0.0.1:9080" reuse = true kms_url = "http://127.0.0.1:8081" event_buffer_size = 20 +max_event_name_len = 2048 node_name = "" image_path = "/var/lib/dstack/images" diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 2b879e140..1e524c3a2 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -1507,9 +1507,7 @@ impl App { } pub(crate) fn vm_event_report(&self, cid: u32, event: &str, body: String) -> Result<()> { - // Guest-chosen and kept in the event ring, so bound it like the body. - const MAX_EVENT_NAME_LEN: usize = 64; - if event.len() > MAX_EVENT_NAME_LEN { + if event.len() > self.config.max_event_name_len { error!(cid, "event name too large, skipping"); return Ok(()); } @@ -3639,8 +3637,8 @@ mod tests { std::fs::create_dir_all(&config.workdir).unwrap(); app.lock().add(VmState::new(config)); - app.vm_event_report(3, &"x".repeat(1024), "body".into()) - .unwrap(); + let too_long = "x".repeat(app.config.max_event_name_len + 1); + app.vm_event_report(3, &too_long, "body".into()).unwrap(); assert!(app.lock().get("vm-1").unwrap().state.events.is_empty()); } } diff --git a/dstack/vmm/src/config.rs b/dstack/vmm/src/config.rs index 0d3d40a3e..61658edf7 100644 --- a/dstack/vmm/src/config.rs +++ b/dstack/vmm/src/config.rs @@ -658,6 +658,9 @@ pub struct Config { /// The buffer size in VMM process for guest events pub event_buffer_size: usize, + /// Maximum length in bytes of a guest-reported event name + pub max_event_name_len: usize, + /// CVM configuration pub cvm: CvmConfig, diff --git a/dstack/vmm/vmm.toml b/dstack/vmm/vmm.toml index 1cf52e0c0..42c0100b9 100644 --- a/dstack/vmm/vmm.toml +++ b/dstack/vmm/vmm.toml @@ -12,6 +12,7 @@ address = "unix:./vmm.sock" reuse = true kms_url = "http://127.0.0.1:8081" event_buffer_size = 20 +max_event_name_len = 2048 node_name = "" [image] From 276f43f3cdca97f3bfa5abdbb74e65c6363c77f8 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 05:46:43 -0700 Subject: [PATCH 4/5] chore(vmm): default max_event_name_len to 128 bytes --- docs/tutorials/vmm-configuration.md | 2 +- dstack/vmm/vmm.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/vmm-configuration.md b/docs/tutorials/vmm-configuration.md index 013b9e731..8d6e3b320 100644 --- a/docs/tutorials/vmm-configuration.md +++ b/docs/tutorials/vmm-configuration.md @@ -97,7 +97,7 @@ address = "127.0.0.1:9080" reuse = true kms_url = "http://127.0.0.1:8081" event_buffer_size = 20 -max_event_name_len = 2048 +max_event_name_len = 128 node_name = "" image_path = "/var/lib/dstack/images" diff --git a/dstack/vmm/vmm.toml b/dstack/vmm/vmm.toml index 42c0100b9..663d424bc 100644 --- a/dstack/vmm/vmm.toml +++ b/dstack/vmm/vmm.toml @@ -12,7 +12,7 @@ address = "unix:./vmm.sock" reuse = true kms_url = "http://127.0.0.1:8081" event_buffer_size = 20 -max_event_name_len = 2048 +max_event_name_len = 128 node_name = "" [image] From f86ea80688a2dbf737f5b4d6a2d2291776c1b8bb Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 05:47:54 -0700 Subject: [PATCH 5/5] test(vmm): drop the event name bound test --- dstack/vmm/src/app.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index 1e524c3a2..e32d95748 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -3628,19 +3628,6 @@ mod tests { .map_err(anyhow::Error::msg)?; Ok(()) } - - #[tokio::test] - async fn guest_event_name_is_bounded() { - let dir = tempfile::tempdir().unwrap(); - let app = app_talking_to(&dir.path().join("netd.sock")); - let (config, _) = bridge_vm(&app, "vm-1"); - std::fs::create_dir_all(&config.workdir).unwrap(); - app.lock().add(VmState::new(config)); - - let too_long = "x".repeat(app.config.max_event_name_len + 1); - app.vm_event_report(3, &too_long, "body".into()).unwrap(); - assert!(app.lock().get("vm-1").unwrap().state.events.is_empty()); - } } /// CIDs that must survive a pool rebuild, mapped to the VM that owns each one.