From 66d14f3e3ac0324a5263e766949010148565426c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:43:18 +0000 Subject: [PATCH 1/6] fix(supervisor): reject untrusted client sockets --- dstack/supervisor/client/Cargo.toml | 1 + dstack/supervisor/client/src/lib.rs | 119 +++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/dstack/supervisor/client/Cargo.toml b/dstack/supervisor/client/Cargo.toml index 99db01f7e..e1a691b05 100644 --- a/dstack/supervisor/client/Cargo.toml +++ b/dstack/supervisor/client/Cargo.toml @@ -27,6 +27,7 @@ serde.workspace = true http-body-util.workspace = true tracing-subscriber.workspace = true log.workspace = true +libc.workspace = true fs-err.workspace = true futures.workspace = true diff --git a/dstack/supervisor/client/src/lib.rs b/dstack/supervisor/client/src/lib.rs index 53257e5c9..398f428e0 100644 --- a/dstack/supervisor/client/src/lib.rs +++ b/dstack/supervisor/client/src/lib.rs @@ -11,6 +11,58 @@ use supervisor::{ProcessConfig, ProcessInfo, Response}; pub use supervisor; +#[cfg(unix)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SocketIdentity { + device: u64, + inode: u64, +} + +#[cfg(unix)] +fn trusted_uds_identity(path: &Path) -> Result { + use std::os::unix::fs::{FileTypeExt as _, MetadataExt as _}; + + let metadata = fs_err::symlink_metadata(path) + .with_context(|| format!("Failed to inspect supervisor socket {}", path.display()))?; + if !metadata.file_type().is_socket() { + anyhow::bail!( + "Supervisor endpoint is not a Unix socket: {}", + path.display() + ); + } + let effective_uid = unsafe { libc::geteuid() }; + if metadata.uid() != effective_uid { + anyhow::bail!("Supervisor socket is not owned by the current user"); + } + if metadata.mode() & 0o022 != 0 { + anyhow::bail!("Supervisor socket is writable by another user"); + } + + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + let parent_metadata = fs_err::symlink_metadata(parent).with_context(|| { + format!( + "Failed to inspect supervisor socket directory {}", + parent.display() + ) + })?; + if !parent_metadata.file_type().is_dir() { + anyhow::bail!("Supervisor socket parent is not a directory"); + } + let parent_owned = parent_metadata.uid() == effective_uid; + let parent_sticky = parent_metadata.mode() & 0o1000 != 0; + if !parent_owned && !parent_sticky { + anyhow::bail!("Supervisor socket directory is not controlled by the current user"); + } + if parent_metadata.mode() & 0o022 != 0 && !parent_sticky { + anyhow::bail!("Supervisor socket directory permits untrusted replacement"); + } + + Ok(SocketIdentity { + device: metadata.dev(), + inode: metadata.ino(), + }) +} + #[derive(Debug, Clone)] pub struct SupervisorClient { base_url: Arc, @@ -31,22 +83,30 @@ impl SupervisorClient { detached: bool, auto_start: bool, ) -> Result { - let uri = format!("unix:{}", uds.as_ref().display()); + let uds = uds.as_ref(); + let uri = format!("unix:{}", uds.display()); let client = Self::new(&uri); - if client.probe(Duration::from_millis(100)).await.is_ok() { - info!("Connected to supervisor at {uri}"); - return Ok(client); + if fs_err::symlink_metadata(uds).is_ok() { + let identity = trusted_uds_identity(uds)?; + if client.probe(Duration::from_millis(100)).await.is_ok() + && trusted_uds_identity(uds)? == identity + { + info!("Connected to supervisor at {uri}"); + return Ok(client); + } } if !auto_start { anyhow::bail!("Failed to connect to supervisor at {uri}"); } info!("Failed to connect to supervisor at {uri}, trying to start supervisor"); - // if the uds exists, remove it - if std::path::Path::new(uds.as_ref()).exists() { - fs_err::remove_file(uds.as_ref())?; + if fs_err::symlink_metadata(uds).is_ok() { + // Validate again immediately before removing a stale endpoint. Never + // delete a path that is not a trusted socket owned by this user. + trusted_uds_identity(uds)?; + fs_err::remove_file(uds)?; } let supervisor_path = supervisor_path.as_ref().to_path_buf(); - let uds = uds.as_ref().to_path_buf(); + let uds = uds.to_path_buf(); let pid_file = pid_file.as_ref().to_path_buf(); let log_file = log_file.as_ref().to_path_buf(); std::thread::spawn(move || { @@ -77,9 +137,13 @@ impl SupervisorClient { }); // wait while ping returns pong for i in 1..=10 { - if client.probe(Duration::from_millis(100)).await.is_ok() { - info!("connected to supervisor at {uri}"); - return Ok(client); + if let Ok(identity) = trusted_uds_identity(&uds) { + if client.probe(Duration::from_millis(100)).await.is_ok() + && trusted_uds_identity(&uds).ok() == Some(identity) + { + info!("connected to supervisor at {uri}"); + return Ok(client); + } } info!("waiting for supervisor at {uri} to start, attempt {i}"); tokio::time::sleep(Duration::from_millis(100 * i)).await; @@ -231,3 +295,36 @@ impl SupervisorClientSync { } } } + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::os::unix::fs::PermissionsExt as _; + use std::os::unix::net::UnixListener; + + #[test] + fn trusted_uds_rejects_regular_file() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("supervisor.sock"); + fs_err::write(&path, b"not a socket").unwrap(); + assert!(trusted_uds_identity(&path).is_err()); + } + + #[test] + fn trusted_uds_accepts_owner_only_socket() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("supervisor.sock"); + let _listener = UnixListener::bind(&path).unwrap(); + fs_err::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); + assert!(trusted_uds_identity(&path).is_ok()); + } + + #[test] + fn trusted_uds_rejects_socket_writable_by_others() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("supervisor.sock"); + let _listener = UnixListener::bind(&path).unwrap(); + fs_err::set_permissions(&path, std::fs::Permissions::from_mode(0o666)).unwrap(); + assert!(trusted_uds_identity(&path).is_err()); + } +} From 62ecad8368210ff53ad17516e90b79f7aee61de1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:43:58 +0000 Subject: [PATCH 2/6] build(supervisor): lock client libc dependency --- dstack/Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index a0167f769..70df79681 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -7385,6 +7385,7 @@ dependencies = [ "hyper", "hyper-util", "hyperlocal", + "libc", "log", "serde", "serde_json", From 939bf6bce3cc0ed66ceaf8ab65fcd9b3fff6e9d0 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:44:37 +0000 Subject: [PATCH 3/6] fix(supervisor): retain socket path for validation --- dstack/supervisor/client/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dstack/supervisor/client/src/lib.rs b/dstack/supervisor/client/src/lib.rs index 398f428e0..7c25f04c0 100644 --- a/dstack/supervisor/client/src/lib.rs +++ b/dstack/supervisor/client/src/lib.rs @@ -107,13 +107,14 @@ impl SupervisorClient { } let supervisor_path = supervisor_path.as_ref().to_path_buf(); let uds = uds.to_path_buf(); + let supervisor_uds = uds.clone(); let pid_file = pid_file.as_ref().to_path_buf(); let log_file = log_file.as_ref().to_path_buf(); std::thread::spawn(move || { // start supervisor let result = std::process::Command::new(supervisor_path) .arg("--uds") - .arg(uds) + .arg(supervisor_uds) .arg("--pid-file") .arg(pid_file) .arg("--log-file") From 4463ac07468cad6a186426cfba9ca13baa980117 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:45:03 +0000 Subject: [PATCH 4/6] test(supervisor): add socket fixture dependency --- dstack/Cargo.lock | 1 + dstack/supervisor/client/Cargo.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index 70df79681..ca761b715 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -7390,6 +7390,7 @@ dependencies = [ "serde", "serde_json", "supervisor", + "tempfile", "tokio", "tracing-subscriber", ] diff --git a/dstack/supervisor/client/Cargo.toml b/dstack/supervisor/client/Cargo.toml index e1a691b05..300f41fd5 100644 --- a/dstack/supervisor/client/Cargo.toml +++ b/dstack/supervisor/client/Cargo.toml @@ -34,5 +34,8 @@ futures.workspace = true supervisor.workspace = true http-client.workspace = true +[dev-dependencies] +tempfile.workspace = true + [features] cli = ["dep:clap", "tokio/full"] From 42f870efd090c538562d688fa11d7d40740e05d2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:45:47 +0000 Subject: [PATCH 5/6] test(supervisor): expose trusted socket rejection --- dstack/supervisor/client/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstack/supervisor/client/src/lib.rs b/dstack/supervisor/client/src/lib.rs index 7c25f04c0..5b9ca3450 100644 --- a/dstack/supervisor/client/src/lib.rs +++ b/dstack/supervisor/client/src/lib.rs @@ -317,7 +317,7 @@ mod tests { let path = directory.path().join("supervisor.sock"); let _listener = UnixListener::bind(&path).unwrap(); fs_err::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); - assert!(trusted_uds_identity(&path).is_ok()); + trusted_uds_identity(&path).expect("owner-only socket should be trusted"); } #[test] From 170fe56e6aa708c78ec534f013b70c52060876ba Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 27 Jul 2026 23:46:17 +0000 Subject: [PATCH 6/6] test(supervisor): secure trusted socket directory --- dstack/supervisor/client/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dstack/supervisor/client/src/lib.rs b/dstack/supervisor/client/src/lib.rs index 5b9ca3450..b0910236e 100644 --- a/dstack/supervisor/client/src/lib.rs +++ b/dstack/supervisor/client/src/lib.rs @@ -314,6 +314,7 @@ mod tests { #[test] fn trusted_uds_accepts_owner_only_socket() { let directory = tempfile::tempdir().unwrap(); + fs_err::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); let path = directory.path().join("supervisor.sock"); let _listener = UnixListener::bind(&path).unwrap(); fs_err::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();