Skip to content
Merged
2 changes: 2 additions & 0 deletions .github/workflows/windows-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ jobs:
run: cargo test --locked -p semwright-driver-host --test windows_secure_host -- secure_windows_driver_workspace --nocapture
- name: Secure Windows Driver secret grants
run: cargo test --locked -p semwright-driver-host --test windows_secure_host -- secure_windows_driver_secret --nocapture
- name: Secure Windows Driver loopback bridge
run: cargo test --locked -p semwright-driver-host --test windows_secure_host -- secure_windows_driver_loopback --nocapture
- name: Secure Windows Plugin Host round-trip
run: cargo test --locked -p semwright-plugin-host --features test-tools --test windows_secure_host -- --nocapture
- name: Secure Windows external MCP round-trip
Expand Down
76 changes: 76 additions & 0 deletions crates/driver-godot/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use std::{
};
#[cfg(unix)]
use tokio::net::UnixListener;
#[cfg(windows)]
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpListener,
Expand Down Expand Up @@ -195,6 +197,42 @@ pub struct Bridge {
stop: CancellationToken,
}

#[cfg(windows)]
async fn connect_loopback_pipe(path: &str, stop: &CancellationToken) -> Result<NamedPipeClient> {
if !path.starts_with(r"\\.\pipe\semwright-loopback-") || path.len() > 256 {
return Err(Error::new(
ErrorCode::PermissionDenied,
"Godot loopback pipe path is not Host-controlled",
));
}
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
loop {
if stop.is_cancelled() {
return Err(Error::new(ErrorCode::Cancelled, "Godot loopback stopped"));
}
match ClientOptions::new().open(path) {
Ok(pipe) => return Ok(pipe),
Err(error)
if (matches!(
error.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::WouldBlock
) || error.raw_os_error() == Some(231))
&& tokio::time::Instant::now() < deadline =>
{
tokio::select! {
_ = stop.cancelled() => {
return Err(Error::new(ErrorCode::Cancelled, "Godot loopback stopped"));
}
_ = tokio::time::sleep(Duration::from_millis(20)) => {}
}
}
Err(error) => return Err(error.into()),
}
}
}

impl Bridge {
pub async fn start(
port: u16,
Expand Down Expand Up @@ -244,6 +282,44 @@ impl Bridge {
return Ok(bridge);
}

#[cfg(windows)]
if let Ok(pipe_path) = std::env::var("SEMWRIGHT_DRIVER_LOOPBACK_PIPE") {
if !pipe_path.starts_with(r"\\.\pipe\semwright-loopback-") || pipe_path.len() > 256 {
return Err(Error::new(
ErrorCode::PermissionDenied,
"Godot loopback pipe path is not Host-controlled",
));
}
let pipe_stop = stop.clone();
let pipe_sessions = sessions.clone();
let pipe_projects = projects.clone();
let pipe_events = events.clone();
tokio::spawn(async move {
loop {
let pipe = tokio::select! {
_ = pipe_stop.cancelled() => break,
result = connect_loopback_pipe(&pipe_path, &pipe_stop) => {
let Ok(pipe) = result else {
if pipe_stop.is_cancelled() {
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
continue;
};
pipe
}
};
let sessions = pipe_sessions.clone();
let projects = pipe_projects.clone();
let events = pipe_events.clone();
tokio::spawn(async move {
let _ = serve_connection(pipe, sessions, projects, events).await;
});
}
});
return Ok(bridge);
}

let listener = TcpListener::bind(("127.0.0.1", port)).await?;
tokio::spawn(async move {
loop {
Expand Down
53 changes: 53 additions & 0 deletions crates/driver-host/src/bin/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ use semwright_types::{
CommandDescriptor, Error, ErrorCode, Idempotency, JobArtifact, JobProgress, Result, Risk,
};
use serde_json::{Value, json};
#[cfg(windows)]
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::windows::named_pipe::ClientOptions,
};

fn capability() -> Capability {
Capability {
Expand Down Expand Up @@ -370,8 +375,56 @@ impl Driver for Fixture {
}
}

#[cfg(windows)]
async fn loopback_echo_connection(
mut pipe: tokio::net::windows::named_pipe::NamedPipeClient,
) -> std::io::Result<()> {
let mut buffer = [0u8; 4096];
loop {
let read = pipe.read(&mut buffer).await?;
if read == 0 {
return Ok(());
}
pipe.write_all(&buffer[..read]).await?;
pipe.flush().await?;
}
}

#[cfg(windows)]
async fn loopback_echo_task(path: String) {
if !path.starts_with(r"\\.\pipe\semwright-loopback-") || path.len() > 256 {
return;
}
loop {
match ClientOptions::new().open(&path) {
Ok(pipe) => {
tokio::spawn(async move {
let _ = loopback_echo_connection(pipe).await;
});
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::WouldBlock
) || error.raw_os_error() == Some(231) =>
{
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
Err(_) => {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
#[cfg(windows)]
if let Ok(path) = std::env::var("SEMWRIGHT_DRIVER_LOOPBACK_PIPE") {
tokio::spawn(loopback_echo_task(path));
}
if let Err(error) = serve(Fixture).await {
eprintln!("{error}");
std::process::exit(error.exit_code());
Expand Down
48 changes: 38 additions & 10 deletions crates/driver-host/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Sandboxed persistent application-driver host. Drivers are mounted into the broker as Providers.
#[cfg(unix)]
#[cfg(any(unix, windows))]
mod loopback;

use async_trait::async_trait;
Expand Down Expand Up @@ -794,6 +794,7 @@ fn sandbox_spec_windows(
staged: &Path,
helper: &Path,
roots: &[FilesystemGrant],
loopback_pipe: Option<&Path>,
) -> Result<semwright_platform_api::launch::SandboxSpec> {
use semwright_platform_api::launch::{
Mount, MountClass, ResourceLimits, SandboxKind, SandboxSpec,
Expand All @@ -805,12 +806,6 @@ fn sandbox_spec_windows(
"Windows tool grants remain fail-closed until their immutable-executable contract is proven",
));
}
if manifest.loopback_port.is_some() {
return Err(Error::new(
ErrorCode::SandboxDenied,
"Windows driver loopback remains fail-closed until a non-Unix bridge is implemented",
));
}
let lookup = |name: &str| -> Result<&FilesystemGrant> {
roots
.iter()
Expand Down Expand Up @@ -876,13 +871,21 @@ fn sandbox_spec_windows(
})
.collect::<Result<Vec<_>>>()?,
);
let environment = loopback_pipe
.map(|path| {
path.to_str()
.ok_or_else(|| Error::invalid("Windows loopback pipe path must be Unicode"))
.map(|value| vec![(loopback::SANDBOX_PIPE_ENV.into(), value.to_owned())])
})
.transpose()?
.unwrap_or_default();
Ok(SandboxSpec {
kind: SandboxKind::Driver,
staged_executable: staged.into(),
helper: helper.into(),
mounts,
args: vec![],
environment: vec![],
environment,
sealed_tools: vec![],
network: manifest.network,
limits: Some(ResourceLimits {
Expand All @@ -909,7 +912,7 @@ pub struct DriverProvider {
cpu_accounting: Option<Arc<dyn SandboxCpuAccounting>>,
operation_cpu_gate: Mutex<()>,
_staged: Arc<StagedFile>,
#[cfg(unix)]
#[cfg(any(unix, windows))]
_loopback: Option<Arc<loopback::LoopbackProxy>>,
#[cfg(unix)]
_tools: Vec<SealedTool>,
Expand Down Expand Up @@ -941,11 +944,27 @@ impl DriverProvider {

// Bind trust checks to the exact staged file that will execute.
let _ = verify_owned_executable(&staged_path, &manifest.sha256)?;
let spec = sandbox_spec_windows(&manifest, &staged_path, helper, roots)?;
let loopback = match manifest.loopback_port {
Some(port) => Some(loopback::start(state, port).await?),
None => None,
};
let spec = sandbox_spec_windows(
&manifest,
&staged_path,
helper,
roots,
loopback.as_deref().map(loopback::LoopbackProxy::pipe_path),
)?;
let mut child = semwright_platform_services::sandbox_spawn(&spec)?;
let process_id = child
.id()
.ok_or_else(|| Error::new(ErrorCode::Internal, "Driver child has no PID"))?;
if let Some(proxy) = &loopback
&& let Err(error) = proxy.activate(process_id).await
{
let _ = child.kill().await;
return Err(error);
}
let cpu_accounting = child.cpu_accounting();
#[cfg(target_os = "windows")]
if manifest.resources.operation_cpu_seconds != 0 && cpu_accounting.is_none() {
Expand Down Expand Up @@ -1113,13 +1132,17 @@ impl DriverProvider {
let monitor_closed = closed.clone();
let monitor_terminate = terminate.clone();
let monitor_staged = staged.clone();
let monitor_loopback = loopback.clone();
tokio::spawn(async move {
tokio::select! {
_ = monitor_terminate.cancelled() => {
let _ = child.kill().await;
}
_ = child.wait() => {}
}
if let Some(proxy) = monitor_loopback {
proxy.shutdown();
}
monitor_closed.cancel();
drop(monitor_staged);
});
Expand All @@ -1138,6 +1161,7 @@ impl DriverProvider {
cpu_accounting,
operation_cpu_gate: Mutex::new(()),
_staged: staged,
_loopback: loopback,
}))
}
#[cfg(unix)]
Expand Down Expand Up @@ -1352,13 +1376,17 @@ impl DriverProvider {
let monitor_closed = closed.clone();
let monitor_terminate = terminate.clone();
let monitor_staged = staged.clone();
let monitor_loopback = loopback.clone();
tokio::spawn(async move {
tokio::select! {
_ = monitor_terminate.cancelled() => {
let _ = child.kill().await;
}
_ = child.wait() => {}
}
if let Some(proxy) = monitor_loopback {
proxy.shutdown();
}
monitor_closed.cancel();
drop(monitor_staged);
});
Expand Down
Loading
Loading