diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 20894f750..e4d7fec95 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -88,6 +88,7 @@ tests\scripts\run_test_configs.ps1 # All test configs via wxc_test_dr tests\scripts\run_basicprocess_test.ps1 # Single process container test tests\scripts\run_isolation_session_tests.ps1 # IsolationSession one-shot E2E (requires host with the OS-side IsoSessionOps service) tests\scripts\run_isolation_session_state_aware_tests.ps1 # IsolationSession state-aware lifecycle E2E (multi-invocation provision/start/exec/stop/deprovision, same host requirements) +tests\scripts\run_windows_sandbox_one_shot_tests.ps1 # Windows Sandbox one-shot E2E (fresh disposable VM per test; requires the Windows Sandbox optional feature) tests\scripts\run_lxc_all_tests.sh # All LXC tests (Linux) tests\scripts\run_bwrap_all_tests.sh # All Bubblewrap tests (Linux, requires bwrap) @@ -108,7 +109,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` |---------|--------|----------|--------| | AppContainer | `wxc-exec.exe` | Windows | `backends/appcontainer/common/src/appcontainer_runner.rs` | | BaseContainer (OS sandbox API) | `wxc-exec.exe` | Windows | `backends/appcontainer/common/src/base_container_runner.rs` — calls `Experimental_CreateProcessInSandbox` via FlatBuffer | -| Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/common/src/windows_sandbox_runner.rs` | +| Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/lifecycle/src/` — transient one-shot `WindowsSandboxRunner` (`ScriptRunner`). Experimental — requires `--experimental`. Each invocation launches a **fresh, disposable** Windows Sandbox VM, runs the script once, and guarantees teardown on every exit path (no warm-VM reuse; the legacy `experimental.windows_sandbox.idleTimeoutMs`/`daemonPipeName` fields are accepted but ignored, with a warning only for non-default values). The boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `lifecycle/src/vm.rs`; per-launch handshake is a 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (`windows_sandbox_common::auth`), so the guest pairs sockets by declared role, not accept order. Feature detection is by `WindowsSandbox.exe` presence (no elevated DISM query). The guest agent `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. (State-aware lifecycle + host daemon land in a follow-up.) | | MicroVM (NanVix) | `wxc-exec.exe` | Windows | `backends/nanvix/runner/src/lib.rs` — feature-gated behind `microvm` | | Hyperlight | `wxc-exec.exe` | Windows | `backends/hyperlight/common/src/lib.rs` — Hyperlight + Unikraft micro-VM backend | | IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` — feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession` `IsoSessionOps` API (loaded from `IsoSessionApp.dll`). Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Honors `readwritePaths` and `readonlyPaths` at provision via `ShareFolderBatchAsync` (rejects `deniedPaths` since the API has no Deny ACE primitive); filesystem policy is immutable post-provision and rejected at later phases. State-aware additionally accepts an optional `user` bundle (`upn`, `wamToken`) at provision and start to provision Entra cloud-agent sandboxes; one-shot rejects the bundle, and hosts that don't support Entra agents surface `backend_unavailable`. Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. | @@ -189,16 +190,16 @@ The workspace is organized into six top-level directories under `src/`: | Directory | Purpose | Examples | |-----------|---------|----------| | `core/` | Cross-platform foundation + per-platform aggregator binaries | `wxc_common/`, `wxc/`, `lxc/`, `mxc_darwin/`, `mxc_engine/`, `mxc-sdk/`, `mxc_pty/`, `mxc_build_common/`, `generated/` | -| `backends/` | Backend-specific code (one subfolder per containment backend) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common}`, `isolation_session/{bindings,common}`, `hyperlight/common`, `nanvix/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` | +| `backends/` | Backend-specific code (one subfolder per containment backend) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `hyperlight/common`, `nanvix/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` | | `ffi/` | Foreign-function-interface crates (C ABI for language bindings) | `mxc_ffi/` | | `host/` | Host-side utilities | `wxc_host_prep/`, `wxc_winhttp_proxy_shim/` | | `testing/` | Test infrastructure crates | `wxc_e2e_tests/`, `wxc_test_driver/`, `wxc_test_proxy/`, `linux_test_proxy/`, `wxc_ui_probe/`, `fuzz/` | | `tools/` | Developer/diagnostic tools | `mxc_diagnostic_console/` | - `wxc_common` is the **cross-platform foundation**: config parsing, models, errors, logger, `ScriptRunner` / `StatefulSandboxBackend` traits, state-aware dispatch helpers, validators, ids, ui-policy, encoding. Plus a few thin Windows API helpers shared by host tools and backends (`process_util`, `string_util`, `filesystem_dacl`, `diagnostic`). It must not depend on any `backends/*` crate. -- Each Windows containment backend lives in its own `backends/*/common` crate (e.g. `appcontainer_common`, `windows_sandbox_common`, `isolation_session_common`, `hyperlight_common`, `nanvix_runner`). Backend crates depend on `wxc_common`; there are no cross-edges between backend crates. +- Each Windows containment backend lives in its own `backends/*/common` crate (e.g. `appcontainer_common`, `windows_sandbox_common`, `isolation_session_common`, `hyperlight_common`, `nanvix_runner`). Backend crates depend on `wxc_common`; there are no cross-edges between backend crates. Windows Sandbox additionally has `windows_sandbox_lifecycle`, which owns the live one-shot runner and depends on `windows_sandbox_common` for the wire protocol. - `wxc`, `lxc`, and `mxc_darwin` are thin binary crates (`wxc-exec` / `lxc-exec` / `mxc-exec-mac`) that wire up CLI args (`clap`), load/validate config, handle maintenance modes (`--probe`, `--delete`, `--setup-*`, `--audit`), and **delegate all backend dispatch to `mxc_engine`**. They contain no `match request.containment` of their own. `wxc-exec` additionally owns the Windows Ctrl-C / DACL-cleanup / `--audit` PLM-trace / telemetry orchestration around the engine call. -- `mxc_engine` is the **single execution engine** — the one home for "given an `ExecutionRequest`, run it". It owns: run-to-completion backend selection (`run` / `resolve_runner`, covering **all** backends, incl. the Windows ProcessContainer BaseContainer/AppContainer BFS/DACL fallback tiers via `appcontainer_common::dispatcher::dispatch_with_fallback`, and every experimental backend, feature-gated); streaming (`spawn` → `Box`); state-aware lifecycle dispatch (`run_state_aware`); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request`, `SandboxPolicy` + sections, `available_tools_policy`/`user_profile_policy`/`temporary_files_policy`). It depends on the backend crates (cfg-split: appcontainer/windows_sandbox/isolation_session/wslc/nanvix on Windows, bubblewrap/lxc/nanvix on Linux, seatbelt on macOS) so it can't live in `wxc_common`. Both the executor binaries and `mxc-sdk` call into it. `ResolvedRunner` carries the boxed runner plus (Windows only) the optional `DaclManager` guard, so `wxc-exec` can park the guard for its signal handler. +- `mxc_engine` is the **single execution engine** — the one home for "given an `ExecutionRequest`, run it". It owns: run-to-completion backend selection (`run` / `resolve_runner`, covering **all** backends, incl. the Windows ProcessContainer BaseContainer/AppContainer BFS/DACL fallback tiers via `appcontainer_common::dispatcher::dispatch_with_fallback`, and every experimental backend, feature-gated); streaming (`spawn` → `Box`); state-aware lifecycle dispatch (`run_state_aware`); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request`, `SandboxPolicy` + sections, `available_tools_policy`/`user_profile_policy`/`temporary_files_policy`). It depends on the backend crates (cfg-split: appcontainer/windows_sandbox lifecycle/isolation_session/wslc/nanvix on Windows, bubblewrap/lxc/nanvix on Linux, seatbelt on macOS) so it can't live in `wxc_common`. Both the executor binaries and `mxc-sdk` call into it. `ResolvedRunner` carries the boxed runner plus (Windows only) the optional `DaclManager` guard, so `wxc-exec` can park the guard for its signal handler. - `mxc-sdk` is the **public Rust SDK** — a thin facade over `mxc_engine`. Build a `SandboxRequest` with `build_request`, then either `run(request)` (run-to-completion; returns an `Output` with the `WaitOutcome` + captured `stdout`/`stderr`) or `spawn_sandbox(request)` (returns a `Sandbox` handle for live bidirectional stdio — `take_stdin`/`take_stdout`/`take_stderr`, `kill()`, `wait()` returning a `WaitOutcome` (`Exited(i32)` / `TimedOut`) as `io::Result`, or `wait_with_output()`). It re-exports the engine's config-building surface (`build_request`, `mxc_sdk::policy::{SandboxPolicy sections}`, discovery helpers) and `platform_support`; `mod sandbox` (wrapping the engine's `SandboxProcess` in `Sandbox`) is its only local module. No pty is ever allocated. Streaming supports Seatbelt (macOS), Bubblewrap (Linux), and Windows ProcessContainer (AppContainer + BaseContainer); other backends return `ErrorCode::UnsupportedContainment`. - The lower-level execution surface lives in `wxc_common::sandbox_process`: the `SandboxBackend` trait (`validate` + `spawn(request, logger, StdioMode) -> Box` + a `diagnose_exit` hook) and the generic `Runner` adapter that bridges any `SandboxBackend` to the run-to-completion `ScriptRunner` (via `spawn(StdioMode::Inherit)` then `wait()`). `StdioMode::Pipes` hands the caller live stdin/stdout/stderr (what the `mxc-sdk` streaming path uses); `StdioMode::Inherit` lets the child inherit the host's stdio (what the executor binaries use, preserving the TTY under a pty). `SandboxBackend` is implemented for Seatbelt, Bubblewrap, and Windows ProcessContainer. - `mxc_ffi` (`ffi/mxc_ffi`, `crate-type = ["cdylib", "staticlib", "lib"]`) is a flat, panic-safe **C ABI over `mxc-sdk`** for language bindings. `mxc_run(policyJson, command, out)` runs a sandbox to completion, filling a `#[repr(C)] MxcRunResult` (status + exit_code + timed_out + owned stdout/stderr/error C strings); every entry point is `catch_unwind`-wrapped so a panic becomes a status code, never an unwind. Its `build.rs` runs **csbindgen** to generate the C# P/Invoke (`sdk/dotnet/Microsoft.Mxc.Sdk/Native/NativeMethods.g.cs`), gated behind the crate's **`dotnetsdk`** feature (off by default, so the whole-workspace backend build matrix doesn't compile csbindgen). The generated file is **not committed** (gitignored); the C# csproj regenerates it at build time and `scripts/check-dotnet-bindings-codegen.js` runs the codegen in CI and asserts the expected entry points are produced. The C ABI is **not a stable external contract** (native + binding are co-versioned and generated together; see the crate docs). Currently exposes run-to-completion only; streaming + state-aware over FFI are deferred. diff --git a/docs/windows-sandbox/windows-sandbox-reference.md b/docs/windows-sandbox/windows-sandbox-reference.md index d1f9672fa..27aabf0cc 100644 --- a/docs/windows-sandbox/windows-sandbox-reference.md +++ b/docs/windows-sandbox/windows-sandbox-reference.md @@ -1,211 +1,248 @@ -# Windows Sandbox Backend — Reference +# Windows Sandbox One-Shot Backend - Reference -Detailed reference for the sandbox backend internals. For the high-level design overview, see [windows-sandbox.md](windows-sandbox.md). +This document describes the one-shot implementation. For the high-level +overview, see [windows-sandbox.md](windows-sandbox.md). -## IPC Protocol +## Host-to-Guest Protocol -### wxc-exec ↔ Daemon (line-based TCP) +The host opens four TCP connections to the guest: -The daemon listens on a localhost TCP port derived deterministically from the pipe name: +| Channel | Purpose | +|---|---| +| Control | Protocol preamble and JSON control messages | +| Stdin | Child standard input | +| Stdout | Child standard output | +| Stderr | Child standard error | -```rust -fn pipe_name_to_port(name: &str) -> u16 { - let hash: u32 = name.bytes() - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)); - let range = 65535 - 49152; - 49152 + (hash % range) as u16 -} +### Authentication and channel roles + +Before any protocol data, each connection sends: + +```text +[32-byte per-launch nonce][1-byte ChannelRole] ``` -**Protocol:** -- Client → Daemon: `EXEC \n` -- Daemon → Client: `RESULT \n` -- Daemon → Client: `ERROR \n` +The nonce is generated for each VM launch, written into the secured rendezvous +directory, consumed and deleted by the guest, and compared without +data-dependent early exit. The role byte identifies control, stdin, stdout, or +stderr, so sockets are paired by declaration rather than TCP accept order. -### Daemon ↔ Agent (length-prefixed JSON over TCP) +Unauthenticated, duplicate-role, stalled, or unexpected-role connections are +dropped. Same-user processes remain within the backend's trust boundary. -4 TCP connections: control channel + stdin + stdout + stderr. +### Control preamble -Control channel frame format: `[4 bytes: u32 LE length][JSON payload]` +The guest begins the control channel with: -| Message | Direction | Purpose | -|---------|-----------|---------| -| `Ready` | Agent → Host | Agent ready for EXEC commands | -| `Exec(ExecRequest)` | Host → Agent | Execute a script | -| `Exit(ExitNotification)` | Agent → Host | Script finished | -| `StreamsReady` | Agent → Host | New data streams ready for next execution | -| `Ping` / `Pong` | Either | Keepalive | - -## Sandbox VM Setup - -### Folder Mapping - -| Host path | Sandbox path | Access | Contents | -|-----------|-------------|--------|----------| -| Daemon's exe directory | `C:\sandbox-guest` | Read-only | `wxc-windows-sandbox-guest.exe` | -| `%TEMP%\wxc-sandbox-rendezvous` | `C:\sandbox-rendezvous` | Read-write | `rendezvous.txt`, `bootstrap.cmd`, `bootstrap.log` | -| Host Python directory | `C:\sandbox-python` | Read-only | Host's Python installation | - -### Bootstrap Sequence - -The `.wsb` LogonCommand runs `C:\sandbox-rendezvous\bootstrap.cmd`: - -1. Adds `C:\sandbox-python` and `C:\sandbox-python\Scripts` to PATH -2. Sets `PYTHONDONTWRITEBYTECODE=1` and `PYTHONNOUSERSITE=1` -3. Logs diagnostics to `bootstrap.log` -4. Launches `wxc-windows-sandbox-guest.exe` - -### .wsb Configuration - -```xml - - - - {agent_dir} - C:\sandbox-guest - true - - - {rendezvous_dir} - C:\sandbox-rendezvous - false - - - {python_dir} - C:\sandbox-python - true - - - - C:\sandbox-rendezvous\bootstrap.cmd - - Disable - Enable - +```text +["WSBP"][protocol version: u32 little-endian] ``` -`vGPU` is disabled to avoid intermittent GPU virtualization failures under nested Hyper-V. +The version is incremented only for incompatible framing, preamble, or required +message changes. A magic or version mismatch rejects the connection before +framed messages are processed. -## Agent Startup & Rendezvous +### Control messages -1. Agent binds TCP on `0.0.0.0:0` (OS-assigned port) -2. Discovers its IP via UDP "fake connect" to `1.1.1.1:80` -3. Writes `:` to `C:\sandbox-rendezvous\rendezvous.txt` -4. Accepts 4 TCP connections from the daemon -5. Locks down Windows Firewall via `netsh` — only allows host IP -6. Sends `Ready` on control channel -7. Enters command loop +Control messages use a four-byte little-endian length followed by JSON. -## Python in the Sandbox +| Message | Direction | Purpose | +|---|---|---| +| `Ready` | Guest to host | Guest is ready for execution | +| `Exec(ExecRequest)` | Host to guest | Execute one command | +| `Exit(ExitNotification)` | Guest to host | Report completion | +| `StreamsReady` | Guest to host | Protocol support for reconnectable data streams; unused for one-shot reuse | +| `Ping` / `Pong` | Either | Liveness | + +The one-shot host pumps stdin, stdout, stderr, and control concurrently to avoid +opposing TCP-window deadlocks. A reset before any output is treated as an empty +stream; a reset after output is a transport failure so partial output is not +silently truncated. + +## Per-Run Host State + +Each invocation creates: + +```text +%TEMP%\wxc-wsb\oneshot\\ + oneshot.marker + config\ + wxc-windows-sandbox.wsb + rendezvous\ + bootstrap.cmd + bootstrap.log + nonce.bin + rendezvous.txt +``` -Python is **not installed** inside the sandbox — the host's Python directory is mapped read-only. +The root and run directory receive owner-only ACLs. A directory owned by +another user is rejected because its owner retains implicit `WRITE_DAC` even +after an ACL replacement. -### Discovery (`find_host_python()`) +The marker records: -1. `where python` — skips Windows Store stubs (`WindowsApps`), verifies `python --version` -2. Hardcoded paths: `C:\Python312`, `C:\Python311`, `C:\Python310`, `C:\Program Files\Python31x` -3. User-scoped: `%LOCALAPPDATA%\Programs\Python\*` +- the `wxc-exec` PID and process creation time; +- VM host-process PID and creation-time proof captured after launch. -### Read-Only Mount Workaround +PID plus creation time prevents a recycled PID from authorising cleanup of an +unrelated process. -Python's `site` module writes `.pyc` cache to its install dir. Read-only mount causes exit code 1. Mitigated by: -- `bootstrap.cmd` sets `PYTHONDONTWRITEBYTECODE=1` and `PYTHONNOUSERSITE=1` -- Test configs use `python -S -B -c "..."` +## VM Configuration -### Other Languages +The generated `.wsb` file always maps: -The sandbox runs any command through `cmd.exe /C