From 07b3d03ea7f9fb7a08885581492af531dbf4c166 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Mon, 5 Oct 2026 10:49:35 +0200 Subject: [PATCH 1/2] fix(driver-mxc): pass gateway name to mocked backend Refs #4191 Signed-off-by: Philippe Martin --- crates/openshell-driver-mxc/src/driver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index 84c36945b3..c7a966a32b 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -455,7 +455,7 @@ impl MxcComputeBackend { /// Test-only constructor wiring the in-process mock `wxc-exec` shim. #[cfg(test)] pub(crate) fn new_mocked(config: MxcComputeConfig) -> Self { - let mut backend = Self::new(config); + let mut backend = Self::new(openshell_core::config::DEFAULT_GATEWAY_NAME, config); backend.invoker = WxcExecInvoker::mocked(&backend.config.wxc_exec_path); backend } From f7dd0e8eb2310ce1268171f31924db0ff910e221 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 7 Oct 2026 19:20:28 +0200 Subject: [PATCH 2/2] fix(windows): use native executable paths in network test fixtures Signed-off-by: Philippe Martin --- .../src/identity.rs | 40 +++++--- .../openshell-supervisor-network/src/lib.rs | 2 +- .../openshell-supervisor-network/src/proxy.rs | 94 ++++++++++++------- .../src/test_support.rs | 41 +++++++- 4 files changed, 130 insertions(+), 47 deletions(-) diff --git a/crates/openshell-supervisor-network/src/identity.rs b/crates/openshell-supervisor-network/src/identity.rs index 5fb123a1c0..d51c0490fc 100644 --- a/crates/openshell-supervisor-network/src/identity.rs +++ b/crates/openshell-supervisor-network/src/identity.rs @@ -283,6 +283,7 @@ impl BinaryIdentityCache { mod tests { use super::*; use crate::procfs; + use crate::test_support::executable_path; use openshell_isolation_interface::contract::{ BinaryIdentity, ExecutableIdentity, Sha256Digest, }; @@ -294,19 +295,19 @@ mod tests { } fn supplied_identity( - executable_path: &str, + path: &str, executable_digest: Option<&str>, ancestors: &[(&str, Option<&str>)], ) -> BinaryIdentity { BinaryIdentity { executable: ExecutableIdentity { - path: PathBuf::from(executable_path), + path: executable_path(path), digest: executable_digest.map(digest), }, ancestors: ancestors .iter() .map(|(path, digest_value)| ExecutableIdentity { - path: PathBuf::from(path), + path: executable_path(path), digest: digest_value.map(digest), }) .collect(), @@ -383,9 +384,22 @@ mod tests { #[test] fn supplied_identity_rejects_non_absolute_or_empty_paths() { let cache = BinaryIdentityCache::new(); - for path in ["", "sandbox/tool"] { + #[cfg(not(target_os = "windows"))] + let paths = ["", "sandbox/tool"]; + #[cfg(target_os = "windows")] + let paths = [ + "", + "sandbox/tool", + "C:sandbox/tool", + "/sandbox/tool", + r"\sandbox\tool", + ]; + for path in paths { + let mut identity = supplied_identity(path, Some("11"), &[]); + // Exercise the raw invalid path, without adapting the fixture. + identity.executable.path = PathBuf::from(path); let error = cache - .verify_or_cache_supplied_identity(&supplied_identity(path, Some("11"), &[])) + .verify_or_cache_supplied_identity(&identity) .unwrap_err() .to_string(); assert!(error.contains("must be absolute")); @@ -450,7 +464,7 @@ mod tests { .hashes .lock() .unwrap() - .contains_key(Path::new("/sandbox/other")) + .contains_key(&executable_path("/sandbox/other")) ); } @@ -480,8 +494,8 @@ mod tests { assert!(error.contains("capacity")); let hashes = cache.hashes.lock().unwrap(); assert_eq!(hashes.len(), 4095); - assert!(!hashes.contains_key(Path::new("/sandbox/new-leaf"))); - assert!(!hashes.contains_key(Path::new("/sandbox/new-ancestor"))); + assert!(!hashes.contains_key(&executable_path("/sandbox/new-leaf"))); + assert!(!hashes.contains_key(&executable_path("/sandbox/new-ancestor"))); } #[test] @@ -491,7 +505,7 @@ mod tests { for index in 0..4096 { cache .verify_or_cache_with_paths( - &PathBuf::from(format!("/sandbox/pinned-{index}")), + &executable_path(&format!("/sandbox/pinned-{index}")), executable.path(), |_| Ok("11".repeat(32)), ) @@ -499,9 +513,11 @@ mod tests { } let error = cache - .verify_or_cache_with_paths(Path::new("/sandbox/overflow"), executable.path(), |_| { - Ok("11".repeat(32)) - }) + .verify_or_cache_with_paths( + &executable_path("/sandbox/overflow"), + executable.path(), + |_| Ok("11".repeat(32)), + ) .unwrap_err() .to_string(); diff --git a/crates/openshell-supervisor-network/src/lib.rs b/crates/openshell-supervisor-network/src/lib.rs index d0377b5d27..d9fcd004b6 100644 --- a/crates/openshell-supervisor-network/src/lib.rs +++ b/crates/openshell-supervisor-network/src/lib.rs @@ -33,5 +33,5 @@ pub(crate) use openshell_driver_vm::allocation_tracking as test_alloc; #[global_allocator] static TEST_ALLOCATOR: openshell_driver_vm::allocation_tracking::CountingAllocator = openshell_driver_vm::allocation_tracking::CountingAllocator; -#[cfg(all(test, target_os = "linux"))] +#[cfg(test)] mod test_support; diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index fc20dca23c..cbc17e184d 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -6998,6 +6998,7 @@ fn is_benign_relay_error(err: &miette::Report) -> bool { )] mod tests { use super::*; + use crate::test_support::{executable_path, policy_with_native_executable_paths}; use openshell_core::proposals::AgentProposals; use std::collections::HashMap as TestHashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -7009,7 +7010,8 @@ mod tests { fn supplied_identity_preserves_authorized_endpoint_metadata() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: inspected: name: inspected @@ -7034,11 +7036,12 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .expect("load policy"); let identity = Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/python3"), + path: executable_path("/usr/bin/python3"), digest: Some("00".repeat(32).parse().expect("digest")), }, ancestors: Vec::new(), @@ -7066,7 +7069,8 @@ process: fn supplied_identity_rejects_same_path_replacement() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: credentialed: name: credentialed @@ -7089,12 +7093,13 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .expect("load policy"); let identity = |digest_byte: &str| { Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/client"), + path: executable_path("/sandbox/bin/client"), digest: Some(digest_byte.repeat(32).parse().expect("digest")), }, ancestors: Vec::new(), @@ -7123,7 +7128,8 @@ process: fn supplied_identity_rejects_replaced_authorizing_ancestor() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: allowed: name: allowed @@ -7142,16 +7148,17 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .expect("load policy"); let identity = |ancestor_digest: &str| { Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/client"), + path: executable_path("/sandbox/bin/client"), digest: Some("11".repeat(32).parse().expect("digest")), }, ancestors: vec![ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/launcher"), + path: executable_path("/sandbox/bin/launcher"), digest: Some(ancestor_digest.repeat(32).parse().expect("digest")), }], cmdline_paths: Vec::new(), @@ -7194,11 +7201,13 @@ process: run_as_group: sandbox "#; let rego = include_str!("../data/sandbox-policy.rego"); - let engine = OpaEngine::from_strings(rego, POLICY_DATA).expect("load policy"); + let engine = + OpaEngine::from_strings(rego, &policy_with_native_executable_paths(POLICY_DATA)) + .expect("load policy"); let identity = |digest_byte: &str| { Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/client"), + path: executable_path("/sandbox/bin/client"), digest: Some(digest_byte.repeat(32).parse().expect("digest")), }, ancestors: Vec::new(), @@ -7212,7 +7221,9 @@ process: authorize_supplied_identity(&engine, &identity_cache, intent(), &identity("11")); assert!(matches!(original.action, NetworkAction::Allow { .. })); - engine.reload(rego, POLICY_DATA).expect("reload policy"); + engine + .reload(rego, &policy_with_native_executable_paths(POLICY_DATA)) + .expect("reload policy"); let replacement = authorize_supplied_identity(&engine, &identity_cache, intent(), &identity("22")); @@ -7228,7 +7239,8 @@ process: fn supplied_identity_rejects_missing_ancestor_digest_before_policy() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: allowed: name: allowed @@ -7247,15 +7259,16 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .expect("load policy"); let identity = Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/client"), + path: executable_path("/sandbox/bin/client"), digest: Some("11".repeat(32).parse().expect("digest")), }, ancestors: vec![ContractExecutableIdentity { - path: PathBuf::from("/sandbox/bin/launcher"), + path: executable_path("/sandbox/bin/launcher"), digest: None, }], cmdline_paths: Vec::new(), @@ -7279,7 +7292,8 @@ process: async fn staged_transparent_open_waits_for_l4_policy() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: allowed: name: allowed @@ -7300,6 +7314,7 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .unwrap(); let identity_cache = BinaryIdentityCache::new(); @@ -7307,7 +7322,7 @@ process: let identity = || { Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/curl"), + path: executable_path("/usr/bin/curl"), digest: Some("00".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), @@ -7400,7 +7415,10 @@ process: .expect("policy denial is sent to mapper"); assert_eq!(event.host, "203.0.113.8"); assert_eq!(event.port, 443); - assert_eq!(event.binary, "/usr/bin/curl"); + assert_eq!( + event.binary, + executable_path("/usr/bin/curl").to_str().unwrap() + ); assert_eq!(event.denial_stage, "transparent_tcp_connect"); assert!(denial_rx.try_recv().is_err(), "exactly one mapper event"); } @@ -7502,7 +7520,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } stream: Box::new(stream), binary_identity: Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/curl"), + path: executable_path("/usr/bin/curl"), digest: Some("00".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), @@ -7688,7 +7706,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } async fn staged_transparent_open_dials_only_pinned_policy_dns_addresses() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - POLICY_DNS_OPEN_POLICY, + &policy_with_native_executable_paths(POLICY_DNS_OPEN_POLICY), ) .unwrap(); let store = policy_dns_test_store(); @@ -7728,7 +7746,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } async fn staged_transparent_open_proposes_a_denied_observation_hostname() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - POLICY_DNS_OPEN_POLICY, + &policy_with_native_executable_paths(POLICY_DNS_OPEN_POLICY), ) .unwrap(); let store = policy_dns_test_store(); @@ -7761,14 +7779,17 @@ process: { run_as_user: sandbox, run_as_group: sandbox } let event = denial_rx.try_recv().expect("denial is sent to the mapper"); assert_eq!(event.host, "unknown.example"); assert_eq!(event.port, 443); - assert_eq!(event.binary, "/usr/bin/curl"); + assert_eq!( + event.binary, + executable_path("/usr/bin/curl").to_str().unwrap() + ); } #[tokio::test] async fn staged_transparent_open_never_relays_an_observation_that_policy_allows() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - POLICY_DNS_OPEN_POLICY, + &policy_with_native_executable_paths(POLICY_DNS_OPEN_POLICY), ) .unwrap(); let identity_cache = BinaryIdentityCache::new(); @@ -7834,7 +7855,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } fn pinned_plan_requires_the_mapping_of_the_deciding_generation() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - POLICY_DNS_OPEN_POLICY, + &policy_with_native_executable_paths(POLICY_DNS_OPEN_POLICY), ) .unwrap(); let store = policy_dns_test_store(); @@ -7877,7 +7898,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } engine .reload( include_str!("../data/sandbox-policy.rego"), - POLICY_DNS_OPEN_POLICY, + &policy_with_native_executable_paths(POLICY_DNS_OPEN_POLICY), ) .unwrap(); let reloaded = decide("db.example", 5432); @@ -7900,7 +7921,10 @@ process: { run_as_user: sandbox, run_as_group: sandbox } ); assert_eq!( mapped.format_shorthand(), - "NET:OPEN [MED] DENIED /usr/bin/curl(0) -> blocked.invalid:80 [reason:transparent_tcp_policy_denied]" + format!( + "NET:OPEN [MED] DENIED {}(0) -> blocked.invalid:80 [reason:transparent_tcp_policy_denied]", + executable_path("/usr/bin/curl").display() + ) ); assert_eq!( serde_json::to_value(mapped).unwrap()["dst_endpoint"], @@ -7932,7 +7956,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } let cache = Arc::new(BinaryIdentityCache::new()); let identity = ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/bash"), + path: executable_path("/usr/bin/bash"), digest: Some("44".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), @@ -8044,7 +8068,7 @@ process: { run_as_user: sandbox, run_as_group: sandbox } let engine = Arc::new( OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - &format!( + &policy_with_native_executable_paths(&format!( r#" network_policies: allowed: @@ -8057,7 +8081,7 @@ network_policies: - path: /usr/bin/curl "#, port = destination.port() - ), + )), ) .unwrap(), ); @@ -8068,7 +8092,7 @@ network_policies: stream: Box::new(stream), binary_identity: Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/curl"), + path: executable_path("/usr/bin/curl"), digest: Some("44".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), @@ -8179,7 +8203,8 @@ network_policies: async fn staged_transparent_open_reports_invalid_identity_as_unavailable() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: allowed: name: allowed @@ -8198,6 +8223,7 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .unwrap(); let identity_cache = BinaryIdentityCache::new(); @@ -8207,7 +8233,7 @@ process: stream: Box::new(stream), binary_identity: Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/usr/bin/curl"), + path: executable_path("/usr/bin/curl"), digest: None, }, ancestors: Vec::new(), @@ -8248,7 +8274,8 @@ process: async fn staged_transparent_open_reports_identity_cache_capacity_exhaustion() { let engine = OpaEngine::from_strings( include_str!("../data/sandbox-policy.rego"), - r#" + &policy_with_native_executable_paths( + r#" network_policies: allowed: name: allowed @@ -8267,6 +8294,7 @@ process: run_as_user: sandbox run_as_group: sandbox "#, + ), ) .unwrap(); let identity_cache = BinaryIdentityCache::new(); @@ -8274,7 +8302,7 @@ process: identity_cache .verify_or_cache_supplied_identity(&ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from(format!("/sandbox/pinned-{index}")), + path: executable_path(&format!("/sandbox/pinned-{index}")), digest: Some("11".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), @@ -8288,7 +8316,7 @@ process: stream: Box::new(stream), binary_identity: Ok(ContractBinaryIdentity { executable: ContractExecutableIdentity { - path: PathBuf::from("/sandbox/overflow"), + path: executable_path("/sandbox/overflow"), digest: Some("22".repeat(32).parse().unwrap()), }, ancestors: Vec::new(), diff --git a/crates/openshell-supervisor-network/src/test_support.rs b/crates/openshell-supervisor-network/src/test_support.rs index 2fe79e3eb5..d1b23b6f31 100644 --- a/crates/openshell-supervisor-network/src/test_support.rs +++ b/crates/openshell-supervisor-network/src/test_support.rs @@ -1,14 +1,19 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Socket-owner subprocess fixtures. +//! Executable identity and socket-owner subprocess fixtures. +#[cfg(target_os = "linux")] use std::net::TcpStream; +#[cfg(target_os = "linux")] use std::os::fd::OwnedFd; +#[cfg(target_os = "linux")] use std::process::{Child, Command, Stdio}; +#[cfg(target_os = "linux")] pub struct SocketHolder(pub Child); +#[cfg(target_os = "linux")] impl Drop for SocketHolder { fn drop(&mut self) { let _ = self.0.kill(); @@ -18,6 +23,7 @@ impl Drop for SocketHolder { /// Keep a duplicate of the parent's connected socket in a descendant process. /// Stdin transfers descriptor ownership through the standard process API. +#[cfg(target_os = "linux")] pub fn spawn_socket_holder(stream: &TcpStream, exec_sleep: bool) -> SocketHolder { let mut command = if exec_sleep { let mut command = Command::new("/bin/sleep"); @@ -45,9 +51,42 @@ pub fn spawn_socket_holder(stream: &TcpStream, exec_sleep: bool) -> SocketHolder ) } +#[cfg(target_os = "linux")] #[test] fn socket_holder_child() { if std::env::var_os("OPENSHELL_SOCKET_HOLDER_CHILD").is_some() { std::thread::sleep(std::time::Duration::from_secs(30)); } } + +use std::path::PathBuf; + +/// Give rooted synthetic executable paths a drive on Windows. Preserve empty, +/// relative, and already native paths so invalid-identity tests remain useful. +pub fn executable_path(path: &str) -> PathBuf { + #[cfg(target_os = "windows")] + if path.starts_with('/') { + return PathBuf::from(format!("C:{path}")); + } + PathBuf::from(path) +} + +/// Match policy binary paths to the synthetic identities without changing HTTP +/// rule paths or other policy fields. +pub fn policy_with_native_executable_paths(data: &str) -> String { + let mut policy: serde_yml::Value = serde_yml::from_str(data).expect("parse test policy"); + if let Some(policies) = policy["network_policies"].as_mapping_mut() { + for (_, policy) in policies { + if let Some(binaries) = policy["binaries"].as_sequence_mut() { + for binary in binaries { + if let Some(path) = binary["path"].as_str() { + binary["path"] = serde_yml::Value::String( + executable_path(path).to_str().unwrap().to_string(), + ); + } + } + } + } + } + serde_yml::to_string(&policy).expect("serialize test policy") +}