From 1115a86f16720f736f26de326774429eca1c96ee Mon Sep 17 00:00:00 2001 From: Jeffrey Quesnelle Date: Wed, 28 Oct 2026 07:53:04 -0400 Subject: [PATCH 1/2] fix(supervisor-network): write TLS output files beyond MAX_PATH on Windows write_tls_output installs each file with NamedTempFile::persist, which passes the temporary file's path to SetFileAttributesW and both paths to MoveFileExW unchanged. On Windows those calls fail with ERROR_PATH_NOT_FOUND for a path longer than MAX_PATH without the \\?\ prefix, and the supervisor then disables TLS termination. A TLS directory inside a deep state directory, such as an MXC sandbox's host state directory, can exceed that length. On Windows, resolve the output directory with canonicalize, which returns the \\?\ form, before creating and installing the file. If canonicalize fails, as it can on volumes that the Mount Manager does not track, keep the path as given. Signed-off-by: Jeffrey Quesnelle --- .../src/l7/tls.rs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/crates/openshell-supervisor-network/src/l7/tls.rs b/crates/openshell-supervisor-network/src/l7/tls.rs index d106eb2060..929f1f1cbc 100644 --- a/crates/openshell-supervisor-network/src/l7/tls.rs +++ b/crates/openshell-supervisor-network/src/l7/tls.rs @@ -392,6 +392,15 @@ fn write_tls_output(path: &Path, contents: &[u8]) -> Result<()> { Err(error) => return Err(error).into_diagnostic(), } + // `NamedTempFile::persist` passes the temporary file's path to + // `SetFileAttributesW` and both paths to `MoveFileExW` unchanged, and those + // calls fail for paths longer than `MAX_PATH` unless they carry the `\\?\` + // prefix. A sandbox's host state directory can be that deep. + #[cfg(windows)] + let extended = extended_length_path(path); + #[cfg(windows)] + let path = extended.as_path(); + let parent = path .parent() .ok_or_else(|| miette!("TLS output has no parent: {}", path.display()))?; @@ -417,6 +426,19 @@ fn write_tls_output(path: &Path, contents: &[u8]) -> Result<()> { Ok(()) } +/// `path` with its directory in the extended-length (`\\?\`) form that +/// `canonicalize` returns on Windows, or `path` unchanged if `canonicalize` +/// fails. It can fail on volumes that the Mount Manager does not track, where +/// paths shorter than `MAX_PATH` still work as given. +#[cfg(windows)] +fn extended_length_path(path: &Path) -> PathBuf { + let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) else { + return path.to_path_buf(); + }; + std::fs::canonicalize(parent) + .map_or_else(|_| path.to_path_buf(), |parent| parent.join(file_name)) +} + /// Load PEM-encoded certificates from a string into a root certificate store. /// /// Returns `(added, ignored)` counts. Invalid or unparseable certificates @@ -697,6 +719,47 @@ mod tests { } } + #[cfg(windows)] + #[test] + fn write_ca_files_accepts_paths_longer_than_max_path() { + use std::os::windows::ffi::OsStrExt; + + let ca = SandboxCa::generate().unwrap(); + let dir = tempfile::tempdir().unwrap(); + let mut output_dir = dir.path().to_path_buf(); + // `MAX_PATH` counts UTF-16 code units. + while output_dir + .join("openshell-ca.pem") + .as_os_str() + .encode_wide() + .count() + <= 260 + { + output_dir.push("0123456789abcdef0123456789abcdef"); + } + + let (ca_path, bundle_path) = write_ca_files(&ca, &output_dir, "").unwrap(); + + assert_eq!(std::fs::read_to_string(&ca_path).unwrap(), ca.cert_pem()); + assert!( + std::fs::read_to_string(&bundle_path) + .unwrap() + .contains(ca.cert_pem()) + ); + } + + #[cfg(windows)] + #[test] + fn extended_length_path_keeps_the_path_when_canonicalize_fails() { + let dir = tempfile::tempdir().unwrap(); + let resolved = extended_length_path(&dir.path().join("openshell-ca.pem")); + assert!(resolved.as_os_str().to_string_lossy().starts_with(r"\\?\")); + + // The directory does not exist, so `canonicalize` fails. + let unresolved = dir.path().join("missing").join("openshell-ca.pem"); + assert_eq!(extended_length_path(&unresolved), unresolved); + } + #[test] fn durable_ca_round_trip_preserves_certificate_bytes() { let generated = SandboxCa::generate().unwrap(); From d2efbe9542016aca456c7c3dd8e7456d1ae45ab6 Mon Sep 17 00:00:00 2001 From: Jeffrey Quesnelle Date: Wed, 28 Oct 2026 06:20:15 -0400 Subject: [PATCH 2/2] fix(mxc): keep host supervisor proxy CA files in sandbox state The MXC driver starts the host supervisor without a proxy TLS directory, so the supervisor uses its default, /etc/openshell-tls, which Windows resolves to \etc\openshell-tls on the gateway's current drive. Every MXC sandbox on the host writes its proxy CA certificate and trust bundle to the same two files there and reads them back when it starts its workload, and the files stay after the sandbox is deleted. Set OPENSHELL_PROXY_TLS_DIR for the host supervisor to proxy-tls\ in the sandbox's host state directory, as the VM driver does with its state directory. delete_sandbox already removes that directory. Signed-off-by: Jeffrey Quesnelle --- crates/openshell-driver-mxc/README.md | 4 ++ crates/openshell-driver-mxc/src/driver.rs | 69 ++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index 9da3c201ef..90a44af802 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -127,6 +127,10 @@ etw_audit = false Only `process_container` supports this architecture. `isolation_session` is rejected during sandbox validation. +The host supervisor writes its proxy CA certificate and trust bundle to +`proxy-tls\` in the sandbox's host state directory, +`\\`, and deleting the sandbox removes them. + MXC 1.0.0 is required. Pre-1.0 executors and the retired `default_configuration_id` setting are not supported; remove that setting before upgrading. Both live launches and standalone mapper output use the diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index 3d2bef6cef..2a72cf77a6 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -40,6 +40,8 @@ const DRIVER_NAME: &str = "mxc"; const DRIVER_VERSION: &str = env!("CARGO_PKG_VERSION"); const DEFAULT_IMAGE_SENTINEL: &str = "mxc:process-container"; const HOST_AUTH_BUNDLE_FILE: &str = "supervisor-auth.json"; +/// Directory in the sandbox host state directory for the proxy CA files. +const HOST_PROXY_TLS_DIR: &str = "proxy-tls"; const HOST_RUNTIME_DESCRIPTOR_FILE: &str = "runtime-descriptor.json"; const BOUNDARY_CONFIG_FILE: &str = "boundary.json"; const BOUNDARY_TLS_CERT_FILE: &str = "sandbox.crt"; @@ -836,6 +838,17 @@ fn spawn_supervisor( auth_bundle_path: &Path, main: &openshell_core::sandbox_env::MainProcessConfig, ) -> Result { + supervisor_command(context, descriptor_path, auth_bundle_path, main)? + .spawn() + .map_err(|error| format!("start host openshell-supervisor: {error}")) +} + +fn supervisor_command( + context: &LifecycleContext, + descriptor_path: &Path, + auth_bundle_path: &Path, + main: &openshell_core::sandbox_env::MainProcessConfig, +) -> Result { let main_process_spec = mxc_main_process_spec(main)?; let mut command = Command::new(&context.config.supervisor_binary_path); command @@ -865,6 +878,14 @@ fn spawn_supervisor( ) .env(openshell_core::sandbox_env::SANDBOX_ID, &context.sandbox.id) .env(openshell_core::sandbox_env::SANDBOX, &context.sandbox.name) + // The supervisor's default `/etc/openshell-tls` resolves to + // `\etc\openshell-tls` on the current drive, where every sandbox on + // the host would write and read the same CA files. Keep them with the + // sandbox's host state, which `delete_sandbox` removes. + .env( + openshell_core::sandbox_env::PROXY_TLS_DIR, + context.host_state_dir.join(HOST_PROXY_TLS_DIR), + ) .env( openshell_core::sandbox_env::LOG_LEVEL, openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "info"), @@ -878,9 +899,7 @@ fn spawn_supervisor( server_name, ); } - command - .spawn() - .map_err(|error| format!("start host openshell-supervisor: {error}")) + Ok(command) } fn mxc_main_process_spec( @@ -1477,6 +1496,50 @@ mod tests { assert!(!backend.config.state_dir.exists()); } + #[test] + fn host_supervisor_proxy_tls_dir_is_in_sandbox_host_state() { + let (backend, sandbox, dir) = preflight_fixture(); + let host_state_dir = backend + .config + .state_dir + .join(&sandbox.id) + .join("generation"); + let context = LifecycleContext { + invoker: backend.invoker.clone(), + config: backend.config.clone(), + gateway: backend.gateway.clone(), + registry: backend.registry.clone(), + watch_tx: backend.watch_tx.clone(), + attribution: backend.attribution.clone(), + sandbox_config: sandbox_config(&sandbox).unwrap(), + sandbox, + generation: "generation".into(), + host_state_dir: host_state_dir.clone(), + boundary_state_dir: dir.path().join("boundary"), + policy_mapper: backend.policy_mapper.clone(), + }; + let main = mxc_main_process_config( + context.sandbox.spec.as_ref(), + &context.sandbox_config.command, + ); + let command = supervisor_command( + &context, + &host_state_dir.join(HOST_RUNTIME_DESCRIPTOR_FILE), + &host_state_dir.join(HOST_AUTH_BUNDLE_FILE), + &main, + ) + .unwrap(); + let tls_dir = command + .as_std() + .get_envs() + .find(|(key, _)| *key == openshell_core::sandbox_env::PROXY_TLS_DIR) + .and_then(|(_, value)| value); + assert_eq!( + tls_dir.map(Path::new), + Some(host_state_dir.join(HOST_PROXY_TLS_DIR).as_path()) + ); + } + fn with_ui_policy( mut sandbox: DriverSandbox, ui: openshell_core::proto::UiPolicy,