diff --git a/crates/openshell-core/src/isolation_registration.rs b/crates/openshell-core/src/isolation_registration.rs index 1633dbb742..8f820e302d 100644 --- a/crates/openshell-core/src/isolation_registration.rs +++ b/crates/openshell-core/src/isolation_registration.rs @@ -34,7 +34,8 @@ pub enum SandboxTransport { impl SandboxTransport { pub fn validate(&self) -> Result<(), &'static str> { let valid = match self { - Self::Unix { socket_path } => socket_path.is_absolute(), + // Unix transport only runs on Unix hosts, but validation may not. + Self::Unix { socket_path } => crate::paths::is_posix_absolute(socket_path), Self::Tcp { authority, addresses, diff --git a/crates/openshell-core/src/paths.rs b/crates/openshell-core/src/paths.rs index 79124738c1..0470ab6764 100644 --- a/crates/openshell-core/src/paths.rs +++ b/crates/openshell-core/src/paths.rs @@ -167,6 +167,11 @@ pub fn is_file_permissions_too_open(path: &Path) -> bool { std::fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o077 != 0) } +/// Whether `path` is absolute under POSIX rules, whatever the host platform. +pub fn is_posix_absolute(path: &Path) -> bool { + path.to_str().is_some_and(|path| path.starts_with('/')) +} + /// Compatibility re-export; authored policy path normalization is owned by /// `openshell-policy-schema`. pub use openshell_policy_schema::normalize_path; @@ -175,6 +180,15 @@ pub use openshell_policy_schema::normalize_path; mod tests { use super::*; + #[test] + fn is_posix_absolute_ignores_host_path_syntax() { + assert!(is_posix_absolute(Path::new("/run/backend.sock"))); + assert!(is_posix_absolute(Path::new("/"))); + for path in ["", "relative", "./run", r"\run", r"C:\run", "C:/run"] { + assert!(!is_posix_absolute(Path::new(path)), "{path}"); + } + } + #[test] fn xdg_config_dir_respects_env() { // This test checks the logic — actual env var mutation is unsafe so diff --git a/crates/openshell-sandbox-backend/src/boundary_protocol.rs b/crates/openshell-sandbox-backend/src/boundary_protocol.rs index 8402a5e7a5..81325d0d22 100644 --- a/crates/openshell-sandbox-backend/src/boundary_protocol.rs +++ b/crates/openshell-sandbox-backend/src/boundary_protocol.rs @@ -283,7 +283,7 @@ impl SandboxTransport { pub fn validate(&self) -> Result<(), &'static str> { let valid = match self { Self::Unix { socket_path } | Self::RuntimeInitiatedUnix { socket_path } => { - socket_path.is_absolute() + openshell_core::paths::is_posix_absolute(socket_path) } Self::Tcp { authority, diff --git a/crates/openshell-server/src/compute/mutation_guard.rs b/crates/openshell-server/src/compute/mutation_guard.rs index 0d137885a5..5b257774bc 100644 --- a/crates/openshell-server/src/compute/mutation_guard.rs +++ b/crates/openshell-server/src/compute/mutation_guard.rs @@ -1169,7 +1169,11 @@ mod tests { if op.is_distributed() { fleet.enter(op); } - tokio::time::sleep(hold).await; + // Windows timers round a 1 ms sleep up to ~16 ms, so yield instead. + let until = std::time::Instant::now() + hold; + while std::time::Instant::now() < until { + tokio::task::yield_now().await; + } if op.is_distributed() { fleet.leave(op); } diff --git a/crates/openshell-server/src/persistence/tests.rs b/crates/openshell-server/src/persistence/tests.rs index be86441245..3493b277dc 100644 --- a/crates/openshell-server/src/persistence/tests.rs +++ b/crates/openshell-server/src/persistence/tests.rs @@ -2379,7 +2379,8 @@ async fn sqlite_concurrent_writes_return_their_own_resource_version() { use std::sync::Arc; const WRITERS: u64 = 8; - const WRITES: u64 = 25; + // Slow Windows fsync can starve a writer past the SQLite busy timeout. + const WRITES: u64 = if cfg!(windows) { 5 } else { 25 }; let tmp = tempfile::tempdir().expect("tempdir"); let url = on_disk_store_url(&tmp.path().join("openshell.db")); diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 46eb9e8839..a84c70a4a3 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -5620,6 +5620,8 @@ network_policies: .await; } + // Baseline enrichment adds host paths that only exist on Unix. + #[cfg(unix)] #[tokio::test(start_paused = true)] async fn startup_policy_write_refusal_baseline_write_back_waits_for_repair() { // The gateway policy has network rules but lacks the proxy baseline @@ -5749,6 +5751,8 @@ network_policies: .await; } + // Baseline enrichment adds host paths that only exist on Unix. + #[cfg(unix)] #[tokio::test(start_paused = true)] async fn startup_policy_write_refusal_baseline_repair_before_report_refetches() { let mut repaired = proto_tcp_policy_fixture(); @@ -5892,6 +5896,8 @@ network_policies: .await; } + // Baseline enrichment adds host paths that only exist on Unix. + #[cfg(unix)] #[tokio::test(start_paused = true)] async fn startup_policy_write_refusal_write_back_outlasts_the_attempt_limit() { // The same limit applies to a refused baseline-path write-back. @@ -5979,6 +5985,8 @@ network_policies: ); } + // Baseline enrichment adds host paths that only exist on Unix. + #[cfg(unix)] #[tokio::test(start_paused = true)] async fn startup_policy_write_refusal_global_policy_skips_baseline_write_back() { use openshell_core::proto::{ConfigurationAdmissionState, PolicySource}; @@ -6067,18 +6075,18 @@ network_policies: // Both writes are checked: the image policy upload to a gateway with // no policy, and the write-back of a stored policy that lacks the // proxy baseline paths. - let writes = [ - ( - "image policy upload", - unset_policy_snapshot(), - ImagePolicyDiscovery::Policy(Box::new(proto_tcp_policy_fixture())), - ), - ( - "baseline write-back", - settings_poll_result(Some(proto_tcp_policy_fixture()), 1, PolicySource::Sandbox), - ImagePolicyDiscovery::Missing, - ), - ]; + let image_upload = ( + "image policy upload", + unset_policy_snapshot(), + ImagePolicyDiscovery::Policy(Box::new(proto_tcp_policy_fixture())), + ); + let baseline_write_back = ( + "baseline write-back", + settings_poll_result(Some(proto_tcp_policy_fixture()), 1, PolicySource::Sandbox), + ImagePolicyDiscovery::Missing, + ); + // Baseline enrichment adds host paths that only exist on Unix. + let writes = std::iter::once(image_upload).chain(cfg!(unix).then_some(baseline_write_back)); for (write, initial, discovery) in writes { for code in [ tonic::Code::PermissionDenied, diff --git a/crates/openshell-supervisor/src/sandbox_instance_host.rs b/crates/openshell-supervisor/src/sandbox_instance_host.rs index 66aa30e57e..7465701460 100644 --- a/crates/openshell-supervisor/src/sandbox_instance_host.rs +++ b/crates/openshell-supervisor/src/sandbox_instance_host.rs @@ -745,7 +745,8 @@ pub(crate) fn prepare_private_dir(_path: &Path) -> Result<()> { )) } -#[cfg(test)] +// Hosted sandbox instances require a Unix host. +#[cfg(all(test, unix))] mod tests { use super::*; use http_body_util::BodyExt as _; @@ -1050,7 +1051,6 @@ mod tests { // Files that cannot be deleted keep the key reserved until a later // removal deletes them. Root ignores the permission this relies on. - #[cfg(unix)] if !nix::unistd::geteuid().is_root() { use std::os::unix::fs::PermissionsExt as _;