Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions crates/openshell-driver-mxc/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ impl MxcBackend {
#[serde(default, deny_unknown_fields)]
#[allow(clippy::struct_excessive_bools)] // Independent, existing gateway TOML options.
pub struct MxcComputeConfig {
/// Path to `wxc-exec.exe`. Required for live runs.
/// Path to `wxc-exec.exe`. Required for live runs, and must be an
/// absolute path: `wxc-exec` is the binary that builds every sandbox, so
/// a relative path (including the unset default) would let PATH-lookup
/// or working-directory-relative resolution execute a decoy binary with
/// the gateway's identity instead of the approved `wxc-exec`. Enforced
/// at gateway startup by the compute-driver config preflight.
pub wxc_exec_path: String,
/// Backend to target. Default: `process_container`.
pub backend: MxcBackend,
Expand Down Expand Up @@ -148,7 +153,12 @@ pub struct MxcComputeConfig {
impl Default for MxcComputeConfig {
fn default() -> Self {
Self {
wxc_exec_path: "wxc-exec.exe".into(),
// No usable default: `wxc_exec_path` must be explicitly set to an
// absolute path (see `validate_configuration` and the field doc
// comment above). Shipping a bare relative filename here would
// silently reintroduce the exact PATH/CWD-hijack risk the
// validation exists to reject.
wxc_exec_path: String::new(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes an existing omitted field from a usable default to a startup error, but docs/reference/gateway-config.mdx and the MXC README only show examples; neither states that wxc_exec_path is now required and absolute. Please document the migration and requirement in this PR, as required for driver configuration default changes.

backend: MxcBackend::default(),
pc_least_privilege: false,
pc_capabilities: Vec::new(),
Expand All @@ -167,6 +177,32 @@ impl Default for MxcComputeConfig {
}
}

impl MxcComputeConfig {
/// Validate startup configuration without touching `wxc-exec` or the
/// filesystem beyond `Path::is_absolute`.
///
/// `wxc_exec_path` must be set to an absolute path: it is the binary
/// that builds every sandbox, so a relative path (including an unset,
/// empty value) would let PATH-lookup or working-directory-relative
/// resolution execute a decoy binary with the gateway's identity instead
/// of the approved `wxc-exec`, turning the containment mechanism itself
/// into an arbitrary-code-execution primitive.
pub fn validate_configuration(&self) -> openshell_core::Result<()> {
if self.wxc_exec_path.trim().is_empty() {
return Err(openshell_core::Error::config(
"[openshell.drivers.mxc] wxc_exec_path must be set to an absolute path to wxc-exec.exe",
));
}
if !Path::new(&self.wxc_exec_path).is_absolute() {
return Err(openshell_core::Error::config(format!(
"[openshell.drivers.mxc] wxc_exec_path must be an absolute path, got '{}'",
self.wxc_exec_path
)));
}
Ok(())
}
}

/// Per-sandbox MXC workload settings supplied through
/// `template.driver_config.mxc` / `--driver-config-json`.
#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -2560,6 +2596,50 @@ mod lifecycle_tests {
}
}

#[test]
fn validate_configuration_rejects_unset_wxc_exec_path() {
// Regression test: the shipped default used to be the bare relative
// filename "wxc-exec.exe", which is exactly the PATH/CWD-hijack
// primitive this validation exists to reject. The default must stay
// rejected, not silently become a usable-but-insecure fallback.
let config = MxcComputeConfig::default();
assert!(config.wxc_exec_path.is_empty());
let error = config.validate_configuration().unwrap_err();
assert!(error.to_string().contains("wxc_exec_path"));
}

#[test]
fn validate_configuration_rejects_relative_wxc_exec_path() {
let config = MxcComputeConfig {
wxc_exec_path: "wxc-exec.exe".into(),
..Default::default()
};
let error = config.validate_configuration().unwrap_err();
assert!(error.to_string().contains("wxc_exec_path"));

let config = MxcComputeConfig {
wxc_exec_path: r"..\wxc-exec.exe".into(),
..Default::default()
};
assert!(config.validate_configuration().is_err());
}

#[test]
fn validate_configuration_accepts_absolute_wxc_exec_path() {
let config = MxcComputeConfig {
wxc_exec_path: r"C:\mxc-kit\bin\wxc-exec.exe".into(),
..Default::default()
};
config.validate_configuration().unwrap();
}

#[test]
fn governed_egress_defaults_off_and_allocates_unique_loopback_ports() {
let config = MxcComputeConfig::default();
assert!(!config.egress_proxy);
assert!(config.egress_proxy_addr.is_empty());
}

#[test]
fn sandbox_proxy_addr_uses_ephemeral_loopback_port() {
let configured = "127.0.0.1:18080".parse().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions crates/openshell-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ impl openshell_server::ComputeDriverFactory for MxcFactory {
&self,
context: openshell_server::ComputeDriverConfigContext<'_>,
) -> openshell_core::Result<()> {
let _: openshell_driver_mxc::MxcComputeConfig = context.driver_config()?;
Ok(())
let config: openshell_driver_mxc::MxcComputeConfig = context.driver_config()?;
config.validate_configuration()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual enforcement boundary for the security fix, but the new tests only call MxcComputeConfig::validate_configuration directly. If this factory call regressed to the previous no-op, all added tests would still pass and relative paths would again reach startup. Please add a Windows gateway/config-preflight test that selects mxc and verifies omitted and relative paths fail while an absolute path passes.

}

async fn build(
Expand Down
Loading