Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,15 @@ 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,lifecycle}`, `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 or backend support component) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `learning_mode/windows`, `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/`, `unix_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. Windows Sandbox additionally has `windows_sandbox_lifecycle`, which owns the one-shot and state-aware runners and depends on `windows_sandbox_common` for the wire protocol, plus separate daemon and guest binaries.
- `learning_mode_windows` (`backends/learning_mode/windows`) is a Windows-only backend support crate for the AppInfo-brokered Learning Mode APIs in `processmodel.dll`. It runtime-resolves the Learning Mode trace and process security-environment exports, owns their typed handle/lifecycle wrappers, and depends only on `wxc_common`; runner integration consumes it from the AppContainer backend layer.
- `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<dyn SandboxProcess>`); state-aware lifecycle dispatch (`run_state_aware`, including Windows Sandbox and IsolationSession); 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`.
Expand Down
12 changes: 12 additions & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"backends/nanvix/build_common",
"backends/nanvix/binaries",
"backends/nanvix/runner",
"backends/learning_mode/windows",
Comment thread
richiemsft marked this conversation as resolved.
"backends/lxc/common",
"backends/bubblewrap/common",
"backends/wslc/common",
Expand Down Expand Up @@ -109,6 +110,7 @@ windows_sandbox_common = { path = "backends/windows_sandbox/common" }
windows_sandbox_lifecycle = { path = "backends/windows_sandbox/lifecycle" }
isolation_session_common = { path = "backends/isolation_session/common" }
hyperlight_common = { path = "backends/hyperlight/common" }
learning_mode_windows = { path = "backends/learning_mode/windows" }
nanvix_runner = { path = "backends/nanvix/runner" }
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
Expand Down
17 changes: 17 additions & 0 deletions src/backends/learning_mode/windows/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "learning_mode_windows"
Comment thread
richiemsft marked this conversation as resolved.
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
thiserror = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
wxc_common = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }

[target.'cfg(target_os = "windows")'.dev-dependencies]
sandbox_spec = { workspace = true }
flatbuffers = { workspace = true }
232 changes: 232 additions & 0 deletions src/backends/learning_mode/windows/examples/lm_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! End-to-end validation for the Learning Mode capture lifecycle, independent of the
//! MXC runner and the `captureDenials` config.
//!
//! It drives the full 2-phase sequence against a real child process:
//!
//! 1. build a minimal FlatBuffer sandbox spec with the `permissiveLearningMode`
//! capability (the token the OS learning-mode path recognises),
//! 2. [`CaptureSession::begin`] β€” create the security environment + start the trace,
//! 3. launch `cmd.exe` inside the environment via
//! `CreateProcessAsUserInsideSecurityEnvironment`,
//! 4. wait for it to exit,
//! 5. [`CaptureSession::finish`] β€” seal the ETL to a temp path + close the environment,
//! 6. assert the ETL file was produced (non-empty).
//!
//! Run on a feature-enabled Windows build (elevated):
//!
//! ```text
//! cargo run -p learning_mode_windows --example lm_capture
//! ```
//!
//! Exit codes: `0` = ETL produced; `2` = API unavailable / off-feature build; `1` = a
//! step failed.

#[cfg(not(target_os = "windows"))]
fn main() {
eprintln!("lm_capture is Windows-only");
std::process::exit(2);
}

#[cfg(target_os = "windows")]
fn main() {
std::process::exit(windows_impl::run());
}

#[cfg(target_os = "windows")]
mod windows_impl {
use std::path::PathBuf;

use flatbuffers::FlatBufferBuilder;
use learning_mode_windows::{
CaptureSession, LearningModeApi, SecurityEnvironmentApi,
PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE,
};
use sandbox_spec::base_container_layout::{
finish_sandbox_spec_buffer, SandboxSpec, SandboxSpecArgs,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{
GetExitCodeProcess, WaitForSingleObject, INFINITE, PROCESS_INFORMATION, STARTUPINFOW,
};

/// Matches the schema version BaseContainer embeds in every spec payload.
const SANDBOX_SPEC_VERSION: &str = "0.1.0";

/// Build a minimal FlatBuffer `SandboxSpec` carrying the learning-mode capability.
fn build_sandbox_spec() -> Vec<u8> {
let mut builder = FlatBufferBuilder::with_capacity(256);
let version = builder.create_string(SANDBOX_SPEC_VERSION);
// `permissiveLearningMode` is the capability the SandboxEngine functest uses to
// exercise the learning-mode trace; it reliably drives recorded events.
let capabilities = builder.create_string("permissiveLearningMode");
let spec = SandboxSpec::create(
&mut builder,
&SandboxSpecArgs {
version: Some(version),
app_container: true,
capabilities: Some(capabilities),
..Default::default()
},
);
finish_sandbox_spec_buffer(&mut builder, spec);
builder.finished_data().to_vec()
}

/// Null-terminated, mutable UTF-16 command line for the child.
fn wide_command_line() -> Vec<u16> {
let cmd = r#"cmd.exe /c echo Hello from the learning-mode sandbox & whoami"#;
cmd.encode_utf16().chain(std::iter::once(0)).collect()
}

fn etl_output_path() -> PathBuf {
std::env::temp_dir().join(format!("lm_capture_{}.etl", std::process::id()))
}

pub fn run() -> i32 {
let secenv_api = match SecurityEnvironmentApi::load() {
Ok(api) => api,
Err(e) => {
eprintln!("SecurityEnvironmentApi::load failed (off-feature build?): {e}");
return 2;
}
};
let learning_mode_api = match LearningModeApi::load() {
Ok(api) => api,
Err(e) => {
eprintln!("LearningModeApi::load failed (off-feature build?): {e}");
return 2;
}
};

let spec = build_sandbox_spec();
println!("built sandbox spec: {} bytes", spec.len());

let session = match CaptureSession::begin(
secenv_api,
learning_mode_api,
&spec,
PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE,
) {
Ok(session) => session,
Err(e) => {
eprintln!("CaptureSession::begin failed: {e}");
return 1;
}
};
println!("CaptureSession::begin OK β€” environment + trace live");

let exit_code = match launch_and_wait(&secenv_api, session.environment()) {
Ok(code) => {
println!("child exited with code {code}");
code
}
Err(e) => {
eprintln!("launch failed: {e}");
// `session` drops here β†’ trace discarded + environment closed.
return 1;
}
};
let _ = exit_code;

let etl_path = etl_output_path();
if let Err(e) = session.finish(Some(&etl_path)) {
eprintln!("CaptureSession::finish failed: {e}");
return 1;
}
println!("CaptureSession::finish OK β€” trace sealed, environment closed");

match std::fs::metadata(&etl_path) {
Ok(meta) => {
println!(
"ETL produced: {} ({} bytes)",
etl_path.display(),
meta.len()
);
if meta.len() == 0 {
eprintln!("ETL validation failed: file is empty");
1
} else {
0
}
}
Err(e) => {
eprintln!("expected ETL at {} but none found: {e}", etl_path.display());
1
}
}
}

/// Launch the child inside `environment` and wait for it to exit, returning its exit
/// code.
fn launch_and_wait(
Comment thread
richiemsft marked this conversation as resolved.
secenv_api: &SecurityEnvironmentApi,
environment: HANDLE,
) -> Result<u32, String> {
let launch = secenv_api.launch_fn();
let mut cmd = wide_command_line();

// SAFETY: a zeroed STARTUPINFOW with only `cb` set is valid; the child inherits
// the caller's console for stdio (no STARTF_USESTDHANDLES).
let mut startup_info: STARTUPINFOW = unsafe { std::mem::zeroed() };
startup_info.cb = u32::try_from(std::mem::size_of::<STARTUPINFOW>())
.map_err(|_| "STARTUPINFOW size overflow".to_string())?;
let mut process_information: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };

// SAFETY: `launch` was resolved from processmodel.dll and matches the declared C
// signature. `cmd` is a mutable, null-terminated UTF-16 buffer; `startup_info`
// and `process_information` are valid; `environment` is the live handle from the
// session. `lpEnvironment` is null, so CREATE_UNICODE_ENVIRONMENT is not needed.
let ok = unsafe {
launch(
HANDLE(std::ptr::null_mut()), // userToken: caller context
std::ptr::null(), // applicationName (from command line)
cmd.as_mut_ptr(), // commandLine
0, // creationFlags
std::ptr::null(), // environment
std::ptr::null(), // currentDirectory
&startup_info,
environment,
&mut process_information,
)
};
if ok == 0 {
// SAFETY: reads the calling thread's last-error slot.
let err = unsafe { windows::Win32::Foundation::GetLastError() };
return Err(format!(
"CreateProcessAsUserInsideSecurityEnvironment failed (GetLastError = {})",
err.0
));
}

// SAFETY: `hProcess` is a valid process handle returned by the launch.
let wait = unsafe { WaitForSingleObject(process_information.hProcess, INFINITE) };
let result = if wait == WAIT_OBJECT_0 {
let mut exit_code: u32 = 0;
// SAFETY: `hProcess` is valid and the process has signalled exit.
unsafe { GetExitCodeProcess(process_information.hProcess, &mut exit_code) }
.map(|()| exit_code)
.map_err(|e| format!("GetExitCodeProcess failed: {e}"))
} else if wait == WAIT_FAILED {
// SAFETY: reads the last-error value set by WaitForSingleObject.
let err = unsafe { windows::Win32::Foundation::GetLastError() };
Err(format!(
"WaitForSingleObject failed (GetLastError = {})",
err.0
))
} else {
Err(format!(
"WaitForSingleObject returned unexpected status: {wait:?}"
))
};

// SAFETY: both handles were returned by the launch and are not used again.
unsafe {
let _ = CloseHandle(process_information.hThread);
let _ = CloseHandle(process_information.hProcess);
}
result
}
}
56 changes: 56 additions & 0 deletions src/backends/learning_mode/windows/examples/lm_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Manual validation probe for the Learning Mode trace + security-environment API.
//!
//! Prints whether `processmodel.dll` on this machine exposes the Learning Mode trace
//! exports (`StartLearningModeTrace` / `StopLearningModeTrace`) and the 2-phase
//! security-environment exports (`CreateProcessSecurityEnvironment` /
//! `CreateProcessAsUserInsideSecurityEnvironment` / `CloseProcessSecurityEnvironment`),
//! reporting the exact resolved name for each (plain vs `Experimental_`). Intended to
//! be run on a feature-enabled Windows build to confirm the runtime FFI resolves
//! against the real API.
//!
//! ```text
//! cargo run -p learning_mode_windows --example lm_probe
//! ```

fn main() {
std::process::exit(run_probe());
}

#[cfg(target_os = "windows")]
fn run_probe() -> i32 {
let learning_mode_available = learning_mode_windows::is_learning_mode_api_available();
println!("is_learning_mode_api_available = {learning_mode_available}");

match learning_mode_windows::LearningModeApi::load() {
Ok(api) => println!("LearningModeApi::load = OK ({api:?})"),
Err(e) => println!("LearningModeApi::load = ERR ({e})"),
}

let secenv_available = learning_mode_windows::is_security_environment_api_available();
println!("is_security_environment_api_available = {secenv_available}");

let report = learning_mode_windows::probe_security_environment_exports();
println!(" create export = {:?}", report.create);
println!(" launch export = {:?}", report.launch);
println!(" close export = {:?}", report.close);

match learning_mode_windows::SecurityEnvironmentApi::load() {
Ok(api) => println!("SecurityEnvironmentApi::load = OK ({api:?})"),
Err(e) => println!("SecurityEnvironmentApi::load = ERR ({e})"),
}

if learning_mode_available && secenv_available {
0
} else {
2
}
}

#[cfg(not(target_os = "windows"))]
fn run_probe() -> i32 {
println!("is_learning_mode_api_available = false");
2
}
Loading
Loading