diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 00b313028..f8c54b4d5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,375 +1,375 @@ -# MXC (Microsoft eXecution Container) — Copilot Instructions - -## Prerequisites - -The Rust toolchain version is pinned in [`src/rust-toolchain.toml`](../src/rust-toolchain.toml) to match what CI uses (currently 1.93). The pin is honored automatically by `rustup` — running any `cargo` command from `src/` (or below) downloads and selects that channel on first use. To opt out for one-off testing on a different toolchain, use `cargo + ...` or set `RUSTUP_TOOLCHAIN`. When bumping the pinned version, bump the matching `version: 'ms-prod-1.'` lines in the two `.azure-pipelines/templates/*.Build.Job.yml` files in the same commit. - -LSP servers are configured in `.github/lsp.json` for Rust and TypeScript. Install them before use: - -``` -rustup component add rust-analyzer -npm install -g typescript-language-server typescript -``` - -Building or testing the C# SDK (`sdk/dotnet/`) additionally requires the .NET SDK (net8.0 or newer; a net8.0 target is used). - -## Build Commands - -### Full build (Windows) - -``` -build.bat # Release build for current architecture -build.bat --debug # Debug build -build.bat --all # Release build for both x64 and ARM64 -build.bat --with-microvm # Include NanVix micro-VM binaries -``` - -### Full build (Linux) - -``` -./build.sh # Release build -./build.sh --debug # Debug build -./build.sh --rust-only # Only Rust binaries, skip SDK -``` - -### Full build (macOS) - -``` -./build-mac.sh # Release build for native architecture (seatbelt backend) -./build-mac.sh --debug # Debug build -./build-mac.sh --all # Build for both aarch64 and x86_64 -./build-mac.sh --rust-only # Only Rust binaries, skip SDK -``` - -Requires Xcode Command Line Tools and Rust. Produces an unsigned `mxc-exec-mac` binary (codesigning + notarization happen at release time). Schema `0.7.0-alpha` or later required for macOS/Seatbelt backend. - -### Individual components - -``` -# Rust workspace (from src/) -cargo build --release --target x86_64-pc-windows-msvc -cargo build --release --target aarch64-pc-windows-msvc -cargo build --release -p lxc # Linux only — builds lxc-exec -cargo build --release -p mxc_darwin --target aarch64-apple-darwin # macOS only — builds mxc-exec-mac -cargo build --release -p mxc_ffi # C ABI cdylib (mxc_ffi.dll/.so/.dylib) for the C# SDK - -# TypeScript SDK (from sdk/node/) -npm install && npm run build - -# C# SDK (from sdk/dotnet/) -dotnet build Microsoft.Mxc.Sdk.slnx -``` - -### Lint and format - -``` -# Rust (from src/) -cargo fmt --all -- --check -cargo clippy --workspace --all-targets -- -D warnings -``` - -### Tests - -``` -# Rust unit tests (from src/) -cargo test --workspace -cargo test -p wxc_common # Single crate -cargo test -p wxc_common -- config_parser # Filter by test name - -# SDK (from sdk/node/) -npm test -npm run test:integration - -# C# SDK (from sdk/dotnet/) -dotnet test Microsoft.Mxc.Sdk.slnx # requires mxc_ffi built (cargo build -p mxc_ffi); resolver finds it in src/target/{debug,release} - -# Local PowerShell helpers — run from repo root, require built binaries -tests\scripts\run_test_configs.ps1 # All test configs via wxc_test_driver -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_windows_sandbox_state_aware_tests.ps1 # Windows Sandbox state-aware lifecycle E2E (provision/start/exec*/stop/deprovision; requires the Windows Sandbox optional feature; skips if absent) -tests\scripts\run_lxc_all_tests.sh # All LXC tests (Linux) -tests\scripts\run_bwrap_all_tests.sh # All Bubblewrap tests (Linux, requires bwrap) - -# E2E test crate — Rust executor integration tests (from src/) -cargo test -p wxc_e2e_tests # Invokes MXC binaries directly -cargo test -p wxc_e2e_tests -- --ignored # Include stress tests (run_on_repeat) -``` - -## Architecture - -MXC is a **sandboxed code execution system** with a Rust core and TypeScript SDK layer. - -### Containment backends - -The Rust workspace (`src/`) implements multiple sandboxing backends behind the `ScriptRunner` trait (`core/wxc_common/src/script_runner.rs`): - -| Backend | Binary | Platform | Module | -|---------|--------|----------|--------| -| 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/lifecycle/src/` (live transient one-shot `WindowsSandboxRunner` + state-aware `StatefulSandboxBackend`). Experimental — requires `--experimental`. Supports both **one-shot** (a fresh, disposable VM per invocation with guaranteed teardown, via `ScriptRunner`) and **state-aware** (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. State-aware holds a single live VM across separate `wxc-exec` phase processes behind a persistent detached host-side daemon (`backends/windows_sandbox/daemon/`); the OS enforces a single running Windows Sandbox VM per host, so the daemon owns it and reclaims an orphaned VM on restart only via positive process-identity proof. The shared boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `backends/windows_sandbox/lifecycle/src/vm.rs::launch_managed_vm`; each mode plugs in its own `LaunchObserver` for the per-caller ownership / proof bookkeeping. Honors `readwritePaths`/`readonlyPaths`/`deniedPaths` (HOST paths) at provision via `.wsb` `` entries (mapped at the same absolute host path inside the guest; rejects `deniedPaths` equal-to or nested-within a mapped share since `.wsb` has no Deny primitive); filesystem policy is immutable post-provision. Network isolation is enforced unconditionally by the in-guest agent; `network`/`ui` and the Entra `user` bundle are not honored. ID prefix `wsb` (strict `wsb:<8-hex>` grammar). Per-launch handshake: 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (boot + reconnect); the guest pairs accepted sockets by declared role, not by accept order. The guest agent binary `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. | -| 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. | -| LXC | `lxc-exec` | Linux | `core/lxc/src/main.rs` + `backends/lxc/common/` | -| Seatbelt | `mxc-exec-mac` | macOS | `core/mxc_darwin/src/main.rs` + `backends/seatbelt/common/` — uses macOS App Sandbox (Seatbelt) profiles for process containment. Requires schema `0.7.0-alpha`+. Supports `network.proxy` via the same cooperative env-var model as Bubblewrap (injects `HTTP_PROXY`/`HTTPS_PROXY` into the sandbox, reusing `wxc_common::unix_proxy_coordinator`; `builtinTestServer` spawns the shared `unix-test-proxy`). See `docs/macos-support/seatbelt-backend.md`. | -| Bubblewrap | `lxc-exec` | Linux | `backends/bubblewrap/common/src/bwrap_runner.rs` — unprivileged sandboxing via Linux user namespaces and `bwrap`. Experimental — requires `--experimental`. Uses shared filesystem/network policy fields; per-host network filtering via `NetworkIptablesManager` from `backends/lxc/common`. See `docs/bwrap-support/bubblewrap-backend.md`. | - -### Config flow - -1. User provides JSON config (file or base64) → `config_deserialize.rs` performs path-aware typed deserialization into the wire model (`wxc_common::wire`) → `config_parser.rs` validates and maps it to `ExecutionRequest` (the internal execution model in `models.rs`) -2. `ExecutionRequest` includes the containment backend selection, process config, filesystem/network policies, and optional experimental features -3. The appropriate `ScriptRunner` implementation executes the process and returns `ScriptResponse` - -### TypeScript layers - -- **SDK** (`sdk/node/`, `@microsoft/mxc-sdk`) — the public API. The one-shot surface (`spawnSandbox` / `spawnSandboxFromConfig` / `spawnSandboxAsync`) builds a `ContainerConfig` from a `SandboxPolicy`, serialises to base64, and spawns the correct native binary (`wxc-exec.exe`, `lxc-exec`, or `mxc-exec-mac`) via `node-pty`. The state-aware surface (`provisionSandbox` / `startSandbox` / `execInSandbox` / `execInSandboxAsync` / `stopSandbox` / `deprovisionSandbox`, in `sdk/node/src/state-aware.ts`) drives a sandbox through a multi-call lifecycle against `StateAwareContainmentBackend` backends; per-(backend, phase) typed `*Config` interfaces and a branded `SandboxId` live in `sdk/node/src/state-aware-types.ts`. Typed wire-format errors live in `sdk/node/src/errors.ts` (closed `ErrorCode` union plus a single `MxcError` class carrying `code: ErrorCode`, mirroring the Rust `MxcError` shape). Platform detection is in `platform.ts`. - -The SDK auto-discovers native binaries by checking `sdk/node/bin//` (npm-packaged) and `src/target//{release,debug}/` (local dev). The `build.bat`/`build.sh`/`build-mac.sh` scripts copy binaries into the SDK bin directory. - -### C# SDK - -- **C# SDK** (`sdk/dotnet/`, `Microsoft.Mxc.Sdk`) — a managed binding that P/Invokes the native `mxc_ffi` library (which wraps the Rust `mxc-sdk` → `mxc_engine`), rather than spawning an executor. `MxcSandbox.Run(policy, command)` / `RunAsync` run a command to completion and return a `RunResult` (`ExitCode`, `TimedOut`, `Stdout`, `Stderr`); policy POCOs (`SandboxPolicy`, `FilesystemPolicy`, `NetworkPolicy`, `UiPolicy`) serialize to the same camelCase JSON the native layer expects. `MxcException` carries a typed `ErrorCode` that mirrors the native `MXC_STATUS_*` codes (parity-gated by `scripts/check-dotnet-errorcode-parity.js`). `Native/NativeMethods.g.cs` is **generated** by csbindgen from the Rust FFI and is **not committed** (gitignored) — the csproj's `GenerateNativeBindings` MSBuild target regenerates it before each C# compile via `cargo build -p mxc_ffi --features dotnetsdk`, so a `dotnet build` needs the Rust toolchain on PATH. `NativeLibraryResolver` finds `mxc_ffi` via `MXC_FFI_DIR`, the assembly dir / `runtimes//native`, or `src/target/{debug,release}`. Projects: `Microsoft.Mxc.Sdk` (library), `Microsoft.Mxc.Sdk.Sample` (console), `Microsoft.Mxc.Sdk.Tests` (xUnit), in `Microsoft.Mxc.Sdk.slnx`. Beyond run-to-completion, it also exposes **streaming** (`MxcSandbox.Spawn` → `MxcSandboxProcess`: `Stream`-based stdio, `Wait`/`WaitAsync`/`Kill`) and the **state-aware lifecycle** (`MxcLifecycle.ProvisionSandbox`/`StartSandbox`/`ExecInSandbox`/`ExecInSandboxAsync`/`StopSandbox`/`DeprovisionSandbox`, with a typed `SandboxId`). - -### Schema system - -- **Stable schemas**: released, immutable schemas live in [`schemas/stable/`](../schemas/stable) (one file per released version) — never edit them after release. -- **Dev schema**: the in-progress schema lives in [`schemas/dev/`](../schemas/dev). It is **generated** from the Rust wire model (`src/core/wxc_common/src/wire.rs`) by the `mxc_schema_gen` tool — **do not hand-edit it**. To change the dev schema, edit the wire model and regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema..json`. `scripts/versioning/check-schema-codegen.js` is a CI gate that regenerates and fails if the committed schema drifts. See [`docs/schema-codegen.md`](../docs/schema-codegen.md). -- **Generated SDK wire types**: `sdk/node/src/generated/wire.ts` is **generated** from the same wire model by the `mxc_schema_gen --ts` TypeScript emitter (`wxc_common::ts_emit`, no third-party generator) — **do not hand-edit it**. It is a drift oracle (not public API); the SDK unit test `sdk/node/tests/unit/wire-conformance.test.ts` asserts the hand-written public types in `sdk/node/src/types.ts` conform to it, and `scripts/versioning/check-sdk-types-codegen.js` is a CI gate that fails if the committed file drifts. Regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/node/src/generated/wire.ts`. -- **Canonical schema-version source**: `schemas/schema-version.json` — the single source of truth for the schema-version constants (min/maxSupported/state-aware/stable/dev). `scripts/versioning/check-schema-versions.js` enforces that the Rust parser, SDK, and schema filenames all agree with it; do not hand-edit a schema-version constant without updating the canonical file. See [`docs/versioning.md`](../docs/versioning.md) for the full design. -- Config files can reference schemas via `"$schema"` for editor validation. `scripts/versioning/validate-configs.js` validates the `tests/examples` + `tests/configs` corpus against the dev schema in CI. - -### Key documentation (`docs/`) - -Core references: - -- `docs/schema.md` — full JSON configuration schema reference -- `docs/versioning.md` — schema versioning design, experimental feature lifecycle, and promotion process -- `docs/authoring-a-new-feature.md` — step-by-step guide for adding experimental features (which files to touch, in what order) -- `docs/examples.md` — annotated configuration examples (see also `tests/examples/` and `tests/configs/`) -- `docs/diagnostics.md` — diagnostic logging knobs (env vars, log file format) -- `docs/host-prep.md` — `wxc-host-prep.exe` host setup binary (`prepare-system-drive` / `unprepare-system-drive` for the AppContainer ACEs on the system-drive root, plus `prepare-null-device` / `verify-null-device` / `dump-null-device` for the `\Device\Null` security descriptor that AppContainer-based backends require). Owns elevation via embedded `requireAdministrator` manifest — `wxc-exec.exe` no longer self-elevates. -- `docs/sandbox-policy/v1/policy.md` — sandbox policy v1 specification - -Per-backend guides: - -- `docs/process-container/guide.md` — process container (Windows AppContainer / BaseContainer) -- `docs/process-container/UIPolicy_Schema.md` — UI policy schema (JOB_OBJECT_UILIMIT_* mappings) -- `docs/process-container/os-version-support.md` — per-Windows-release policy-support matrix (filesystem / network / UI) -- `docs/lxc-support/lxc-backend.md` — LXC container backend (Linux) -- `docs/macos-support/seatbelt-backend.md` — macOS Seatbelt backend -- `docs/windows-sandbox/windows-sandbox.md` / `docs/windows-sandbox/windows-sandbox-reference.md` — Windows Sandbox backend -- `docs/wsl/wsl-container-getting-started.md` / `docs/wsl/wsl-container-support-plan.md` — WSL Container (WSLC SDK) -- `docs/wsl/wslc-sdk-bindings.md` — WSLC SDK FFI bindings: `src/backends/wslc/common/src/wslcsdk_sys.rs` is **generated** by bindgen from `wslcsdk.h` (do NOT hand-edit); `wslc_bindings.rs` is a thin facade over it. On every WSLC SDK version bump, regenerate via `scripts/generate-wslc-bindings.ps1` (needs libclang + `bindgen-cli`, required only on the regen machine — normal/CI builds need neither) and commit the regenerated file with the `WSLC_SDK_VERSION` + hash change. See the doc for the full runbook. -- `docs/nanvix-microvm/nanvix.md` / `docs/nanvix-microvm/nanvix-integration-plan.md` — MicroVM via NanVix - -State-aware lifecycle: - -- `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md` — state-aware sandbox lifecycle API (cross-backend wire format, Rust `StatefulSandboxBackend` trait, and dispatcher contract) -- `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md` — companion overview to the full state-aware design -- `docs/isolation-session/initial-bringup-plan.md` — IsolationSession backend, one-shot bringup (experimental, isolated user account per execution via the OS-side service) -- `docs/isolation-session/state-aware-rust-initial-plan.md` — IsolationSession state-aware lifecycle, Rust-layer plan (per-phase config / metadata, policy honor matrix, idempotence, concurrency, error mapping) -- `docs/isolation-session/state-aware-typescript-initial-plan.md` — IsolationSession state-aware lifecycle, TypeScript SDK plan - -## Key Conventions - -### Experimental features - -New features go under the `experimental` JSON section and are only active when `--experimental` is passed. See `docs/authoring-a-new-feature.md` for the full checklist. The pattern: - -1. Add the field to the Rust wire model (`src/core/wxc_common/src/wire.rs`) under the `Experimental` section, then regenerate the dev schema (`cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema..json`) — do not hand-edit the generated schema -2. Add the matching field to the wire model's `Experimental` struct (`src/core/wxc_common/src/wire.rs`) and the domain `ExperimentalConfig` in `models.rs`, then map wire→domain in `config_parser.rs` (use `From` impls beside the domain type for trivial enum/struct conversions) -3. Guard execution behind `if request.experimental_enabled` in the runner -4. Never modify files in `schemas/stable/` — those are immutable release artifacts - -### Rust workspace structure - -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/`, `learning_mode_core/`, `generated/` | -| `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_core` is the cross-platform learning-mode denial model and output layer. It owns denial types, summaries, analyzer abstractions, plain-JSON document emission, and the serializable output-pointer type, and must not depend on any `backends/*` crate. -- `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, decodes sealed ETL traces through `learning_mode_core`, and depends on `wxc_common` plus `learning_mode_core`; 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`); state-aware lifecycle dispatch (`run_state_aware`, including Windows Sandbox and IsolationSession); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request` / `build_request_with_containment`, `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`, warnings, and optional structured output metadata) 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`, `output_metadata()` after terminal completion, or `wait_with_output()`). It re-exports the engine's config-building surface (`build_request`, `build_request_with_containment` + `Containment`/`WslcSection`, `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), Windows ProcessContainer (AppContainer + BaseContainer), and WSLC (Windows, experimental — needs the crate's `wslc` feature plus `SandboxRequest::set_experimental(true)`; no stdin and `id() == 0`, since the WSLC SDK exposes neither); 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()`). `SandboxProcess::output_metadata()` carries backend-produced structured outputs after terminal teardown without writing to process-global stdio. `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, Windows ProcessContainer, and WSLC (on `wslc_common::WSLContainerRunner` itself, which shares one container lifecycle — `start_container` — between its streaming `SandboxBackend` and run-to-completion `ScriptRunner` impls, differing only in where the WSLC SDK's output callbacks write). -- `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/output-metadata 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). It exposes three surfaces: **run-to-completion** (`mxc_run`), **streaming** (`mxc_spawn` → opaque `MxcSandbox` handle; `mxc_stream_read`/`write`/`flush`, `mxc_sandbox_take_stdin`/`stdout`/`stderr`, `mxc_sandbox_id`/`try_wait`/`wait`/`kill`/`output_metadata_json`/`free`, in `src/streaming.rs`), and the **state-aware lifecycle** (`mxc_state_aware` for the envelope phases + `mxc_state_aware_exec` returning a live streaming handle, in `src/state_aware.rs`). All three `.rs` files are csbindgen inputs in `build.rs`; the `MXC_STATUS_*` space already reserves the state-aware phase codes. -- `mxc_pty` is the shared pty bridge used by the LXC backend (`lxc_common::lxc_bindings::attach_run`) so the inner shell sees a real TTY and host stdio is streamed live. (Seatbelt and Bubblewrap no longer use it: they spawn directly and let the child inherit the host's stdio — a TTY when the executor binary runs under a pty — via `SandboxBackend::spawn(StdioMode::Inherit)`.) -- `learning_mode_core` is the **cross-platform learning-mode / captureDenials model + output emitter**: `DeniedResource` (+ `ResourceType`/`AccessType`), `DenialSummary`, the `DenialAnalyzer` decode trait, and `emit` — which writes the on-disk denials deliverable as a **single JSON document** `{ "denials": [...], "summary": {...} }` (`write_document` / `DenialsDocument`) and defines the serializable `DenialsOutputPointer`. It carries no OS-specific code (must not depend on any `backends/*` crate); the Windows ETL decoder implementing `DenialAnalyzer` lives in `backends/learning_mode/windows`. When `processContainer.captureDenials` is set, the BaseContainer runner seals a unique internal ETL temp, decodes it via that backend with bounded event/unique-denial processing, writes the JSON file (caller's `outputPath` with a unique per-run identifier stamped into the stem, e.g. `denials..json`, or a managed temp), deletes the ETL, and returns neutral `wxc_common` output metadata. `wxc-exec` serializes that metadata as the one-line stderr pointer at the CLI boundary; Rust/C#/FFI callers receive it programmatically. Each denial's `resource` field holds the file path or the AppContainer capability name; capability denials resolve their capability SID to a friendly name via `backends/learning_mode/windows`'s `capability_names` (well-known `S-1-15-3-…` SID → policy name; custom hashed SIDs fall back to the SID string). -- `mxc_build_common` is a build-time helper crate — all Windows binary crates use it in their `build.rs` to embed VersionInfo (ProductName, FileDescription, copyright, version+commit). When adding a new Windows binary crate, add `mxc_build_common` as a build-dependency and call `mxc_build_common::embed_version_info()` from `build.rs` -- `nanvix_build_common` is a **build-only** helper crate (never linked into the runtime): it stages NanVix binaries next to the executable and resolves the `NANVIX_BIN` prefetch directory. The `nanvix_binaries`, `wxc`, and `lxc` build scripts consume it as a `[build-dependencies]` entry. Runtime constants it needs (binary/snapshot filenames) stay in `nanvix_common`. Keep build-only file-staging logic here, not in `nanvix_common` (which is a runtime dependency of `nanvix_runner`). -- Platform-specific modules use `#[cfg(target_os = "windows")]` / `#[cfg(target_os = "linux")]` -- Workspace edition is 2021; shared dependencies are declared in the root `Cargo.toml` `[workspace.dependencies]` - -### Config parser pattern - -The parser deserializes JSON directly into the typed wire model (`wxc_common::wire`), the single source of truth for the config shape (it also generates the JSON schema). All typed config deserialization goes through `config_deserialize.rs`, which distinguishes syntax errors from typed policy errors and adds the complete JSON path plus source line/column when available; state-aware backend errors are prefixed with their full `experimental..` location. `config_parser.rs` then maps the wire types to the validated domain structs in `models.rs`. The stable surface uses `deny_unknown_fields` (closed); the `experimental` block is permissive. - -### TypeScript conventions - -- Target ES2022, ESM modules (`module`/`moduleResolution: NodeNext`, `"type": "module"`), strict mode — relative imports use explicit `.js` extensions -- Tests use Node.js built-in test runner (`node --test`) - -### Binary naming - -- Windows: `wxc-exec.exe` (AppContainer / Windows Sandbox / MicroVM); `wxc-host-prep.exe` (host setup — see `docs/host-prep.md`) -- Linux: `lxc-exec` (LXC containers) -- macOS: `mxc-exec-mac` (Seatbelt) -- Target triples: `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`, `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` - -### Package versioning - -All Rust crates use `version.workspace = true` to inherit the version from `src/Cargo.toml` `[workspace.package]`. The npm SDK version in `sdk/node/package.json` and the C# SDK version (`` in `sdk/dotnet/Microsoft.Mxc.Sdk/Microsoft.Mxc.Sdk.csproj`) must match. Run `node scripts/check-version-sync.js` to validate they are in sync. When bumping the version, update `src/Cargo.toml` (workspace version), `sdk/node/package.json`, and the C# csproj in the same commit. - -### Keeping docs up to date - -When changing behavior covered by existing documentation, update the relevant docs in the same change: - -- **Schema changes** (adding/removing/renaming config fields) → update `docs/schema.md` and the appropriate JSON schema in `schemas/dev/` or `schemas/stable/` -- **New experimental features** → follow `docs/authoring-a-new-feature.md`, which includes schema, Rust, and test config steps -- **SDK API changes** (new exports, changed signatures, new options) → update `sdk/node/README.md` and the JSDoc in `sdk/node/src/index.ts` (TypeScript SDK); the Rust `mxc-sdk` crate docs/`README.md`; and `sdk/dotnet/README.md` (C# SDK). If the `mxc_ffi` C ABI surface changes, the C# P/Invoke regenerates on the next C# build; keep the `ErrorCode` parity + bindings-codegen gates green. -- **New containment backends or major backend changes** → update the relevant doc in `docs/` (e.g., `lxc-support/lxc-backend.md`, `windows-sandbox/windows-sandbox.md`) -- **Versioning or promotion changes** → update `docs/versioning.md` - -### Policy versioning - -The `SandboxPolicy.version` in the SDK must match a JSON schema version in the supported range (`0.6.0-alpha` minimum, `0.8.0-alpha` maximum). The SDK validates this in `sandbox.ts` — if the policy version is older than `MIN_VERSION` or newer than `SUPPORTED_VERSION` it throws. State-aware lifecycle requests use `0.6.0-alpha`. These bounds are mirrored from the canonical `schemas/schema-version.json` and enforced by `scripts/versioning/check-schema-versions.js`. See `docs/versioning.md` for the full design. - -## Creating Issues - -When creating issues in this repository, follow the structure defined by the issue templates in `.github/ISSUE_TEMPLATE/`. Every issue **must** match one of the four categories below and include the corresponding labels, issue type, and required fields. - -### Issue categories, types, and labels - -| Category | GitHub Issue Type | Labels | Template | -|----------|------------------|--------|----------| -| 🐛 Bug Report | `Bug` | `Issue-Bug`, `Needs-Triage` | `Bug_Report.yml` | -| 🚀 Feature Request / Idea | `Feature` | `Issue-Feature`, `Needs-Triage` | `Feature_Request.yml` | -| 📚 Documentation Issue | `Task` | `Issue-Docs`, `Needs-Triage` | `Documentation_Issue.yml` | -| 📋 Task | `Task` | `Issue-Task`, `Needs-Triage` | `Task.yml` | - -- Always apply `Needs-Triage` alongside the category-specific label. -- Apply exactly the labels listed above — do not invent new labels. -- When creating issues via the API, set labels and issue type explicitly — they are not applied automatically. - -### Required body structure by category - -Issues created via the API or by agents do not inherit the form layout from the YAML templates. Reproduce the structure in the issue body using the markdown skeletons below. - -**🐛 Bug Report** — use when something is broken or behaving unexpectedly: - -> ⚠️ **Security notice:** When reporting BSODs or security issues, **DO NOT** attach memory dumps, logs, or traces to GitHub issues. Instead, send them to secure@microsoft.com referencing the GitHub issue. For application crashes, include a Feedback Hub link if possible (open with Win+F, choose "Share My Feedback" after submission). - -```markdown -### Relevant area(s) - - -### Brief description of your issue - -### Steps to reproduce -1. -2. -3. - -### Expected behavior - -### Actual behavior -``` - -All five sections are **required**. - -**🚀 Feature Request / Idea** — use for new functionality or improvements: - -```markdown -### Description of the new feature / enhancement - - -### Proposed technical implementation details - -``` - -"Description of the new feature / enhancement" is **required**. Omit "Proposed technical implementation details" if there is nothing meaningful to add. - -**📚 Documentation Issue** — use when docs are incorrect, incomplete, or confusing: - -```markdown -### Brief description of your issue - -``` - -This section is **required**. - -**📋 Task** — use for actionable work items: - -```markdown -### Description of the task - - -### Additional context - -``` - -"Description of the task" is **required**. Omit "Additional context" if there is nothing meaningful to add. - -### Choosing the right category - -- Something **used to work** or **doesn't work as documented** → Bug Report -- Proposing **new behavior or capabilities** → Feature Request / Idea -- **Incorrect, missing, or unclear documentation** → Documentation Issue -- A **discrete unit of work** that doesn't fit the above → Task - -### Style guidelines - -- Use the section headers exactly as shown in the skeletons above -- Be specific and concise — avoid vague descriptions like "it doesn't work" -- For bug reports, always include concrete reproduction steps -- For feature requests, explain the *why* (user problem) before the *how* (implementation) -- Reference relevant source files, config fields, or docs when applicable -- If any required field is unknown, **ask for the information rather than fabricating content** - -## Creating Pull Requests - -Pull requests must follow the template in `.github/PULL_REQUEST_TEMPLATE.md`. Complete all checklist items and add content below the separator (`-----`). - -### Required structure - -Every PR body should include: - -1. **Template checklist** — check the boxes that apply (CLA, related issue, copilot-instructions update). -2. **Summary** — a brief description of what the PR does and why. -3. **Issue references** — if the PR is intended to close an issue, use GitHub closing keywords (`Closes #NNN`, `Fixes #NNN`, or `Resolves #NNN`). If the PR is related but does not close an issue, use an unordered list under a "Related Issues" heading (`- #NNN`). - -### Example - -```markdown -- [x] I have signed the [Contributor License Agreement](https://opensource.microsoft.com/cla/). -- [x] This pull request is related to an issue. -- [ ] If this PR changes build commands, project architecture, or key conventions, I have updated [`.github/copilot-instructions.md`](.github/copilot-instructions.md). - ------ - -## Summary - -Brief description of the change. - -Closes #42 -``` - -### Guidelines - -- One PR should address one issue or concern. Avoid bundling unrelated changes. -- If the PR updates build commands, project architecture, or key conventions, update `.github/copilot-instructions.md` in the same PR. -- Draft PRs are appropriate for work-in-progress that needs early feedback. +# MXC (Microsoft eXecution Container) — Copilot Instructions + +## Prerequisites + +The Rust toolchain version is pinned in [`src/rust-toolchain.toml`](../src/rust-toolchain.toml) to match what CI uses (currently 1.93). The pin is honored automatically by `rustup` — running any `cargo` command from `src/` (or below) downloads and selects that channel on first use. To opt out for one-off testing on a different toolchain, use `cargo + ...` or set `RUSTUP_TOOLCHAIN`. When bumping the pinned version, bump the matching `version: 'ms-prod-1.'` lines in the two `.azure-pipelines/templates/*.Build.Job.yml` files in the same commit. + +LSP servers are configured in `.github/lsp.json` for Rust and TypeScript. Install them before use: + +``` +rustup component add rust-analyzer +npm install -g typescript-language-server typescript +``` + +Building or testing the C# SDK (`sdk/dotnet/`) additionally requires the .NET SDK (net8.0 or newer; a net8.0 target is used). + +## Build Commands + +### Full build (Windows) + +``` +build.bat # Release build for current architecture +build.bat --debug # Debug build +build.bat --all # Release build for both x64 and ARM64 +build.bat --with-microvm # Include NanVix micro-VM binaries +``` + +### Full build (Linux) + +``` +./build.sh # Release build +./build.sh --debug # Debug build +./build.sh --rust-only # Only Rust binaries, skip SDK +``` + +### Full build (macOS) + +``` +./build-mac.sh # Release build for native architecture (seatbelt backend) +./build-mac.sh --debug # Debug build +./build-mac.sh --all # Build for both aarch64 and x86_64 +./build-mac.sh --rust-only # Only Rust binaries, skip SDK +``` + +Requires Xcode Command Line Tools and Rust. Produces an unsigned `mxc-exec-mac` binary (codesigning + notarization happen at release time). Schema `0.7.0-alpha` or later required for macOS/Seatbelt backend. + +### Individual components + +``` +# Rust workspace (from src/) +cargo build --release --target x86_64-pc-windows-msvc +cargo build --release --target aarch64-pc-windows-msvc +cargo build --release -p lxc # Linux only — builds lxc-exec +cargo build --release -p mxc_darwin --target aarch64-apple-darwin # macOS only — builds mxc-exec-mac +cargo build --release -p mxc_ffi # C ABI cdylib (mxc_ffi.dll/.so/.dylib) for the C# SDK + +# TypeScript SDK (from sdk/node/) +npm install && npm run build + +# C# SDK (from sdk/dotnet/) +dotnet build Microsoft.Mxc.Sdk.slnx +``` + +### Lint and format + +``` +# Rust (from src/) +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +``` + +### Tests + +``` +# Rust unit tests (from src/) +cargo test --workspace +cargo test -p wxc_common # Single crate +cargo test -p wxc_common -- config_parser # Filter by test name + +# SDK (from sdk/node/) +npm test +npm run test:integration + +# C# SDK (from sdk/dotnet/) +dotnet test Microsoft.Mxc.Sdk.slnx # requires mxc_ffi built (cargo build -p mxc_ffi); resolver finds it in src/target/{debug,release} + +# Local PowerShell helpers — run from repo root, require built binaries +tests\scripts\run_test_configs.ps1 # All test configs via wxc_test_driver +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_windows_sandbox_state_aware_tests.ps1 # Windows Sandbox state-aware lifecycle E2E (provision/start/exec*/stop/deprovision; requires the Windows Sandbox optional feature; skips if absent) +tests\scripts\run_lxc_all_tests.sh # All LXC tests (Linux) +tests\scripts\run_bwrap_all_tests.sh # All Bubblewrap tests (Linux, requires bwrap) + +# E2E test crate — Rust executor integration tests (from src/) +cargo test -p wxc_e2e_tests # Invokes MXC binaries directly +cargo test -p wxc_e2e_tests -- --ignored # Include stress tests (run_on_repeat) +``` + +## Architecture + +MXC is a **sandboxed code execution system** with a Rust core and TypeScript SDK layer. + +### Containment backends + +The Rust workspace (`src/`) implements multiple sandboxing backends behind the `ScriptRunner` trait (`core/wxc_common/src/script_runner.rs`): + +| Backend | Binary | Platform | Module | +|---------|--------|----------|--------| +| 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` — schema versions through 0.7 call `Experimental_CreateProcessInSandbox` with the SBOX FlatBuffer contract. Schema 0.8+ prefers `CreateProcessSecurityEnvironment` with PSEC when its runtime probe succeeds, temporarily falls back to SBOX when PSEC is unavailable, then retains the AppContainer tier fallback. Proxy requests use legacy SBOX only on query-less hosts; capability-aware SBOX hosts fall back to AppContainer until MXC can author the model-2 AppContainer-peer contract. `captureDenials` still requires the official V2 PSEC + Learning Mode exports and cannot use a lower tier. | +| Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/lifecycle/src/` (live transient one-shot `WindowsSandboxRunner` + state-aware `StatefulSandboxBackend`). Experimental — requires `--experimental`. Supports both **one-shot** (a fresh, disposable VM per invocation with guaranteed teardown, via `ScriptRunner`) and **state-aware** (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. State-aware holds a single live VM across separate `wxc-exec` phase processes behind a persistent detached host-side daemon (`backends/windows_sandbox/daemon/`); the OS enforces a single running Windows Sandbox VM per host, so the daemon owns it and reclaims an orphaned VM on restart only via positive process-identity proof. The shared boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `backends/windows_sandbox/lifecycle/src/vm.rs::launch_managed_vm`; each mode plugs in its own `LaunchObserver` for the per-caller ownership / proof bookkeeping. Honors `readwritePaths`/`readonlyPaths`/`deniedPaths` (HOST paths) at provision via `.wsb` `` entries (mapped at the same absolute host path inside the guest; rejects `deniedPaths` equal-to or nested-within a mapped share since `.wsb` has no Deny primitive); filesystem policy is immutable post-provision. Network isolation is enforced unconditionally by the in-guest agent; `network`/`ui` and the Entra `user` bundle are not honored. ID prefix `wsb` (strict `wsb:<8-hex>` grammar). Per-launch handshake: 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (boot + reconnect); the guest pairs accepted sockets by declared role, not by accept order. The guest agent binary `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. | +| 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. | +| LXC | `lxc-exec` | Linux | `core/lxc/src/main.rs` + `backends/lxc/common/` | +| Seatbelt | `mxc-exec-mac` | macOS | `core/mxc_darwin/src/main.rs` + `backends/seatbelt/common/` — uses macOS App Sandbox (Seatbelt) profiles for process containment. Requires schema `0.7.0-alpha`+. Supports `network.proxy` via the same cooperative env-var model as Bubblewrap (injects `HTTP_PROXY`/`HTTPS_PROXY` into the sandbox, reusing `wxc_common::unix_proxy_coordinator`; `builtinTestServer` spawns the shared `unix-test-proxy`). See `docs/macos-support/seatbelt-backend.md`. | +| Bubblewrap | `lxc-exec` | Linux | `backends/bubblewrap/common/src/bwrap_runner.rs` — unprivileged sandboxing via Linux user namespaces and `bwrap`. Experimental — requires `--experimental`. Uses shared filesystem/network policy fields; per-host network filtering via `NetworkIptablesManager` from `backends/lxc/common`. See `docs/bwrap-support/bubblewrap-backend.md`. | + +### Config flow + +1. User provides JSON config (file or base64) → `config_deserialize.rs` performs path-aware typed deserialization into the wire model (`wxc_common::wire`) → `config_parser.rs` validates and maps it to `ExecutionRequest` (the internal execution model in `models.rs`) +2. `ExecutionRequest` includes the containment backend selection, process config, filesystem/network policies, and optional experimental features +3. The appropriate `ScriptRunner` implementation executes the process and returns `ScriptResponse` + +### TypeScript layers + +- **SDK** (`sdk/node/`, `@microsoft/mxc-sdk`) — the public API. The one-shot surface (`spawnSandbox` / `spawnSandboxFromConfig` / `spawnSandboxAsync`) builds a `ContainerConfig` from a `SandboxPolicy`, serialises to base64, and spawns the correct native binary (`wxc-exec.exe`, `lxc-exec`, or `mxc-exec-mac`) via `node-pty`. The state-aware surface (`provisionSandbox` / `startSandbox` / `execInSandbox` / `execInSandboxAsync` / `stopSandbox` / `deprovisionSandbox`, in `sdk/node/src/state-aware.ts`) drives a sandbox through a multi-call lifecycle against `StateAwareContainmentBackend` backends; per-(backend, phase) typed `*Config` interfaces and a branded `SandboxId` live in `sdk/node/src/state-aware-types.ts`. Typed wire-format errors live in `sdk/node/src/errors.ts` (closed `ErrorCode` union plus a single `MxcError` class carrying `code: ErrorCode`, mirroring the Rust `MxcError` shape). Platform detection is in `platform.ts`. + +The SDK auto-discovers native binaries by checking `sdk/node/bin//` (npm-packaged) and `src/target//{release,debug}/` (local dev). The `build.bat`/`build.sh`/`build-mac.sh` scripts copy binaries into the SDK bin directory. + +### C# SDK + +- **C# SDK** (`sdk/dotnet/`, `Microsoft.Mxc.Sdk`) — a managed binding that P/Invokes the native `mxc_ffi` library (which wraps the Rust `mxc-sdk` → `mxc_engine`), rather than spawning an executor. `MxcSandbox.Run(policy, command)` / `RunAsync` run a command to completion and return a `RunResult` (`ExitCode`, `TimedOut`, `Stdout`, `Stderr`); policy POCOs (`SandboxPolicy`, `FilesystemPolicy`, `NetworkPolicy`, `UiPolicy`) serialize to the same camelCase JSON the native layer expects. `MxcException` carries a typed `ErrorCode` that mirrors the native `MXC_STATUS_*` codes (parity-gated by `scripts/check-dotnet-errorcode-parity.js`). `Native/NativeMethods.g.cs` is **generated** by csbindgen from the Rust FFI and is **not committed** (gitignored) — the csproj's `GenerateNativeBindings` MSBuild target regenerates it before each C# compile via `cargo build -p mxc_ffi --features dotnetsdk`, so a `dotnet build` needs the Rust toolchain on PATH. `NativeLibraryResolver` finds `mxc_ffi` via `MXC_FFI_DIR`, the assembly dir / `runtimes//native`, or `src/target/{debug,release}`. Projects: `Microsoft.Mxc.Sdk` (library), `Microsoft.Mxc.Sdk.Sample` (console), `Microsoft.Mxc.Sdk.Tests` (xUnit), in `Microsoft.Mxc.Sdk.slnx`. Beyond run-to-completion, it also exposes **streaming** (`MxcSandbox.Spawn` → `MxcSandboxProcess`: `Stream`-based stdio, `Wait`/`WaitAsync`/`Kill`) and the **state-aware lifecycle** (`MxcLifecycle.ProvisionSandbox`/`StartSandbox`/`ExecInSandbox`/`ExecInSandboxAsync`/`StopSandbox`/`DeprovisionSandbox`, with a typed `SandboxId`). + +### Schema system + +- **Stable schemas**: released, immutable schemas live in [`schemas/stable/`](../schemas/stable) (one file per released version) — never edit them after release. +- **Dev schema**: the in-progress schema lives in [`schemas/dev/`](../schemas/dev). It is **generated** from the Rust wire model (`src/core/wxc_common/src/wire.rs`) by the `mxc_schema_gen` tool — **do not hand-edit it**. To change the dev schema, edit the wire model and regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema..json`. `scripts/versioning/check-schema-codegen.js` is a CI gate that regenerates and fails if the committed schema drifts. See [`docs/schema-codegen.md`](../docs/schema-codegen.md). +- **Generated SDK wire types**: `sdk/node/src/generated/wire.ts` is **generated** from the same wire model by the `mxc_schema_gen --ts` TypeScript emitter (`wxc_common::ts_emit`, no third-party generator) — **do not hand-edit it**. It is a drift oracle (not public API); the SDK unit test `sdk/node/tests/unit/wire-conformance.test.ts` asserts the hand-written public types in `sdk/node/src/types.ts` conform to it, and `scripts/versioning/check-sdk-types-codegen.js` is a CI gate that fails if the committed file drifts. Regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/node/src/generated/wire.ts`. +- **Canonical schema-version source**: `schemas/schema-version.json` — the single source of truth for the schema-version constants (min/maxSupported/state-aware/stable/dev). `scripts/versioning/check-schema-versions.js` enforces that the Rust parser, SDK, and schema filenames all agree with it; do not hand-edit a schema-version constant without updating the canonical file. See [`docs/versioning.md`](../docs/versioning.md) for the full design. +- Config files can reference schemas via `"$schema"` for editor validation. `scripts/versioning/validate-configs.js` validates the `tests/examples` + `tests/configs` corpus against the dev schema in CI. + +### Key documentation (`docs/`) + +Core references: + +- `docs/schema.md` — full JSON configuration schema reference +- `docs/versioning.md` — schema versioning design, experimental feature lifecycle, and promotion process +- `docs/authoring-a-new-feature.md` — step-by-step guide for adding experimental features (which files to touch, in what order) +- `docs/examples.md` — annotated configuration examples (see also `tests/examples/` and `tests/configs/`) +- `docs/diagnostics.md` — diagnostic logging knobs (env vars, log file format) +- `docs/host-prep.md` — `wxc-host-prep.exe` host setup binary (`prepare-system-drive` / `unprepare-system-drive` for the AppContainer ACEs on the system-drive root, plus `prepare-null-device` / `verify-null-device` / `dump-null-device` for the `\Device\Null` security descriptor that AppContainer-based backends require). Owns elevation via embedded `requireAdministrator` manifest — `wxc-exec.exe` no longer self-elevates. +- `docs/sandbox-policy/v1/policy.md` — sandbox policy v1 specification + +Per-backend guides: + +- `docs/process-container/guide.md` — process container (Windows AppContainer / BaseContainer) +- `docs/process-container/UIPolicy_Schema.md` — UI policy schema (JOB_OBJECT_UILIMIT_* mappings) +- `docs/process-container/os-version-support.md` — per-Windows-release policy-support matrix (filesystem / network / UI) +- `docs/lxc-support/lxc-backend.md` — LXC container backend (Linux) +- `docs/macos-support/seatbelt-backend.md` — macOS Seatbelt backend +- `docs/windows-sandbox/windows-sandbox.md` / `docs/windows-sandbox/windows-sandbox-reference.md` — Windows Sandbox backend +- `docs/wsl/wsl-container-getting-started.md` / `docs/wsl/wsl-container-support-plan.md` — WSL Container (WSLC SDK) +- `docs/wsl/wslc-sdk-bindings.md` — WSLC SDK FFI bindings: `src/backends/wslc/common/src/wslcsdk_sys.rs` is **generated** by bindgen from `wslcsdk.h` (do NOT hand-edit); `wslc_bindings.rs` is a thin facade over it. On every WSLC SDK version bump, regenerate via `scripts/generate-wslc-bindings.ps1` (needs libclang + `bindgen-cli`, required only on the regen machine — normal/CI builds need neither) and commit the regenerated file with the `WSLC_SDK_VERSION` + hash change. See the doc for the full runbook. +- `docs/nanvix-microvm/nanvix.md` / `docs/nanvix-microvm/nanvix-integration-plan.md` — MicroVM via NanVix + +State-aware lifecycle: + +- `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md` — state-aware sandbox lifecycle API (cross-backend wire format, Rust `StatefulSandboxBackend` trait, and dispatcher contract) +- `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md` — companion overview to the full state-aware design +- `docs/isolation-session/initial-bringup-plan.md` — IsolationSession backend, one-shot bringup (experimental, isolated user account per execution via the OS-side service) +- `docs/isolation-session/state-aware-rust-initial-plan.md` — IsolationSession state-aware lifecycle, Rust-layer plan (per-phase config / metadata, policy honor matrix, idempotence, concurrency, error mapping) +- `docs/isolation-session/state-aware-typescript-initial-plan.md` — IsolationSession state-aware lifecycle, TypeScript SDK plan + +## Key Conventions + +### Experimental features + +New features go under the `experimental` JSON section and are only active when `--experimental` is passed. See `docs/authoring-a-new-feature.md` for the full checklist. The pattern: + +1. Add the field to the Rust wire model (`src/core/wxc_common/src/wire.rs`) under the `Experimental` section, then regenerate the dev schema (`cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema..json`) — do not hand-edit the generated schema +2. Add the matching field to the wire model's `Experimental` struct (`src/core/wxc_common/src/wire.rs`) and the domain `ExperimentalConfig` in `models.rs`, then map wire→domain in `config_parser.rs` (use `From` impls beside the domain type for trivial enum/struct conversions) +3. Guard execution behind `if request.experimental_enabled` in the runner +4. Never modify files in `schemas/stable/` — those are immutable release artifacts + +### Rust workspace structure + +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/`, `learning_mode_core/`, `generated/` | +| `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_core` is the cross-platform learning-mode denial model and output layer. It owns denial types, summaries, analyzer abstractions, plain-JSON document emission, and the serializable output-pointer type, and must not depend on any `backends/*` crate. +- `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, decodes sealed ETL traces through `learning_mode_core`, and depends on `wxc_common` plus `learning_mode_core`; runner integration consumes it from the AppContainer backend layer. The trace contract is `HRESULT Start` + retryable `HRESULT Stop` + infallible `Close`: `Stop` never consumes the trace handle, and every started trace must be closed exactly once (closing without stopping is the early-exit discard path). The process security-environment contract is `HRESULT Create` + infallible by-value `Close` and consumes a PSEC 1.0 FlatBuffer, not the legacy SBOX buffer; generated PSEC bindings live in `core/generated/process_security_environment_specification`. +- `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`, including Windows Sandbox and IsolationSession); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request` / `build_request_with_containment`, `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`, warnings, and optional structured output metadata) 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`, `output_metadata()` after terminal completion, or `wait_with_output()`). It re-exports the engine's config-building surface (`build_request`, `build_request_with_containment` + `Containment`/`WslcSection`, `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), Windows ProcessContainer (AppContainer + BaseContainer), and WSLC (Windows, experimental — needs the crate's `wslc` feature plus `SandboxRequest::set_experimental(true)`; no stdin and `id() == 0`, since the WSLC SDK exposes neither); 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()`). `SandboxProcess::output_metadata()` carries backend-produced structured outputs after terminal teardown without writing to process-global stdio. `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, Windows ProcessContainer, and WSLC (on `wslc_common::WSLContainerRunner` itself, which shares one container lifecycle — `start_container` — between its streaming `SandboxBackend` and run-to-completion `ScriptRunner` impls, differing only in where the WSLC SDK's output callbacks write). +- `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/output-metadata 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). It exposes three surfaces: **run-to-completion** (`mxc_run`), **streaming** (`mxc_spawn` → opaque `MxcSandbox` handle; `mxc_stream_read`/`write`/`flush`, `mxc_sandbox_take_stdin`/`stdout`/`stderr`, `mxc_sandbox_id`/`try_wait`/`wait`/`kill`/`output_metadata_json`/`free`, in `src/streaming.rs`), and the **state-aware lifecycle** (`mxc_state_aware` for the envelope phases + `mxc_state_aware_exec` returning a live streaming handle, in `src/state_aware.rs`). All three `.rs` files are csbindgen inputs in `build.rs`; the `MXC_STATUS_*` space already reserves the state-aware phase codes. +- `mxc_pty` is the shared pty bridge used by the LXC backend (`lxc_common::lxc_bindings::attach_run`) so the inner shell sees a real TTY and host stdio is streamed live. (Seatbelt and Bubblewrap no longer use it: they spawn directly and let the child inherit the host's stdio — a TTY when the executor binary runs under a pty — via `SandboxBackend::spawn(StdioMode::Inherit)`.) +- `learning_mode_core` is the **cross-platform learning-mode / captureDenials model + output emitter**: `DeniedResource` (+ `ResourceType`/`AccessType`), `DenialSummary`, the `DenialAnalyzer` decode trait, and `emit` — which writes the on-disk denials deliverable as a **single JSON document** `{ "denials": [...], "summary": {...} }` (`write_document` / `DenialsDocument`) and defines the serializable `DenialsOutputPointer`. It carries no OS-specific code (must not depend on any `backends/*` crate); the Windows ETL decoder implementing `DenialAnalyzer` lives in `backends/learning_mode/windows`. When `processContainer.captureDenials` is set, the BaseContainer runner seals a unique internal ETL temp, decodes it via that backend with bounded event/unique-denial processing, writes the JSON file (caller's `outputPath` with a unique per-run identifier stamped into the stem, e.g. `denials..json`, or a managed temp), deletes the ETL, and returns neutral `wxc_common` output metadata. `wxc-exec` serializes that metadata as the one-line stderr pointer at the CLI boundary; Rust/C#/FFI callers receive it programmatically. Each denial's `resource` field holds the file path or the AppContainer capability name; capability denials resolve their capability SID to a friendly name via `backends/learning_mode/windows`'s `capability_names` (well-known `S-1-15-3-…` SID → policy name; custom hashed SIDs fall back to the SID string). +- `mxc_build_common` is a build-time helper crate — all Windows binary crates use it in their `build.rs` to embed VersionInfo (ProductName, FileDescription, copyright, version+commit). When adding a new Windows binary crate, add `mxc_build_common` as a build-dependency and call `mxc_build_common::embed_version_info()` from `build.rs` +- `nanvix_build_common` is a **build-only** helper crate (never linked into the runtime): it stages NanVix binaries next to the executable and resolves the `NANVIX_BIN` prefetch directory. The `nanvix_binaries`, `wxc`, and `lxc` build scripts consume it as a `[build-dependencies]` entry. Runtime constants it needs (binary/snapshot filenames) stay in `nanvix_common`. Keep build-only file-staging logic here, not in `nanvix_common` (which is a runtime dependency of `nanvix_runner`). +- Platform-specific modules use `#[cfg(target_os = "windows")]` / `#[cfg(target_os = "linux")]` +- Workspace edition is 2021; shared dependencies are declared in the root `Cargo.toml` `[workspace.dependencies]` + +### Config parser pattern + +The parser deserializes JSON directly into the typed wire model (`wxc_common::wire`), the single source of truth for the config shape (it also generates the JSON schema). All typed config deserialization goes through `config_deserialize.rs`, which distinguishes syntax errors from typed policy errors and adds the complete JSON path plus source line/column when available; state-aware backend errors are prefixed with their full `experimental..` location. `config_parser.rs` then maps the wire types to the validated domain structs in `models.rs`. The stable surface uses `deny_unknown_fields` (closed); the `experimental` block is permissive. + +### TypeScript conventions + +- Target ES2022, ESM modules (`module`/`moduleResolution: NodeNext`, `"type": "module"`), strict mode — relative imports use explicit `.js` extensions +- Tests use Node.js built-in test runner (`node --test`) + +### Binary naming + +- Windows: `wxc-exec.exe` (AppContainer / Windows Sandbox / MicroVM); `wxc-host-prep.exe` (host setup — see `docs/host-prep.md`) +- Linux: `lxc-exec` (LXC containers) +- macOS: `mxc-exec-mac` (Seatbelt) +- Target triples: `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`, `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` + +### Package versioning + +All Rust crates use `version.workspace = true` to inherit the version from `src/Cargo.toml` `[workspace.package]`. The npm SDK version in `sdk/node/package.json` and the C# SDK version (`` in `sdk/dotnet/Microsoft.Mxc.Sdk/Microsoft.Mxc.Sdk.csproj`) must match. Run `node scripts/check-version-sync.js` to validate they are in sync. When bumping the version, update `src/Cargo.toml` (workspace version), `sdk/node/package.json`, and the C# csproj in the same commit. + +### Keeping docs up to date + +When changing behavior covered by existing documentation, update the relevant docs in the same change: + +- **Schema changes** (adding/removing/renaming config fields) → update `docs/schema.md` and the appropriate JSON schema in `schemas/dev/` or `schemas/stable/` +- **New experimental features** → follow `docs/authoring-a-new-feature.md`, which includes schema, Rust, and test config steps +- **SDK API changes** (new exports, changed signatures, new options) → update `sdk/node/README.md` and the JSDoc in `sdk/node/src/index.ts` (TypeScript SDK); the Rust `mxc-sdk` crate docs/`README.md`; and `sdk/dotnet/README.md` (C# SDK). If the `mxc_ffi` C ABI surface changes, the C# P/Invoke regenerates on the next C# build; keep the `ErrorCode` parity + bindings-codegen gates green. +- **New containment backends or major backend changes** → update the relevant doc in `docs/` (e.g., `lxc-support/lxc-backend.md`, `windows-sandbox/windows-sandbox.md`) +- **Versioning or promotion changes** → update `docs/versioning.md` + +### Policy versioning + +The `SandboxPolicy.version` in the SDK must match a JSON schema version in the supported range (`0.6.0-alpha` minimum, `0.8.0-alpha` maximum). The SDK validates this in `sandbox.ts` — if the policy version is older than `MIN_VERSION` or newer than `SUPPORTED_VERSION` it throws. State-aware lifecycle requests use `0.6.0-alpha`. These bounds are mirrored from the canonical `schemas/schema-version.json` and enforced by `scripts/versioning/check-schema-versions.js`. See `docs/versioning.md` for the full design. + +## Creating Issues + +When creating issues in this repository, follow the structure defined by the issue templates in `.github/ISSUE_TEMPLATE/`. Every issue **must** match one of the four categories below and include the corresponding labels, issue type, and required fields. + +### Issue categories, types, and labels + +| Category | GitHub Issue Type | Labels | Template | +|----------|------------------|--------|----------| +| 🐛 Bug Report | `Bug` | `Issue-Bug`, `Needs-Triage` | `Bug_Report.yml` | +| 🚀 Feature Request / Idea | `Feature` | `Issue-Feature`, `Needs-Triage` | `Feature_Request.yml` | +| 📚 Documentation Issue | `Task` | `Issue-Docs`, `Needs-Triage` | `Documentation_Issue.yml` | +| 📋 Task | `Task` | `Issue-Task`, `Needs-Triage` | `Task.yml` | + +- Always apply `Needs-Triage` alongside the category-specific label. +- Apply exactly the labels listed above — do not invent new labels. +- When creating issues via the API, set labels and issue type explicitly — they are not applied automatically. + +### Required body structure by category + +Issues created via the API or by agents do not inherit the form layout from the YAML templates. Reproduce the structure in the issue body using the markdown skeletons below. + +**🐛 Bug Report** — use when something is broken or behaving unexpectedly: + +> ⚠️ **Security notice:** When reporting BSODs or security issues, **DO NOT** attach memory dumps, logs, or traces to GitHub issues. Instead, send them to secure@microsoft.com referencing the GitHub issue. For application crashes, include a Feedback Hub link if possible (open with Win+F, choose "Share My Feedback" after submission). + +```markdown +### Relevant area(s) + + +### Brief description of your issue + +### Steps to reproduce +1. +2. +3. + +### Expected behavior + +### Actual behavior +``` + +All five sections are **required**. + +**🚀 Feature Request / Idea** — use for new functionality or improvements: + +```markdown +### Description of the new feature / enhancement + + +### Proposed technical implementation details + +``` + +"Description of the new feature / enhancement" is **required**. Omit "Proposed technical implementation details" if there is nothing meaningful to add. + +**📚 Documentation Issue** — use when docs are incorrect, incomplete, or confusing: + +```markdown +### Brief description of your issue + +``` + +This section is **required**. + +**📋 Task** — use for actionable work items: + +```markdown +### Description of the task + + +### Additional context + +``` + +"Description of the task" is **required**. Omit "Additional context" if there is nothing meaningful to add. + +### Choosing the right category + +- Something **used to work** or **doesn't work as documented** → Bug Report +- Proposing **new behavior or capabilities** → Feature Request / Idea +- **Incorrect, missing, or unclear documentation** → Documentation Issue +- A **discrete unit of work** that doesn't fit the above → Task + +### Style guidelines + +- Use the section headers exactly as shown in the skeletons above +- Be specific and concise — avoid vague descriptions like "it doesn't work" +- For bug reports, always include concrete reproduction steps +- For feature requests, explain the *why* (user problem) before the *how* (implementation) +- Reference relevant source files, config fields, or docs when applicable +- If any required field is unknown, **ask for the information rather than fabricating content** + +## Creating Pull Requests + +Pull requests must follow the template in `.github/PULL_REQUEST_TEMPLATE.md`. Complete all checklist items and add content below the separator (`-----`). + +### Required structure + +Every PR body should include: + +1. **Template checklist** — check the boxes that apply (CLA, related issue, copilot-instructions update). +2. **Summary** — a brief description of what the PR does and why. +3. **Issue references** — if the PR is intended to close an issue, use GitHub closing keywords (`Closes #NNN`, `Fixes #NNN`, or `Resolves #NNN`). If the PR is related but does not close an issue, use an unordered list under a "Related Issues" heading (`- #NNN`). + +### Example + +```markdown +- [x] I have signed the [Contributor License Agreement](https://opensource.microsoft.com/cla/). +- [x] This pull request is related to an issue. +- [ ] If this PR changes build commands, project architecture, or key conventions, I have updated [`.github/copilot-instructions.md`](.github/copilot-instructions.md). + +----- + +## Summary + +Brief description of the change. + +Closes #42 +``` + +### Guidelines + +- One PR should address one issue or concern. Avoid bundling unrelated changes. +- If the PR updates build commands, project architecture, or key conventions, update `.github/copilot-instructions.md` in the same PR. +- Draft PRs are appropriate for work-in-progress that needs early feedback. diff --git a/.github/workflows/Versioning.Checks.Job.yml b/.github/workflows/Versioning.Checks.Job.yml index eafefcd3e..9d6ecf5fe 100644 --- a/.github/workflows/Versioning.Checks.Job.yml +++ b/.github/workflows/Versioning.Checks.Job.yml @@ -33,6 +33,9 @@ jobs: - name: Check schema is in sync with the Rust wire model (codegen) run: node scripts/versioning/check-schema-codegen.js + - name: Check PSEC generated contract (provenance + drift) + run: node scripts/versioning/check-psec-codegen.js + - name: Check SDK wire types are in sync with the Rust wire model (codegen) run: node scripts/versioning/check-sdk-types-codegen.js diff --git a/docs/learning-mode/capabilities.md b/docs/learning-mode/capabilities.md index 9f9645b34..0db29ffe9 100644 --- a/docs/learning-mode/capabilities.md +++ b/docs/learning-mode/capabilities.md @@ -118,9 +118,33 @@ surfacing the resulting denials to the caller. Its `mode` selects how each ungranted access is handled while it is recorded: > **Host requirement.** `captureDenials` requires a feature-enabled Windows -> build exposing the BaseContainer security-environment and Learning Mode APIs. -> It is not supported by the AppContainer fallback tiers; unsupported hosts -> return `backend_unavailable`. +> build exposing the complete official V2 API set: +> `StartLearningModeTrace`, `StopLearningModeTrace`, +> `CloseLearningModeTrace`, `CreateProcessSecurityEnvironment`, +> `QueryProcessSecurityEnvironmentSupport`, and +> `CloseProcessSecurityEnvironment`. It is not supported by the AppContainer +> fallback tiers; unsupported hosts return `backend_unavailable`. +> +> Internal validation confirmed that build `26657.1002` exposes only the +> incompatible earlier contract and is rejected, while build `26663.1000` +> exposes the complete V2 contract. These are validation points, not a public +> Windows release-floor commitment; callers should rely on the runtime probe. +> +> `captureDenials` cannot be combined with `processContainer.leastPrivilege`; +> the Windows process security-environment API used for capture does not expose +> an LPAC token option, so MXC rejects that combination rather than silently +> weakening the requested policy. +> +> `captureDenials` also cannot currently be combined with `network.proxy`. +> The V2 process security-environment proxy contract requires a separate proxy +> AppContainer peer identity; MXC rejects the combination until that peer is +> provisioned by the capture launch path. +> +> `filesystem.deniedPaths` requires +> `QueryProcessSecurityEnvironmentSupport` to advertise +> `PSE_SUPPORT_FS_DENY`. When the bit is absent, capture fails as +> `backend_unavailable`; it cannot fall back to AppContainer or host-DACL +> enforcement. - `mode: "block"` (default) maps onto `learningModeLogging` (deny-and-record) — the app / user-configurable flow. diff --git a/docs/process-container/os-version-support.md b/docs/process-container/os-version-support.md index 7df7abd30..a3f663d96 100644 --- a/docs/process-container/os-version-support.md +++ b/docs/process-container/os-version-support.md @@ -49,6 +49,49 @@ available bounds what policy can be enforced. - **T3 (AppContainer + DACL)** is the universal fallback and enforces filesystem policy via host path ACEs on every release. +## Schema 0.8 process security environment preference + +BaseContainer requests using schema versions through 0.7 use the SBOX contract +and the T1/T2/T3 fallback chain above. Schema 0.8 and later prefer the PSEC +process-security-environment contract when its complete export set resolves and +`QueryProcessSecurityEnvironmentSupport` succeeds. During the transition from +the experimental SBOX API to PSEC, an ordinary schema 0.8 request falls back to +SBOX when PSEC is unavailable, then continues through the existing AppContainer +fallback tiers when neither BaseContainer contract is usable. + +The PSEC probe requires: + +- `CreateProcessSecurityEnvironment` +- `QueryProcessSecurityEnvironmentSupport` +- `CloseProcessSecurityEnvironment` + +When `processContainer.captureDenials` is present, fallback is not possible: +capture requires a PSEC handle to key the trace. The host must additionally +expose the complete official V2 Learning Mode export set: + +- `StartLearningModeTrace` +- `StopLearningModeTrace` +- `CloseLearningModeTrace` + +For capture, unsupported or earlier-contract hosts fail as +`backend_unavailable`. Ordinary ProcessContainer execution still follows the +fallback chain. Internal validation confirmed the earlier contract on build +`26657.1002` is rejected for capture while schema 0.7 SBOX execution remains +functional, and the full V2 contract on build `26663.1000` is accepted. These +builds are validation points, not a public release-floor commitment; runtime +probing is the source of truth. + +The PSEC contract cannot represent `processContainer.leastPrivilege`, so +ordinary schema 0.8 requests using that option use the transitional SBOX +contract instead of failing. MXC also does not yet supply the AppContainer peer +identity required by the current model-2 SBOX proxy contract. On hosts with +`Experimental_QuerySandboxSupport`, proxy requests therefore skip +BaseContainer and continue to the AppContainer fallback; older query-less hosts +retain the legacy SBOX proxy path. Similarly, `filesystem.deniedPaths` uses +PSEC only when `QueryProcessSecurityEnvironmentSupport` advertises +`PSE_SUPPORT_FS_DENY`; otherwise MXC continues through the SBOX/AppContainer +fallback chain. + ## Filesystem policy | Aspect | 23H2 | 24H2 | 25H2 | 25H2+ | @@ -74,17 +117,19 @@ Notes: |--------|:--:|:--:|:--:|:--:| | Capabilities (`internetClient`) | ✅ | ✅ | ✅ | ✅ | | Firewall rules (`netsh advfirewall`, needs admin) | ✅ | ✅ | ✅ | ✅ | -| Proxy via OS / BaseContainer (`appinfosvc`, FlatBuffer `network_policy.proxy`) | ❌ | ❌ | ❌ | ✅ (T1 only) | +| Proxy | ✅ (AppContainer compatibility) | ✅ (AppContainer compatibility) | ✅ (AppContainer compatibility) | ✅ (legacy T1 or AppContainer compatibility) | Notes: - Capability- and firewall-based network enforcement is an AppContainer primitive and works on every release. - OS-configured WinHTTP proxy (passed in the FlatBuffer spec to - `CreateProcessInSandbox`) is a T1-only path and therefore 25H2+ only. -- The earlier AppContainer WinHTTP proxy shim (`winhttp-proxy-shim.exe`) is - being retired and is intentionally omitted here: the new WinHTTP cleanup APIs - it depended on are not moving down-level, so it is not a forward-looking - option. + `CreateProcessInSandbox`) is used only on legacy query-less T1 hosts. The + capability-aware model-2 contract requires an AppContainer proxy peer + identity that MXC does not yet author, so those hosts use the AppContainer + compatibility fallback. +- The AppContainer compatibility path uses `winhttp-proxy-shim.exe`. It is not + the forward-looking proxy architecture; support for the model-2 BaseContainer + contract should replace this fallback in a separate change. ## UI restrictions diff --git a/docs/schema.md b/docs/schema.md index a43b29e75..63eaccfe4 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -72,6 +72,8 @@ production configs and the dev schema when working on experimental features: } // dir must already exist; a unique per-run id is stamped // into the stem (denials..json) and the actual // path printed on stderr. Omit outputPath for a managed temp file. + // captureDenials cannot be combined with leastPrivilege. + // captureDenials cannot currently be combined with network.proxy. }, "lxc": { // LXC-specific diff --git a/external/windows-sdk/ProcessSecurityEnvironment.fbs b/external/windows-sdk/ProcessSecurityEnvironment.fbs new file mode 100644 index 000000000..a4403a8d6 --- /dev/null +++ b/external/windows-sdk/ProcessSecurityEnvironment.fbs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// FlatBuffers schema for the BaseContainer sandbox specification used in the +// CreateProcessSecurityEnvironment Windows API. +// +// This is the CPSE wire format. The legacy CPIS path continues to use +// SandboxSpec.fbs and its "SBOX" file identifier. +// +// ---- FlatBuffers Schema Evolution Rules ---- +// +// Compatible (non-breaking) changes: +// - Add new fields to the END of a table (they get the next vtable slot). +// - Add new tables, structs, enums, or union members. +// - Deprecate a field with (deprecated) - the slot is preserved, readers skip it. +// - Rename a field or table (wire format uses slot indices, not names). +// +// BREAKING changes (will corrupt existing buffers): +// - Remove or reorder fields in a table or struct. +// - Change a field's type (e.g. uint32 -> int32, or scalar -> string). +// - Change a field's default value. +// - Add/remove/reorder values in an enum that is already serialized. +// - Change the file_identifier or root_type. +// - Change a field from scalar to non-scalar (or vice versa). +// +// To machine-check for breaking changes, keep a copy of the last shipped schema +// (e.g. ProcessSecurityEnvironment.previous.fbs) and run: +// +// flatc --conform ProcessSecurityEnvironment.previous.fbs ProcessSecurityEnvironment.fbs +// +// --conform verifies that every field/enum/table in the old schema still exists +// at the same vtable slot, type, and default in the new schema. + +namespace ProcessSecurityEnvironmentLayout; + +struct SchemaVersion { + major:uint16; + minor:uint16; +} + +table ProcessSecurityEnvironment { + version:SchemaVersion (required); + capabilities:string; + disallow_win32k_system_calls:bool = false; + ui_restrictions:uint64 = 0; + fs_read_write:[string]; + fs_read_only:[string]; + fs_deny:[string]; + network_policy:NetworkPolicy; +} + +table ProxyInfo { + url:string; +} + +enum FilterAction : byte { deny, allow } +enum IpProtocol : byte { any, tcp, udp, icmpv4, icmpv6 } + +table IpSubnet { + address:string; + prefix_length:ubyte = 0; +} + +table DestinationRule { + subnet:IpSubnet; + except:[IpSubnet]; +} + +table PortRule { + protocol:IpProtocol = any; + port:uint16 = 0; + end_port:uint16 = 0; +} + +table EndpointRule { + destinations:[DestinationRule]; + ports:[PortRule]; +} + +table EndpointPolicy { + default_action:FilterAction = deny; + allow:[EndpointRule]; + deny:[EndpointRule]; +} + +table NetworkPolicy { + proxy:ProxyInfo; + egress:EndpointPolicy; + allowed_appcontainer_peer:string; +} + +root_type ProcessSecurityEnvironment; +file_identifier "PSEC"; diff --git a/external/windows-sdk/ProcessSecurityEnvironment.provenance.toml b/external/windows-sdk/ProcessSecurityEnvironment.provenance.toml new file mode 100644 index 000000000..530706ac9 --- /dev/null +++ b/external/windows-sdk/ProcessSecurityEnvironment.provenance.toml @@ -0,0 +1,52 @@ +# Provenance for the vendored ProcessSecurityEnvironment FlatBuffers schema. +# +# This file is the single source of truth for the pinned regeneration toolchain +# and the authoritative hash of the vendored `ProcessSecurityEnvironment.fbs`. +# It is consumed by: +# * src/core/generated/process_security_environment_specification/regenerate.ps1 +# (validates the schema hash + pins the exact flatc version before generating) +# * scripts/versioning/check-psec-codegen.js +# (CI drift gate — verifies the committed schema + generated crate) +# +# Update this file whenever the vendored schema is refreshed from the OS source, +# then regenerate the bindings (see the crate README). + +[source] +# The schema originates from the internal Microsoft Windows OS repository and is +# not publicly redistributable. Only the schema text (not the OS tree) is vendored. +repository = "Windows OS (internal Azure DevOps)" +# The PSEC schema is a Windows OS containment contract; there is no public OS +# revision to cite. Instead of guessing a source path/revision, we record the +# Windows build the vendored schema contract was validated against. +validated_windows_build = "10.0.26663.1000" +# Azure DevOps PR 16307987 is the RELATED Learning Mode trace ABI change — it is +# NOT the pull request that introduced or last modified this PSEC schema. +# Recorded for traceability only; do not represent it as the schema's origin. +related_trace_abi_pull_request = 16307987 + +[schema] +# SHA-256 of external/windows-sdk/ProcessSecurityEnvironment.fbs, computed over +# the LF-normalized (git-blob) content so it is checkout-independent regardless +# of autocrlf. Verified by regenerate.ps1 and the CI drift gate. +sha256 = "7d14b01850a735329da00cde4d4d2e32e463f49026a39708be9059fd64e764d3" + +[tool] +# Exact flatc version used to generate the committed bindings. Regeneration +# pins this exact version (not a floor) so output is byte-reproducible. +# 25.12.19 is the first release carrying flatbuffers PR #8709, which stops flatc +# emitting elided lifetimes that trip the `mismatched_lifetime_syntaxes` lint +# (added in Rust 1.89). +flatc_version = "25.12.19" +flatc_release = "https://github.com/google/flatbuffers/releases/tag/v25.12.19" +generated_date = "2026-08-03" + +# Exact GitHub release assets for flatc 25.12.19. The CI drift gate downloads the +# platform asset, verifies its SHA-256 against these values, unzips it, and +# regenerates into a temp directory to diff against the committed bindings. +[tool.flatc_assets.linux] +name = "Linux.flatc.binary.clang++-18.zip" +sha256 = "50c1915deeeb714f2a05c8ec795bd1af898d251a62e2774067703b29188efc90" + +[tool.flatc_assets.windows] +name = "Windows.flatc.binary.zip" +sha256 = "fff9445c9db907227bc64b54cc98743084c4949282aa4e576cff6a955724ddc8" diff --git a/schemas/dev/mxc-config.schema.0.8.0-dev.json b/schemas/dev/mxc-config.schema.0.8.0-dev.json index b62e63198..504446da6 100644 --- a/schemas/dev/mxc-config.schema.0.8.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.8.0-dev.json @@ -46,7 +46,7 @@ }, "CaptureDenials": { "additionalProperties": false, - "description": "Windows denial-capture settings. The presence of the `captureDenials` object enables capture; all fields are optional.", + "description": "Windows denial-capture settings. The presence of the `captureDenials` object enables capture; all fields are optional. Capture is incompatible with `processContainer.leastPrivilege` and `network.proxy`. Explicit `filesystem.deniedPaths` requires the host's V2 process security-environment support query to advertise native deny enforcement.", "properties": { "mode": { "anyOf": [ @@ -699,7 +699,7 @@ "type": "null" } ], - "description": "Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the learning-mode OS API." + "description": "Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the complete official V2 Learning Mode and process security-environment API set. Cannot be combined with `leastPrivilege` or `network.proxy`; `filesystem.deniedPaths` additionally requires the V2 deny-support capability." }, "learningMode": { "description": "AppContainer learning mode (deny-and-record): failed access checks are logged for diagnostics while the accesses stay denied; containment is unchanged. Distinct from the allow-all `permissiveLearningMode` capability, which is injected internally by the `--audit` CLI flag or dedicated denial-capture configuration.", diff --git a/scripts/versioning/check-psec-codegen.js b/scripts/versioning/check-psec-codegen.js new file mode 100644 index 000000000..dc04e46d5 --- /dev/null +++ b/scripts/versioning/check-psec-codegen.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// PSEC generated-contract drift gate. +// +// The `process_security_environment_spec` crate under src/core/generated/ is +// generated by `flatc` from the vendored schema +// external/windows-sdk/ProcessSecurityEnvironment.fbs. This gate: +// +// 1. verifies the vendored schema SHA-256 matches the recorded provenance +// (external/windows-sdk/ProcessSecurityEnvironment.provenance.toml); +// 2. verifies every committed generated file carries the flatc "do not +// modify." header (a hand-edit guard); +// 3. performs a REAL regenerate-and-diff with the EXACT pinned flatc: it +// downloads the pinned platform release asset from the flatbuffers GitHub +// release, verifies its SHA-256 against the provenance, unzips it, +// regenerates into a temp directory with the same flags, reorganizes + +// patches lib.rs, runs `cargo fmt`, and recursively diffs the result +// against the committed src/; and +// 4. `cargo check`s the committed workspace crate for cross-target compile +// validation. +// +// To reuse a cached flatc (e.g. on a Windows dev box, or an air-gapped runner) +// and skip the download, pass `--flatc ` or set the `FLATC` env var. The +// binary's version must equal the pinned flatc_version. +// +// Run from anywhere (paths resolved relative to repo root): +// node scripts/versioning/check-psec-codegen.js [--flatc ] + +const fs = require("fs"); +const os = require("os"); +const crypto = require("crypto"); +const { join, relative, sep } = require("path"); +const { execFileSync } = require("child_process"); + +const repoRoot = join(__dirname, "..", ".."); +const schemaPath = join(repoRoot, "external", "windows-sdk", "ProcessSecurityEnvironment.fbs"); +const provenancePath = join( + repoRoot, + "external", + "windows-sdk", + "ProcessSecurityEnvironment.provenance.toml" +); +const crateDir = join( + repoRoot, + "src", + "core", + "generated", + "process_security_environment_specification" +); +const committedSrcDir = join(crateDir, "src"); + +function fail(msg) { + console.error("PSEC codegen check FAILED:"); + console.error(" - " + msg); + process.exit(1); +} + +const sha256Hex = (buf) => crypto.createHash("sha256").update(buf).digest("hex"); +const sha256File = (p) => sha256Hex(fs.readFileSync(p)); +// LF-normalize so autocrlf checkouts don't produce false differences. +const lfNormalize = (buf) => + Buffer.from(buf.toString("binary").replace(/\r\n/g, "\n"), "binary"); + +// --- Minimal TOML reader (targeted, no dependency) -------------------------- +function tomlSection(text, header) { + const lines = text.split(/\r?\n/); + const marker = `[${header}]`; + const start = lines.findIndex((line) => line.trim() === marker); + if (start === -1) return null; + let end = start + 1; + while (end < lines.length && !lines[end].trimStart().startsWith("[")) { + end++; + } + return lines.slice(start + 1, end).join("\n"); +} +function tomlString(block, key) { + if (block == null) return null; + for (const line of block.split(/\r?\n/)) { + const separator = line.indexOf("="); + if (separator === -1 || line.slice(0, separator).trim() !== key) continue; + const value = line.slice(separator + 1).trim(); + const match = /^"([^"]+)"(?:\s+#.*)?$/.exec(value); + return match ? match[1] : null; + } + return null; +} + +function readProvenance() { + let text; + try { + text = fs.readFileSync(provenancePath, "utf8"); + } catch (e) { + fail(`could not read provenance ${provenancePath}: ${e.message}`); + } + const schemaSha = tomlString(tomlSection(text, "schema"), "sha256"); + if (!schemaSha) fail(`provenance missing [schema].sha256: ${provenancePath}`); + const flatcVersion = tomlString(tomlSection(text, "tool"), "flatc_version"); + if (!flatcVersion) fail(`provenance missing [tool].flatc_version: ${provenancePath}`); + const asset = (osName) => { + const b = tomlSection(text, `tool.flatc_assets.${osName}`); + const name = tomlString(b, "name"); + const sha256 = tomlString(b, "sha256"); + return name && sha256 ? { name, sha256: sha256.toLowerCase() } : null; + }; + return { + schemaSha: schemaSha.toLowerCase(), + flatcVersion, + assets: { linux: asset("linux"), windows: asset("windows") }, + }; +} + +// --- flatc acquisition ------------------------------------------------------ +function flatcReportedVersion(flatc) { + const out = execFileSync(flatc, ["--version"], { encoding: "utf8" }); + const m = out.match(/flatc version (\d+\.\d+\.\d+)/); + if (!m) fail(`could not parse flatc version from: ${out.trim()}`); + return m[1]; +} + +function cliFlatc() { + const i = process.argv.indexOf("--flatc"); + if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1]; + if (process.env.FLATC) return process.env.FLATC; + return null; +} + +function acquireFlatc(prov, tmp) { + const provided = cliFlatc(); + if (provided) { + const v = flatcReportedVersion(provided); + if (v !== prov.flatcVersion) { + fail( + `provided flatc version ${v} != pinned ${prov.flatcVersion} ` + + `(--flatc / FLATC=${provided})` + ); + } + return provided; + } + + const platform = process.platform; + const asset = + platform === "win32" + ? prov.assets.windows + : platform === "linux" + ? prov.assets.linux + : null; + if (!asset) { + fail( + `no pinned flatc release asset for platform '${platform}'. ` + + `Set FLATC= to run the drift check here.` + ); + } + + const url = `https://github.com/google/flatbuffers/releases/download/v${prov.flatcVersion}/${asset.name}`; + const zip = join(tmp, asset.name); + console.log(`Downloading pinned flatc asset ${asset.name} ...`); + execFileSync("curl", ["-fsSL", "-o", zip, url], { + stdio: ["ignore", "ignore", "inherit"], + }); + const got = sha256File(zip); + if (got !== asset.sha256) { + fail( + `flatc asset SHA-256 mismatch for ${asset.name}:\n` + + ` expected: ${asset.sha256}\n` + + ` actual: ${got}` + ); + } + + const outDir = join(tmp, "flatc"); + fs.mkdirSync(outDir, { recursive: true }); + if (platform === "win32") { + execFileSync( + "powershell", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + `Expand-Archive -Path '${zip}' -DestinationPath '${outDir}' -Force`, + ], + { stdio: ["ignore", "ignore", "inherit"] } + ); + } else { + execFileSync("unzip", ["-o", "-q", zip, "-d", outDir], { + stdio: ["ignore", "ignore", "inherit"], + }); + } + + const bin = join(outDir, platform === "win32" ? "flatc.exe" : "flatc"); + if (!fs.existsSync(bin)) fail(`flatc binary not found after extracting ${asset.name}`); + if (platform !== "win32") fs.chmodSync(bin, 0o755); + + const v = flatcReportedVersion(bin); + if (v !== prov.flatcVersion) { + fail(`downloaded flatc version ${v} != pinned ${prov.flatcVersion}`); + } + return bin; +} + +// --- Regeneration (mirrors regenerate.ps1, into a temp dir) ----------------- +const FMT_MANIFEST = `[package] +name = "process_security_environment_spec" +version = "0.7.0" +edition = "2021" +license = "MIT" +publish = false + +[dependencies] +flatbuffers = "25" +`; + +function regenerate(flatc, tmp) { + const genCrate = join(tmp, "gen"); + fs.mkdirSync(genCrate, { recursive: true }); + execFileSync( + flatc, + [ + "--rust", + "--gen-object-api", + "--force-empty", + "--no-prefix", + "--rust-module-root-file", + "--gen-all", + "-o", + genCrate, + schemaPath, + ], + { stdio: ["ignore", "ignore", "inherit"] } + ); + + const genSrc = join(genCrate, "src"); + fs.mkdirSync(genSrc); + fs.renameSync(join(genCrate, "mod.rs"), join(genSrc, "lib.rs")); + fs.renameSync( + join(genCrate, "process_security_environment_layout"), + join(genSrc, "process_security_environment_layout") + ); + + const libRs = join(genSrc, "lib.rs"); + const patched = fs + .readFileSync(libRs, "utf8") + .replace( + "// @generated", + "// @generated\n#![allow(unused_imports, non_snake_case, non_camel_case_types, clippy::all)]" + ); + fs.writeFileSync(libRs, patched); + + fs.writeFileSync(join(genCrate, "Cargo.toml"), FMT_MANIFEST); + execFileSync("cargo", ["fmt", "--manifest-path", join(genCrate, "Cargo.toml")], { + stdio: ["ignore", "ignore", "inherit"], + }); + return genSrc; +} + +function collectRel(dir) { + const out = []; + (function walk(d) { + for (const e of fs.readdirSync(d).sort()) { + const p = join(d, e); + if (fs.statSync(p).isDirectory()) walk(p); + else out.push(relative(dir, p).split(sep).join("/")); + } + })(dir); + return out.sort(); +} + +function diffTrees(committed, generated) { + const a = collectRel(committed); + const b = collectRel(generated); + const setA = new Set(a); + const setB = new Set(b); + const onlyCommitted = a.filter((x) => !setB.has(x)); + const onlyGen = b.filter((x) => !setA.has(x)); + if (onlyCommitted.length || onlyGen.length) { + fail( + "generated file set drifted from committed:\n" + + (onlyCommitted.length + ? " committed-only: " + onlyCommitted.join(", ") + "\n" + : "") + + (onlyGen.length ? " generated-only: " + onlyGen.join(", ") : "") + ); + } + for (const rel of a) { + const c = lfNormalize(fs.readFileSync(join(committed, rel))).toString().split("\n"); + const g = lfNormalize(fs.readFileSync(join(generated, rel))).toString().split("\n"); + if (c.join("\n") !== g.join("\n")) { + let line = 0; + while (line < c.length && line < g.length && c[line] === g[line]) line++; + const show = (arr) => (line < arr.length ? JSON.stringify(arr[line]) : ""); + fail( + `committed generated output is stale at src/${rel}.\n` + + ` First difference at line ${line + 1}:\n` + + ` committed: ${show(c)}\n` + + ` regenerated: ${show(g)}\n` + + ` Regenerate with the crate's regenerate.ps1 (exact flatc ` + + `${provInfo.flatcVersion}).` + ); + } + } + return a.length; +} + +// ============================================================================ +const provInfo = readProvenance(); + +// --- 1. Schema hash matches provenance -------------------------------------- +let schemaBytes; +try { + schemaBytes = fs.readFileSync(schemaPath); +} catch (e) { + fail(`could not read schema ${schemaPath}: ${e.message}`); +} +const actualSchemaSha = sha256Hex(lfNormalize(schemaBytes)); +if (actualSchemaSha !== provInfo.schemaSha) { + fail( + `vendored schema hash drifted from provenance.\n` + + ` schema: ${schemaPath}\n` + + ` expected: ${provInfo.schemaSha} (${provenancePath})\n` + + ` actual: ${actualSchemaSha}\n` + + ` If you intentionally refreshed the schema, update the provenance\n` + + ` (sha256 + source build) and regenerate the bindings (see the crate README).` + ); +} + +// --- 2. Generated files carry the "do not modify" header -------------------- +function isMarkedGenerated(text) { + const head = text.slice(0, 400).toLowerCase(); + return head.includes("@generated") && head.includes("do not modify"); +} +if (!fs.existsSync(committedSrcDir)) { + fail(`generated source directory not found: ${committedSrcDir}`); +} +const committedFiles = collectRel(committedSrcDir); +if (committedFiles.length === 0) { + fail(`no generated files found under ${committedSrcDir}`); +} +const unmarked = committedFiles.filter( + (rel) => !isMarkedGenerated(fs.readFileSync(join(committedSrcDir, rel), "utf8")) +); +if (unmarked.length > 0) { + fail( + "generated file(s) are missing the flatc 'do not modify' header (hand-edited?):\n" + + unmarked.map((f) => " src/" + f).join("\n") + ); +} + +// --- 3. Real regenerate-and-diff with the exact pinned flatc ---------------- +const tmpBase = fs.mkdtempSync(join(os.tmpdir(), "mxc-psec-")); +let comparedCount; +try { + const flatc = acquireFlatc(provInfo, tmpBase); + const genSrc = regenerate(flatc, tmpBase); + comparedCount = diffTrees(committedSrcDir, genSrc); +} finally { + fs.rmSync(tmpBase, { recursive: true, force: true }); +} + +// --- 4. Committed workspace crate compiles (cross-target validation) -------- +try { + execFileSync("cargo", ["check", "-q", "-p", "process_security_environment_spec"], { + cwd: join(repoRoot, "src"), + stdio: ["ignore", "ignore", "inherit"], + }); +} catch (e) { + fail( + `committed crate failed to compile via ` + + `'cargo check -p process_security_environment_spec': ${e.message}` + ); +} + +console.log( + `PSEC codegen OK: schema matches provenance (sha256 ${provInfo.schemaSha.slice(0, 12)}…), ` + + `regenerated with pinned flatc ${provInfo.flatcVersion} and diffed ${comparedCount} files ` + + `(no drift), all carry the do-not-modify header, and the crate compiles.` +); diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 9c5b31a8a..4d18d61ea 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -38,7 +38,7 @@ export interface BaseProcessUi { } /** - * Windows denial-capture settings. The presence of the `captureDenials` object enables capture; all fields are optional. + * Windows denial-capture settings. The presence of the `captureDenials` object enables capture; all fields are optional. Capture is incompatible with `processContainer.leastPrivilege` and `network.proxy`. Explicit `filesystem.deniedPaths` requires the host's V2 process security-environment support query to advertise native deny enforcement. */ export interface CaptureDenials { /** @@ -319,7 +319,7 @@ export interface ProcessContainer { */ capabilities?: string[] | null; /** - * Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the learning-mode OS API. + * Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the complete official V2 Learning Mode and process security-environment API set. Cannot be combined with `leastPrivilege` or `network.proxy`; `filesystem.deniedPaths` additionally requires the V2 deny-support capability. */ captureDenials?: CaptureDenials | null; /** diff --git a/src/Cargo.lock b/src/Cargo.lock index 15e7a1b9b..bd80afa2c 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -90,7 +90,9 @@ dependencies = [ "getrandom 0.2.17", "learning_mode_core", "learning_mode_windows", + "process_security_environment_spec", "sandbox_spec", + "semver", "serde", "serde_json", "tempfile", @@ -1281,7 +1283,7 @@ version = "0.7.0" dependencies = [ "flatbuffers", "learning_mode_core", - "sandbox_spec", + "process_security_environment_spec", "thiserror", "windows", "windows-core", @@ -1777,6 +1779,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process_security_environment_spec" +version = "0.7.0" +dependencies = [ + "flatbuffers", +] + [[package]] name = "quick-xml" version = "0.41.0" diff --git a/src/Cargo.toml b/src/Cargo.toml index 4a9f7e2ce..767e5e4c4 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -11,6 +11,7 @@ members = [ "core/mxc_build_common", "host/plm", "core/generated/base_container_specification", + "core/generated/process_security_environment_specification", "backends/appcontainer/common", "backends/windows_sandbox/daemon", "backends/windows_sandbox/guest", @@ -91,8 +92,8 @@ windows = { version = "0.62", features = [ "Win32_System_SystemInformation", "Win32_System_Time", "Win32_System_SystemServices", - "Win32_System_SystemInformation", - "Win32_System_JobObjects", + "Win32_System_WindowsProgramming", + "Win32_System_JobObjects", ] } windows-core = "0.62" serde = { version = "1", features = ["derive"] } @@ -129,6 +130,7 @@ isolation_session_bindings = { path = "backends/isolation_session/bindings" } mxc_pty = { path = "core/mxc_pty" } flatbuffers = "25" sandbox_spec = { path = "core/generated/base_container_specification" } +process_security_environment_spec = { path = "core/generated/process_security_environment_specification" } mxc_telemetry = { path = "mxc_telemetry" } widestring = "1" url = "2" diff --git a/src/backends/appcontainer/common/Cargo.toml b/src/backends/appcontainer/common/Cargo.toml index 4da831a40..87a635559 100644 --- a/src/backends/appcontainer/common/Cargo.toml +++ b/src/backends/appcontainer/common/Cargo.toml @@ -18,12 +18,14 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } getrandom = { workspace = true } +semver = "1" [target.'cfg(target_os = "windows")'.dependencies] windows = { workspace = true } windows-core = { workspace = true } flatbuffers = { workspace = true } sandbox_spec = { workspace = true } +process_security_environment_spec = { workspace = true } widestring = { workspace = true } winreg = { workspace = true } learning_mode_windows = { workspace = true } diff --git a/src/backends/appcontainer/common/src/base_container_runner.rs b/src/backends/appcontainer/common/src/base_container_runner.rs index e3dff5935..00de69dde 100644 --- a/src/backends/appcontainer/common/src/base_container_runner.rs +++ b/src/backends/appcontainer/common/src/base_container_runner.rs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! `BaseContainerRunner` — executes scripts via `Experimental_CreateProcessInSandbox` API. +//! `BaseContainerRunner` — executes scripts through the Windows BaseContainer APIs. //! -//! When `wxc-exec` receives a config with `schema_version` >= 0.5, this runner: -//! 1. Builds a FlatBuffer `SandboxSpec` from the container policy -//! 2. Loads `processmodel.dll` dynamically -//! 3. Calls `Experimental_CreateProcessInSandbox` to launch the child process -//! 4. Waits for the process to exit and returns the result +//! Schema versions through 0.7 use the legacy `SandboxSpec` / one-shot +//! `Experimental_CreateProcessInSandbox` path. Schema 0.8 and later prefer the +//! PSEC 1.0 / `CreateProcessSecurityEnvironment` two-phase contract and attach +//! the resulting environment to `CreateProcessW`, but temporarily fall back to +//! the legacy SBOX contract when PSEC is unavailable. use std::ffi::c_void; use std::fmt::Write; @@ -20,9 +20,10 @@ use learning_mode_core::{ write_document, DenialAnalyzer, DenialSummary, DenialsDocument, DenialsOutputPointer, }; use learning_mode_windows::{ - CaptureSession, EtlDenialAnalyzer, LearningModeApi, SecurityEnvironmentApi, - SecurityEnvironmentStartupInfo, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, + CaptureSession, EtlDenialAnalyzer, LearningModeApi, ProcessSecurityEnvironment, + SecurityEnvironmentApi, SecurityEnvironmentStartupInfo, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, }; +use semver::Version; use windows::Win32::Foundation::{ CloseHandle, GetLastError, SetHandleInformation, ERROR_CALL_NOT_IMPLEMENTED, @@ -48,10 +49,18 @@ use crate::launch_diagnostics::{ }; use crate::proxy_coordinator::ProxyCoordinator; use crate::sandbox_tracking::{self, TrackingEntry}; +use process_security_environment_spec::process_security_environment_layout::{ + finish_process_security_environment_buffer, EndpointPolicy as PsecEndpointPolicy, + EndpointPolicyArgs as PsecEndpointPolicyArgs, FilterAction as PsecFilterAction, + NetworkPolicy as PsecNetworkPolicy, NetworkPolicyArgs as PsecNetworkPolicyArgs, + ProcessSecurityEnvironment as PsecProcessSecurityEnvironment, + ProcessSecurityEnvironmentArgs as PsecProcessSecurityEnvironmentArgs, + ProxyInfo as PsecProxyInfo, ProxyInfoArgs as PsecProxyInfoArgs, SchemaVersion, +}; use sandbox_spec::base_container_layout::{ endpoint_policy, endpoint_policyArgs, finish_sandbox_spec_buffer, proxy_info, proxy_infoArgs, - FilterAction, IntegrityLevel, NetworkPolicy as FbsNetworkPolicy, NetworkPolicyArgs, - SandboxSpec, SandboxSpecArgs, + FilterAction as SboxFilterAction, IntegrityLevel, NetworkPolicy as FbsNetworkPolicy, + NetworkPolicyArgs, SandboxSpec, SandboxSpecArgs, }; use wxc_common::log_symbols::{ EMOJI_ALLOWED, EMOJI_BLOCKED, EMOJI_NEUTRAL, EMOJI_SECTION, EMOJI_WARNING, @@ -96,6 +105,21 @@ fn encode_env_block(env_vars: &[String]) -> Vec { block } +fn create_string_vector<'a>( + builder: &mut flatbuffers::FlatBufferBuilder<'a>, + values: &'a [String], +) -> Option>>> +{ + if values.is_empty() { + return None; + } + let offsets: Vec<_> = values + .iter() + .map(|value| builder.create_string(value)) + .collect(); + Some(builder.create_vector(&offsets)) +} + /// Function pointer type matching `Experimental_CreateProcessInSandbox` from processmodel.dll. type PfnCreateProcessInSandbox = unsafe extern "system" fn( application_name: *const u16, @@ -205,11 +229,17 @@ const SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX: u64 = 0x0000_0000_0000_0001; /// assumed bit 1). When clear, `deniedPaths` is rejected at launch and callers /// must rely on default-deny plus explicit `readwrite`/`readonly` grants. const SANDBOX_CAP_FS_DENY: u64 = 0x0000_0000_0000_0002; +/// `SANDBOX_CAP_NETWORK_PROXY`: when set, SBOX uses the model-2 proxy +/// contract, which requires an AppContainer proxy peer identity that MXC does +/// not yet provide. +const SANDBOX_CAP_NETWORK_PROXY: u64 = 0x0000_0000_0000_0004; const CAPTURE_API_AVAILABLE_LOG: &str = "captureDenials: learning-mode trace API available (processmodel.dll)"; -const CAPTURE_SECURITY_ENVIRONMENT_CLEANUP_DEFERRED_REASON: &str = - "capture teardown failed and the process security environment may still be live"; -const CLOSE_PROCESS_SECURITY_ENVIRONMENT_API: &str = "CloseProcessSecurityEnvironment"; +const PSEC_DENIED_PATHS_UNSUPPORTED_MSG: &str = + "schema version 0.8.0 and later with filesystem.deniedPaths requires \ + QueryProcessSecurityEnvironmentSupport to advertise PSE_SUPPORT_FS_DENY; \ + this OS build does not support that policy, and the process-security-environment \ + path cannot fall back to AppContainer or host-DACL enforcement"; const CREATE_PROCESS_IN_SANDBOX_API: &str = "Experimental_CreateProcessInSandbox"; const CREATE_PROCESS_IN_SECURITY_ENVIRONMENT_API: &str = "CreateProcessW(PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT)"; @@ -220,45 +250,34 @@ fn is_api_not_implemented(err: u32) -> bool { err == ERROR_CALL_NOT_IMPLEMENTED.0 || err == E_NOTIMPL.0 as u32 } +/// The schema 0.8 proxy compatibility path deliberately selects transitional +/// SBOX because PSEC cannot yet supply the proxy peer identity. If that older +/// contract reports `ERROR_NOT_SUPPORTED`, expose it as backend availability +/// without changing error classification for unrelated SBOX policies. +fn is_schema_0_8_proxy_fallback_unavailable( + err: u32, + request: &ExecutionRequest, + use_process_security_environment: bool, +) -> bool { + err == ERROR_NOT_SUPPORTED.0 + && !use_process_security_environment + && BaseContainerRunner::schema_prefers_process_security_environment(request) + && request.policy.network_proxy.is_enabled() +} + fn learning_mode_api_not_implemented(error: &learning_mode_windows::LearningModeError) -> bool { match error { - learning_mode_windows::LearningModeError::DllLoad(_) + learning_mode_windows::LearningModeError::ApiSetUnavailable { .. } + | learning_mode_windows::LearningModeError::DllLoad(_) | learning_mode_windows::LearningModeError::ExportMissing { .. } => true, + learning_mode_windows::LearningModeError::HResultCall { code, .. } => *code == E_NOTIMPL.0, learning_mode_windows::LearningModeError::ApiCall { code, .. } => { - is_api_not_implemented(*code) || *code == ERROR_NOT_SUPPORTED.0 - } - learning_mode_windows::LearningModeError::CleanupFailed { primary, .. } => { - learning_mode_api_not_implemented(primary) + is_api_not_implemented(*code) } _ => false, } } -fn learning_mode_cleanup_failed(error: &learning_mode_windows::LearningModeError) -> bool { - match error { - learning_mode_windows::LearningModeError::ApiCall { function, .. } => { - *function == CLOSE_PROCESS_SECURITY_ENVIRONMENT_API - } - learning_mode_windows::LearningModeError::CleanupFailed { primary, cleanup } => { - learning_mode_cleanup_failed(primary) || learning_mode_cleanup_failed(cleanup) - } - _ => false, - } -} - -fn combine_capture_operation_and_cleanup_errors( - primary: learning_mode_windows::LearningModeError, - cleanup: Result<(), learning_mode_windows::LearningModeError>, -) -> learning_mode_windows::LearningModeError { - match cleanup { - Ok(()) => primary, - Err(cleanup) => learning_mode_windows::LearningModeError::CleanupFailed { - primary: Box::new(primary), - cleanup: Box::new(cleanup), - }, - } -} - trait CaptureSessionOps { fn environment(&self) -> HANDLE; fn finish( @@ -288,6 +307,11 @@ trait CaptureSessionFactory: Send + Sync { ) -> Result, learning_mode_windows::LearningModeError>; } +trait CapturePlatformSupport: Send + Sync { + fn check_apis(&self, require_learning_mode: bool) -> Result<(), String>; + fn supports_deny_paths(&self) -> Result; +} + struct RealCaptureSessionFactory; impl CaptureSessionFactory for RealCaptureSessionFactory { @@ -308,11 +332,48 @@ impl CaptureSessionFactory for RealCaptureSessionFactory { } } +struct RealCapturePlatformSupport; + +impl CapturePlatformSupport for RealCapturePlatformSupport { + fn check_apis(&self, require_learning_mode: bool) -> Result<(), String> { + SecurityEnvironmentApi::load() + .map_err(|error| format!("security-environment API: {error}"))?; + if require_learning_mode { + LearningModeApi::load().map_err(|error| format!("learning-mode trace API: {error}"))?; + } + Ok(()) + } + + fn supports_deny_paths(&self) -> Result { + SecurityEnvironmentApi::load() + .map_err(|error| format!("process security-environment API unavailable: {error}"))? + .supports_deny_paths() + .map_err(|error| { + format!("could not query process security-environment support: {error}") + }) + } +} + +enum ResolvedNetworkPolicy<'a> { + Proxy(Option<&'a ProxyAddress>), + Egress(&'a NetworkPolicy), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SboxProxyContract { + LegacyOrUnknown, + Unavailable, + Model2PeerIdentity, +} + /// Script runner that uses `Experimental_CreateProcessInSandbox` API /// to launch a sandboxed process. pub struct BaseContainerRunner { proxy_coordinator: ProxyCoordinator, capture_factory: Arc, + capture_support: Arc, + #[cfg(test)] + psec_usable_override: Option, } impl Default for BaseContainerRunner { @@ -320,6 +381,9 @@ impl Default for BaseContainerRunner { Self { proxy_coordinator: ProxyCoordinator::default(), capture_factory: Arc::new(RealCaptureSessionFactory), + capture_support: Arc::new(RealCapturePlatformSupport), + #[cfg(test)] + psec_usable_override: None, } } } @@ -354,34 +418,27 @@ impl BaseContainerRunner { Self { proxy_coordinator: ProxyCoordinator::default(), capture_factory, + capture_support: Arc::new(RealCapturePlatformSupport), + psec_usable_override: Some(true), } } - fn cleanup_capture_prelaunch_failure( - &mut self, - cleanup_error: Option<&learning_mode_windows::LearningModeError>, - request: &ExecutionRequest, - sid_string: &str, - logger: &mut Logger, - ) { - // This cannot be deferred to BaseContainerRunner::drop: the runner may - // outlive a failed spawn, and the exact error determines whether - // profile deletion is safe or recovery tracking must be retained. - if request.lifecycle.destroy_on_exit { - if cleanup_error.is_some_and(learning_mode_cleanup_failed) { - sandbox_tracking::mark_cleanup_deferred( - sid_string, - CAPTURE_SECURITY_ENVIRONMENT_CLEANUP_DEFERRED_REASON, - logger, - ); - } else { - // CaptureSession::begin receives the sandbox specification, - // not the later process identity. Once its environment is - // closed, only the pre-launch tracking entry needs removal. - sandbox_tracking::remove_tracking_entry(sid_string, logger); - } - sandbox_tracking::unregister_ctrl_c_cleanup(); + #[cfg(test)] + fn with_capture_components( + capture_factory: Arc, + capture_support: Arc, + ) -> Self { + Self { + proxy_coordinator: ProxyCoordinator::default(), + capture_factory, + capture_support, + psec_usable_override: Some(true), } + } + + fn cleanup_capture_begin_failure(&mut self, logger: &mut Logger) { + // CaptureSession owns and closes the PSEC environment. No legacy + // identity/tracking state is created for this path. self.proxy_coordinator.stop(logger); } @@ -402,10 +459,53 @@ impl BaseContainerRunner { /// symbol-present? Resolves enablement up front so tier selection /// never picks a Tier 1 that cannot launch: /// - /// 1. `Experimental_QuerySandboxSupport`, when present, is authoritative. - /// 2. Otherwise, probe the create API itself (older builds lack the query). - /// 3. If even the create symbol is absent, the OS is down-level. + /// 1. Probe the PSEC create/close contract. + /// 2. Otherwise query transitional SBOX support when available. + /// 3. Otherwise probe the SBOX create API itself. pub fn is_base_container_usable() -> bool { + #[cfg(test)] + if let Ok(forced) = std::env::var("MXC_FORCE_BC_USABLE") { + return forced == "1"; + } + if Self::is_process_security_environment_usable() { + return true; + } + Self::is_legacy_base_container_usable() + } + + /// Whether PSEC can create and close a minimal security environment on + /// this host. Export presence and the support query alone are insufficient + /// on transitional builds where the API surface exists before the feature + /// is enabled. + pub fn is_process_security_environment_usable() -> bool { + static USABLE: std::sync::OnceLock = std::sync::OnceLock::new(); + *USABLE.get_or_init(|| { + let request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + let specification = Self::build_process_security_environment_spec(&request); + SecurityEnvironmentApi::load() + .and_then(|api| api.create(&specification, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE)) + .and_then(|environment| { + let startup_info = SecurityEnvironmentStartupInfo::new( + STARTUPINFOW::default(), + environment.raw(), + &[], + ); + environment.close(); + startup_info.map(drop) + }) + .is_ok() + }) + } + + /// Whether the transitional SBOX BaseContainer contract is usable. + fn is_legacy_base_container_usable() -> bool { + #[cfg(test)] + if let Ok(forced) = std::env::var("MXC_FORCE_BC_USABLE") { + return forced == "1"; + } match Self::query_sandbox_create_capability() { Some(enabled) => enabled, None => Self::probe_create_process_feature_enabled(), @@ -422,14 +522,8 @@ impl BaseContainerRunner { /// rejected at launch. Tier 3 (AppContainer + DACL) enforces `deniedPaths` /// via DENY ACEs independently of this bit. pub fn base_container_supports_deny_paths() -> bool { - let Some(query) = Self::load_query_sandbox_support() else { - return false; - }; - let mut capabilities: u64 = 0; - // SAFETY: `query` is the resolved export; `capabilities` is a valid - // out-param. - let succeeded = unsafe { query(&mut capabilities) }; - Self::decode_deny_capability(succeeded, capabilities) + Self::query_sandbox_capabilities() + .is_some_and(|capabilities| Self::decode_deny_capability(1, capabilities)) } /// Decode a `QuerySandboxSupport` result for the deny-paths capability. @@ -445,6 +539,13 @@ impl BaseContainerRunner { /// itself failed), so the caller must probe another way rather than assume /// "unusable". fn query_sandbox_create_capability() -> Option { + Self::query_sandbox_capabilities() + .map(|capabilities| Self::decode_create_capability(1, capabilities)) + } + + /// Query the capability-aware SBOX contract. `None` means the export is + /// absent or the query failed, so callers must use the legacy probe path. + fn query_sandbox_capabilities() -> Option { let query = Self::load_query_sandbox_support()?; let mut capabilities: u64 = 0; // SAFETY: `query` is the resolved export; `capabilities` is a valid @@ -456,7 +557,7 @@ impl BaseContainerRunner { if ok == 0 { return None; } - Some(Self::decode_create_capability(ok, capabilities)) + Some(capabilities) } /// Decode a `QuerySandboxSupport` result: the create-process capability is @@ -465,6 +566,38 @@ impl BaseContainerRunner { ok != 0 && (capabilities & SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX) != 0 } + /// Whether the request can use the legacy SBOX contract selected by MXC. + /// + /// A successful support query identifies the capability-aware OS contract: + /// with `SANDBOX_CAP_NETWORK_PROXY` clear, proxy is unavailable; with it + /// set, proxy requires `allowed_appcontainer_peer` and an AppContainer-hosted + /// proxy. MXC supports neither shape yet, so proxy requests use the + /// AppContainer fallback. Query-less builds retain the older SBOX proxy + /// behavior. + fn legacy_sbox_compatible_with_request( + request: &ExecutionRequest, + queried_capabilities: Option, + ) -> bool { + if !request.policy.network_proxy.is_enabled() { + return true; + } + + matches!( + Self::decode_sbox_proxy_contract(queried_capabilities), + SboxProxyContract::LegacyOrUnknown + ) + } + + fn decode_sbox_proxy_contract(queried_capabilities: Option) -> SboxProxyContract { + match queried_capabilities { + None => SboxProxyContract::LegacyOrUnknown, + Some(capabilities) if (capabilities & SANDBOX_CAP_NETWORK_PROXY) != 0 => { + SboxProxyContract::Model2PeerIdentity + } + Some(_) => SboxProxyContract::Unavailable, + } + } + /// Resolve `Experimental_QuerySandboxSupport`; `None` if not present. fn load_query_sandbox_support() -> Option { let dll_name = string_util::to_wide("processmodel.dll"); @@ -535,45 +668,231 @@ impl BaseContainerRunner { !is_api_not_implemented(err.0) } + fn resolved_network_policy(policy: &ContainerPolicy) -> ResolvedNetworkPolicy<'_> { + if policy.network_proxy.is_enabled() { + ResolvedNetworkPolicy::Proxy(policy.network_proxy.address.as_ref()) + } else { + ResolvedNetworkPolicy::Egress(&policy.default_network_policy) + } + } + // A BaseContainer network policy contains either proxy settings or an egress policy. fn build_network_policy<'a>( builder: &mut flatbuffers::FlatBufferBuilder<'a>, policy: &ContainerPolicy, ) -> flatbuffers::WIPOffset> { - if policy.network_proxy.is_enabled() { - let proxy = policy.network_proxy.address.as_ref().map(|address| { - let url = builder.create_string(&address.to_url()); - proxy_info::create(builder, &proxy_infoArgs { url: Some(url) }) - }); + match Self::resolved_network_policy(policy) { + ResolvedNetworkPolicy::Proxy(address) => { + let proxy = address.map(|address| { + let url = builder.create_string(&address.to_url()); + proxy_info::create(builder, &proxy_infoArgs { url: Some(url) }) + }); - return FbsNetworkPolicy::create( - builder, - &NetworkPolicyArgs { - proxy, - ..Default::default() - }, - ); + FbsNetworkPolicy::create( + builder, + &NetworkPolicyArgs { + proxy, + ..Default::default() + }, + ) + } + ResolvedNetworkPolicy::Egress(default_policy) => { + let default_action = match default_policy { + NetworkPolicy::Allow => SboxFilterAction::allow, + NetworkPolicy::Block => SboxFilterAction::deny, + }; + let egress = endpoint_policy::create( + builder, + &endpoint_policyArgs { + default_action, + ..Default::default() + }, + ); + + FbsNetworkPolicy::create( + builder, + &NetworkPolicyArgs { + egress: Some(egress), + ..Default::default() + }, + ) + } + } + } + + fn build_process_security_environment_network_policy<'a>( + builder: &mut flatbuffers::FlatBufferBuilder<'a>, + policy: &ContainerPolicy, + ) -> flatbuffers::WIPOffset> { + match Self::resolved_network_policy(policy) { + ResolvedNetworkPolicy::Proxy(address) => { + let proxy = address.map(|address| { + let url = builder.create_string(&address.to_url()); + PsecProxyInfo::create(builder, &PsecProxyInfoArgs { url: Some(url) }) + }); + + PsecNetworkPolicy::create( + builder, + &PsecNetworkPolicyArgs { + proxy, + ..Default::default() + }, + ) + } + ResolvedNetworkPolicy::Egress(default_policy) => { + let default_action = match default_policy { + NetworkPolicy::Allow => PsecFilterAction::allow, + NetworkPolicy::Block => PsecFilterAction::deny, + }; + let egress = PsecEndpointPolicy::create( + builder, + &PsecEndpointPolicyArgs { + default_action, + ..Default::default() + }, + ); + + PsecNetworkPolicy::create( + builder, + &PsecNetworkPolicyArgs { + egress: Some(egress), + ..Default::default() + }, + ) + } + } + } + + pub(crate) fn schema_prefers_process_security_environment(request: &ExecutionRequest) -> bool { + Version::parse(&request.schema_version).is_ok_and(|version| { + let comparable = Version::new(version.major, version.minor, version.patch); + comparable >= Version::new(0, 8, 0) + }) + } + + fn should_use_process_security_environment( + request: &ExecutionRequest, + psec_usable: bool, + psec_supports_deny_paths: bool, + ) -> bool { + if !Self::schema_prefers_process_security_environment(request) || !psec_usable { + return false; + } + if request.policy.capture_denials.is_some() { + return true; + } + !request.policy.least_privilege_mode + && !request.policy.network_proxy.is_enabled() + && (request.policy.denied_paths.is_empty() || psec_supports_deny_paths) + } + + fn process_security_environment_usable(&self) -> bool { + #[cfg(test)] + if let Some(usable) = self.psec_usable_override { + return usable; } + Self::is_process_security_environment_usable() + } + + fn uses_process_security_environment(&self, request: &ExecutionRequest) -> bool { + let supports_deny_paths = request.policy.capture_denials.is_some() + || request.policy.denied_paths.is_empty() + || self.capture_support.supports_deny_paths().unwrap_or(false); + Self::should_use_process_security_environment( + request, + self.process_security_environment_usable(), + supports_deny_paths, + ) + } + + pub(crate) fn is_usable_for_request(request: &ExecutionRequest) -> bool { + #[cfg(test)] + if let Ok(forced) = std::env::var("MXC_FORCE_BC_USABLE") { + return forced == "1"; + } + let psec_usable = Self::is_process_security_environment_usable(); + if request.policy.capture_denials.is_some() { + return Self::schema_prefers_process_security_environment(request) && psec_usable; + } + let psec_supports_deny_paths = request.policy.denied_paths.is_empty() + || SecurityEnvironmentApi::load() + .and_then(|api| api.supports_deny_paths()) + .unwrap_or(false); + if Self::should_use_process_security_environment( + request, + psec_usable, + psec_supports_deny_paths, + ) { + return true; + } + if !Self::legacy_sbox_compatible_with_request(request, Self::query_sandbox_capabilities()) { + return false; + } + Self::is_legacy_base_container_usable() + } + + pub(crate) fn supports_deny_paths_for_request(request: &ExecutionRequest) -> bool { + let psec_supports_deny_paths = SecurityEnvironmentApi::load() + .and_then(|api| api.supports_deny_paths()) + .unwrap_or(false); + if Self::should_use_process_security_environment( + request, + Self::is_process_security_environment_usable(), + psec_supports_deny_paths, + ) { + return true; + } + crate::fallback_detector::base_container_supports_deny_paths() + } + + fn build_process_security_environment_spec(request: &ExecutionRequest) -> Vec { + let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(1024); + let version = SchemaVersion::new(1, 0); - let default_action = match &policy.default_network_policy { - NetworkPolicy::Allow => FilterAction::allow, - NetworkPolicy::Block => FilterAction::deny, + let needs_internet_client = Self::needs_internet_client(request); + let capabilities = if request.policy.capabilities.is_empty() && !needs_internet_client { + None + } else { + let mut capabilities = request.policy.capabilities.join(","); + if needs_internet_client { + if !capabilities.is_empty() { + capabilities.push(','); + } + capabilities.push_str("internetClient"); + } + Some(builder.create_string(&capabilities)) }; - let egress = endpoint_policy::create( - builder, - &endpoint_policyArgs { - default_action, - ..Default::default() - }, - ); - FbsNetworkPolicy::create( - builder, - &NetworkPolicyArgs { - egress: Some(egress), - ..Default::default() + let fs_read_write = create_string_vector(&mut builder, &request.policy.readwrite_paths); + let fs_read_only = create_string_vector(&mut builder, &request.policy.readonly_paths); + let fs_deny = create_string_vector(&mut builder, &request.policy.denied_paths); + let network_policy = Some(Self::build_process_security_environment_network_policy( + &mut builder, + &request.policy, + )); + + let ui_restrictions = crate::job_object::to_job_object_uilimit_mask( + &wxc_common::ui_policy::resolve_ui_restrictions( + &request.policy.ui, + &request.policy.base_process_ui, + ), + ) as u64; + + let spec = PsecProcessSecurityEnvironment::create( + &mut builder, + &PsecProcessSecurityEnvironmentArgs { + version: Some(&version), + capabilities, + disallow_win32k_system_calls: request.policy.ui.disable, + ui_restrictions, + fs_read_write, + fs_read_only, + fs_deny, + network_policy, }, - ) + ); + finish_process_security_environment_buffer(&mut builder, spec); + builder.finished_data().to_vec() } /// Build a FlatBuffer `SandboxSpec` from the container policy in the request. @@ -594,21 +913,7 @@ impl BaseContainerRunner { let version = builder.create_string(SANDBOX_SPEC_VERSION); - // Match legacy AppContainer behaviour: when network enforcement uses - // capabilities and the default policy is Allow, ensure internetClient - // is present so the sandboxed process has network access. - let mut caps = request.policy.capabilities.clone(); - let use_caps_for_network = matches!( - request.policy.network_enforcement_mode, - NetworkEnforcementMode::Capabilities | NetworkEnforcementMode::Both - ); - if use_caps_for_network - && request.policy.default_network_policy == NetworkPolicy::Allow - && !caps.iter().any(|c| c == "internetClient") - { - caps.push("internetClient".to_string()); - } - + let caps = Self::effective_capabilities(request); let capabilities = if caps.is_empty() { None } else { @@ -682,6 +987,31 @@ impl BaseContainerRunner { builder.finished_data().to_vec() } + fn needs_internet_client(request: &ExecutionRequest) -> bool { + let use_caps_for_network = matches!( + request.policy.network_enforcement_mode, + NetworkEnforcementMode::Capabilities | NetworkEnforcementMode::Both + ); + use_caps_for_network + && request.policy.default_network_policy == NetworkPolicy::Allow + && !request + .policy + .capabilities + .iter() + .any(|capability| capability == "internetClient") + } + + fn effective_capabilities(request: &ExecutionRequest) -> Vec { + // Match legacy AppContainer behaviour: when network enforcement uses + // capabilities and the default policy is Allow, ensure internetClient + // is present so the sandboxed process has network access. + let mut caps = request.policy.capabilities.clone(); + if Self::needs_internet_client(request) { + caps.push("internetClient".to_string()); + } + caps + } + /// Log the contents of a built sandbox spec FlatBuffer for debug verification. /// /// Reads back token, network, and UI restriction fields from the serialised @@ -912,16 +1242,29 @@ impl BaseContainerRunner { let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: Build sandbox spec"); - // 1. Build the FlatBuffer sandbox spec from the request policy. - let spec_bytes = Self::build_sandbox_spec(&request); - - Self::log_sandbox_spec(&spec_bytes, logger); - let capture_denials = request.policy.capture_denials.clone(); + let use_process_security_environment = self.uses_process_security_environment(&request); + let spec_bytes = if !use_process_security_environment { + let bytes = Self::build_sandbox_spec(&request); + Self::log_sandbox_spec(&bytes, logger); + Some(bytes) + } else { + None + }; if capture_denials.is_some() { let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: captureDenials"); } + let process_security_environment_spec = use_process_security_environment + .then(|| Self::build_process_security_environment_spec(&request)); + if let Some(psec_spec) = process_security_environment_spec.as_ref() { + let _ = writeln!( + logger, + "process security environment spec built (PSEC 1.0, {} bytes)", + psec_spec.len() + ); + } + // Resolve two paths for the capture: // * `capture_etl_path` — an always-internal, runner-managed temp `.etl` // that the OS broker seals into. It is decoded then deleted in @@ -943,15 +1286,21 @@ impl BaseContainerRunner { let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: Load API"); - // 2. Dynamically load the API from processmodel.dll. - let create_process_in_sandbox = match Self::load_api() { - Ok(f) => f, - Err(e) => return Err(ScriptResponse::error(&e)), + // Schema versions through 0.7 use the SBOX one-shot API. Schema 0.8+ + // uses only the process-security-environment APIs. + let create_process_in_sandbox = if !use_process_security_environment { + let api = match Self::load_api() { + Ok(f) => f, + Err(e) => return Err(ScriptResponse::error(&e)), + }; + let _ = writeln!( + logger, + "loaded Experimental_CreateProcessInSandbox from processmodel.dll" + ); + Some(api) + } else { + None }; - let _ = writeln!( - logger, - "loaded Experimental_CreateProcessInSandbox from processmodel.dll" - ); let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: Launch process"); @@ -967,10 +1316,15 @@ impl BaseContainerRunner { cwd_wide.as_ptr() }; - // Identity: when destroy_on_exit is true we generate a random ephemeral - // identity so each sandbox gets a unique, cleanable AppContainer profile. + let legacy_destroy_on_exit = + !use_process_security_environment && request.lifecycle.destroy_on_exit; + + // Identity applies only to the SBOX one-shot API. PSEC creates and owns + // its own AppContainer identity and profile. // Otherwise we honour whatever the caller passed in (or the default). - let (identity, sid_string) = if request.lifecycle.destroy_on_exit { + let (identity, sid_string) = if use_process_security_environment { + ("".to_string(), String::new()) + } else if legacy_destroy_on_exit { let ephemeral = sandbox_tracking::generate_sandbox_identity(); let _ = writeln!( logger, @@ -1022,7 +1376,7 @@ impl BaseContainerRunner { // Register Ctrl+C handler early so cleanup runs if wxc-exec is interrupted // during or after the create call. - if request.lifecycle.destroy_on_exit { + if legacy_destroy_on_exit { sandbox_tracking::register_ctrl_c_cleanup( &identity, &sid_string, @@ -1178,7 +1532,7 @@ impl BaseContainerRunner { // attribute-based CreateProcessW capture path must receive an explicit // clean block or it would inherit all wxc-exec process variables. let env_block: Option> = if request.env.is_empty() { - if capture_denials.is_some() { + if use_process_security_environment { let entries = crate::appcontainer_runner::create_default_env_entries().map_err(|error| { ScriptResponse::error(&format!( @@ -1249,51 +1603,83 @@ impl BaseContainerRunner { let current_env_ptr = env_ptr; let current_creation_flags = creation_flags; - // When captureDenials is active, launch inside a process security - // environment that already has a learning-mode trace started against it, - // instead of the one-shot CreateProcessInSandbox. `begin` creates the - // environment and starts the trace *before* the child launches (so no - // early denials are missed); the environment handle is attached to a - // normal CreateProcessW call via PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT. - // On any early return below, `capture_session` drops and its Drop - // discards the trace and closes the environment (no broker leak). + // Schema 0.8 and later prefer a process security environment when its + // runtime probe succeeds. During the SBOX-to-PSEC transition, ordinary + // requests fall back to the legacy contract when PSEC is unavailable. + // captureDenials still requires PSEC because SBOX cannot provide the + // environment handle needed to key the trace. let mut capture_session: Option> = None; - if capture_denials.is_some() { - match self - .capture_factory - .begin(&spec_bytes, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE) - { - Ok(session) => { - let _ = writeln!( - logger, - "{CAPTURE_API_AVAILABLE_LOG}; security environment and trace started" - ); - capture_session = Some(session); + let mut security_environment: Option = None; + if use_process_security_environment { + let psec_spec = process_security_environment_spec + .as_deref() + .expect("PSEC spec is initialized for schema version 0.8 and later"); + if capture_denials.is_some() { + match self + .capture_factory + .begin(psec_spec, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE) + { + Ok(session) => { + let _ = writeln!( + logger, + "{CAPTURE_API_AVAILABLE_LOG}; security environment and trace started" + ); + capture_session = Some(session); + } + Err(e) => { + let msg = + format!("captureDenials: failed to start learning-mode capture: {e}"); + let _ = writeln!(logger, "Error: {msg}"); + let failure_phase = if learning_mode_api_not_implemented(&e) { + FailurePhase::BackendUnavailable + } else { + FailurePhase::LaunchFailed + }; + self.cleanup_capture_begin_failure(logger); + return Err(ScriptResponse { + exit_code: -1, + error_message: msg.clone(), + standard_err: msg, + failure_phase, + ..Default::default() + }); + } } - Err(e) => { - let msg = format!("captureDenials: failed to start learning-mode capture: {e}"); - let _ = writeln!(logger, "Error: {msg}"); - let failure_phase = if learning_mode_api_not_implemented(&e) { - FailurePhase::BackendUnavailable - } else { - FailurePhase::LaunchFailed - }; - self.cleanup_capture_prelaunch_failure(Some(&e), &request, &sid_string, logger); - return Err(ScriptResponse { - exit_code: -1, - error_message: msg.clone(), - standard_err: msg, - failure_phase, - ..Default::default() - }); + } else { + let result = SecurityEnvironmentApi::load() + .and_then(|api| api.create(psec_spec, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE)); + match result { + Ok(environment) => { + let _ = writeln!( + logger, + "process security environment created (processmodel.dll)" + ); + security_environment = Some(environment); + } + Err(error) => { + let msg = + format!("failed to create the process security environment: {error}"); + let _ = writeln!(logger, "Error: {msg}"); + let failure_phase = if learning_mode_api_not_implemented(&error) { + FailurePhase::BackendUnavailable + } else { + FailurePhase::LaunchFailed + }; + return Err(ScriptResponse { + exit_code: -1, + error_message: msg.clone(), + standard_err: msg, + failure_phase, + ..Default::default() + }); + } } } } // The launch yields (api_return_code, last_win32_error_on_failure). - let (success, last_error, launch_api_name) = if let Some(session) = capture_session.as_ref() - { - // Single-attempt in-environment launch. The learning-mode security + let (success, last_error, launch_api_name) = if use_process_security_environment { + // Single-attempt in-environment launch. The process security // environment is attached as a process-thread attribute; the // CreateProcessInSandbox environment fallback does not apply here. pi = unsafe { std::mem::zeroed() }; @@ -1302,33 +1688,48 @@ impl BaseContainerRunner { } else { Vec::new() }; + let environment_handle = capture_session + .as_ref() + .map(|session| session.environment()) + .or_else(|| { + security_environment + .as_ref() + .map(ProcessSecurityEnvironment::raw) + }) + .expect("PSEC environment owner is initialized before launch"); let extended_startup = match SecurityEnvironmentStartupInfo::new( si, - session.environment(), + environment_handle, &inherited_handles, ) { Ok(startup) => startup, Err(primary) => { - let cleanup = capture_session + let cleanup_error = capture_session .take() .map(|session| session.finish(None)) - .unwrap_or(Ok(())); - let error = combine_capture_operation_and_cleanup_errors(primary, cleanup); - let msg = format!( - "captureDenials: failed to attach the process security environment: {error}" + .unwrap_or(Ok(())) + .err(); + let mut msg = + format!("failed to attach the process security environment: {primary}"); + if let Some(cleanup_error) = &cleanup_error { + let _ = write!( + msg, + "; additionally failed to discard the learning-mode trace: {cleanup_error}" ); + } let _ = writeln!(logger, "Error: {msg}"); - let failure_phase = if learning_mode_api_not_implemented(&error) { + let failure_phase = if learning_mode_api_not_implemented(&primary) + || cleanup_error + .as_ref() + .is_some_and(learning_mode_api_not_implemented) + { FailurePhase::BackendUnavailable } else { FailurePhase::LaunchFailed }; - self.cleanup_capture_prelaunch_failure( - Some(&error), - &request, - &sid_string, - logger, - ); + if capture_denials.is_some() { + self.cleanup_capture_begin_failure(logger); + } return Err(ScriptResponse { exit_code: -1, error_message: msg.clone(), @@ -1363,13 +1764,29 @@ impl BaseContainerRunner { ) } } else { + let create_process_in_sandbox = match create_process_in_sandbox { + Some(api) => api, + None => { + return Err(ScriptResponse::error( + "internal error: SBOX launch API was not initialized", + )) + } + }; + let spec_bytes = match spec_bytes.as_deref() { + Some(bytes) => bytes, + None => { + return Err(ScriptResponse::error( + "internal error: SBOX specification was not initialized", + )) + } + }; let (success, error) = SandboxLaunchArgs { api: create_process_in_sandbox, command_line: &mut cmd_wide, current_directory: cwd_ptr, startup_info: &si, identity: &identity_wide, - sandbox_specification: &spec_bytes, + sandbox_specification: spec_bytes, no_window_flag, } .launch_with_environment_fallback( @@ -1396,13 +1813,8 @@ impl BaseContainerRunner { .take() .and_then(|session| session.finish(None).err()); if capture_denials.is_some() { - self.cleanup_capture_prelaunch_failure( - capture_cleanup_error.as_ref(), - &request, - &sid_string, - logger, - ); - } else if request.lifecycle.destroy_on_exit { + self.cleanup_capture_begin_failure(logger); + } else if legacy_destroy_on_exit { // The OS may have created the AppContainer profile before // failing, so run the same cleanup logic used on normal exit. run_sandbox_cleanup( @@ -1439,7 +1851,12 @@ impl BaseContainerRunner { // Classify a disabled-feature error as BackendUnavailable; any // other launch error stays LaunchFailed. - let failure_phase = if is_api_not_implemented(err.0) { + let failure_phase = if is_api_not_implemented(err.0) + || is_schema_0_8_proxy_fallback_unavailable( + err.0, + &request, + use_process_security_environment, + ) { FailurePhase::BackendUnavailable } else { FailurePhase::LaunchFailed @@ -1515,13 +1932,8 @@ impl BaseContainerRunner { .take() .and_then(|session| session.finish(None).err()); if capture_denials.is_some() { - self.cleanup_capture_prelaunch_failure( - capture_cleanup_error.as_ref(), - &request, - &sid_string, - logger, - ); - } else if request.lifecycle.destroy_on_exit { + self.cleanup_capture_begin_failure(logger); + } else if legacy_destroy_on_exit { run_sandbox_cleanup( &identity, &sid_string, @@ -1579,12 +1991,13 @@ impl BaseContainerRunner { stdout_read, stderr_read, timeout_ms: get_timeout_milliseconds(request.script_timeout), - destroy_on_exit: request.lifecycle.destroy_on_exit, + destroy_on_exit: legacy_destroy_on_exit, proxy_enabled: request.policy.network_proxy.is_enabled(), identity, sid_string, proxy_coordinator: std::mem::take(&mut self.proxy_coordinator), capture_session, + security_environment, capture_etl_path, capture_output_path, }) @@ -1617,6 +2030,9 @@ struct BaseChild { /// is configured and the OS API is available). Sealed in `run_teardown` /// after the child exits. capture_session: Option>, + /// Non-capture PSEC environment for schema 0.8+ requests. Retained until + /// the child exits so policy enforcement outlives the process tree. + security_environment: Option, /// Internal runner-managed temp `.etl` the broker seals into. Decoded /// then deleted in `run_teardown`. `Some` iff `capture_session` is `Some`. capture_etl_path: Option, @@ -1627,14 +2043,37 @@ struct BaseChild { impl SandboxBackend for BaseContainerRunner { fn validate(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { - // deniedPaths reaches the OS via the SandboxSpec `fs_deny` field, honored - // only when the OS advertises SANDBOX_CAP_FS_DENY. The dispatcher only - // routes deny here when supported; fail closed for direct callers. - if !request.policy.denied_paths.is_empty() - && !crate::fallback_detector::base_container_supports_deny_paths() - { + let capture_denials = request.policy.capture_denials.is_some(); + let schema_prefers_process_security_environment = + Self::schema_prefers_process_security_environment(request); + let use_process_security_environment = self.uses_process_security_environment(request); + if capture_denials && !schema_prefers_process_security_environment { return Err(ScriptResponse::error( - wxc_common::error::DENIED_PATHS_FEATURE_DISABLED_MSG, + "processContainer.captureDenials requires schema version 0.8.0 or later", + )); + } + if capture_denials && !use_process_security_environment { + return Err(ScriptResponse { + failure_phase: FailurePhase::BackendUnavailable, + ..ScriptResponse::error( + "processContainer.captureDenials requires the official process \ + security-environment APIs; this host can only use a legacy \ + ProcessContainer fallback", + ) + }); + } + if use_process_security_environment && request.policy.least_privilege_mode { + return Err(ScriptResponse::error( + "schema version 0.8.0 and later cannot be combined with \ + processContainer.leastPrivilege because the Windows process \ + security-environment contract does not support LPAC tokens", + )); + } + if use_process_security_environment && request.policy.network_proxy.is_enabled() { + return Err(ScriptResponse::error( + "schema version 0.8.0 and later cannot be combined with network.proxy \ + until the process-security-environment path can supply the required \ + proxy AppContainer peer identity", )); } if !request.policy.allowed_hosts.is_empty() || !request.policy.blocked_hosts.is_empty() { @@ -1642,6 +2081,51 @@ impl SandboxBackend for BaseContainerRunner { wxc_common::error::HOST_LISTS_NOT_SUPPORTED_MSG, )); } + // Dry-run validates the schema and policy shape without requiring the + // current host to expose the selected schema's OS APIs. + if request.dry_run { + return Ok(()); + } + if use_process_security_environment { + self.capture_support + .check_apis(capture_denials) + .map_err(|detail| ScriptResponse { + failure_phase: FailurePhase::BackendUnavailable, + ..ScriptResponse::error(&format!( + "schema version 0.8.0 and later requires the official process \ + security-environment APIs ({detail})" + )) + })?; + } + // deniedPaths reaches BaseContainer through whichever contract the + // runtime probe selected. Each path has a distinct support query; fail + // closed rather than silently dropping the deny policy. + if !request.policy.denied_paths.is_empty() { + let deny_supported = if use_process_security_environment { + self.capture_support + .supports_deny_paths() + .map_err(|message| ScriptResponse { + failure_phase: FailurePhase::BackendUnavailable, + ..ScriptResponse::error(&message) + })? + } else { + crate::fallback_detector::base_container_supports_deny_paths() + }; + if !deny_supported { + return Err(if use_process_security_environment { + ScriptResponse { + failure_phase: FailurePhase::BackendUnavailable, + ..ScriptResponse::error(PSEC_DENIED_PATHS_UNSUPPORTED_MSG) + } + } else { + ScriptResponse::error(wxc_common::error::DENIED_PATHS_FEATURE_DISABLED_MSG) + }); + } + } + if use_process_security_environment { + return Ok(()); + } + Self::is_base_container_api_present().map_err(|e| { let hint = format!( "BaseContainer API unavailable: {e}\n\ @@ -1714,6 +2198,8 @@ struct BaseContainerSandboxProcess { /// Live learning-mode capture session, moved from the `BaseChild`. Sealed /// in `run_teardown` once the child has exited and been reaped. capture_session: Option>, + /// Non-capture PSEC environment, closed after the child exits and is reaped. + security_environment: Option, /// Internal runner-managed temp `.etl` the broker seals into. capture_etl_path: Option, /// Resolved JSON denials deliverable path. @@ -1758,6 +2244,7 @@ impl BaseContainerSandboxProcess { proxy_coordinator: std::mem::take(&mut child.proxy_coordinator), teardown_result: None, capture_session: child.capture_session.take(), + security_environment: child.security_environment.take(), capture_etl_path: child.capture_etl_path.take(), capture_output_path: child.capture_output_path.take(), last_exit_code: None, @@ -1777,7 +2264,6 @@ impl BaseContainerSandboxProcess { // deliverable that consuming apps read, delete the temp, and retain // structured metadata for the caller. Any seal/decode/write failure is // returned through `wait()`. - let mut defer_capture_cleanup = false; let capture_result = if let Some(session) = self.capture_session.take() { let etl_path = self.capture_etl_path.take(); let output_path = self.capture_output_path.take(); @@ -1798,18 +2284,15 @@ impl BaseContainerSandboxProcess { .unwrap_or(Ok(())), ), }, - Err(error) => { - defer_capture_cleanup = learning_mode_cleanup_failed(&error); - combine_capture_and_cleanup_results( - Err(std::io::Error::other(format!( - "captureDenials failed to finalize the denial capture: {error}" - ))), - etl_path - .as_deref() - .map(remove_internal_capture_file) - .unwrap_or(Ok(())), - ) - } + Err(error) => combine_capture_and_cleanup_results( + Err(std::io::Error::other(format!( + "captureDenials failed to finalize the denial capture: {error}" + ))), + etl_path + .as_deref() + .map(remove_internal_capture_file) + .unwrap_or(Ok(())), + ), }; if let Ok(Some(metadata)) = &result { self.output_metadata = Some(SandboxOutputMetadata { @@ -1820,22 +2303,15 @@ impl BaseContainerSandboxProcess { } else { Ok(None) }; + self.security_environment.take(); if self.destroy_on_exit { - if defer_capture_cleanup { - sandbox_tracking::mark_cleanup_deferred( - &self.sid_string, - CAPTURE_SECURITY_ENVIRONMENT_CLEANUP_DEFERRED_REASON, - &mut logger, - ); - } else { - run_sandbox_cleanup( - &self.identity, - &self.sid_string, - self.proxy_enabled, - &mut logger, - ); - } + run_sandbox_cleanup( + &self.identity, + &self.sid_string, + self.proxy_enabled, + &mut logger, + ); sandbox_tracking::unregister_ctrl_c_cleanup(); } self.proxy_coordinator.stop(&mut logger); @@ -2196,13 +2672,14 @@ mod tests { use learning_mode_core::{ AccessType, AnalysisResult, AnalyzeError, DeniedResource, ResourceType, }; + use process_security_environment_spec::process_security_environment_layout as psec_layout; use sandbox_spec::base_container_layout; use std::sync::atomic::{AtomicUsize, Ordering}; use wxc_common::models::{ClipboardPolicy, ProxyConfig, UiPolicy}; use wxc_common::ui_policy::EffectiveUiRestrictions; struct FakeCaptureSession { - finish_error: Option<(&'static str, u32)>, + finish_error: Option<(&'static str, i32)>, finish_calls: Arc, } @@ -2218,7 +2695,7 @@ mod tests { self.finish_calls.fetch_add(1, Ordering::SeqCst); match self.finish_error { Some((function, code)) => { - Err(learning_mode_windows::LearningModeError::ApiCall { function, code }) + Err(learning_mode_windows::LearningModeError::HResultCall { function, code }) } None => Ok(()), } @@ -2226,8 +2703,8 @@ mod tests { } struct FakeCaptureFactory { - begin_error: Option<(&'static str, u32)>, - finish_error: Option<(&'static str, u32)>, + begin_error: Option<(&'static str, i32)>, + finish_error: Option<(&'static str, i32)>, begin_calls: AtomicUsize, finish_calls: Arc, } @@ -2240,7 +2717,10 @@ mod tests { ) -> Result, learning_mode_windows::LearningModeError> { self.begin_calls.fetch_add(1, Ordering::SeqCst); if let Some((function, code)) = self.begin_error { - return Err(learning_mode_windows::LearningModeError::ApiCall { function, code }); + return Err(learning_mode_windows::LearningModeError::HResultCall { + function, + code, + }); } Ok(Box::new(FakeCaptureSession { finish_error: self.finish_error, @@ -2249,6 +2729,51 @@ mod tests { } } + struct FakeCaptureSupport { + api_error: Option<&'static str>, + deny_error: Option<&'static str>, + deny_supported: bool, + api_calls: AtomicUsize, + learning_mode_api_calls: AtomicUsize, + deny_calls: AtomicUsize, + } + + impl CapturePlatformSupport for FakeCaptureSupport { + fn check_apis(&self, require_learning_mode: bool) -> Result<(), String> { + self.api_calls.fetch_add(1, Ordering::SeqCst); + if require_learning_mode { + self.learning_mode_api_calls.fetch_add(1, Ordering::SeqCst); + } + self.api_error + .map_or(Ok(()), |error| Err(error.to_string())) + } + + fn supports_deny_paths(&self) -> Result { + self.deny_calls.fetch_add(1, Ordering::SeqCst); + self.deny_error + .map_or(Ok(self.deny_supported), |error| Err(error.to_string())) + } + } + + fn fake_capture_factory() -> Arc { + Arc::new(FakeCaptureFactory { + begin_error: None, + finish_error: None, + begin_calls: AtomicUsize::new(0), + finish_calls: Arc::new(AtomicUsize::new(0)), + }) + } + + fn capture_request_with_denied_path() -> ExecutionRequest { + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + request.policy.capture_denials = Some(Default::default()); + request.policy.denied_paths = vec![r"C:\secret".to_string()]; + request + } + struct FakeAnalyzer { result: Result, } @@ -2447,30 +2972,66 @@ mod tests { fn is_api_not_implemented_classifies_disabled_feature() { assert!(is_api_not_implemented(ERROR_CALL_NOT_IMPLEMENTED.0)); assert!(is_api_not_implemented(E_NOTIMPL.0 as u32)); - // ERROR_INVALID_PARAMETER (87) and success are ordinary, not "disabled". + // ERROR_NOT_SUPPORTED, ERROR_INVALID_PARAMETER, and success are not + // globally classified as disabled-feature failures. + assert!(!is_api_not_implemented(ERROR_NOT_SUPPORTED.0)); assert!(!is_api_not_implemented(87)); assert!(!is_api_not_implemented(0)); } + #[test] + fn error_not_supported_is_backend_unavailable_only_for_schema_0_8_proxy_fallback() { + let mut proxy_request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + proxy_request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + + assert!(is_schema_0_8_proxy_fallback_unavailable( + ERROR_NOT_SUPPORTED.0, + &proxy_request, + false + )); + assert!(!is_schema_0_8_proxy_fallback_unavailable( + ERROR_NOT_SUPPORTED.0, + &proxy_request, + true + )); + + proxy_request.schema_version = "0.7.0-alpha".to_string(); + assert!(!is_schema_0_8_proxy_fallback_unavailable( + ERROR_NOT_SUPPORTED.0, + &proxy_request, + false + )); + + let ordinary_request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + assert!(!is_schema_0_8_proxy_fallback_unavailable( + ERROR_NOT_SUPPORTED.0, + &ordinary_request, + false + )); + } + #[test] fn learning_mode_api_not_implemented_checks_primary_failure() { use learning_mode_windows::LearningModeError; - let disabled = LearningModeError::CleanupFailed { - primary: Box::new(LearningModeError::ApiCall { - function: "CreateProcessSecurityEnvironment", - code: ERROR_CALL_NOT_IMPLEMENTED.0, - }), - cleanup: Box::new(LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - code: 87, - }), + let disabled = LearningModeError::HResultCall { + function: "StartLearningModeTrace", + code: E_NOTIMPL.0, }; assert!(learning_mode_api_not_implemented(&disabled)); - let ordinary = LearningModeError::ApiCall { + let ordinary = LearningModeError::HResultCall { function: "StartLearningModeTrace", - code: 87, + code: windows::Win32::Foundation::E_INVALIDARG.0, }; assert!(!learning_mode_api_not_implemented(&ordinary)); @@ -2486,62 +3047,13 @@ mod tests { )); } - #[test] - fn learning_mode_cleanup_failure_is_detected() { - use learning_mode_windows::LearningModeError; - - let error = LearningModeError::CleanupFailed { - primary: Box::new(LearningModeError::ApiCall { - function: "StartLearningModeTrace", - code: 87, - }), - cleanup: Box::new(LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - code: 5, - }), - }; - assert!(learning_mode_cleanup_failed(&error)); - - let close_only = LearningModeError::ApiCall { - function: CLOSE_PROCESS_SECURITY_ENVIRONMENT_API, - code: 5, - }; - assert!(learning_mode_cleanup_failed(&close_only)); - - let ordinary = LearningModeError::ApiCall { - function: "StartLearningModeTrace", - code: 87, - }; - assert!(!learning_mode_cleanup_failed(&ordinary)); - } - - #[test] - fn prelaunch_failure_preserves_capture_cleanup_error() { - use learning_mode_windows::LearningModeError; - - let error = combine_capture_operation_and_cleanup_errors( - LearningModeError::ApiCall { - function: "UpdateProcThreadAttribute(SecurityEnvironment)", - code: 87, - }, - Err(LearningModeError::ApiCall { - function: CLOSE_PROCESS_SECURITY_ENVIRONMENT_API, - code: 5, - }), - ); - - assert!(matches!(error, LearningModeError::CleanupFailed { .. })); - assert!(learning_mode_cleanup_failed(&error)); - assert!(error.to_string().contains("UpdateProcThreadAttribute")); - assert!(error - .to_string() - .contains(CLOSE_PROCESS_SECURITY_ENVIRONMENT_API)); - } - #[test] fn capture_factory_injects_begin_failure() { let factory = Arc::new(FakeCaptureFactory { - begin_error: Some(("StartLearningModeTrace", 5)), + begin_error: Some(( + "StartLearningModeTrace", + windows::Win32::Foundation::E_FAIL.0, + )), finish_error: None, begin_calls: AtomicUsize::new(0), finish_calls: Arc::new(AtomicUsize::new(0)), @@ -2565,7 +3077,10 @@ mod tests { fn capture_factory_injects_finish_failure_once() { let factory = Arc::new(FakeCaptureFactory { begin_error: None, - finish_error: Some((CLOSE_PROCESS_SECURITY_ENVIRONMENT_API, 5)), + finish_error: Some(( + "StopLearningModeTrace", + windows::Win32::Foundation::E_FAIL.0, + )), begin_calls: AtomicUsize::new(0), finish_calls: Arc::new(AtomicUsize::new(0)), }); @@ -2577,7 +3092,13 @@ mod tests { let error = session.finish(None).expect_err("fake finish must fail"); - assert!(learning_mode_cleanup_failed(&error)); + assert!(matches!( + error, + learning_mode_windows::LearningModeError::HResultCall { + function: "StopLearningModeTrace", + .. + } + )); assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 1); assert_eq!(factory.finish_calls.load(Ordering::SeqCst), 1); } @@ -2608,6 +3129,53 @@ mod tests { )); } + #[test] + fn legacy_sbox_proxy_compatibility_uses_appcontainer_on_query_aware_hosts() { + let mut request = ExecutionRequest::default(); + request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + + assert!(BaseContainerRunner::legacy_sbox_compatible_with_request( + &request, None + )); + assert!(!BaseContainerRunner::legacy_sbox_compatible_with_request( + &request, + Some(SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX) + )); + assert!(!BaseContainerRunner::legacy_sbox_compatible_with_request( + &request, + Some(SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX | SANDBOX_CAP_NETWORK_PROXY) + )); + assert_eq!( + BaseContainerRunner::decode_sbox_proxy_contract(None), + SboxProxyContract::LegacyOrUnknown + ); + assert_eq!( + BaseContainerRunner::decode_sbox_proxy_contract(Some( + SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX + )), + SboxProxyContract::Unavailable + ); + assert_eq!( + BaseContainerRunner::decode_sbox_proxy_contract(Some( + SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX | SANDBOX_CAP_NETWORK_PROXY + )), + SboxProxyContract::Model2PeerIdentity + ); + } + + #[test] + fn legacy_sbox_non_proxy_requests_ignore_proxy_contract_capability() { + let request = ExecutionRequest::default(); + + assert!(BaseContainerRunner::legacy_sbox_compatible_with_request( + &request, + Some(SANDBOX_CAP_CREATE_PROCESS_IN_SANDBOX | SANDBOX_CAP_NETWORK_PROXY) + )); + } + #[test] fn build_sandbox_spec_produces_valid_flatbuffer() { let mut request = ExecutionRequest::default(); @@ -2669,6 +3237,159 @@ mod tests { ); } + #[test] + fn build_process_security_environment_spec_produces_valid_psec() { + let mut request = ExecutionRequest::default(); + request.policy.capabilities = vec!["internetClient".into(), "registryRead".into()]; + request.policy.readwrite_paths = vec!["C:\\temp".into()]; + request.policy.readonly_paths = vec!["C:\\Windows".into()]; + request.policy.denied_paths = vec!["C:\\secret".into()]; + + let bytes = BaseContainerRunner::build_process_security_environment_spec(&request); + + assert!(psec_layout::process_security_environment_buffer_has_identifier(&bytes)); + let spec = psec_layout::root_as_process_security_environment(&bytes).unwrap(); + let version = spec.version(); + assert_eq!(version.major(), 1); + assert_eq!(version.minor(), 0); + assert_eq!(spec.capabilities(), Some("internetClient,registryRead")); + assert_eq!( + spec.fs_read_write().unwrap().iter().collect::>(), + vec!["C:\\temp"] + ); + assert_eq!( + spec.fs_read_only().unwrap().iter().collect::>(), + vec!["C:\\Windows"] + ); + assert_eq!( + spec.fs_deny().unwrap().iter().collect::>(), + vec!["C:\\secret"] + ); + let egress = spec + .network_policy() + .and_then(|policy| policy.egress()) + .expect("PSEC must carry an explicit egress default"); + assert_eq!(egress.default_action(), psec_layout::FilterAction::deny); + assert!(egress.allow().is_none()); + assert!(egress.deny().is_none()); + } + + #[test] + fn build_process_security_environment_spec_preserves_allow_egress() { + let mut request = ExecutionRequest::default(); + request.policy.default_network_policy = NetworkPolicy::Allow; + + let bytes = BaseContainerRunner::build_process_security_environment_spec(&request); + let spec = psec_layout::root_as_process_security_environment(&bytes).unwrap(); + let egress = spec + .network_policy() + .and_then(|policy| policy.egress()) + .expect("PSEC must carry an explicit egress default"); + + assert_eq!(egress.default_action(), psec_layout::FilterAction::allow); + assert_eq!(spec.capabilities(), Some("internetClient")); + } + + #[test] + fn build_process_security_environment_spec_preserves_proxy_url() { + let mut request = ExecutionRequest::default(); + request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + + let bytes = BaseContainerRunner::build_process_security_environment_spec(&request); + let spec = psec_layout::root_as_process_security_environment(&bytes).unwrap(); + let network = spec.network_policy().expect("network policy"); + assert_eq!( + network.proxy().and_then(|proxy| proxy.url()), + Some("http://127.0.0.1:8080") + ); + assert!(network.egress().is_none()); + } + + #[test] + fn process_security_environment_preference_uses_schema_version() { + for (version, expected) in [ + ("", false), + ("0.6.0-alpha", false), + ("0.7.99", false), + ("0.8.0-alpha", true), + ("0.8.0", true), + ("1.0.0", true), + ] { + let request = ExecutionRequest { + schema_version: version.to_string(), + ..Default::default() + }; + assert_eq!( + BaseContainerRunner::schema_prefers_process_security_environment(&request), + expected, + "schema version {version}" + ); + } + } + + #[test] + fn schema_0_8_uses_psec_only_when_runtime_probe_succeeds() { + let request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + + assert!(BaseContainerRunner::should_use_process_security_environment(&request, true, true)); + assert!( + !BaseContainerRunner::should_use_process_security_environment(&request, false, true) + ); + } + + #[test] + fn schema_0_8_proxy_uses_legacy_contract() { + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + + let runner = BaseContainerRunner::with_capture_factory(fake_capture_factory()); + assert!( + !BaseContainerRunner::should_use_process_security_environment(&request, true, true) + ); + assert!( + !runner.uses_process_security_environment(&request), + "schema 0.8 proxy requests must build the legacy SBOX contract" + ); + } + + #[test] + fn schema_0_8_least_privilege_uses_legacy_contract() { + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + request.policy.least_privilege_mode = true; + + assert!( + !BaseContainerRunner::should_use_process_security_environment(&request, true, true) + ); + } + + #[test] + fn schema_0_8_denied_paths_use_legacy_contract_when_psec_lacks_support() { + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + request.policy.denied_paths = vec![r"C:\secret".to_string()]; + + assert!( + !BaseContainerRunner::should_use_process_security_environment(&request, true, false) + ); + } + #[test] fn build_sandbox_spec_empty_policy() { // Default network policy is Block — no internetClient auto-add. @@ -2894,7 +3615,10 @@ mod tests { #[test] fn validate_runner_rejects_allowed_hosts() { let runner = BaseContainerRunner::new(); - let mut request = ExecutionRequest::default(); + let mut request = ExecutionRequest { + dry_run: true, + ..Default::default() + }; request.policy.allowed_hosts = vec!["example.com".into()]; let err = runner @@ -2906,7 +3630,10 @@ mod tests { #[test] fn validate_runner_rejects_blocked_hosts() { let runner = BaseContainerRunner::new(); - let mut request = ExecutionRequest::default(); + let mut request = ExecutionRequest { + dry_run: true, + ..Default::default() + }; request.policy.blocked_hosts = vec!["bad.example.com".into()]; let err = runner @@ -2927,4 +3654,196 @@ mod tests { assert!(runner.validate(&request).is_ok()); } } + + #[test] + fn capture_denied_paths_error_names_v2_capability() { + assert!( + PSEC_DENIED_PATHS_UNSUPPORTED_MSG.contains("QueryProcessSecurityEnvironmentSupport") + ); + assert!(PSEC_DENIED_PATHS_UNSUPPORTED_MSG.contains("PSE_SUPPORT_FS_DENY")); + assert!(!PSEC_DENIED_PATHS_UNSUPPORTED_MSG.contains("Experimental_QuerySandboxSupport")); + assert!(PSEC_DENIED_PATHS_UNSUPPORTED_MSG.contains("cannot fall back to AppContainer")); + } + + #[test] + fn capture_validation_fails_closed_when_v2_api_is_unavailable() { + let factory = fake_capture_factory(); + let support = Arc::new(FakeCaptureSupport { + api_error: Some("missing CloseLearningModeTrace"), + deny_error: None, + deny_supported: true, + api_calls: AtomicUsize::new(0), + learning_mode_api_calls: AtomicUsize::new(0), + deny_calls: AtomicUsize::new(0), + }); + let runner = BaseContainerRunner::with_capture_components(factory.clone(), support.clone()); + + let error = runner + .validate(&capture_request_with_denied_path()) + .expect_err("missing V2 API must fail closed"); + + assert_eq!(error.failure_phase, FailurePhase::BackendUnavailable); + assert!(error + .error_message + .contains("missing CloseLearningModeTrace")); + assert_eq!(support.api_calls.load(Ordering::SeqCst), 1); + assert_eq!(support.learning_mode_api_calls.load(Ordering::SeqCst), 1); + assert_eq!(support.deny_calls.load(Ordering::SeqCst), 0); + assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn capture_validation_fails_closed_when_deny_query_fails() { + let factory = fake_capture_factory(); + let support = Arc::new(FakeCaptureSupport { + api_error: None, + deny_error: Some("query failed"), + deny_supported: false, + api_calls: AtomicUsize::new(0), + learning_mode_api_calls: AtomicUsize::new(0), + deny_calls: AtomicUsize::new(0), + }); + let runner = BaseContainerRunner::with_capture_components(factory.clone(), support.clone()); + + let error = runner + .validate(&capture_request_with_denied_path()) + .expect_err("deny query failure must fail closed"); + + assert_eq!(error.failure_phase, FailurePhase::BackendUnavailable); + assert!(error.error_message.contains("query failed")); + assert_eq!(support.api_calls.load(Ordering::SeqCst), 1); + assert_eq!(support.deny_calls.load(Ordering::SeqCst), 1); + assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn capture_validation_fails_closed_when_deny_bit_is_clear() { + let factory = fake_capture_factory(); + let support = Arc::new(FakeCaptureSupport { + api_error: None, + deny_error: None, + deny_supported: false, + api_calls: AtomicUsize::new(0), + learning_mode_api_calls: AtomicUsize::new(0), + deny_calls: AtomicUsize::new(0), + }); + let runner = BaseContainerRunner::with_capture_components(factory.clone(), support.clone()); + + let error = runner + .validate(&capture_request_with_denied_path()) + .expect_err("missing deny support bit must fail closed"); + + assert_eq!(error.failure_phase, FailurePhase::BackendUnavailable); + assert_eq!(error.error_message, PSEC_DENIED_PATHS_UNSUPPORTED_MSG); + assert_eq!(support.api_calls.load(Ordering::SeqCst), 1); + assert_eq!(support.deny_calls.load(Ordering::SeqCst), 1); + assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn schema_0_8_without_capture_requires_only_security_environment_api() { + let factory = fake_capture_factory(); + let support = Arc::new(FakeCaptureSupport { + api_error: None, + deny_error: None, + deny_supported: true, + api_calls: AtomicUsize::new(0), + learning_mode_api_calls: AtomicUsize::new(0), + deny_calls: AtomicUsize::new(0), + }); + let runner = BaseContainerRunner::with_capture_components(factory.clone(), support.clone()); + let request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..Default::default() + }; + + runner + .validate(&request) + .expect("schema 0.8 requires PSEC but not Learning Mode"); + + assert_eq!(support.api_calls.load(Ordering::SeqCst), 1); + assert_eq!(support.learning_mode_api_calls.load(Ordering::SeqCst), 0); + assert_eq!(support.deny_calls.load(Ordering::SeqCst), 0); + assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn schema_0_8_dry_run_skips_host_api_probes() { + let factory = fake_capture_factory(); + let support = Arc::new(FakeCaptureSupport { + api_error: Some("V2 exports unavailable"), + deny_error: Some("deny support query unavailable"), + deny_supported: false, + api_calls: AtomicUsize::new(0), + learning_mode_api_calls: AtomicUsize::new(0), + deny_calls: AtomicUsize::new(0), + }); + let runner = BaseContainerRunner::with_capture_components(factory.clone(), support.clone()); + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + dry_run: true, + ..Default::default() + }; + request.policy.capture_denials = Some(Default::default()); + request.policy.denied_paths = vec![r"C:\secret".to_string()]; + + runner + .validate(&request) + .expect("dry-run should validate policy without probing host APIs"); + + assert_eq!(support.api_calls.load(Ordering::SeqCst), 0); + assert_eq!(support.learning_mode_api_calls.load(Ordering::SeqCst), 0); + assert_eq!(support.deny_calls.load(Ordering::SeqCst), 0); + assert_eq!(factory.begin_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn validate_runner_allows_schema_0_8_least_privilege_via_legacy_contract() { + let runner = BaseContainerRunner::with_capture_factory(fake_capture_factory()); + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + dry_run: true, + ..Default::default() + }; + request.policy.least_privilege_mode = true; + + runner + .validate(&request) + .expect("leastPrivilege should route through the legacy SBOX contract"); + } + + #[test] + fn validate_runner_allows_schema_0_8_proxy_via_legacy_contract() { + let runner = BaseContainerRunner::with_capture_factory(fake_capture_factory()); + let mut request = ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + dry_run: true, + ..Default::default() + }; + request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + + runner + .validate(&request) + .expect("network.proxy should route through the legacy SBOX contract"); + } + + #[test] + fn validate_runner_rejects_capture_denials_before_schema_0_8() { + let runner = BaseContainerRunner::new(); + let mut request = ExecutionRequest { + schema_version: "0.7.0-alpha".to_string(), + dry_run: true, + ..Default::default() + }; + request.policy.capture_denials = Some(Default::default()); + + let error = runner + .validate(&request) + .expect_err("captureDenials is part of the schema 0.8 PSEC contract"); + + assert!(error.error_message.contains("schema version 0.8.0")); + } } diff --git a/src/backends/appcontainer/common/src/dispatcher.rs b/src/backends/appcontainer/common/src/dispatcher.rs index 24b9fda76..d46ac1bd7 100644 --- a/src/backends/appcontainer/common/src/dispatcher.rs +++ b/src/backends/appcontainer/common/src/dispatcher.rs @@ -338,13 +338,23 @@ fn select_backend_with_fallback( ), DispatchError, > { - let decision = fallback_detector::detect(&request.policy, /*prefer_bc=*/ true)?; + // Keep the established tier fallback behavior for every schema version. + // For schema 0.8+, BaseContainerRunner prefers PSEC when available and + // otherwise uses the transitional SBOX contract. If neither BaseContainer + // contract is usable, detection continues to the AppContainer tiers. + let prefer_base_container = BaseContainerRunner::is_usable_for_request(request); + let supports_deny_paths = BaseContainerRunner::supports_deny_paths_for_request(request); + let decision = fallback_detector::detect_with_base_container_capabilities( + &request.policy, + prefer_base_container, + prefer_base_container, + supports_deny_paths, + )?; if request.policy.capture_denials.is_some() && decision.tier != IsolationTier::BaseContainer { return Err(DispatchError::CaptureDenialsUnsupported { tier: decision.tier, }); } - let (backend, dacl_manager): (SelectedBackend, Option) = match decision.tier { IsolationTier::BaseContainer => { // Tier 1 delegates filesystem-policy enforcement to @@ -606,7 +616,7 @@ impl SandboxProcess for DaclGuardedProcess { #[cfg(test)] mod tests { use super::*; - use wxc_common::models::{ContainerPolicy, ExecutionRequest}; + use wxc_common::models::{ContainerPolicy, ExecutionRequest, ProxyAddress, ProxyConfig}; // `ForceTierGuard` lives in `crate::test_env` so the lock is // shared with the `fallback_detector::tests` module — otherwise // a dispatcher test and a fallback-detector test running on @@ -622,6 +632,13 @@ mod tests { } } + fn schema_0_8_request(policy: ContainerPolicy) -> ExecutionRequest { + ExecutionRequest { + schema_version: "0.8.0-alpha".to_string(), + ..test_request(policy) + } + } + fn empty_policy() -> ContainerPolicy { ContainerPolicy::default() } @@ -689,23 +706,68 @@ mod tests { } #[test] - fn capture_denials_rejects_fallback_before_backend_or_dacl_setup() { + fn capture_denials_rejects_appcontainer_fallback() { let _g = ForceTierGuard::set("appcontainer-dacl"); let (mut policy, _tmp) = policy_with_rw_temp(); policy.capture_denials = Some(Default::default()); - let req = test_request(policy); + let req = schema_0_8_request(policy); - let error = match dispatch_with_fallback(&req) { - Ok(_) => panic!("capture fallback must be rejected"), - Err(error) => error, - }; + let result = dispatch_with_fallback(&req); assert!(matches!( - error, - DispatchError::CaptureDenialsUnsupported { + result, + Err(DispatchError::CaptureDenialsUnsupported { tier: IsolationTier::AppContainerDacl - } + }) )); } + + #[test] + fn schema_0_8_without_capture_keeps_legacy_fallback() { + let _g = ForceTierGuard::set("appcontainer-dacl"); + let (policy, _tmp) = policy_with_rw_temp(); + let req = schema_0_8_request(policy); + + let dispatched = dispatch_with_fallback(&req).expect("fallback should be selected"); + assert!(matches!(dispatched.tier, IsolationTier::AppContainerDacl)); + assert!( + dispatched.has_dacl_guard(), + "schema 0.8 ordinary requests retain AppContainer + DACL fallback" + ); + } + + #[test] + fn schema_0_8_proxy_keeps_base_container_on_legacy_sbox_hosts() { + let _g = BcUsableGuard::set(true); + let mut policy = empty_policy(); + policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + let req = schema_0_8_request(policy); + + let (backend, dacl, tier, _warnings) = + select_backend_with_fallback(&req).expect("SBOX should remain eligible"); + assert!(matches!(tier, IsolationTier::BaseContainer)); + assert!(matches!(backend, SelectedBackend::BaseContainer(_))); + assert!(dacl.is_none()); + } + + #[test] + fn schema_0_8_proxy_uses_appcontainer_when_base_container_is_incompatible() { + let _g = BcUsableGuard::set(false); + let mut policy = empty_policy(); + policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + let req = schema_0_8_request(policy); + + let (backend, _dacl, tier, _warnings) = + select_backend_with_fallback(&req).expect("AppContainer fallback should be selected"); + assert_ne!(tier, IsolationTier::BaseContainer); + assert!(matches!(backend, SelectedBackend::AppContainer(_))); + } + #[test] fn dispatch_fallback_disabled_errors() { let _g = ForceTierGuard::set("appcontainer-dacl"); diff --git a/src/backends/appcontainer/common/src/fallback_detector.rs b/src/backends/appcontainer/common/src/fallback_detector.rs index ffc6eb24b..c3e4ab0cb 100644 --- a/src/backends/appcontainer/common/src/fallback_detector.rs +++ b/src/backends/appcontainer/common/src/fallback_detector.rs @@ -7,7 +7,8 @@ //! runtime probes, produces a [`TierDecision`]. Tiers are described in //! `docs/proposals/downlevel_support/basecontainer-fallback-plan-v2.md`: //! -//! 1. **Tier 1 — BaseContainer** (`Experimental_CreateProcessInSandbox`) +//! 1. **Tier 1 — BaseContainer** (PSEC preferred for schema 0.8+, with +//! transitional `Experimental_CreateProcessInSandbox` fallback) //! 2. **Tier 2 — AppContainer + BFS** (`bfscfg.exe`-driven filesystem policy) //! 3. **Tier 3 — AppContainer + DACL** (host-side DACL ACE augmentation) //! @@ -24,7 +25,7 @@ use wxc_common::models::ContainerPolicy; /// security strength. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IsolationTier { - /// Tier 1 — `Experimental_CreateProcessInSandbox` from `processmodel.dll`. + /// Tier 1 — a supported BaseContainer contract from `processmodel.dll`. BaseContainer, /// Tier 2 — AppContainer + `bfscfg.exe` BFS filesystem policy. AppContainerBfs, @@ -134,6 +135,23 @@ pub enum FallbackError { pub fn detect( policy: &ContainerPolicy, prefer_base_container: bool, +) -> Result { + detect_with_base_container_capabilities( + policy, + prefer_base_container, + is_base_container_usable(), + base_container_supports_deny_paths(), + ) +} + +/// Variant of [`detect`] for callers that have already selected which +/// BaseContainer contract applies to a request and probed that contract's +/// capabilities. +pub(crate) fn detect_with_base_container_capabilities( + policy: &ContainerPolicy, + prefer_base_container: bool, + base_container_usable: bool, + base_container_supports_deny_paths: bool, ) -> Result { let denied = !policy.denied_paths.is_empty(); let has_fs_policy = @@ -163,11 +181,11 @@ pub fn detect( let mut warnings: Vec = Vec::new(); // Tier 1 — BaseContainer - if prefer_base_container && is_base_container_usable() { - // Keep deny on Tier 1 only with native fs_deny support - // (SANDBOX_CAP_FS_DENY); T1 applies no host DACL, so otherwise - // fall through to a DACL-enforcing tier. - if !denied || base_container_supports_deny_paths() { + if prefer_base_container && base_container_usable { + // Keep deny on Tier 1 only with native deny-path support from the + // selected PSEC or SBOX contract. T1 applies no host DACL, so + // otherwise fall through to a DACL-enforcing tier. + if !denied || base_container_supports_deny_paths { return Ok(TierDecision { tier: IsolationTier::BaseContainer, needs_dacl_augmentation: false, @@ -176,7 +194,8 @@ pub fn detect( }); } warnings.push( - "BaseContainer usable but this OS does not advertise SANDBOX_CAP_FS_DENY; \ + "BaseContainer usable but the selected OS contract does not advertise native \ + deniedPaths support; \ deniedPaths cannot be enforced natively at Tier 1 — falling back to AppContainer \ for deniedPaths enforcement" .to_string(), @@ -785,7 +804,7 @@ mod tests { ); assert!(d.needs_dacl_augmentation); assert!( - d.warnings.iter().any(|w| w.contains("SANDBOX_CAP_FS_DENY")), + d.warnings.iter().any(|w| w.contains("deniedPaths support")), "expected the capability-absent fall-through warning, got: {:?}", d.warnings ); diff --git a/src/backends/learning_mode/windows/Cargo.toml b/src/backends/learning_mode/windows/Cargo.toml index 515448a39..36a742bab 100644 --- a/src/backends/learning_mode/windows/Cargo.toml +++ b/src/backends/learning_mode/windows/Cargo.toml @@ -14,5 +14,5 @@ windows = { workspace = true } windows-core = { workspace = true } [target.'cfg(target_os = "windows")'.dev-dependencies] -sandbox_spec = { workspace = true } flatbuffers = { workspace = true } +process_security_environment_spec = { workspace = true } diff --git a/src/backends/learning_mode/windows/examples/lm_analyze.rs b/src/backends/learning_mode/windows/examples/lm_analyze.rs index 84185b156..078745a75 100644 --- a/src/backends/learning_mode/windows/examples/lm_analyze.rs +++ b/src/backends/learning_mode/windows/examples/lm_analyze.rs @@ -4,10 +4,11 @@ //! Decode a sealed learning-mode `.etl` into the captureDenials JSON //! output document, or dump its raw ETW events for schema discovery. //! -//! This is a developer diagnostic for inspecting captured traces manually. -//! End users and SDK agents do not invoke it: the BaseContainer runner seals -//! the trace and reports its path, while this example performs the manual -//! analysis until runner integration consumes the trace automatically. +//! This is a developer diagnostic for inspecting saved traces independently +//! of the production pipeline. Normal `captureDenials` execution seals and +//! decodes its internal ETL automatically, writes the JSON denials document, +//! deletes the ETL, and returns structured output metadata. End users and SDK +//! callers therefore do not invoke this example. //! //! Usage: //! diff --git a/src/backends/learning_mode/windows/examples/lm_capture.rs b/src/backends/learning_mode/windows/examples/lm_capture.rs index 1a925481d..e433ce70f 100644 --- a/src/backends/learning_mode/windows/examples/lm_capture.rs +++ b/src/backends/learning_mode/windows/examples/lm_capture.rs @@ -13,7 +13,8 @@ //! `PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT` and launch `cmd.exe` with //! `CreateProcessW`, //! 4. wait for it to exit, -//! 5. [`CaptureSession::finish`] — seal the ETL to a temp path + close the environment, +//! 5. [`CaptureSession::finish`] — stop and deliver the ETL, close the trace, then close +//! the environment, //! 6. assert the ETL file was produced (non-empty). //! //! Run on a feature-enabled Windows build (elevated): @@ -40,13 +41,13 @@ fn main() { mod windows_impl { use std::path::PathBuf; - use flatbuffers::FlatBufferBuilder; use learning_mode_windows::{ CaptureSession, LearningModeApi, SecurityEnvironmentApi, SecurityEnvironmentStartupInfo, PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, }; - use sandbox_spec::base_container_layout::{ - finish_sandbox_spec_buffer, SandboxSpec, SandboxSpecArgs, + use process_security_environment_spec::process_security_environment_layout::{ + finish_process_security_environment_buffer, ProcessSecurityEnvironment, + ProcessSecurityEnvironmentArgs, SchemaVersion, }; use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED, WAIT_OBJECT_0}; use windows::Win32::System::Threading::{ @@ -55,26 +56,22 @@ mod windows_impl { }; use windows_core::{PCWSTR, PWSTR}; - /// 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. + /// Build a minimal PSEC 1.0 FlatBuffer carrying the learning-mode capability. fn build_sandbox_spec() -> Vec { - let mut builder = FlatBufferBuilder::with_capacity(256); - let version = builder.create_string(SANDBOX_SPEC_VERSION); + let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(256); + let version = SchemaVersion::new(1, 0); // `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( + let spec = ProcessSecurityEnvironment::create( &mut builder, - &SandboxSpecArgs { - version: Some(version), - app_container: true, + &ProcessSecurityEnvironmentArgs { + version: Some(&version), capabilities: Some(capabilities), ..Default::default() }, ); - finish_sandbox_spec_buffer(&mut builder, spec); + finish_process_security_environment_buffer(&mut builder, spec); builder.finished_data().to_vec() } @@ -128,7 +125,7 @@ mod windows_impl { } Err(e) => { eprintln!("launch failed: {e}"); - // `session` drops here → trace discarded + environment closed. + // `session` drops here → trace closed/discarded + environment closed. return 1; } }; @@ -139,7 +136,7 @@ mod windows_impl { eprintln!("CaptureSession::finish failed: {e}"); return 1; } - println!("CaptureSession::finish OK — trace sealed, environment closed"); + println!("CaptureSession::finish OK — trace delivered and closed, environment closed"); match std::fs::metadata(&etl_path) { Ok(meta) => { diff --git a/src/backends/learning_mode/windows/examples/lm_probe.rs b/src/backends/learning_mode/windows/examples/lm_probe.rs index 1dc3b8e93..125255355 100644 --- a/src/backends/learning_mode/windows/examples/lm_probe.rs +++ b/src/backends/learning_mode/windows/examples/lm_probe.rs @@ -4,12 +4,13 @@ //! 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` / +//! exports (`StartLearningModeTrace` / `StopLearningModeTrace` / +//! `CloseLearningModeTrace`) and the 2-phase security-environment exports +//! (`CreateProcessSecurityEnvironment` / +//! `QueryProcessSecurityEnvironmentSupport` / //! `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. +//! reporting each official export that resolves. 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 @@ -34,6 +35,7 @@ fn run_probe() -> i32 { let report = learning_mode_windows::probe_security_environment_exports(); println!(" create export = {:?}", report.create); + println!(" query support = {:?}", report.query_support); println!(" close export = {:?}", report.close); match learning_mode_windows::SecurityEnvironmentApi::load() { diff --git a/src/backends/learning_mode/windows/src/ffi.rs b/src/backends/learning_mode/windows/src/ffi.rs index 3b32288de..706463953 100644 --- a/src/backends/learning_mode/windows/src/ffi.rs +++ b/src/backends/learning_mode/windows/src/ffi.rs @@ -3,20 +3,25 @@ //! Windows runtime FFI for the `processmodel.dll` Learning Mode trace exports. //! -//! The two exports are resolved once via `LoadLibraryExW(LOAD_LIBRARY_SEARCH_SYSTEM32)` -//! and `GetProcAddress`. As with the sibling `Experimental_CreateProcessInSandbox` -//! adapter, `processmodel.dll` is intentionally never freed: it is a system DLL that +//! The three official V2 exports are resolved via +//! `LoadLibraryExW(LOAD_LIBRARY_SEARCH_SYSTEM32)` and `GetProcAddress`. +//! `processmodel.dll` is intentionally never freed: it is a system DLL that //! stays resident for the process lifetime, so the module handle is used only to //! resolve exports and then dropped without `FreeLibrary`. use std::path::Path; use std::ptr; +use std::sync::OnceLock; +use std::time::Duration; -use windows::Win32::Foundation::{GetLastError, HANDLE, HMODULE}; +use windows::Win32::Foundation::{ + GetLastError, ERROR_BUSY, ERROR_LOCK_VIOLATION, ERROR_RETRY, ERROR_SHARING_VIOLATION, HANDLE, + HMODULE, +}; use windows::Win32::System::LibraryLoader::{ GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, }; -use windows_core::{PCSTR, PCWSTR}; +use windows_core::{HRESULT, PCSTR, PCWSTR}; use wxc_common::string_util; use crate::LearningModeError; @@ -24,38 +29,80 @@ use crate::LearningModeError; /// System DLL that hosts the flat Learning Mode trace exports. const PROCESSMODEL_DLL: &str = "processmodel.dll"; -/// `BOOL StartLearningModeTrace(HANDLE hProcessSecurityEnvironment, HLEARNINGMODE_TRACE* pphTrace)`. +/// `HRESULT StartLearningModeTrace(HANDLE securityEnvironment, HLEARNINGMODE_TRACE* trace)`. /// /// `HLEARNINGMODE_TRACE` is a `typedef HANDLE`; the export surfaces it through the -/// out-parameter. A zero (`FALSE`) return signals failure (`GetLastError`). -type PfnStartLearningModeTrace = - unsafe extern "system" fn(process_security_environment: HANDLE, trace_out: *mut HANDLE) -> i32; +/// out-parameter. +type PfnStartLearningModeTrace = unsafe extern "system" fn( + process_security_environment: HANDLE, + trace_out: *mut HANDLE, +) -> HRESULT; -/// `BOOL StopLearningModeTrace(HLEARNINGMODE_TRACE* pphTrace, LPCWSTR lpOutputPath)`. +/// `HRESULT StopLearningModeTrace(HLEARNINGMODE_TRACE trace, LPCWSTR outputEtlPath)`. /// /// A non-null `output_path` names a file the export opens under the caller's own -/// identity; the broker seals the ETL into it. A null `output_path` discards the -/// trace. `*trace` is set to null on return regardless. +/// identity; the broker seals and copies the ETL into it. A null `output_path` +/// stops without delivery. The handle remains valid so the caller may retry +/// delivery until it closes the trace. type PfnStopLearningModeTrace = - unsafe extern "system" fn(trace: *mut HANDLE, output_path: *const u16) -> i32; + unsafe extern "system" fn(trace: HANDLE, output_path: *const u16) -> HRESULT; + +/// `void CloseLearningModeTrace(HLEARNINGMODE_TRACE trace)`. +type PfnCloseLearningModeTrace = unsafe extern "system" fn(trace: HANDLE); /// Opaque handle to an in-progress Learning Mode trace (`HLEARNINGMODE_TRACE`). /// -/// Obtained from [`LearningModeApi::start_trace`] and consumed by -/// [`LearningModeApi::stop_trace`]. The handle is owned by the AppInfo broker and -/// bound to this process; if the process exits without stopping, the broker discards -/// the trace automatically. -#[derive(Debug)] -pub struct LearningModeTraceHandle(HANDLE); +/// Obtained from [`LearningModeApi::start_trace`]. [`LearningModeApi::stop_trace`] +/// borrows it so delivery can be retried. Dropping or explicitly closing the +/// handle releases all broker state; closing without stopping discards the trace. +pub struct LearningModeTraceHandle { + raw: HANDLE, + close: PfnCloseLearningModeTrace, +} + +impl LearningModeTraceHandle { + fn new(raw: HANDLE, close: PfnCloseLearningModeTrace) -> Self { + Self { raw, close } + } + + /// Close the trace and release all service-managed state. + pub fn close(mut self) { + self.close_inner(); + } + + fn close_inner(&mut self) { + if !self.raw.0.is_null() { + // SAFETY: `raw` was returned by `StartLearningModeTrace`, and + // `close` was resolved from the same processmodel.dll contract. + unsafe { (self.close)(self.raw) }; + self.raw = HANDLE(ptr::null_mut()); + } + } +} + +impl std::fmt::Debug for LearningModeTraceHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("LearningModeTraceHandle") + .field(&self.raw) + .finish() + } +} + +impl Drop for LearningModeTraceHandle { + fn drop(&mut self) { + self.close_inner(); + } +} /// Resolved Learning Mode trace exports from `processmodel.dll`. /// -/// Construct with [`LearningModeApi::load`]. Cloning is cheap (the struct holds two +/// Construct with [`LearningModeApi::load`]. Cloning is cheap (the struct holds three /// function pointers into the resident system DLL). #[derive(Clone, Copy)] pub struct LearningModeApi { start: PfnStartLearningModeTrace, stop: PfnStopLearningModeTrace, + close: PfnCloseLearningModeTrace, } impl std::fmt::Debug for LearningModeApi { @@ -63,18 +110,36 @@ impl std::fmt::Debug for LearningModeApi { f.debug_struct("LearningModeApi") .field("start", &(self.start as *const ())) .field("stop", &(self.stop as *const ())) + .field("close", &(self.close as *const ())) .finish() } } impl LearningModeApi { + const STOP_DELIVERY_ATTEMPTS: usize = 3; + const STOP_RETRY_DELAYS: [Duration; 2] = [Duration::from_millis(25), Duration::from_millis(75)]; + /// Load `processmodel.dll` and resolve the Learning Mode trace exports. /// + /// The result — success or failure — is memoized for the lifetime of the + /// process: `processmodel.dll` is a resident system DLL whose export set does + /// not change while the process runs, so repeated probes would only repeat the + /// same `LoadLibraryExW`/`GetProcAddress` work and return the same answer. The + /// cached error is cloned (see [`LearningModeError`]), preserving the original + /// diagnostic on every call. + /// /// # Errors /// - [`LearningModeError::DllLoad`] if `processmodel.dll` cannot be loaded. - /// - [`LearningModeError::ExportMissing`] if either export is absent (the OS - /// build predates the API or has it gated off). + /// - [`LearningModeError::ExportMissing`] if any export is absent. Requiring + /// `CloseLearningModeTrace` rejects builds that expose the incompatible + /// earlier two-export ABI. pub fn load() -> Result { + static CACHE: OnceLock> = OnceLock::new(); + CACHE.get_or_init(Self::load_uncached).clone() + } + + /// Perform the actual DLL load and export resolution, bypassing the cache. + fn load_uncached() -> Result { let dll = string_util::to_wide(PROCESSMODEL_DLL); // SAFETY: `dll` is a valid null-terminated wide string that outlives the call. @@ -87,16 +152,31 @@ impl LearningModeApi { let hmodule = LoadLibraryExW(PCWSTR(dll.as_ptr()), None, LOAD_LIBRARY_SEARCH_SYSTEM32) .map_err(|e| LearningModeError::DllLoad(e.to_string()))?; - let start_proc = resolve_export(hmodule, c"StartLearningModeTrace")?; - let stop_proc = resolve_export(hmodule, c"StopLearningModeTrace")?; + let start_proc = resolve_export(hmodule, START_NAME)?; + let stop_proc = resolve_export(hmodule, STOP_NAME)?; + let close_proc = resolve_export(hmodule, CLOSE_NAME)?; let start: PfnStartLearningModeTrace = std::mem::transmute(start_proc); let stop: PfnStopLearningModeTrace = std::mem::transmute(stop_proc); + let close: PfnCloseLearningModeTrace = std::mem::transmute(close_proc); - Ok(Self { start, stop }) + Ok(Self { start, stop, close }) } } + /// Construct an API surface directly from raw export pointers, bypassing the + /// DLL load. Test-only: lets sibling modules (e.g. `lifecycle`) inject fakes to + /// exercise the capture lifecycle host-independently without going through the + /// memoized [`load`](Self::load) path, so fakes never populate the process cache. + #[cfg(test)] + pub(crate) fn from_raw_parts( + start: PfnStartLearningModeTrace, + stop: PfnStopLearningModeTrace, + close: PfnCloseLearningModeTrace, + ) -> Self { + Self { start, stop, close } + } + /// Start a Learning Mode trace for the sandbox identified by /// `security_environment`. /// @@ -106,8 +186,7 @@ impl LearningModeApi { /// AppContainer SID server-side. /// /// # Errors - /// [`LearningModeError::ApiCall`] carrying `GetLastError` if the export returns - /// `FALSE`. + /// [`LearningModeError::HResultCall`] if the export returns a failing HRESULT. pub unsafe fn start_trace( &self, security_environment: HANDLE, @@ -116,76 +195,103 @@ impl LearningModeApi { // SAFETY: `self.start` was resolved from `processmodel.dll` and matches the // declared C signature; `trace` is a valid out-pointer. The caller upholds // the validity of `security_environment` per this method's safety contract. - let ok = (self.start)(security_environment, &mut trace); - if ok == 0 { - return Err(LearningModeError::ApiCall { + let result = (self.start)(security_environment, &mut trace); + if result.is_err() { + return Err(LearningModeError::HResultCall { + function: "StartLearningModeTrace", + code: result.0, + }); + } + if trace.0.is_null() { + return Err(LearningModeError::HResultCall { function: "StartLearningModeTrace", - code: last_error(), + code: windows::Win32::Foundation::E_UNEXPECTED.0, }); } - Ok(LearningModeTraceHandle(trace)) + Ok(LearningModeTraceHandle::new(trace, self.close)) } - /// Stop `trace`, sealing the ETL into `output_path`. Passing `None` discards the - /// trace (used for early-exit teardown). + /// Stop `trace`, sealing and copying the ETL into `output_path`. Passing `None` + /// stops without delivery. /// - /// The handle is consumed; the export nulls it internally on return. + /// The handle remains live after success or failure, so callers may retry with + /// the same or a different output path before closing it. /// /// # Errors /// - [`LearningModeError::InvalidInput`] if `output_path` contains an embedded NUL. - /// - [`LearningModeError::ApiCall`] carrying `GetLastError` if the export returns - /// `FALSE`. - /// - [`LearningModeError::CleanupFailed`] if rejecting an invalid path also fails - /// to discard the live trace. + /// - [`LearningModeError::HResultCall`] if the export returns a failing HRESULT. pub fn stop_trace( &self, - trace: LearningModeTraceHandle, + trace: &LearningModeTraceHandle, output_path: Option<&Path>, ) -> Result<(), LearningModeError> { - let wide_path = match encode_output_path(output_path) { - Ok(path) => path, - Err(primary) => return Err(self.discard_trace_after_error(trace, primary)), - }; + let wide_path = encode_output_path(output_path)?; self.stop_trace_encoded(trace, wide_path.as_deref()) } - fn discard_trace_after_error( + /// Stop and deliver the trace, retrying only transient output-delivery + /// failures. The trace remains live throughout the attempts and is still + /// owned by the caller when this method returns. + pub(crate) fn stop_trace_with_retry( &self, - trace: LearningModeTraceHandle, - primary: LearningModeError, - ) -> LearningModeError { - match self.stop_trace_encoded(trace, None) { - Ok(()) => primary, - Err(cleanup) => LearningModeError::CleanupFailed { - primary: Box::new(primary), - cleanup: Box::new(cleanup), - }, + trace: &LearningModeTraceHandle, + output_path: Option<&Path>, + ) -> Result<(), LearningModeError> { + for attempt in 0..Self::STOP_DELIVERY_ATTEMPTS { + match self.stop_trace(trace, output_path) { + Err(error) + if attempt + 1 < Self::STOP_DELIVERY_ATTEMPTS + && is_retryable_stop_error(&error) => + { + std::thread::sleep(Self::STOP_RETRY_DELAYS[attempt]); + } + result => return result, + } } + unreachable!("STOP_DELIVERY_ATTEMPTS is non-zero") } fn stop_trace_encoded( &self, - trace: LearningModeTraceHandle, + trace: &LearningModeTraceHandle, wide_path: Option<&[u16]>, ) -> Result<(), LearningModeError> { let path_ptr = wide_path.map_or(ptr::null(), |path| path.as_ptr()); - let mut handle = trace.0; // SAFETY: `self.stop` was resolved from `processmodel.dll` and matches the - // declared C signature. `handle` came from a prior `start_trace`, and + // declared C signature. `trace.raw` came from a prior `start_trace`, and // `path_ptr` is either null or points at the null-terminated `wide_path` // buffer, which outlives the call. - let ok = unsafe { (self.stop)(&mut handle, path_ptr) }; - if ok == 0 { - return Err(LearningModeError::ApiCall { + let result = unsafe { (self.stop)(trace.raw, path_ptr) }; + if result.is_err() { + return Err(LearningModeError::HResultCall { function: "StopLearningModeTrace", - code: last_error(), + code: result.0, }); } Ok(()) } } +fn is_retryable_stop_error(error: &LearningModeError) -> bool { + let LearningModeError::HResultCall { + function: "StopLearningModeTrace", + code, + } = error + else { + return false; + }; + + [ + ERROR_SHARING_VIOLATION, + ERROR_LOCK_VIOLATION, + ERROR_BUSY, + ERROR_RETRY, + ] + .into_iter() + .any(|win32| *code == HRESULT::from_win32(win32.0).0) +} + fn encode_output_path(output_path: Option<&Path>) -> Result>, LearningModeError> { output_path .map(|path| { @@ -230,11 +336,79 @@ fn last_error() -> u32 { unsafe { GetLastError().0 } } -/// Capability probe: `true` only when `processmodel.dll` exposes both Learning Mode +/// Undecorated names of the three Learning Mode trace exports, in the order the +/// 2-phase capture lifecycle uses them. +const START_NAME: &core::ffi::CStr = c"StartLearningModeTrace"; +const STOP_NAME: &core::ffi::CStr = c"StopLearningModeTrace"; +const CLOSE_NAME: &core::ffi::CStr = c"CloseLearningModeTrace"; + +/// Which Learning Mode trace exports resolved on this machine. +/// +/// This mirrors the security-environment report shape and isolates the pure +/// all-or-nothing completeness rule so it can be unit-tested without a live DLL. +/// Requiring `close` in addition to `start`/`stop` is what rejects the incompatible +/// earlier two-export ("V1") ABI. +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct LearningModeExportReport { + /// Resolved name of `StartLearningModeTrace`, if present. + pub start: Option<&'static str>, + /// Resolved name of `StopLearningModeTrace`, if present. + pub stop: Option<&'static str>, + /// Resolved name of `CloseLearningModeTrace`, if present. + pub close: Option<&'static str>, +} + +impl LearningModeExportReport { + /// `true` only when all three trace exports resolved. A start+stop-only build + /// (the legacy two-export ABI) is deliberately incomplete. + pub(crate) fn is_complete(&self) -> bool { + self.start.is_some() && self.stop.is_some() && self.close.is_some() + } +} + +/// Probe `processmodel.dll` for the three Learning Mode trace exports. Returns an +/// all-`None` report if the DLL itself cannot be loaded. +fn probe_learning_mode_exports() -> LearningModeExportReport { + let dll = string_util::to_wide(PROCESSMODEL_DLL); + // SAFETY: `dll` is a valid null-terminated wide string that outlives the call; + // `LOAD_LIBRARY_SEARCH_SYSTEM32` restricts the search to System32. + let hmodule = + match unsafe { LoadLibraryExW(PCWSTR(dll.as_ptr()), None, LOAD_LIBRARY_SEARCH_SYSTEM32) } { + Ok(h) => h, + Err(_) => return LearningModeExportReport::default(), + }; + + // SAFETY: `hmodule` is valid; `export_name_if_present` only reads exports. + unsafe { + LearningModeExportReport { + start: export_name_if_present(hmodule, START_NAME), + stop: export_name_if_present(hmodule, STOP_NAME), + close: export_name_if_present(hmodule, CLOSE_NAME), + } + } +} + +/// Return `name` if it resolves in `hmodule`, otherwise `None`. +/// +/// # Safety +/// `hmodule` must be a valid module handle. +unsafe fn export_name_if_present( + hmodule: HMODULE, + name: &'static core::ffi::CStr, +) -> Option<&'static str> { + // SAFETY: `name` is a valid null-terminated C string; `hmodule` is valid. + if unsafe { GetProcAddress(hmodule, PCSTR(name.as_ptr().cast())) }.is_some() { + name.to_str().ok() + } else { + None + } +} + +/// Capability probe: `true` only when `processmodel.dll` exposes all three Learning Mode /// trace exports on this machine. #[must_use] pub fn is_learning_mode_api_available() -> bool { - LearningModeApi::load().is_ok() + probe_learning_mode_exports().is_complete() } #[cfg(test)] @@ -243,6 +417,61 @@ mod tests { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; + use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; + use std::sync::Mutex; + use windows::Win32::Foundation::{E_FAIL, S_FALSE, S_OK}; + + static TEST_LOCK: Mutex<()> = Mutex::new(()); + static START_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static STOP_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static STOP_FAILURE_RESULT: AtomicI32 = AtomicI32::new(E_FAIL.0); + static STOP_FAILURES_REMAINING: AtomicUsize = AtomicUsize::new(0); + static STOP_CALLS: AtomicUsize = AtomicUsize::new(0); + static CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); + + unsafe extern "system" fn fake_start(_: HANDLE, trace_out: *mut HANDLE) -> HRESULT { + let result = HRESULT(START_RESULT.load(Ordering::SeqCst)); + if result.is_ok() { + unsafe { + *trace_out = HANDLE(std::ptr::dangling_mut::()); + } + } + result + } + + unsafe extern "system" fn fake_stop(_: HANDLE, _: *const u16) -> HRESULT { + STOP_CALLS.fetch_add(1, Ordering::SeqCst); + if STOP_FAILURES_REMAINING + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |remaining| { + remaining.checked_sub(1) + }) + .is_ok() + { + return HRESULT(STOP_FAILURE_RESULT.load(Ordering::SeqCst)); + } + HRESULT(STOP_RESULT.load(Ordering::SeqCst)) + } + + unsafe extern "system" fn fake_close(_: HANDLE) { + CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); + } + + fn fake_api() -> LearningModeApi { + LearningModeApi::from_raw_parts(fake_start, fake_stop, fake_close) + } + + fn fake_environment() -> HANDLE { + HANDLE(std::ptr::dangling_mut::()) + } + + fn reset_fakes() { + START_RESULT.store(S_OK.0, Ordering::SeqCst); + STOP_RESULT.store(S_OK.0, Ordering::SeqCst); + STOP_FAILURE_RESULT.store(E_FAIL.0, Ordering::SeqCst); + STOP_FAILURES_REMAINING.store(0, Ordering::SeqCst); + STOP_CALLS.store(0, Ordering::SeqCst); + CLOSE_CALLS.store(0, Ordering::SeqCst); + } #[test] fn probe_does_not_panic_and_matches_load() { @@ -277,9 +506,15 @@ mod tests { #[test] fn output_path_rejects_embedded_nul() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); let path = PathBuf::from(OsString::from_wide(&['a' as u16, 0, 'b' as u16])); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; - let error = encode_output_path(Some(&path)).expect_err("embedded NUL must be rejected"); + let error = api + .stop_trace(&trace, Some(&path)) + .expect_err("embedded NUL must be rejected"); assert!(matches!( error, @@ -288,5 +523,201 @@ mod tests { .. } )); + assert_eq!(STOP_CALLS.load(Ordering::SeqCst), 0); + drop(trace); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn successful_hresult_starts_retryable_trace() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + START_RESULT.store(S_FALSE.0, Ordering::SeqCst); + let api = fake_api(); + let trace = unsafe { + api.start_trace(fake_environment()) + .expect("non-failing HRESULT should succeed") + }; + + api.stop_trace(&trace, None).unwrap(); + api.stop_trace(&trace, None).unwrap(); + assert_eq!(STOP_CALLS.load(Ordering::SeqCst), 2); + + trace.close(); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn failed_hresult_keeps_trace_live_until_close() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; + STOP_RESULT.store(E_FAIL.0, Ordering::SeqCst); + + let error = api.stop_trace(&trace, None).unwrap_err(); + assert!(matches!( + error, + LearningModeError::HResultCall { + function: "StopLearningModeTrace", + code + } if code == E_FAIL.0 + )); + + drop(trace); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn transient_stop_failure_is_retried() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + STOP_FAILURE_RESULT.store( + HRESULT::from_win32(ERROR_SHARING_VIOLATION.0).0, + Ordering::SeqCst, + ); + STOP_FAILURES_REMAINING.store(2, Ordering::SeqCst); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; + + api.stop_trace_with_retry(&trace, None).unwrap(); + + assert_eq!(STOP_CALLS.load(Ordering::SeqCst), 3); + trace.close(); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn permanent_stop_failure_is_not_retried() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + STOP_RESULT.store(E_FAIL.0, Ordering::SeqCst); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; + + let error = api.stop_trace_with_retry(&trace, None).unwrap_err(); + + assert!(matches!( + error, + LearningModeError::HResultCall { + function: "StopLearningModeTrace", + code + } if code == E_FAIL.0 + )); + assert_eq!(STOP_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn exhausted_transient_stop_retries_preserve_hresult() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + let retry_hresult = HRESULT::from_win32(ERROR_LOCK_VIOLATION.0).0; + STOP_FAILURE_RESULT.store(retry_hresult, Ordering::SeqCst); + STOP_FAILURES_REMAINING.store(3, Ordering::SeqCst); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; + + let error = api.stop_trace_with_retry(&trace, None).unwrap_err(); + + assert!(matches!( + error, + LearningModeError::HResultCall { + function: "StopLearningModeTrace", + code + } if code == retry_hresult + )); + assert_eq!( + STOP_CALLS.load(Ordering::SeqCst), + LearningModeApi::STOP_DELIVERY_ATTEMPTS + ); + } + + #[test] + fn failed_start_preserves_hresult_and_does_not_close() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + START_RESULT.store(E_FAIL.0, Ordering::SeqCst); + let api = fake_api(); + + let error = unsafe { api.start_trace(fake_environment()).unwrap_err() }; + + assert!(matches!( + error, + LearningModeError::HResultCall { + function: "StartLearningModeTrace", + code + } if code == E_FAIL.0 + )); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 0); + } + + #[test] + fn explicit_close_is_exactly_once() { + let _guard = TEST_LOCK.lock().unwrap(); + reset_fakes(); + let api = fake_api(); + let trace = unsafe { api.start_trace(fake_environment()).unwrap() }; + + trace.close(); + + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn load_result_is_memoized_and_consistent() { + // `load` memoizes success or failure for the process. Repeated calls must + // agree with each other and with the capability probe, and never panic — + // regardless of whether the API is present on this host. + let first = LearningModeApi::load().is_ok(); + let second = LearningModeApi::load().is_ok(); + assert_eq!(first, second); + assert_eq!(first, is_learning_mode_api_available()); + } + + #[test] + fn learning_mode_report_all_present_is_complete() { + let report = LearningModeExportReport { + start: Some("StartLearningModeTrace"), + stop: Some("StopLearningModeTrace"), + close: Some("CloseLearningModeTrace"), + }; + assert!(report.is_complete()); + } + + #[test] + fn learning_mode_report_each_missing_export_is_incomplete() { + let complete = LearningModeExportReport { + start: Some("StartLearningModeTrace"), + stop: Some("StopLearningModeTrace"), + close: Some("CloseLearningModeTrace"), + }; + + assert!(!LearningModeExportReport { + start: None, + ..complete + } + .is_complete()); + assert!(!LearningModeExportReport { + stop: None, + ..complete + } + .is_complete()); + assert!(!LearningModeExportReport { + close: None, + ..complete + } + .is_complete()); + assert!(!LearningModeExportReport::default().is_complete()); + } + + #[test] + fn learning_mode_report_v1_two_export_subset_is_incomplete() { + // The legacy ABI exposed only Start/Stop. Requiring Close rejects it. + let v1_subset = LearningModeExportReport { + start: Some("StartLearningModeTrace"), + stop: Some("StopLearningModeTrace"), + close: None, + }; + assert!(!v1_subset.is_complete()); } } diff --git a/src/backends/learning_mode/windows/src/lib.rs b/src/backends/learning_mode/windows/src/lib.rs index 31384dbae..213cf2eec 100644 --- a/src/backends/learning_mode/windows/src/lib.rs +++ b/src/backends/learning_mode/windows/src/lib.rs @@ -5,24 +5,24 @@ //! **Learning Mode trace API** exported by `processmodel.dll`. //! //! Supported Windows builds expose a privileged, per-client learning-mode -//! ETW trace behind two flat C exports in `processmodel.dll` — the same system DLL -//! the BaseContainer backend already loads for `Experimental_CreateProcessInSandbox`: +//! ETW trace behind three official flat C exports in `processmodel.dll`: //! //! ```c -//! BOOL StartLearningModeTrace(HANDLE hProcessSecurityEnvironment, HLEARNINGMODE_TRACE* pphTrace); -//! BOOL StopLearningModeTrace (HLEARNINGMODE_TRACE* pphTrace, LPCWSTR lpOutputPath); +//! HRESULT StartLearningModeTrace(HPROCESS_SECURITY_ENVIRONMENT environment, HLEARNINGMODE_TRACE* trace); +//! HRESULT StopLearningModeTrace(HLEARNINGMODE_TRACE trace, PCWSTR outputEtlPath); +//! void CloseLearningModeTrace(HLEARNINGMODE_TRACE trace); //! ``` //! //! The broker collects and filters the trace to the caller's user SID and the -//! sandbox identified by the supplied security-environment handle, then — on stop — -//! writes the sealed ETL into a caller-named `outputPath` (opened under the caller's -//! own identity to avoid a confused-deputy). There is **no real-time event access**; -//! denials are read from the ETL after the sandboxed process exits. +//! sandbox identified by the supplied security-environment handle. `Stop` seals and +//! copies the ETL into a caller-named `outputPath` (opened under the caller's own +//! identity to avoid a confused-deputy) and may be retried; `Close` releases the +//! broker state and staged ETL. There is **no real-time event access**; denials are +//! read from the ETL after the sandboxed process exits. //! //! Because the exports only exist on feature-enabled OS builds, this crate resolves //! them at runtime via `LoadLibrary`/`GetProcAddress` behind the [`is_learning_mode_api_available`] -//! capability probe, mirroring the existing `Experimental_CreateProcessInSandbox` -//! adapter. The crate compiles on every platform: the capability probe returns +//! capability probe. The crate compiles on every platform: the capability probe returns //! `false` on non-Windows targets, while the loader and capture lifecycle types are //! exported only on Windows. @@ -62,8 +62,25 @@ pub use secenv::{ }; /// Errors surfaced while loading or invoking the Learning Mode trace API. -#[derive(Debug, Error)] +/// +/// `Clone` is derived so that [`crate::LearningModeApi::load`] and +/// [`crate::SecurityEnvironmentApi::load`] can memoize a failed load and hand +/// every caller an owned, typed copy of the original diagnostic. Every variant +/// already owns its data (`&'static str`, `String`, or plain integers), so the +/// clone preserves the full message and source information without erasing it +/// behind a stringified surrogate. +#[derive(Debug, Clone, Error)] pub enum LearningModeError { + /// The named API-set group for an API surface is not implemented by this + /// Windows build. + #[error("API set `{api_set}` is not implemented; this OS build lacks the required {api} API")] + ApiSetUnavailable { + /// The API surface guarded by the named group. + api: &'static str, + /// The API-set contract queried with `IsApiSetImplemented`. + api_set: &'static str, + }, + /// `processmodel.dll` itself could not be loaded from System32. #[error("failed to load processmodel.dll: {0}")] DllLoad(String), @@ -80,12 +97,21 @@ pub enum LearningModeError { detail: String, }, - /// An API call returned `FALSE`; `code` is the captured `GetLastError` value. - #[error("{function} failed (GetLastError = {code})")] - ApiCall { + /// An API call returned a failing HRESULT. + #[error("{function} failed (HRESULT = 0x{code:08X})")] + HResultCall { /// The name of the export that returned failure. function: &'static str, - /// The `GetLastError` value captured immediately after the failed call. + /// The raw HRESULT value. + code: i32, + }, + + /// A Win32 API call failed and set the thread's last-error value. + #[error("{function} failed (Win32 error = {code})")] + ApiCall { + /// The API operation that failed. + function: &'static str, + /// The raw `GetLastError` value. code: u32, }, @@ -97,15 +123,6 @@ pub enum LearningModeError { /// Why the value is invalid. detail: String, }, - - /// A primary operation failed and the subsequent cleanup operation also failed. - #[error("{primary}; cleanup also failed: {cleanup}")] - CleanupFailed { - /// The error that triggered cleanup. - primary: Box, - /// The error returned while attempting cleanup. - cleanup: Box, - }, } /// Capability probe: `true` only when `processmodel.dll` exposes the Learning Mode @@ -142,24 +159,6 @@ mod stub_tests { mod error_tests { use super::*; - #[test] - fn cleanup_error_preserves_both_failures() { - let error = LearningModeError::CleanupFailed { - primary: Box::new(LearningModeError::ApiCall { - function: "StartLearningModeTrace", - code: 5, - }), - cleanup: Box::new(LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - code: 6, - }), - }; - - let message = error.to_string(); - assert!(message.contains("StartLearningModeTrace")); - assert!(message.contains("CloseProcessSecurityEnvironment")); - } - #[test] fn missing_export_identifies_the_api_surface() { let error = LearningModeError::ExportMissing { diff --git a/src/backends/learning_mode/windows/src/lifecycle.rs b/src/backends/learning_mode/windows/src/lifecycle.rs index b76e03369..b2d50ee5d 100644 --- a/src/backends/learning_mode/windows/src/lifecycle.rs +++ b/src/backends/learning_mode/windows/src/lifecycle.rs @@ -14,14 +14,16 @@ //! `CreateProcessW` (**runner's job**; the session exposes the handle via //! [`CaptureSession::environment`]) //! 4. wait for the child to exit -//! 5. `StopLearningModeTrace(trace, outputPath)` → sealed ETL (NULL path discards) -//! 6. `CloseProcessSecurityEnvironment(env)` → teardown +//! 5. `StopLearningModeTrace(trace, outputPath)` → sealed ETL (bounded retries +//! for transient delivery failures) +//! 6. `CloseLearningModeTrace(trace)` → release broker state and staged ETL +//! 7. `CloseProcessSecurityEnvironment(env)` → teardown //! //! [`CaptureSession::begin`] performs steps 1–2; the runner performs steps 3–4 with the //! handle from [`CaptureSession::environment`]; [`CaptureSession::finish`] performs steps -//! 5–6 in order. If the session is dropped without `finish` (e.g. the launch failed or a -//! `?` unwound the stack), [`Drop`] runs a best-effort teardown — discard the trace, then -//! close the environment — so no broker-side trace or environment is leaked. +//! 5–7 in order. If the session is dropped without `finish` (e.g. the launch failed or a +//! `?` unwound the stack), [`Drop`] closes the trace without stopping it — the OS-supported +//! discard path — then closes the environment. use std::path::Path; @@ -36,15 +38,13 @@ use crate::LearningModeError; /// /// Construct with [`CaptureSession::begin`]; drive the child launch with the handle from /// [`CaptureSession::environment`]; seal and tear down with [`CaptureSession::finish`]. -/// Dropping without `finish` discards the trace and closes the environment on a -/// best-effort basis. +/// Dropping without `finish` closes and discards the trace, then closes the environment. #[derive(Debug)] pub struct CaptureSession { - secenv_api: SecurityEnvironmentApi, learning_mode_api: LearningModeApi, /// `Some` until `finish`/`Drop` closes it. environment: Option, - /// `Some` until `finish`/`Drop` seals or discards it. + /// `Some` until `finish`/`Drop` closes it. trace: Option, } @@ -55,36 +55,28 @@ impl CaptureSession { /// `flags` is normally [`crate::PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE`]. /// /// # Errors - /// - [`LearningModeError::ApiCall`] if `CreateProcessSecurityEnvironment` fails. - /// - [`LearningModeError::ApiCall`] if `StartLearningModeTrace` fails — in which case - /// the just-created environment is closed before returning so it is not leaked. - /// - [`LearningModeError::CleanupFailed`] if starting the trace fails and closing - /// the just-created environment also fails. + /// - [`LearningModeError::HResultCall`] if `CreateProcessSecurityEnvironment` fails. + /// - [`LearningModeError::HResultCall`] if `StartLearningModeTrace` fails — in which + /// case the just-created environment is closed before returning so it is not leaked. pub fn begin( secenv_api: SecurityEnvironmentApi, learning_mode_api: LearningModeApi, sandbox_specification: &[u8], flags: u32, ) -> Result { - let mut environment = secenv_api.create(sandbox_specification, flags)?; + let environment = secenv_api.create(sandbox_specification, flags)?; // SAFETY: `environment` was just created by `secenv_api.create` and is live for // the duration of this call; `start_trace` only reads it. let trace = match unsafe { learning_mode_api.start_trace(environment.raw()) } { Ok(trace) => trace, Err(start_err) => { - return match secenv_api.close(&mut environment) { - Ok(()) => Err(start_err), - Err(cleanup) => Err(LearningModeError::CleanupFailed { - primary: Box::new(start_err), - cleanup: Box::new(cleanup), - }), - }; + environment.close(); + return Err(start_err); } }; Ok(Self { - secenv_api, learning_mode_api, environment: Some(environment), trace: Some(trace), @@ -110,54 +102,33 @@ impl CaptureSession { } } - /// Seal the trace to `output_path` (or discard it when `None`), then close the - /// security environment. Call **after** the child has exited. - /// - /// Both teardown steps are attempted even if the first fails. If both fail, - /// [`LearningModeError::CleanupFailed`] preserves both errors. + /// Stop the trace and deliver it to `output_path` (or skip delivery when + /// `None`), retry transient delivery failures, close the trace, then close + /// the security environment. Call **after** the child has exited. /// /// # Errors - /// - [`LearningModeError::ApiCall`] from `StopLearningModeTrace` or - /// `CloseProcessSecurityEnvironment`. - /// - [`LearningModeError::CleanupFailed`] if both teardown calls fail. + /// - [`LearningModeError::HResultCall`] from `StopLearningModeTrace`. pub fn finish(mut self, output_path: Option<&Path>) -> Result<(), LearningModeError> { - let stop_result = match self.trace.take() { - Some(trace) => self.learning_mode_api.stop_trace(trace, output_path), + let stop_result = match self.trace.as_ref() { + Some(trace) => self + .learning_mode_api + .stop_trace_with_retry(trace, output_path), None => Ok(()), }; - let close_result = match self.environment.as_mut() { - Some(environment) => self.secenv_api.close(environment), - None => Ok(()), - }; - if close_result.is_ok() { - self.environment.take(); + if let Some(trace) = self.trace.take() { + trace.close(); } - combine_teardown_results(stop_result, close_result) - } -} - -fn combine_teardown_results( - stop_result: Result<(), LearningModeError>, - close_result: Result<(), LearningModeError>, -) -> Result<(), LearningModeError> { - match (stop_result, close_result) { - (Ok(()), Ok(())) => Ok(()), - (Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error), - (Err(primary), Err(cleanup)) => Err(LearningModeError::CleanupFailed { - primary: Box::new(primary), - cleanup: Box::new(cleanup), - }), + if let Some(environment) = self.environment.take() { + environment.close(); + } + stop_result } } impl Drop for CaptureSession { fn drop(&mut self) { - // Best-effort teardown for the early-exit / unwind path: discard the trace - // (NULL output path) before closing the environment. Errors are unrecoverable - // here and are intentionally ignored — `finish` is the fallible path. - if let Some(trace) = self.trace.take() { - let _ = self.learning_mode_api.stop_trace(trace, None); - } + // Close without Stop is the OS-supported early-exit discard path. + drop(self.trace.take()); if let Some(environment) = self.environment.take() { drop(environment); } @@ -167,40 +138,203 @@ impl Drop for CaptureSession { #[cfg(test)] mod tests { use super::*; + use std::ffi::c_void; + use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering}; + use std::sync::{Mutex, MutexGuard}; + use windows::Win32::Foundation::{ERROR_SHARING_VIOLATION, E_FAIL, S_OK}; + use windows_core::HRESULT; + + /// Serializes access to the shared fake-call event log and result knobs. + static TEST_LOCK: Mutex<()> = Mutex::new(()); + /// Ordered log of the fake export calls, as they happen across both APIs. + static EVENTS: Mutex> = Mutex::new(Vec::new()); + + static CREATE_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static START_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static STOP_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static STOP_FAILURE_RESULT: AtomicI32 = AtomicI32::new(E_FAIL.0); + static STOP_FAILURES_REMAINING: AtomicUsize = AtomicUsize::new(0); + + fn record(event: &'static str) { + EVENTS.lock().unwrap().push(event); + } + + fn take_events() -> Vec<&'static str> { + std::mem::take(&mut *EVENTS.lock().unwrap()) + } + + fn dangling_handle() -> HANDLE { + HANDLE(std::ptr::dangling_mut::()) + } + + unsafe extern "system" fn fake_create( + _: *const c_void, + _: u32, + _: u32, + out: *mut HANDLE, + ) -> HRESULT { + record("create"); + let result = HRESULT(CREATE_RESULT.load(Ordering::SeqCst)); + if result.is_ok() { + unsafe { *out = dangling_handle() }; + } + result + } + + unsafe extern "system" fn fake_query(_: *mut u64) -> HRESULT { + S_OK + } + + unsafe extern "system" fn fake_env_close(_: HANDLE) { + record("env_close"); + } + + unsafe extern "system" fn fake_start(_: HANDLE, out: *mut HANDLE) -> HRESULT { + record("start"); + let result = HRESULT(START_RESULT.load(Ordering::SeqCst)); + if result.is_ok() { + unsafe { *out = dangling_handle() }; + } + result + } + + unsafe extern "system" fn fake_stop(_: HANDLE, _: *const u16) -> HRESULT { + record("stop"); + if STOP_FAILURES_REMAINING + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |remaining| { + remaining.checked_sub(1) + }) + .is_ok() + { + return HRESULT(STOP_FAILURE_RESULT.load(Ordering::SeqCst)); + } + HRESULT(STOP_RESULT.load(Ordering::SeqCst)) + } - fn api_error(function: &'static str, code: u32) -> LearningModeError { - LearningModeError::ApiCall { function, code } + unsafe extern "system" fn fake_trace_close(_: HANDLE) { + record("trace_close"); + } + + fn reset() -> MutexGuard<'static, ()> { + let guard = TEST_LOCK + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + take_events(); + CREATE_RESULT.store(S_OK.0, Ordering::SeqCst); + START_RESULT.store(S_OK.0, Ordering::SeqCst); + STOP_RESULT.store(S_OK.0, Ordering::SeqCst); + STOP_FAILURE_RESULT.store(E_FAIL.0, Ordering::SeqCst); + STOP_FAILURES_REMAINING.store(0, Ordering::SeqCst); + guard + } + + fn fake_secenv_api() -> SecurityEnvironmentApi { + SecurityEnvironmentApi::from_raw_parts(fake_create, fake_query, fake_env_close) + } + + fn fake_learning_mode_api() -> LearningModeApi { + LearningModeApi::from_raw_parts(fake_start, fake_stop, fake_trace_close) + } + + fn begin_session() -> Result { + CaptureSession::begin( + fake_secenv_api(), + fake_learning_mode_api(), + b"PSEC-fake-spec", + crate::PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE, + ) } #[test] - fn teardown_preserves_both_failures() { - let result = combine_teardown_results( - Err(api_error("StopLearningModeTrace", 5)), - Err(api_error("CloseProcessSecurityEnvironment", 6)), + fn begin_creates_environment_before_starting_trace() { + let _guard = reset(); + let session = begin_session().expect("begin should succeed with passing fakes"); + + // The environment must be created first so the trace keys on a live handle. + assert_eq!(take_events(), vec!["create", "start"]); + assert_eq!(session.environment(), dangling_handle()); + + // Tidy up deterministically so Drop bookkeeping does not leak into siblings. + drop(session); + } + + #[test] + fn begin_start_failure_closes_environment_and_leaves_no_trace() { + let _guard = reset(); + START_RESULT.store(E_FAIL.0, Ordering::SeqCst); + + let error = begin_session().expect_err("start failure must propagate"); + assert!(matches!( + error, + LearningModeError::HResultCall { + function: "StartLearningModeTrace", + code + } if code == E_FAIL.0 + )); + + // The just-created environment is torn down; the trace was never created so + // it is never closed, and Stop is never attempted. + assert_eq!(take_events(), vec!["create", "start", "env_close"]); + } + + #[test] + fn finish_retries_stop_then_closes_trace_then_environment() { + let _guard = reset(); + STOP_FAILURE_RESULT.store( + HRESULT::from_win32(ERROR_SHARING_VIOLATION.0).0, + Ordering::SeqCst, ); + STOP_FAILURES_REMAINING.store(2, Ordering::SeqCst); - let LearningModeError::CleanupFailed { primary, cleanup } = - result.expect_err("both teardown failures must be returned") - else { - panic!("expected CleanupFailed"); - }; - assert!(primary.to_string().contains("StopLearningModeTrace")); - assert!(cleanup - .to_string() - .contains("CloseProcessSecurityEnvironment")); + let session = begin_session().expect("begin should succeed"); + assert_eq!(take_events(), vec!["create", "start"]); + + session + .finish(None) + .expect("finish should succeed after retries"); + + // Stop is retried until it succeeds, THEN the trace closes, THEN the + // environment closes — the exact teardown ordering the OS requires. + assert_eq!( + take_events(), + vec!["stop", "stop", "stop", "trace_close", "env_close"] + ); } #[test] - fn teardown_returns_single_failure_unchanged() { - let result = - combine_teardown_results(Ok(()), Err(api_error("CloseProcessSecurityEnvironment", 6))); + fn finish_propagates_permanent_stop_failure_but_still_tears_down() { + let _guard = reset(); + STOP_RESULT.store(E_FAIL.0, Ordering::SeqCst); + + let session = begin_session().expect("begin should succeed"); + take_events(); + let error = session + .finish(None) + .expect_err("a permanent stop failure must surface"); assert!(matches!( - result, - Err(LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - code: 6 - }) + error, + LearningModeError::HResultCall { + function: "StopLearningModeTrace", + .. + } )); + + // Even when Stop fails permanently, the trace and environment are still + // closed, in order, so nothing leaks. + assert_eq!(take_events(), vec!["stop", "trace_close", "env_close"]); + } + + #[test] + fn drop_without_finish_discards_trace_then_closes_environment() { + let _guard = reset(); + let session = begin_session().expect("begin should succeed"); + take_events(); + + drop(session); + + // Dropping without `finish` closes (discards) the trace WITHOUT calling + // Stop, then closes the environment. + assert_eq!(take_events(), vec!["trace_close", "env_close"]); } } diff --git a/src/backends/learning_mode/windows/src/secenv.rs b/src/backends/learning_mode/windows/src/secenv.rs index 610a7ba8d..3d68ac9b0 100644 --- a/src/backends/learning_mode/windows/src/secenv.rs +++ b/src/backends/learning_mode/windows/src/secenv.rs @@ -10,30 +10,30 @@ //! resolves it to the target AppContainer SID server-side). Neither of MXC's existing //! launch paths yields that handle — classic AppContainer uses `CreateProcess` + //! `SECURITY_CAPABILITIES`, and BaseContainer uses the one-shot RPC-brokered -//! `Experimental_CreateProcessInSandbox`. To capture denials, MXC adopts the flat -//! 2-phase model exported by the same `processmodel.dll`: +//! `Experimental_CreateProcessInSandbox`. To capture denials, MXC uses the +//! official process security-environment model exported by `processmodel.dll`: //! //! ```c -//! BOOL CreateProcessSecurityEnvironment( +//! HRESULT CreateProcessSecurityEnvironment( //! LPCVOID sandboxSpecification, DWORD sandboxSpecificationSize, //! PROCESS_SECURITY_ENVIRONMENT_FLAGS flags, //! HPROCESS_SECURITY_ENVIRONMENT* processSecurityEnvironment); -//! BOOL CloseProcessSecurityEnvironment(HPROCESS_SECURITY_ENVIRONMENT* processSecurityEnvironment); +//! void CloseProcessSecurityEnvironment(HPROCESS_SECURITY_ENVIRONMENT processSecurityEnvironment); //! ``` //! -//! `sandboxSpecification`/`...Size` is a compiled FlatBuffer sandbox-spec blob (the -//! same `"SBOX"` format the BaseContainer runner already builds via `sandbox_spec`); +//! `sandboxSpecification`/`...Size` is a `"PSEC"` process-security-environment +//! FlatBuffer; //! the spec must encode the learning-mode capability. The environment handle is //! attached to a normal `CreateProcessW` launch through //! `PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT`; KernelBase routes that launch //! through the security environment internally. `Close` tears the environment down. //! -//! As with the trace exports, each function is resolved at runtime and tolerates the -//! `Experimental_`-prefixed name as a fallback for OS builds that predate the -//! graduation out of the `Experimental_` prefix. +//! As with the trace exports, each function is resolved at runtime. The +//! ABI-changing create/close exports require their official plain names. use std::ffi::c_void; use std::ptr; +use std::sync::OnceLock; use windows::Win32::Foundation::{GetLastError, ERROR_INSUFFICIENT_BUFFER, HANDLE, HMODULE}; use windows::Win32::System::LibraryLoader::{ @@ -43,24 +43,27 @@ use windows::Win32::System::Threading::{ DeleteProcThreadAttributeList, InitializeProcThreadAttributeList, UpdateProcThreadAttribute, LPPROC_THREAD_ATTRIBUTE_LIST, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, STARTUPINFOEXW, STARTUPINFOW, }; -use windows_core::{PCSTR, PCWSTR}; +use windows::Win32::System::WindowsProgramming::IsApiSetImplemented; +use windows_core::{HRESULT, PCSTR, PCWSTR}; use wxc_common::string_util; use crate::LearningModeError; /// System DLL that hosts the flat process security-environment exports. const PROCESSMODEL_DLL: &str = "processmodel.dll"; +const SECURITY_ENVIRONMENT_API_SET_NAME: &str = "api-win-appmodel-processmodel~securityenvironment"; +const SECURITY_ENVIRONMENT_API_SET: &core::ffi::CStr = + c"api-win-appmodel-processmodel~securityenvironment"; /// No special behaviour when creating the security environment /// (`PROCESS_SECURITY_ENVIRONMENT_FLAGS` value `0`). /// -/// A `KILL_ON_CLOSE` bit exists (tears the child down when the environment closes) but -/// its numeric value is intentionally not declared here yet: explicit -/// [`SecurityEnvironmentApi::close`] after the child has exited already provides -/// deterministic teardown, so shipping code does not need to guess the flag value. +/// A terminate-on-close bit exists, but its numeric value is intentionally not +/// declared here: explicit [`ProcessSecurityEnvironment::close`] after the child +/// has exited already provides deterministic teardown. pub const PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE: u32 = 0; -/// `BOOL CreateProcessSecurityEnvironment(LPCVOID sandboxSpecification, +/// `HRESULT CreateProcessSecurityEnvironment(LPCVOID sandboxSpecification, /// DWORD sandboxSpecificationSize, PROCESS_SECURITY_ENVIRONMENT_FLAGS flags, /// HPROCESS_SECURITY_ENVIRONMENT* processSecurityEnvironment)`. /// @@ -70,22 +73,23 @@ type PfnCreateProcessSecurityEnvironment = unsafe extern "system" fn( sandbox_specification_size: u32, flags: u32, process_security_environment: *mut HANDLE, -) -> i32; +) -> HRESULT; -/// `BOOL CloseProcessSecurityEnvironment(HPROCESS_SECURITY_ENVIRONMENT* processSecurityEnvironment)`. -/// -/// The export nulls `*processSecurityEnvironment` on success. +/// `HRESULT QueryProcessSecurityEnvironmentSupport(UINT64* supportFlags)`. +type PfnQueryProcessSecurityEnvironmentSupport = + unsafe extern "system" fn(support_flags: *mut u64) -> HRESULT; + +/// `void CloseProcessSecurityEnvironment(HPROCESS_SECURITY_ENVIRONMENT processSecurityEnvironment)`. type PfnCloseProcessSecurityEnvironment = - unsafe extern "system" fn(process_security_environment: *mut HANDLE) -> i32; + unsafe extern "system" fn(process_security_environment: HANDLE); /// Opaque handle to a process security environment (`HPROCESS_SECURITY_ENVIRONMENT`, a /// `HANDLE`). /// /// Produced by [`SecurityEnvironmentApi::create`], threaded into the trace start and -/// the in-environment launch, and torn down by [`SecurityEnvironmentApi::close`]. The -/// wrapped [`HANDLE`] is passed by value to the launch/trace exports and by pointer to -/// the close export (which nulls it on success). If explicit close fails, the -/// wrapper retains ownership and retries once when dropped. +/// the in-environment launch, and torn down by [`ProcessSecurityEnvironment::close`]. The +/// wrapped [`HANDLE`] is passed by value to the launch, trace, and close exports. +/// Drop guarantees the infallible close is called exactly once. pub struct ProcessSecurityEnvironment { handle: HANDLE, close: PfnCloseProcessSecurityEnvironment, @@ -107,32 +111,26 @@ impl ProcessSecurityEnvironment { self.handle } - fn close_with( - &mut self, - close: PfnCloseProcessSecurityEnvironment, - ) -> Result<(), LearningModeError> { + /// Close the environment and release its server-side state. + pub fn close(mut self) { + self.close_inner(); + } + + fn close_inner(&mut self) { if self.handle.0.is_null() { - return Ok(()); + return; } // SAFETY: `close` was resolved from `processmodel.dll`; `self.handle` // came from a successful create call and remains owned by this wrapper. - let ok = unsafe { close(&mut self.handle) }; - if ok == 0 { - return Err(LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - code: last_error(), - }); - } + unsafe { (self.close)(self.handle) }; self.handle = HANDLE(ptr::null_mut()); - Ok(()) } } impl Drop for ProcessSecurityEnvironment { fn drop(&mut self) { - let close = self.close; - let _ = self.close_with(close); + self.close_inner(); } } @@ -291,13 +289,13 @@ impl Drop for SecurityEnvironmentStartupInfo { } } -/// Which candidate export name resolved for each function on this machine — a -/// diagnostic used by the capability probe to report the exact live surface (plain vs -/// `Experimental_`). +/// Which official export resolved for each function on this machine. #[derive(Debug, Clone, Copy, Default)] pub struct SecurityEnvironmentExportReport { /// Resolved name of the create export, if present. pub create: Option<&'static str>, + /// Resolved name of the support-query export, if present. + pub query_support: Option<&'static str>, /// Resolved name of the close export, if present. pub close: Option<&'static str>, } @@ -306,45 +304,75 @@ impl SecurityEnvironmentExportReport { /// `true` only when every export required for the 2-phase launch resolved. #[must_use] pub fn is_complete(&self) -> bool { - self.create.is_some() && self.close.is_some() + self.create.is_some() && self.query_support.is_some() && self.close.is_some() } } -/// Candidate names for each export: the graduated (plain) name is preferred, with the -/// `Experimental_`-prefixed name kept as a fallback for older feature builds. -const CREATE_NAMES: &[&core::ffi::CStr] = &[ - c"CreateProcessSecurityEnvironment", - c"Experimental_CreateProcessSecurityEnvironment", -]; -const CLOSE_NAMES: &[&core::ffi::CStr] = &[ - c"CloseProcessSecurityEnvironment", - c"Experimental_CloseProcessSecurityEnvironment", -]; +const CREATE_NAMES: &[&core::ffi::CStr] = &[c"CreateProcessSecurityEnvironment"]; +const QUERY_SUPPORT_NAMES: &[&core::ffi::CStr] = &[c"QueryProcessSecurityEnvironmentSupport"]; +const CLOSE_NAMES: &[&core::ffi::CStr] = &[c"CloseProcessSecurityEnvironment"]; /// Resolved process security-environment exports from `processmodel.dll`. +/// +/// `cacheable` records whether this surface was produced by the memoizing +/// [`SecurityEnvironmentApi::load`] (the real, process-wide singleton) as opposed +/// to a test fake. Only cacheable surfaces are allowed to populate the process-wide +/// [`supports_deny_paths`](Self::supports_deny_paths) cache, so injected fakes can +/// never poison it for the real API or for each other. #[derive(Clone, Copy)] pub struct SecurityEnvironmentApi { create: PfnCreateProcessSecurityEnvironment, + query_support: PfnQueryProcessSecurityEnvironmentSupport, close: PfnCloseProcessSecurityEnvironment, + cacheable: bool, } impl std::fmt::Debug for SecurityEnvironmentApi { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SecurityEnvironmentApi") .field("create", &(self.create as *const ())) + .field("query_support", &(self.query_support as *const ())) .field("close", &(self.close as *const ())) + .field("cacheable", &self.cacheable) .finish() } } +fn is_security_environment_api_set_implemented() -> bool { + // SAFETY: the contract is a valid static null-terminated string. + unsafe { IsApiSetImplemented(PCSTR(SECURITY_ENVIRONMENT_API_SET.as_ptr().cast())).as_bool() } +} + impl SecurityEnvironmentApi { /// Load `processmodel.dll` and resolve the 2-phase security-environment exports. /// + /// The result — success or failure — is memoized for the lifetime of the + /// process: `processmodel.dll` is a resident system DLL whose export set does + /// not change while the process runs, so repeated probes would only repeat the + /// same work and return the same answer. The cached error is cloned (see + /// [`LearningModeError`]), preserving the original diagnostic on every call. The + /// cached surface is marked cacheable so its + /// [`supports_deny_paths`](Self::supports_deny_paths) result is memoized too. + /// /// # Errors + /// - [`LearningModeError::ApiSetUnavailable`] if the security-environment + /// API-set named group is not implemented. /// - [`LearningModeError::DllLoad`] if `processmodel.dll` cannot be loaded. - /// - [`LearningModeError::ExportMissing`] if any required export is absent under - /// either its plain or `Experimental_`-prefixed name. + /// - [`LearningModeError::ExportMissing`] if any required export is absent. pub fn load() -> Result { + static CACHE: OnceLock> = OnceLock::new(); + CACHE.get_or_init(Self::load_uncached).clone() + } + + /// Perform the actual DLL load and export resolution, bypassing the cache. + fn load_uncached() -> Result { + if !is_security_environment_api_set_implemented() { + return Err(LearningModeError::ApiSetUnavailable { + api: "process security-environment", + api_set: SECURITY_ENVIRONMENT_API_SET_NAME, + }); + } + let dll = string_util::to_wide(PROCESSMODEL_DLL); // SAFETY: `dll` is a valid null-terminated wide string that outlives the call. @@ -357,6 +385,7 @@ impl SecurityEnvironmentApi { .map_err(|e| LearningModeError::DllLoad(e.to_string()))?; let create_proc = resolve_any(hmodule, CREATE_NAMES)?; + let query_support_proc = resolve_any(hmodule, QUERY_SUPPORT_NAMES)?; let close_proc = resolve_any(hmodule, CLOSE_NAMES)?; Ok(Self { @@ -364,38 +393,110 @@ impl SecurityEnvironmentApi { unsafe extern "system" fn() -> isize, PfnCreateProcessSecurityEnvironment, >(create_proc), + query_support: std::mem::transmute::< + unsafe extern "system" fn() -> isize, + PfnQueryProcessSecurityEnvironmentSupport, + >(query_support_proc), close: std::mem::transmute::< unsafe extern "system" fn() -> isize, PfnCloseProcessSecurityEnvironment, >(close_proc), + cacheable: true, }) } } - /// Create a process security environment from a compiled FlatBuffer sandbox-spec + /// Construct an API surface directly from raw export pointers, bypassing the + /// DLL load. Test-only: lets sibling modules inject fakes. The surface is marked + /// non-cacheable so its [`supports_deny_paths`](Self::supports_deny_paths) result + /// never populates the process-wide cache. + #[cfg(test)] + pub(crate) fn from_raw_parts( + create: PfnCreateProcessSecurityEnvironment, + query_support: PfnQueryProcessSecurityEnvironmentSupport, + close: PfnCloseProcessSecurityEnvironment, + ) -> Self { + Self { + create, + query_support, + close, + cacheable: false, + } + } + + /// Like [`from_raw_parts`](Self::from_raw_parts) but marked cacheable, so the + /// memoization of [`supports_deny_paths`](Self::supports_deny_paths) can be + /// exercised host-independently. Test-only. + #[cfg(test)] + pub(crate) fn from_raw_parts_cacheable( + create: PfnCreateProcessSecurityEnvironment, + query_support: PfnQueryProcessSecurityEnvironmentSupport, + close: PfnCloseProcessSecurityEnvironment, + ) -> Self { + Self { + create, + query_support, + close, + cacheable: true, + } + } + + /// Whether the official V2 API supports native deny paths. + /// + /// The answer is a fixed host capability, so for the real (cacheable) API the + /// result — including a typed error — is memoized once per process. Non-cacheable + /// test fakes always query directly and never touch the process-wide cache. + pub fn supports_deny_paths(&self) -> Result { + if self.cacheable { + static CACHE: OnceLock> = OnceLock::new(); + CACHE + .get_or_init(|| self.query_deny_paths_support()) + .clone() + } else { + self.query_deny_paths_support() + } + } + + /// Query `QueryProcessSecurityEnvironmentSupport` for the native-deny-path bit, + /// without consulting or populating the process-wide cache. + fn query_deny_paths_support(&self) -> Result { + const PSE_SUPPORT_FS_DENY: u64 = 0x0000_0000_0000_0001; + let mut support_flags = 0u64; + // SAFETY: `query_support` matches the official V2 declaration and + // `support_flags` is a valid out-pointer. + let result = unsafe { (self.query_support)(&mut support_flags) }; + if result.is_err() { + return Err(LearningModeError::HResultCall { + function: "QueryProcessSecurityEnvironmentSupport", + code: result.0, + }); + } + Ok(support_flags & PSE_SUPPORT_FS_DENY != 0) + } + + /// Create a process security environment from a PSEC FlatBuffer /// blob. `flags` is currently always [`PROCESS_SECURITY_ENVIRONMENT_FLAG_NONE`]. /// /// # Errors - /// [`LearningModeError::ApiCall`] carrying `GetLastError` if the export returns - /// `FALSE` (including a spec larger than `u32::MAX`, reported as - /// `ERROR_INVALID_PARAMETER`). + /// [`LearningModeError::HResultCall`] if the export returns a failing HRESULT. pub fn create( &self, sandbox_specification: &[u8], flags: u32, ) -> Result { let mut env = HANDLE(ptr::null_mut()); - let spec_len = - u32::try_from(sandbox_specification.len()).map_err(|_| LearningModeError::ApiCall { + let spec_len = u32::try_from(sandbox_specification.len()).map_err(|_| { + LearningModeError::HResultCall { function: "CreateProcessSecurityEnvironment", - code: windows::Win32::Foundation::ERROR_INVALID_PARAMETER.0, - })?; + code: windows::Win32::Foundation::E_INVALIDARG.0, + } + })?; // SAFETY: `self.create` was resolved from `processmodel.dll` and matches the // declared C signature. `sandbox_specification`/`spec_len` describe a valid, // contiguous byte buffer that outlives the call, and `env` is a valid // out-pointer. - let ok = unsafe { + let result = unsafe { (self.create)( sandbox_specification.as_ptr().cast(), spec_len, @@ -403,10 +504,16 @@ impl SecurityEnvironmentApi { &mut env, ) }; - if ok == 0 { - return Err(LearningModeError::ApiCall { + if result.is_err() { + return Err(LearningModeError::HResultCall { function: "CreateProcessSecurityEnvironment", - code: last_error(), + code: result.0, + }); + } + if env.0.is_null() { + return Err(LearningModeError::HResultCall { + function: "CreateProcessSecurityEnvironment", + code: windows::Win32::Foundation::E_UNEXPECTED.0, }); } Ok(ProcessSecurityEnvironment { @@ -414,18 +521,6 @@ impl SecurityEnvironmentApi { close: self.close, }) } - - /// Close a process security environment, tearing down its server-side state and - /// (per the create flags) the child. The export nulls the handle on success. - /// On failure, `env` retains ownership so the caller can retry; its [`Drop`] - /// implementation also makes one best-effort retry. - /// - /// # Errors - /// [`LearningModeError::ApiCall`] carrying `GetLastError` if the export returns - /// `FALSE`. - pub fn close(&self, env: &mut ProcessSecurityEnvironment) -> Result<(), LearningModeError> { - env.close_with(self.close) - } } /// Resolve the first name in `names` that is present in `hmodule`. @@ -465,11 +560,14 @@ fn last_error() -> u32 { unsafe { GetLastError().0 } } -/// Diagnostic probe reporting which security-environment export name resolved for each -/// function (plain vs `Experimental_`). Returns an all-`None` report if the DLL itself -/// cannot be loaded. +/// Diagnostic probe reporting which official security-environment exports +/// resolved. Returns an all-`None` report if the DLL itself cannot be loaded. #[must_use] pub fn probe_security_environment_exports() -> SecurityEnvironmentExportReport { + if !is_security_environment_api_set_implemented() { + return SecurityEnvironmentExportReport::default(); + } + let dll = string_util::to_wide(PROCESSMODEL_DLL); // SAFETY: `dll` is a valid null-terminated wide string that outlives the call; // `LOAD_LIBRARY_SEARCH_SYSTEM32` restricts the search to System32. @@ -483,6 +581,7 @@ pub fn probe_security_environment_exports() -> SecurityEnvironmentExportReport { unsafe { SecurityEnvironmentExportReport { create: first_present(hmodule, CREATE_NAMES), + query_support: first_present(hmodule, QUERY_SUPPORT_NAMES), close: first_present(hmodule, CLOSE_NAMES), } } @@ -515,20 +614,51 @@ pub fn is_security_environment_api_available() -> bool { #[cfg(test)] mod tests { use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::atomic::{AtomicI32, AtomicU64, AtomicUsize, Ordering}; + use std::sync::Mutex; + use windows::Win32::Foundation::{E_FAIL, S_OK}; static CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0); - unsafe extern "system" fn close_fails_then_succeeds(handle: *mut HANDLE) -> i32 { - if CLOSE_CALLS.fetch_add(1, Ordering::SeqCst) == 0 { - 0 - } else { - // SAFETY: the test passes a valid pointer to its owned HANDLE. - unsafe { - *handle = HANDLE(ptr::null_mut()); - } - 1 + /// Serializes the tests that share the query fakes' global counters. + static QUERY_LOCK: Mutex<()> = Mutex::new(()); + static QUERY_CALLS: AtomicUsize = AtomicUsize::new(0); + static QUERY_RESULT: AtomicI32 = AtomicI32::new(S_OK.0); + static QUERY_FLAGS: AtomicU64 = AtomicU64::new(0); + + /// Native-deny-path support bit reported by `QueryProcessSecurityEnvironmentSupport`. + const PSE_SUPPORT_FS_DENY: u64 = 0x0000_0000_0000_0001; + + unsafe extern "system" fn fake_close(_: HANDLE) { + CLOSE_CALLS.fetch_add(1, Ordering::SeqCst); + } + + unsafe extern "system" fn fake_create( + _: *const c_void, + _: u32, + _: u32, + _: *mut HANDLE, + ) -> HRESULT { + S_OK + } + + unsafe extern "system" fn fake_query(support_flags: *mut u64) -> HRESULT { + QUERY_CALLS.fetch_add(1, Ordering::SeqCst); + let result = HRESULT(QUERY_RESULT.load(Ordering::SeqCst)); + if result.is_ok() { + unsafe { *support_flags = QUERY_FLAGS.load(Ordering::SeqCst) }; } + result + } + + fn reset_query_fakes() { + QUERY_CALLS.store(0, Ordering::SeqCst); + QUERY_RESULT.store(S_OK.0, Ordering::SeqCst); + QUERY_FLAGS.store(0, Ordering::SeqCst); + } + + fn fake_uncached_api() -> SecurityEnvironmentApi { + SecurityEnvironmentApi::from_raw_parts(fake_create, fake_query, fake_close) } #[test] @@ -550,7 +680,9 @@ mod tests { Err(e) => assert!( matches!( e, - LearningModeError::DllLoad(_) | LearningModeError::ExportMissing { .. } + LearningModeError::ApiSetUnavailable { .. } + | LearningModeError::DllLoad(_) + | LearningModeError::ExportMissing { .. } ), "unexpected error variant: {e}" ), @@ -590,26 +722,117 @@ mod tests { } #[test] - fn failed_close_retains_ownership_and_drop_retries() { + fn explicit_close_is_exactly_once() { CLOSE_CALLS.store(0, Ordering::SeqCst); - let mut environment = ProcessSecurityEnvironment { + let environment = ProcessSecurityEnvironment { handle: HANDLE(std::ptr::dangling_mut::()), - close: close_fails_then_succeeds, + close: fake_close, }; - let error = environment - .close_with(close_fails_then_succeeds) - .expect_err("first close must fail"); + environment.close(); + assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 1); + } + + #[test] + fn export_report_all_present_is_complete() { + let report = SecurityEnvironmentExportReport { + create: Some("CreateProcessSecurityEnvironment"), + query_support: Some("QueryProcessSecurityEnvironmentSupport"), + close: Some("CloseProcessSecurityEnvironment"), + }; + assert!(report.is_complete()); + } + + #[test] + fn export_report_each_missing_export_is_incomplete() { + let complete = SecurityEnvironmentExportReport { + create: Some("CreateProcessSecurityEnvironment"), + query_support: Some("QueryProcessSecurityEnvironmentSupport"), + close: Some("CloseProcessSecurityEnvironment"), + }; + + assert!(!SecurityEnvironmentExportReport { + create: None, + ..complete + } + .is_complete()); + assert!(!SecurityEnvironmentExportReport { + query_support: None, + ..complete + } + .is_complete()); + assert!(!SecurityEnvironmentExportReport { + close: None, + ..complete + } + .is_complete()); + assert!(!SecurityEnvironmentExportReport::default().is_complete()); + } + + #[test] + fn supports_deny_paths_reports_flag_state() { + let _guard = QUERY_LOCK.lock().unwrap(); + let api = fake_uncached_api(); + + reset_query_fakes(); + QUERY_FLAGS.store(PSE_SUPPORT_FS_DENY, Ordering::SeqCst); + assert!(api.supports_deny_paths().unwrap()); + assert_eq!(QUERY_CALLS.load(Ordering::SeqCst), 1); + + reset_query_fakes(); + QUERY_FLAGS.store(0, Ordering::SeqCst); + assert!(!api.supports_deny_paths().unwrap()); + + reset_query_fakes(); + // Unrelated support bits must not be mistaken for deny-path support. + QUERY_FLAGS.store(0xFFFF_FFFF_FFFF_FFFE, Ordering::SeqCst); + assert!(!api.supports_deny_paths().unwrap()); + } + + #[test] + fn supports_deny_paths_maps_failing_hresult() { + let _guard = QUERY_LOCK.lock().unwrap(); + reset_query_fakes(); + QUERY_RESULT.store(E_FAIL.0, Ordering::SeqCst); + let api = fake_uncached_api(); + + let error = api.supports_deny_paths().unwrap_err(); assert!(matches!( error, - LearningModeError::ApiCall { - function: "CloseProcessSecurityEnvironment", - .. - } + LearningModeError::HResultCall { + function: "QueryProcessSecurityEnvironmentSupport", + code + } if code == E_FAIL.0 )); - assert!(!environment.raw().0.is_null()); + } - drop(environment); - assert_eq!(CLOSE_CALLS.load(Ordering::SeqCst), 2); + #[test] + fn non_cacheable_api_queries_every_call() { + let _guard = QUERY_LOCK.lock().unwrap(); + reset_query_fakes(); + QUERY_FLAGS.store(PSE_SUPPORT_FS_DENY, Ordering::SeqCst); + let api = fake_uncached_api(); + + assert!(api.supports_deny_paths().unwrap()); + assert!(api.supports_deny_paths().unwrap()); + // A test fake must never be memoized: both calls hit the underlying query. + assert_eq!(QUERY_CALLS.load(Ordering::SeqCst), 2); + } + + #[test] + fn cacheable_api_memoizes_support_query() { + // The only test that drives the cacheable (process-wide) support cache, so + // the `OnceLock` initializer runs deterministically here. + let _guard = QUERY_LOCK.lock().unwrap(); + reset_query_fakes(); + QUERY_FLAGS.store(PSE_SUPPORT_FS_DENY, Ordering::SeqCst); + let api = + SecurityEnvironmentApi::from_raw_parts_cacheable(fake_create, fake_query, fake_close); + + let first = api.supports_deny_paths().unwrap(); + let second = api.supports_deny_paths().unwrap(); + assert_eq!(first, second); + // The result is memoized for the process: the query runs at most once. + assert_eq!(QUERY_CALLS.load(Ordering::SeqCst), 1); } } diff --git a/src/core/generated/process_security_environment_specification/Cargo.toml b/src/core/generated/process_security_environment_specification/Cargo.toml new file mode 100644 index 000000000..394accdd8 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/Cargo.toml @@ -0,0 +1,14 @@ +# Generated crate: bindings under src/ are produced by `flatc` from +# external/windows-sdk/ProcessSecurityEnvironment.fbs. Do not hand-edit them — +# see README.md and regenerate.ps1. Provenance (schema hash, pinned flatc +# version, source build) lives in the schema's .provenance.toml. +[package] +name = "process_security_environment_spec" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false +description = "Generated FlatBuffers bindings for ProcessSecurityEnvironment" + +[dependencies] +flatbuffers = { workspace = true } diff --git a/src/core/generated/process_security_environment_specification/README.md b/src/core/generated/process_security_environment_specification/README.md new file mode 100644 index 000000000..8322e9679 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/README.md @@ -0,0 +1,46 @@ +# `process_security_environment_spec` + +Rust bindings **generated** from `external/windows-sdk/ProcessSecurityEnvironment.fbs` +by the FlatBuffers compiler (`flatc`). + +> **Do not hand-edit the files under `src/`.** They are generated; every file +> carries a `// Automatically generated by the Flatbuffers compiler. Do not +> modify.` header. Change the `.fbs` schema (with provenance) and rerun the +> regeneration script instead. The CI drift gate +> (`scripts/versioning/check-psec-codegen.js`) fails if the committed schema or +> generated crate drifts. + +## Provenance + +The vendored schema originates from the internal Microsoft Windows OS repository +and is not publicly redistributable. Authoritative provenance — the source +pull request, the schema SHA-256, and the exact `flatc` version used — is +recorded in +[`external/windows-sdk/ProcessSecurityEnvironment.provenance.toml`](../../../../external/windows-sdk/ProcessSecurityEnvironment.provenance.toml). +That file is the single source of truth consumed by both the regeneration +script and the CI drift gate. + +## Regenerating + +Install the **exact** `flatc` version pinned in the provenance file +(currently `25.12.19`), then run from the repository root: + +```powershell +pwsh -File src/core/generated/process_security_environment_specification/regenerate.ps1 +``` + +Pass `-Flatc ` when `flatc.exe` is not on `PATH`. The script validates the +schema SHA-256 against the provenance file and refuses any `flatc` version other +than the pinned one, so output is byte-reproducible. Older `flatc` releases emit +elided lifetimes that trip the `mismatched_lifetime_syntaxes` lint (Rust 1.89+); +`25.12.19` is the first release with the upstream fix (flatbuffers PR #8709). + +## Workspace membership + +The schema describes a **Windows-only** OS contract, and the crate is consumed +only under `[target.'cfg(target_os = "windows")'.dependencies]` by the +AppContainer/Learning-Mode backends. It remains a workspace member for now, +matching the existing generated `sandbox_spec` crate. The generated bindings +are pure Rust, so cross-platform workspace builds remain valid. Avoiding this +minor non-Windows build cost can be considered separately from the V2 contract +correctness work. diff --git a/src/core/generated/process_security_environment_specification/regenerate.ps1 b/src/core/generated/process_security_environment_specification/regenerate.ps1 new file mode 100644 index 000000000..c36917bca --- /dev/null +++ b/src/core/generated/process_security_environment_specification/regenerate.ps1 @@ -0,0 +1,125 @@ +<# +.SYNOPSIS + Regenerates the FlatBuffers Rust bindings for the + process_security_environment_spec crate, reproducibly. + +.DESCRIPTION + Runs `flatc` against external/windows-sdk/ProcessSecurityEnvironment.fbs and + rewrites the output into the crate's module layout. Before generating it: + * validates the vendored schema's SHA-256 against the recorded provenance + (external/windows-sdk/ProcessSecurityEnvironment.provenance.toml), and + * pins the EXACT flatc version recorded in that provenance file, so the + generated output is byte-reproducible. + + The generated files are checked in and must NOT be hand-edited; rerun this + script instead. The CI drift gate (scripts/versioning/check-psec-codegen.js) + fails if the committed schema or generated crate drifts. + +.PARAMETER Flatc + Path to flatc.exe. Defaults to "flatc.exe" (must be on PATH). + +.EXAMPLE + pwsh -File src/core/generated/process_security_environment_specification/regenerate.ps1 +#> +[CmdletBinding()] +param( + [string]$Flatc = "flatc.exe" +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = (& git rev-parse --show-toplevel) 2>$null +if (-not $repoRoot) { + throw "Not inside a git repository." +} +Set-Location $repoRoot + +$crateDir = $PSScriptRoot +$srcDir = Join-Path $crateDir "src" +$fbs = "external\windows-sdk\ProcessSecurityEnvironment.fbs" +$provenanceFile = "external\windows-sdk\ProcessSecurityEnvironment.provenance.toml" + +if (-not (Test-Path $fbs)) { + throw "FlatBuffers schema not found: $fbs" +} +if (-not (Test-Path $provenanceFile)) { + throw "Provenance file not found: $provenanceFile" +} +if (-not (Test-Path $Flatc) -and -not (Get-Command $Flatc -ErrorAction SilentlyContinue)) { + throw "flatc not found: $Flatc. Download from https://github.com/google/flatbuffers/releases" +} + +# --- Read pinned toolchain + expected schema hash from provenance ------------ +$provenance = Get-Content $provenanceFile -Raw +$pinnedFlatc = [regex]::Match($provenance, 'flatc_version\s*=\s*"([^"]+)"') +$expectedHash = [regex]::Match($provenance, 'sha256\s*=\s*"([^"]+)"') +if (-not $pinnedFlatc.Success) { + throw "Could not read flatc_version from $provenanceFile" +} +if (-not $expectedHash.Success) { + throw "Could not read schema sha256 from $provenanceFile" +} +$pinnedFlatcVersion = $pinnedFlatc.Groups[1].Value +$expectedSchemaHash = $expectedHash.Groups[1].Value.ToLower() + +# --- Validate the vendored schema hash matches provenance -------------------- +# Hash the LF-normalized content so the check is checkout-independent (autocrlf). +$schemaText = (Get-Content $fbs -Raw) -replace "`r`n", "`n" +$schemaBytes = [System.Text.Encoding]::UTF8.GetBytes($schemaText) +$sha = [System.Security.Cryptography.SHA256]::Create() +$actualSchemaHash = (($sha.ComputeHash($schemaBytes) | ForEach-Object { $_.ToString("x2") }) -join "") +if ($actualSchemaHash -ne $expectedSchemaHash) { + throw "Schema hash mismatch for $fbs.`n expected (provenance): $expectedSchemaHash`n actual (on disk): $actualSchemaHash`nIf you intentionally refreshed the schema, update $provenanceFile (sha256 + source revision) first." +} +Write-Host "Schema hash OK ($expectedSchemaHash)" -ForegroundColor Cyan + +# --- Pin the EXACT flatc version for reproducible output --------------------- +$versionOutput = (& $Flatc --version) 2>&1 | Out-String +$match = [regex]::Match($versionOutput, 'flatc version (\d+\.\d+\.\d+)') +if (-not $match.Success) { + throw "Could not parse flatc version from output: $versionOutput" +} +$flatcVersion = $match.Groups[1].Value +if ($flatcVersion -ne $pinnedFlatcVersion) { + throw "flatc version $flatcVersion does not match the pinned version $pinnedFlatcVersion (from $provenanceFile). Install the exact version for reproducible output: https://github.com/google/flatbuffers/releases/tag/v$pinnedFlatcVersion" +} +Write-Host "Using pinned flatc version $flatcVersion" -ForegroundColor Cyan + +Write-Host "Cleaning previous generated output..." -ForegroundColor Cyan +if (Test-Path $srcDir) { + Remove-Item $srcDir -Recurse -Force +} + +Write-Host "Running flatc..." -ForegroundColor Cyan +& $Flatc ` + --rust --gen-object-api --force-empty --no-prefix --rust-module-root-file --gen-all ` + -o $crateDir ` + $fbs +if ($LASTEXITCODE -ne 0) { + throw "flatc failed with exit code $LASTEXITCODE" +} + +Write-Host "Reorganizing generated files..." -ForegroundColor Cyan +New-Item -ItemType Directory -Path $srcDir | Out-Null +Move-Item (Join-Path $crateDir "mod.rs") (Join-Path $srcDir "lib.rs") +Move-Item (Join-Path $crateDir "process_security_environment_layout") ` + (Join-Path $srcDir "process_security_environment_layout") + +Write-Host "Patching lib.rs (lint suppression)..." -ForegroundColor Cyan +$libRs = Join-Path $srcDir "lib.rs" +(Get-Content $libRs) ` + -replace '// @generated', "// @generated`n#![allow(unused_imports, non_snake_case, non_camel_case_types, clippy::all)]" | + Set-Content $libRs + +Write-Host "Formatting with cargo fmt..." -ForegroundColor Cyan +Push-Location src +try { + & cargo fmt -p process_security_environment_spec + if ($LASTEXITCODE -ne 0) { + throw "cargo fmt failed with exit code $LASTEXITCODE" + } +} finally { + Pop-Location +} + +Write-Host "Done. Regenerated bindings in $srcDir" -ForegroundColor Green diff --git a/src/core/generated/process_security_environment_specification/src/lib.rs b/src/core/generated/process_security_environment_specification/src/lib.rs new file mode 100644 index 000000000..fd7e9b81e --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/lib.rs @@ -0,0 +1,28 @@ +// Automatically generated by the Flatbuffers compiler. Do not modify. +// @generated +#![allow(unused_imports, non_snake_case, non_camel_case_types, clippy::all)] +pub mod process_security_environment_layout { + use super::*; + mod filter_action_generated; + pub use self::filter_action_generated::*; + mod ip_protocol_generated; + pub use self::ip_protocol_generated::*; + mod schema_version_generated; + pub use self::schema_version_generated::*; + mod process_security_environment_generated; + pub use self::process_security_environment_generated::*; + mod proxy_info_generated; + pub use self::proxy_info_generated::*; + mod ip_subnet_generated; + pub use self::ip_subnet_generated::*; + mod destination_rule_generated; + pub use self::destination_rule_generated::*; + mod port_rule_generated; + pub use self::port_rule_generated::*; + mod endpoint_rule_generated; + pub use self::endpoint_rule_generated::*; + mod endpoint_policy_generated; + pub use self::endpoint_policy_generated::*; + mod network_policy_generated; + pub use self::network_policy_generated::*; +} // process_security_environment_layout diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/destination_rule_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/destination_rule_generated.rs new file mode 100644 index 000000000..03f986475 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/destination_rule_generated.rs @@ -0,0 +1,194 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum DestinationRuleOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct DestinationRule<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for DestinationRule<'a> { + type Inner = DestinationRule<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> DestinationRule<'a> { + pub const VT_SUBNET: ::flatbuffers::VOffsetT = 4; + pub const VT_EXCEPT: ::flatbuffers::VOffsetT = 6; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + DestinationRule { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args DestinationRuleArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = DestinationRuleBuilder::new(_fbb); + if let Some(x) = args.except { + builder.add_except(x); + } + if let Some(x) = args.subnet { + builder.add_subnet(x); + } + builder.finish() + } + + pub fn unpack(&self) -> DestinationRuleT { + let subnet = self.subnet().map(|x| alloc::boxed::Box::new(x.unpack())); + let except = self + .except() + .map(|x| x.iter().map(|t| t.unpack()).collect()); + DestinationRuleT { subnet, except } + } + + #[inline] + pub fn subnet(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset>(DestinationRule::VT_SUBNET, None) + } + } + #[inline] + pub fn except( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, + >>(DestinationRule::VT_EXCEPT, None) + } + } +} + +impl ::flatbuffers::Verifiable for DestinationRule<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset>( + "subnet", + Self::VT_SUBNET, + false, + )? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, + >>("except", Self::VT_EXCEPT, false)? + .finish(); + Ok(()) + } +} +pub struct DestinationRuleArgs<'a> { + pub subnet: Option<::flatbuffers::WIPOffset>>, + pub except: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, + >, + >, +} +impl<'a> Default for DestinationRuleArgs<'a> { + #[inline] + fn default() -> Self { + DestinationRuleArgs { + subnet: None, + except: None, + } + } +} + +pub struct DestinationRuleBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> DestinationRuleBuilder<'a, 'b, A> { + #[inline] + pub fn add_subnet(&mut self, subnet: ::flatbuffers::WIPOffset>) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset>( + DestinationRule::VT_SUBNET, + subnet, + ); + } + #[inline] + pub fn add_except( + &mut self, + except: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, + >, + ) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(DestinationRule::VT_EXCEPT, except); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> DestinationRuleBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + DestinationRuleBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for DestinationRule<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("DestinationRule"); + ds.field("subnet", &self.subnet()); + ds.field("except", &self.except()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct DestinationRuleT { + pub subnet: Option>, + pub except: Option>, +} +impl Default for DestinationRuleT { + fn default() -> Self { + Self { + subnet: None, + except: None, + } + } +} +impl DestinationRuleT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let subnet = self.subnet.as_ref().map(|x| x.pack(_fbb)); + let except = self.except.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect(); + _fbb.create_vector(&w) + }); + DestinationRule::create(_fbb, &DestinationRuleArgs { subnet, except }) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_policy_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_policy_generated.rs new file mode 100644 index 000000000..db66f4a50 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_policy_generated.rs @@ -0,0 +1,242 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum EndpointPolicyOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct EndpointPolicy<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for EndpointPolicy<'a> { + type Inner = EndpointPolicy<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> EndpointPolicy<'a> { + pub const VT_DEFAULT_ACTION: ::flatbuffers::VOffsetT = 4; + pub const VT_ALLOW: ::flatbuffers::VOffsetT = 6; + pub const VT_DENY: ::flatbuffers::VOffsetT = 8; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + EndpointPolicy { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args EndpointPolicyArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = EndpointPolicyBuilder::new(_fbb); + if let Some(x) = args.deny { + builder.add_deny(x); + } + if let Some(x) = args.allow { + builder.add_allow(x); + } + builder.add_default_action(args.default_action); + builder.finish() + } + + pub fn unpack(&self) -> EndpointPolicyT { + let default_action = self.default_action(); + let allow = self.allow().map(|x| x.iter().map(|t| t.unpack()).collect()); + let deny = self.deny().map(|x| x.iter().map(|t| t.unpack()).collect()); + EndpointPolicyT { + default_action, + allow, + deny, + } + } + + #[inline] + pub fn default_action(&self) -> FilterAction { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(EndpointPolicy::VT_DEFAULT_ACTION, Some(FilterAction::deny)) + .unwrap() + } + } + #[inline] + pub fn allow( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, + >>(EndpointPolicy::VT_ALLOW, None) + } + } + #[inline] + pub fn deny( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, + >>(EndpointPolicy::VT_DENY, None) + } + } +} + +impl ::flatbuffers::Verifiable for EndpointPolicy<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::("default_action", Self::VT_DEFAULT_ACTION, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, + >>("allow", Self::VT_ALLOW, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, + >>("deny", Self::VT_DENY, false)? + .finish(); + Ok(()) + } +} +pub struct EndpointPolicyArgs<'a> { + pub default_action: FilterAction, + pub allow: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, + >, + >, + pub deny: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, + >, + >, +} +impl<'a> Default for EndpointPolicyArgs<'a> { + #[inline] + fn default() -> Self { + EndpointPolicyArgs { + default_action: FilterAction::deny, + allow: None, + deny: None, + } + } +} + +pub struct EndpointPolicyBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> EndpointPolicyBuilder<'a, 'b, A> { + #[inline] + pub fn add_default_action(&mut self, default_action: FilterAction) { + self.fbb_.push_slot::( + EndpointPolicy::VT_DEFAULT_ACTION, + default_action, + FilterAction::deny, + ); + } + #[inline] + pub fn add_allow( + &mut self, + allow: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, + >, + ) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(EndpointPolicy::VT_ALLOW, allow); + } + #[inline] + pub fn add_deny( + &mut self, + deny: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, + >, + ) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(EndpointPolicy::VT_DENY, deny); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> EndpointPolicyBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + EndpointPolicyBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for EndpointPolicy<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("EndpointPolicy"); + ds.field("default_action", &self.default_action()); + ds.field("allow", &self.allow()); + ds.field("deny", &self.deny()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct EndpointPolicyT { + pub default_action: FilterAction, + pub allow: Option>, + pub deny: Option>, +} +impl Default for EndpointPolicyT { + fn default() -> Self { + Self { + default_action: FilterAction::deny, + allow: None, + deny: None, + } + } +} +impl EndpointPolicyT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let default_action = self.default_action; + let allow = self.allow.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect(); + _fbb.create_vector(&w) + }); + let deny = self.deny.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect(); + _fbb.create_vector(&w) + }); + EndpointPolicy::create( + _fbb, + &EndpointPolicyArgs { + default_action, + allow, + deny, + }, + ) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_rule_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_rule_generated.rs new file mode 100644 index 000000000..b7deaa0ce --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/endpoint_rule_generated.rs @@ -0,0 +1,216 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum EndpointRuleOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct EndpointRule<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for EndpointRule<'a> { + type Inner = EndpointRule<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> EndpointRule<'a> { + pub const VT_DESTINATIONS: ::flatbuffers::VOffsetT = 4; + pub const VT_PORTS: ::flatbuffers::VOffsetT = 6; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + EndpointRule { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args EndpointRuleArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = EndpointRuleBuilder::new(_fbb); + if let Some(x) = args.ports { + builder.add_ports(x); + } + if let Some(x) = args.destinations { + builder.add_destinations(x); + } + builder.finish() + } + + pub fn unpack(&self) -> EndpointRuleT { + let destinations = self + .destinations() + .map(|x| x.iter().map(|t| t.unpack()).collect()); + let ports = self.ports().map(|x| x.iter().map(|t| t.unpack()).collect()); + EndpointRuleT { + destinations, + ports, + } + } + + #[inline] + pub fn destinations( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> + { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, + >>(EndpointRule::VT_DESTINATIONS, None) + } + } + #[inline] + pub fn ports( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, + >>(EndpointRule::VT_PORTS, None) + } + } +} + +impl ::flatbuffers::Verifiable for EndpointRule<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, + >>("destinations", Self::VT_DESTINATIONS, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, + >>("ports", Self::VT_PORTS, false)? + .finish(); + Ok(()) + } +} +pub struct EndpointRuleArgs<'a> { + pub destinations: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, + >, + >, + pub ports: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, + >, + >, +} +impl<'a> Default for EndpointRuleArgs<'a> { + #[inline] + fn default() -> Self { + EndpointRuleArgs { + destinations: None, + ports: None, + } + } +} + +pub struct EndpointRuleBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> EndpointRuleBuilder<'a, 'b, A> { + #[inline] + pub fn add_destinations( + &mut self, + destinations: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, + >, + ) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + EndpointRule::VT_DESTINATIONS, + destinations, + ); + } + #[inline] + pub fn add_ports( + &mut self, + ports: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, + >, + ) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(EndpointRule::VT_PORTS, ports); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> EndpointRuleBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + EndpointRuleBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for EndpointRule<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("EndpointRule"); + ds.field("destinations", &self.destinations()); + ds.field("ports", &self.ports()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct EndpointRuleT { + pub destinations: Option>, + pub ports: Option>, +} +impl Default for EndpointRuleT { + fn default() -> Self { + Self { + destinations: None, + ports: None, + } + } +} +impl EndpointRuleT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let destinations = self.destinations.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect(); + _fbb.create_vector(&w) + }); + let ports = self.ports.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|t| t.pack(_fbb)).collect(); + _fbb.create_vector(&w) + }); + EndpointRule::create( + _fbb, + &EndpointRuleArgs { + destinations, + ports, + }, + ) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/filter_action_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/filter_action_generated.rs new file mode 100644 index 000000000..027385b89 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/filter_action_generated.rs @@ -0,0 +1,92 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MIN_FILTER_ACTION: i8 = 0; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MAX_FILTER_ACTION: i8 = 1; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +#[allow(non_camel_case_types)] +pub const ENUM_VALUES_FILTER_ACTION: [FilterAction; 2] = [FilterAction::deny, FilterAction::allow]; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(transparent)] +pub struct FilterAction(pub i8); +#[allow(non_upper_case_globals)] +impl FilterAction { + pub const deny: Self = Self(0); + pub const allow: Self = Self(1); + + pub const ENUM_MIN: i8 = 0; + pub const ENUM_MAX: i8 = 1; + pub const ENUM_VALUES: &'static [Self] = &[Self::deny, Self::allow]; + /// Returns the variant's name or "" if unknown. + pub fn variant_name(self) -> Option<&'static str> { + match self { + Self::deny => Some("deny"), + Self::allow => Some("allow"), + _ => None, + } + } +} +impl ::core::fmt::Debug for FilterAction { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + if let Some(name) = self.variant_name() { + f.write_str(name) + } else { + f.write_fmt(format_args!("", self.0)) + } + } +} +impl<'a> ::flatbuffers::Follow<'a> for FilterAction { + type Inner = Self; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + let b = unsafe { ::flatbuffers::read_scalar_at::(buf, loc) }; + Self(b) + } +} + +impl ::flatbuffers::Push for FilterAction { + type Output = FilterAction; + #[inline] + unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { + unsafe { ::flatbuffers::emplace_scalar::(dst, self.0) }; + } +} + +impl ::flatbuffers::EndianScalar for FilterAction { + type Scalar = i8; + #[inline] + fn to_little_endian(self) -> i8 { + self.0.to_le() + } + #[inline] + #[allow(clippy::wrong_self_convention)] + fn from_little_endian(v: i8) -> Self { + let b = i8::from_le(v); + Self(b) + } +} + +impl<'a> ::flatbuffers::Verifiable for FilterAction { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + i8::run_verifier(v, pos) + } +} + +impl ::flatbuffers::SimpleToVerifyInSlice for FilterAction {} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_protocol_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_protocol_generated.rs new file mode 100644 index 000000000..3dc6bfa28 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_protocol_generated.rs @@ -0,0 +1,105 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MIN_IP_PROTOCOL: i8 = 0; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MAX_IP_PROTOCOL: i8 = 4; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +#[allow(non_camel_case_types)] +pub const ENUM_VALUES_IP_PROTOCOL: [IpProtocol; 5] = [ + IpProtocol::any, + IpProtocol::tcp, + IpProtocol::udp, + IpProtocol::icmpv4, + IpProtocol::icmpv6, +]; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(transparent)] +pub struct IpProtocol(pub i8); +#[allow(non_upper_case_globals)] +impl IpProtocol { + pub const any: Self = Self(0); + pub const tcp: Self = Self(1); + pub const udp: Self = Self(2); + pub const icmpv4: Self = Self(3); + pub const icmpv6: Self = Self(4); + + pub const ENUM_MIN: i8 = 0; + pub const ENUM_MAX: i8 = 4; + pub const ENUM_VALUES: &'static [Self] = + &[Self::any, Self::tcp, Self::udp, Self::icmpv4, Self::icmpv6]; + /// Returns the variant's name or "" if unknown. + pub fn variant_name(self) -> Option<&'static str> { + match self { + Self::any => Some("any"), + Self::tcp => Some("tcp"), + Self::udp => Some("udp"), + Self::icmpv4 => Some("icmpv4"), + Self::icmpv6 => Some("icmpv6"), + _ => None, + } + } +} +impl ::core::fmt::Debug for IpProtocol { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + if let Some(name) = self.variant_name() { + f.write_str(name) + } else { + f.write_fmt(format_args!("", self.0)) + } + } +} +impl<'a> ::flatbuffers::Follow<'a> for IpProtocol { + type Inner = Self; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + let b = unsafe { ::flatbuffers::read_scalar_at::(buf, loc) }; + Self(b) + } +} + +impl ::flatbuffers::Push for IpProtocol { + type Output = IpProtocol; + #[inline] + unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { + unsafe { ::flatbuffers::emplace_scalar::(dst, self.0) }; + } +} + +impl ::flatbuffers::EndianScalar for IpProtocol { + type Scalar = i8; + #[inline] + fn to_little_endian(self) -> i8 { + self.0.to_le() + } + #[inline] + #[allow(clippy::wrong_self_convention)] + fn from_little_endian(v: i8) -> Self { + let b = i8::from_le(v); + Self(b) + } +} + +impl<'a> ::flatbuffers::Verifiable for IpProtocol { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + i8::run_verifier(v, pos) + } +} + +impl ::flatbuffers::SimpleToVerifyInSlice for IpProtocol {} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_subnet_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_subnet_generated.rs new file mode 100644 index 000000000..a51fdb4f1 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/ip_subnet_generated.rs @@ -0,0 +1,182 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum IpSubnetOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct IpSubnet<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for IpSubnet<'a> { + type Inner = IpSubnet<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> IpSubnet<'a> { + pub const VT_ADDRESS: ::flatbuffers::VOffsetT = 4; + pub const VT_PREFIX_LENGTH: ::flatbuffers::VOffsetT = 6; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + IpSubnet { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args IpSubnetArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = IpSubnetBuilder::new(_fbb); + if let Some(x) = args.address { + builder.add_address(x); + } + builder.add_prefix_length(args.prefix_length); + builder.finish() + } + + pub fn unpack(&self) -> IpSubnetT { + let address = self + .address() + .map(|x| alloc::string::ToString::to_string(x)); + let prefix_length = self.prefix_length(); + IpSubnetT { + address, + prefix_length, + } + } + + #[inline] + pub fn address(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset<&str>>(IpSubnet::VT_ADDRESS, None) + } + } + #[inline] + pub fn prefix_length(&self) -> u8 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(IpSubnet::VT_PREFIX_LENGTH, Some(0)) + .unwrap() + } + } +} + +impl ::flatbuffers::Verifiable for IpSubnet<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( + "address", + Self::VT_ADDRESS, + false, + )? + .visit_field::("prefix_length", Self::VT_PREFIX_LENGTH, false)? + .finish(); + Ok(()) + } +} +pub struct IpSubnetArgs<'a> { + pub address: Option<::flatbuffers::WIPOffset<&'a str>>, + pub prefix_length: u8, +} +impl<'a> Default for IpSubnetArgs<'a> { + #[inline] + fn default() -> Self { + IpSubnetArgs { + address: None, + prefix_length: 0, + } + } +} + +pub struct IpSubnetBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> IpSubnetBuilder<'a, 'b, A> { + #[inline] + pub fn add_address(&mut self, address: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(IpSubnet::VT_ADDRESS, address); + } + #[inline] + pub fn add_prefix_length(&mut self, prefix_length: u8) { + self.fbb_ + .push_slot::(IpSubnet::VT_PREFIX_LENGTH, prefix_length, 0); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> IpSubnetBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + IpSubnetBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for IpSubnet<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("IpSubnet"); + ds.field("address", &self.address()); + ds.field("prefix_length", &self.prefix_length()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct IpSubnetT { + pub address: Option, + pub prefix_length: u8, +} +impl Default for IpSubnetT { + fn default() -> Self { + Self { + address: None, + prefix_length: 0, + } + } +} +impl IpSubnetT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let address = self.address.as_ref().map(|x| _fbb.create_string(x)); + let prefix_length = self.prefix_length; + IpSubnet::create( + _fbb, + &IpSubnetArgs { + address, + prefix_length, + }, + ) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/network_policy_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/network_policy_generated.rs new file mode 100644 index 000000000..d619b7038 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/network_policy_generated.rs @@ -0,0 +1,242 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum NetworkPolicyOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct NetworkPolicy<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for NetworkPolicy<'a> { + type Inner = NetworkPolicy<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> NetworkPolicy<'a> { + pub const VT_PROXY: ::flatbuffers::VOffsetT = 4; + pub const VT_EGRESS: ::flatbuffers::VOffsetT = 6; + pub const VT_ALLOWED_APPCONTAINER_PEER: ::flatbuffers::VOffsetT = 8; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + NetworkPolicy { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args NetworkPolicyArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = NetworkPolicyBuilder::new(_fbb); + if let Some(x) = args.allowed_appcontainer_peer { + builder.add_allowed_appcontainer_peer(x); + } + if let Some(x) = args.egress { + builder.add_egress(x); + } + if let Some(x) = args.proxy { + builder.add_proxy(x); + } + builder.finish() + } + + pub fn unpack(&self) -> NetworkPolicyT { + let proxy = self.proxy().map(|x| alloc::boxed::Box::new(x.unpack())); + let egress = self.egress().map(|x| alloc::boxed::Box::new(x.unpack())); + let allowed_appcontainer_peer = self + .allowed_appcontainer_peer() + .map(|x| alloc::string::ToString::to_string(x)); + NetworkPolicyT { + proxy, + egress, + allowed_appcontainer_peer, + } + } + + #[inline] + pub fn proxy(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset>(NetworkPolicy::VT_PROXY, None) + } + } + #[inline] + pub fn egress(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset>( + NetworkPolicy::VT_EGRESS, + None, + ) + } + } + #[inline] + pub fn allowed_appcontainer_peer(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>( + NetworkPolicy::VT_ALLOWED_APPCONTAINER_PEER, + None, + ) + } + } +} + +impl ::flatbuffers::Verifiable for NetworkPolicy<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset>( + "proxy", + Self::VT_PROXY, + false, + )? + .visit_field::<::flatbuffers::ForwardsUOffset>( + "egress", + Self::VT_EGRESS, + false, + )? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( + "allowed_appcontainer_peer", + Self::VT_ALLOWED_APPCONTAINER_PEER, + false, + )? + .finish(); + Ok(()) + } +} +pub struct NetworkPolicyArgs<'a> { + pub proxy: Option<::flatbuffers::WIPOffset>>, + pub egress: Option<::flatbuffers::WIPOffset>>, + pub allowed_appcontainer_peer: Option<::flatbuffers::WIPOffset<&'a str>>, +} +impl<'a> Default for NetworkPolicyArgs<'a> { + #[inline] + fn default() -> Self { + NetworkPolicyArgs { + proxy: None, + egress: None, + allowed_appcontainer_peer: None, + } + } +} + +pub struct NetworkPolicyBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> NetworkPolicyBuilder<'a, 'b, A> { + #[inline] + pub fn add_proxy(&mut self, proxy: ::flatbuffers::WIPOffset>) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset>( + NetworkPolicy::VT_PROXY, + proxy, + ); + } + #[inline] + pub fn add_egress(&mut self, egress: ::flatbuffers::WIPOffset>) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset>( + NetworkPolicy::VT_EGRESS, + egress, + ); + } + #[inline] + pub fn add_allowed_appcontainer_peer( + &mut self, + allowed_appcontainer_peer: ::flatbuffers::WIPOffset<&'b str>, + ) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + NetworkPolicy::VT_ALLOWED_APPCONTAINER_PEER, + allowed_appcontainer_peer, + ); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> NetworkPolicyBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + NetworkPolicyBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for NetworkPolicy<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("NetworkPolicy"); + ds.field("proxy", &self.proxy()); + ds.field("egress", &self.egress()); + ds.field( + "allowed_appcontainer_peer", + &self.allowed_appcontainer_peer(), + ); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct NetworkPolicyT { + pub proxy: Option>, + pub egress: Option>, + pub allowed_appcontainer_peer: Option, +} +impl Default for NetworkPolicyT { + fn default() -> Self { + Self { + proxy: None, + egress: None, + allowed_appcontainer_peer: None, + } + } +} +impl NetworkPolicyT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let proxy = self.proxy.as_ref().map(|x| x.pack(_fbb)); + let egress = self.egress.as_ref().map(|x| x.pack(_fbb)); + let allowed_appcontainer_peer = self + .allowed_appcontainer_peer + .as_ref() + .map(|x| _fbb.create_string(x)); + NetworkPolicy::create( + _fbb, + &NetworkPolicyArgs { + proxy, + egress, + allowed_appcontainer_peer, + }, + ) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/port_rule_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/port_rule_generated.rs new file mode 100644 index 000000000..e689be18d --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/port_rule_generated.rs @@ -0,0 +1,198 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum PortRuleOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct PortRule<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for PortRule<'a> { + type Inner = PortRule<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> PortRule<'a> { + pub const VT_PROTOCOL: ::flatbuffers::VOffsetT = 4; + pub const VT_PORT: ::flatbuffers::VOffsetT = 6; + pub const VT_END_PORT: ::flatbuffers::VOffsetT = 8; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + PortRule { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args PortRuleArgs, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = PortRuleBuilder::new(_fbb); + builder.add_end_port(args.end_port); + builder.add_port(args.port); + builder.add_protocol(args.protocol); + builder.finish() + } + + pub fn unpack(&self) -> PortRuleT { + let protocol = self.protocol(); + let port = self.port(); + let end_port = self.end_port(); + PortRuleT { + protocol, + port, + end_port, + } + } + + #[inline] + pub fn protocol(&self) -> IpProtocol { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(PortRule::VT_PROTOCOL, Some(IpProtocol::any)) + .unwrap() + } + } + #[inline] + pub fn port(&self) -> u16 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(PortRule::VT_PORT, Some(0)).unwrap() } + } + #[inline] + pub fn end_port(&self) -> u16 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(PortRule::VT_END_PORT, Some(0)) + .unwrap() + } + } +} + +impl ::flatbuffers::Verifiable for PortRule<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::("protocol", Self::VT_PROTOCOL, false)? + .visit_field::("port", Self::VT_PORT, false)? + .visit_field::("end_port", Self::VT_END_PORT, false)? + .finish(); + Ok(()) + } +} +pub struct PortRuleArgs { + pub protocol: IpProtocol, + pub port: u16, + pub end_port: u16, +} +impl<'a> Default for PortRuleArgs { + #[inline] + fn default() -> Self { + PortRuleArgs { + protocol: IpProtocol::any, + port: 0, + end_port: 0, + } + } +} + +pub struct PortRuleBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> PortRuleBuilder<'a, 'b, A> { + #[inline] + pub fn add_protocol(&mut self, protocol: IpProtocol) { + self.fbb_ + .push_slot::(PortRule::VT_PROTOCOL, protocol, IpProtocol::any); + } + #[inline] + pub fn add_port(&mut self, port: u16) { + self.fbb_.push_slot::(PortRule::VT_PORT, port, 0); + } + #[inline] + pub fn add_end_port(&mut self, end_port: u16) { + self.fbb_ + .push_slot::(PortRule::VT_END_PORT, end_port, 0); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> PortRuleBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + PortRuleBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for PortRule<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("PortRule"); + ds.field("protocol", &self.protocol()); + ds.field("port", &self.port()); + ds.field("end_port", &self.end_port()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct PortRuleT { + pub protocol: IpProtocol, + pub port: u16, + pub end_port: u16, +} +impl Default for PortRuleT { + fn default() -> Self { + Self { + protocol: IpProtocol::any, + port: 0, + end_port: 0, + } + } +} +impl PortRuleT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let protocol = self.protocol; + let port = self.port; + let end_port = self.end_port; + PortRule::create( + _fbb, + &PortRuleArgs { + protocol, + port, + end_port, + }, + ) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/process_security_environment_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/process_security_environment_generated.rs new file mode 100644 index 000000000..254fbefc5 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/process_security_environment_generated.rs @@ -0,0 +1,565 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum ProcessSecurityEnvironmentOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct ProcessSecurityEnvironment<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for ProcessSecurityEnvironment<'a> { + type Inner = ProcessSecurityEnvironment<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> ProcessSecurityEnvironment<'a> { + pub const VT_VERSION: ::flatbuffers::VOffsetT = 4; + pub const VT_CAPABILITIES: ::flatbuffers::VOffsetT = 6; + pub const VT_DISALLOW_WIN32K_SYSTEM_CALLS: ::flatbuffers::VOffsetT = 8; + pub const VT_UI_RESTRICTIONS: ::flatbuffers::VOffsetT = 10; + pub const VT_FS_READ_WRITE: ::flatbuffers::VOffsetT = 12; + pub const VT_FS_READ_ONLY: ::flatbuffers::VOffsetT = 14; + pub const VT_FS_DENY: ::flatbuffers::VOffsetT = 16; + pub const VT_NETWORK_POLICY: ::flatbuffers::VOffsetT = 18; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + ProcessSecurityEnvironment { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args ProcessSecurityEnvironmentArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = ProcessSecurityEnvironmentBuilder::new(_fbb); + builder.add_ui_restrictions(args.ui_restrictions); + if let Some(x) = args.network_policy { + builder.add_network_policy(x); + } + if let Some(x) = args.fs_deny { + builder.add_fs_deny(x); + } + if let Some(x) = args.fs_read_only { + builder.add_fs_read_only(x); + } + if let Some(x) = args.fs_read_write { + builder.add_fs_read_write(x); + } + if let Some(x) = args.capabilities { + builder.add_capabilities(x); + } + if let Some(x) = args.version { + builder.add_version(x); + } + builder.add_disallow_win32k_system_calls(args.disallow_win32k_system_calls); + builder.finish() + } + + pub fn unpack(&self) -> ProcessSecurityEnvironmentT { + let version = { + let x = self.version(); + x.unpack() + }; + let capabilities = self + .capabilities() + .map(|x| alloc::string::ToString::to_string(x)); + let disallow_win32k_system_calls = self.disallow_win32k_system_calls(); + let ui_restrictions = self.ui_restrictions(); + let fs_read_write = self.fs_read_write().map(|x| { + x.iter() + .map(|s| alloc::string::ToString::to_string(s)) + .collect() + }); + let fs_read_only = self.fs_read_only().map(|x| { + x.iter() + .map(|s| alloc::string::ToString::to_string(s)) + .collect() + }); + let fs_deny = self.fs_deny().map(|x| { + x.iter() + .map(|s| alloc::string::ToString::to_string(s)) + .collect() + }); + let network_policy = self + .network_policy() + .map(|x| alloc::boxed::Box::new(x.unpack())); + ProcessSecurityEnvironmentT { + version, + capabilities, + disallow_win32k_system_calls, + ui_restrictions, + fs_read_write, + fs_read_only, + fs_deny, + network_policy, + } + } + + #[inline] + pub fn version(&self) -> &'a SchemaVersion { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(ProcessSecurityEnvironment::VT_VERSION, None) + .unwrap() + } + } + #[inline] + pub fn capabilities(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>( + ProcessSecurityEnvironment::VT_CAPABILITIES, + None, + ) + } + } + #[inline] + pub fn disallow_win32k_system_calls(&self) -> bool { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::( + ProcessSecurityEnvironment::VT_DISALLOW_WIN32K_SYSTEM_CALLS, + Some(false), + ) + .unwrap() + } + } + #[inline] + pub fn ui_restrictions(&self) -> u64 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(ProcessSecurityEnvironment::VT_UI_RESTRICTIONS, Some(0)) + .unwrap() + } + } + #[inline] + pub fn fs_read_write( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >>(ProcessSecurityEnvironment::VT_FS_READ_WRITE, None) + } + } + #[inline] + pub fn fs_read_only( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >>(ProcessSecurityEnvironment::VT_FS_READ_ONLY, None) + } + } + #[inline] + pub fn fs_deny( + &self, + ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab.get::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >>(ProcessSecurityEnvironment::VT_FS_DENY, None) + } + } + #[inline] + pub fn network_policy(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset>( + ProcessSecurityEnvironment::VT_NETWORK_POLICY, + None, + ) + } + } +} + +impl ::flatbuffers::Verifiable for ProcessSecurityEnvironment<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::("version", Self::VT_VERSION, true)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( + "capabilities", + Self::VT_CAPABILITIES, + false, + )? + .visit_field::( + "disallow_win32k_system_calls", + Self::VT_DISALLOW_WIN32K_SYSTEM_CALLS, + false, + )? + .visit_field::("ui_restrictions", Self::VT_UI_RESTRICTIONS, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset<&'_ str>>, + >>("fs_read_write", Self::VT_FS_READ_WRITE, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset<&'_ str>>, + >>("fs_read_only", Self::VT_FS_READ_ONLY, false)? + .visit_field::<::flatbuffers::ForwardsUOffset< + ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset<&'_ str>>, + >>("fs_deny", Self::VT_FS_DENY, false)? + .visit_field::<::flatbuffers::ForwardsUOffset>( + "network_policy", + Self::VT_NETWORK_POLICY, + false, + )? + .finish(); + Ok(()) + } +} +pub struct ProcessSecurityEnvironmentArgs<'a> { + pub version: Option<&'a SchemaVersion>, + pub capabilities: Option<::flatbuffers::WIPOffset<&'a str>>, + pub disallow_win32k_system_calls: bool, + pub ui_restrictions: u64, + pub fs_read_write: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >, + >, + pub fs_read_only: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >, + >, + pub fs_deny: Option< + ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset<&'a str>>, + >, + >, + pub network_policy: Option<::flatbuffers::WIPOffset>>, +} +impl<'a> Default for ProcessSecurityEnvironmentArgs<'a> { + #[inline] + fn default() -> Self { + ProcessSecurityEnvironmentArgs { + version: None, // required field + capabilities: None, + disallow_win32k_system_calls: false, + ui_restrictions: 0, + fs_read_write: None, + fs_read_only: None, + fs_deny: None, + network_policy: None, + } + } +} + +pub struct ProcessSecurityEnvironmentBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> ProcessSecurityEnvironmentBuilder<'a, 'b, A> { + #[inline] + pub fn add_version(&mut self, version: &SchemaVersion) { + self.fbb_ + .push_slot_always::<&SchemaVersion>(ProcessSecurityEnvironment::VT_VERSION, version); + } + #[inline] + pub fn add_capabilities(&mut self, capabilities: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + ProcessSecurityEnvironment::VT_CAPABILITIES, + capabilities, + ); + } + #[inline] + pub fn add_disallow_win32k_system_calls(&mut self, disallow_win32k_system_calls: bool) { + self.fbb_.push_slot::( + ProcessSecurityEnvironment::VT_DISALLOW_WIN32K_SYSTEM_CALLS, + disallow_win32k_system_calls, + false, + ); + } + #[inline] + pub fn add_ui_restrictions(&mut self, ui_restrictions: u64) { + self.fbb_.push_slot::( + ProcessSecurityEnvironment::VT_UI_RESTRICTIONS, + ui_restrictions, + 0, + ); + } + #[inline] + pub fn add_fs_read_write( + &mut self, + fs_read_write: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset<&'b str>>, + >, + ) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + ProcessSecurityEnvironment::VT_FS_READ_WRITE, + fs_read_write, + ); + } + #[inline] + pub fn add_fs_read_only( + &mut self, + fs_read_only: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset<&'b str>>, + >, + ) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + ProcessSecurityEnvironment::VT_FS_READ_ONLY, + fs_read_only, + ); + } + #[inline] + pub fn add_fs_deny( + &mut self, + fs_deny: ::flatbuffers::WIPOffset< + ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset<&'b str>>, + >, + ) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( + ProcessSecurityEnvironment::VT_FS_DENY, + fs_deny, + ); + } + #[inline] + pub fn add_network_policy( + &mut self, + network_policy: ::flatbuffers::WIPOffset>, + ) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset>( + ProcessSecurityEnvironment::VT_NETWORK_POLICY, + network_policy, + ); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> ProcessSecurityEnvironmentBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + ProcessSecurityEnvironmentBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + self.fbb_ + .required(o, ProcessSecurityEnvironment::VT_VERSION, "version"); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for ProcessSecurityEnvironment<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("ProcessSecurityEnvironment"); + ds.field("version", &self.version()); + ds.field("capabilities", &self.capabilities()); + ds.field( + "disallow_win32k_system_calls", + &self.disallow_win32k_system_calls(), + ); + ds.field("ui_restrictions", &self.ui_restrictions()); + ds.field("fs_read_write", &self.fs_read_write()); + ds.field("fs_read_only", &self.fs_read_only()); + ds.field("fs_deny", &self.fs_deny()); + ds.field("network_policy", &self.network_policy()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct ProcessSecurityEnvironmentT { + pub version: SchemaVersionT, + pub capabilities: Option, + pub disallow_win32k_system_calls: bool, + pub ui_restrictions: u64, + pub fs_read_write: Option>, + pub fs_read_only: Option>, + pub fs_deny: Option>, + pub network_policy: Option>, +} +impl Default for ProcessSecurityEnvironmentT { + fn default() -> Self { + Self { + version: Default::default(), + capabilities: None, + disallow_win32k_system_calls: false, + ui_restrictions: 0, + fs_read_write: None, + fs_read_only: None, + fs_deny: None, + network_policy: None, + } + } +} +impl ProcessSecurityEnvironmentT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let version_tmp = Some(self.version.pack()); + let version = version_tmp.as_ref(); + let capabilities = self.capabilities.as_ref().map(|x| _fbb.create_string(x)); + let disallow_win32k_system_calls = self.disallow_win32k_system_calls; + let ui_restrictions = self.ui_restrictions; + let fs_read_write = self.fs_read_write.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|s| _fbb.create_string(s)).collect(); + _fbb.create_vector(&w) + }); + let fs_read_only = self.fs_read_only.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|s| _fbb.create_string(s)).collect(); + _fbb.create_vector(&w) + }); + let fs_deny = self.fs_deny.as_ref().map(|x| { + let w: alloc::vec::Vec<_> = x.iter().map(|s| _fbb.create_string(s)).collect(); + _fbb.create_vector(&w) + }); + let network_policy = self.network_policy.as_ref().map(|x| x.pack(_fbb)); + ProcessSecurityEnvironment::create( + _fbb, + &ProcessSecurityEnvironmentArgs { + version, + capabilities, + disallow_win32k_system_calls, + ui_restrictions, + fs_read_write, + fs_read_only, + fs_deny, + network_policy, + }, + ) + } +} +#[inline] +/// Verifies that a buffer of bytes contains a `ProcessSecurityEnvironment` +/// and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_process_security_environment_unchecked`. +pub fn root_as_process_security_environment( + buf: &[u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root::(buf) +} +#[inline] +/// Verifies that a buffer of bytes contains a size prefixed +/// `ProcessSecurityEnvironment` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `size_prefixed_root_as_process_security_environment_unchecked`. +pub fn size_prefixed_root_as_process_security_environment( + buf: &[u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root::(buf) +} +#[inline] +/// Verifies, with the given options, that a buffer of bytes +/// contains a `ProcessSecurityEnvironment` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_process_security_environment_unchecked`. +pub fn root_as_process_security_environment_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root_with_opts::>(opts, buf) +} +#[inline] +/// Verifies, with the given verifier options, that a buffer of +/// bytes contains a size prefixed `ProcessSecurityEnvironment` and returns +/// it. Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_process_security_environment_unchecked`. +pub fn size_prefixed_root_as_process_security_environment_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root_with_opts::>(opts, buf) +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a ProcessSecurityEnvironment and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid `ProcessSecurityEnvironment`. +pub unsafe fn root_as_process_security_environment_unchecked( + buf: &[u8], +) -> ProcessSecurityEnvironment<'_> { + unsafe { ::flatbuffers::root_unchecked::(buf) } +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a size prefixed ProcessSecurityEnvironment and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid size prefixed `ProcessSecurityEnvironment`. +pub unsafe fn size_prefixed_root_as_process_security_environment_unchecked( + buf: &[u8], +) -> ProcessSecurityEnvironment<'_> { + unsafe { ::flatbuffers::size_prefixed_root_unchecked::(buf) } +} +pub const PROCESS_SECURITY_ENVIRONMENT_IDENTIFIER: &str = "PSEC"; + +#[inline] +pub fn process_security_environment_buffer_has_identifier(buf: &[u8]) -> bool { + ::flatbuffers::buffer_has_identifier(buf, PROCESS_SECURITY_ENVIRONMENT_IDENTIFIER, false) +} + +#[inline] +pub fn process_security_environment_size_prefixed_buffer_has_identifier(buf: &[u8]) -> bool { + ::flatbuffers::buffer_has_identifier(buf, PROCESS_SECURITY_ENVIRONMENT_IDENTIFIER, true) +} + +#[inline] +pub fn finish_process_security_environment_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( + fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + root: ::flatbuffers::WIPOffset>, +) { + fbb.finish(root, Some(PROCESS_SECURITY_ENVIRONMENT_IDENTIFIER)); +} + +#[inline] +pub fn finish_size_prefixed_process_security_environment_buffer< + 'a, + 'b, + A: ::flatbuffers::Allocator + 'a, +>( + fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + root: ::flatbuffers::WIPOffset>, +) { + fbb.finish_size_prefixed(root, Some(PROCESS_SECURITY_ENVIRONMENT_IDENTIFIER)); +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/proxy_info_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/proxy_info_generated.rs new file mode 100644 index 000000000..2b97f1025 --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/proxy_info_generated.rs @@ -0,0 +1,137 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +pub enum ProxyInfoOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct ProxyInfo<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for ProxyInfo<'a> { + type Inner = ProxyInfo<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> ProxyInfo<'a> { + pub const VT_URL: ::flatbuffers::VOffsetT = 4; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + ProxyInfo { _tab: table } + } + #[allow(unused_mut)] + pub fn create< + 'bldr: 'args, + 'args: 'mut_bldr, + 'mut_bldr, + A: ::flatbuffers::Allocator + 'bldr, + >( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args ProxyInfoArgs<'args>, + ) -> ::flatbuffers::WIPOffset> { + let mut builder = ProxyInfoBuilder::new(_fbb); + if let Some(x) = args.url { + builder.add_url(x); + } + builder.finish() + } + + pub fn unpack(&self) -> ProxyInfoT { + let url = self.url().map(|x| alloc::string::ToString::to_string(x)); + ProxyInfoT { url } + } + + #[inline] + pub fn url(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::<::flatbuffers::ForwardsUOffset<&str>>(ProxyInfo::VT_URL, None) + } + } +} + +impl ::flatbuffers::Verifiable for ProxyInfo<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("url", Self::VT_URL, false)? + .finish(); + Ok(()) + } +} +pub struct ProxyInfoArgs<'a> { + pub url: Option<::flatbuffers::WIPOffset<&'a str>>, +} +impl<'a> Default for ProxyInfoArgs<'a> { + #[inline] + fn default() -> Self { + ProxyInfoArgs { url: None } + } +} + +pub struct ProxyInfoBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> ProxyInfoBuilder<'a, 'b, A> { + #[inline] + pub fn add_url(&mut self, url: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_ + .push_slot_always::<::flatbuffers::WIPOffset<_>>(ProxyInfo::VT_URL, url); + } + #[inline] + pub fn new( + _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + ) -> ProxyInfoBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + ProxyInfoBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for ProxyInfo<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("ProxyInfo"); + ds.field("url", &self.url()); + ds.finish() + } +} +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub struct ProxyInfoT { + pub url: Option, +} +impl Default for ProxyInfoT { + fn default() -> Self { + Self { url: None } + } +} +impl ProxyInfoT { + pub fn pack<'b, A: ::flatbuffers::Allocator + 'b>( + &self, + _fbb: &mut ::flatbuffers::FlatBufferBuilder<'b, A>, + ) -> ::flatbuffers::WIPOffset> { + let url = self.url.as_ref().map(|x| _fbb.create_string(x)); + ProxyInfo::create(_fbb, &ProxyInfoArgs { url }) + } +} diff --git a/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/schema_version_generated.rs b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/schema_version_generated.rs new file mode 100644 index 000000000..1b36e3d5b --- /dev/null +++ b/src/core/generated/process_security_environment_specification/src/process_security_environment_layout/schema_version_generated.rs @@ -0,0 +1,152 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +use super::*; +// struct SchemaVersion, aligned to 2 +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq)] +pub struct SchemaVersion(pub [u8; 4]); +impl Default for SchemaVersion { + fn default() -> Self { + Self([0; 4]) + } +} +impl ::core::fmt::Debug for SchemaVersion { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + f.debug_struct("SchemaVersion") + .field("major", &self.major()) + .field("minor", &self.minor()) + .finish() + } +} + +impl ::flatbuffers::SimpleToVerifyInSlice for SchemaVersion {} +impl<'a> ::flatbuffers::Follow<'a> for SchemaVersion { + type Inner = &'a SchemaVersion; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + unsafe { <&'a SchemaVersion>::follow(buf, loc) } + } +} +impl<'a> ::flatbuffers::Follow<'a> for &'a SchemaVersion { + type Inner = &'a SchemaVersion; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + unsafe { ::flatbuffers::follow_cast_ref::(buf, loc) } + } +} +impl<'b> ::flatbuffers::Push for SchemaVersion { + type Output = SchemaVersion; + #[inline] + unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { + let src = unsafe { + ::core::slice::from_raw_parts( + self as *const SchemaVersion as *const u8, + ::size(), + ) + }; + dst.copy_from_slice(src); + } + #[inline] + fn alignment() -> ::flatbuffers::PushAlignment { + ::flatbuffers::PushAlignment::new(2) + } +} + +impl<'a> ::flatbuffers::Verifiable for SchemaVersion { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, + pos: usize, + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.in_buffer::(pos) + } +} + +impl<'a> SchemaVersion { + #[allow(clippy::too_many_arguments)] + pub fn new(major: u16, minor: u16) -> Self { + let mut s = Self([0; 4]); + s.set_major(major); + s.set_minor(minor); + s + } + + pub fn major(&self) -> u16 { + let mut mem = + ::core::mem::MaybeUninit::<::Scalar>::uninit(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid value in this slot + ::flatbuffers::EndianScalar::from_little_endian(unsafe { + ::core::ptr::copy_nonoverlapping( + self.0[0..].as_ptr(), + mem.as_mut_ptr() as *mut u8, + ::core::mem::size_of::<::Scalar>(), + ); + mem.assume_init() + }) + } + + pub fn set_major(&mut self, x: u16) { + let x_le = ::flatbuffers::EndianScalar::to_little_endian(x); + // Safety: + // Created from a valid Table for this object + // Which contains a valid value in this slot + unsafe { + ::core::ptr::copy_nonoverlapping( + &x_le as *const _ as *const u8, + self.0[0..].as_mut_ptr(), + ::core::mem::size_of::<::Scalar>(), + ); + } + } + + pub fn minor(&self) -> u16 { + let mut mem = + ::core::mem::MaybeUninit::<::Scalar>::uninit(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid value in this slot + ::flatbuffers::EndianScalar::from_little_endian(unsafe { + ::core::ptr::copy_nonoverlapping( + self.0[2..].as_ptr(), + mem.as_mut_ptr() as *mut u8, + ::core::mem::size_of::<::Scalar>(), + ); + mem.assume_init() + }) + } + + pub fn set_minor(&mut self, x: u16) { + let x_le = ::flatbuffers::EndianScalar::to_little_endian(x); + // Safety: + // Created from a valid Table for this object + // Which contains a valid value in this slot + unsafe { + ::core::ptr::copy_nonoverlapping( + &x_le as *const _ as *const u8, + self.0[2..].as_mut_ptr(), + ::core::mem::size_of::<::Scalar>(), + ); + } + } + + pub fn unpack(&self) -> SchemaVersionT { + SchemaVersionT { + major: self.major(), + minor: self.minor(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct SchemaVersionT { + pub major: u16, + pub minor: u16, +} +impl SchemaVersionT { + pub fn pack(&self) -> SchemaVersion { + SchemaVersion::new(self.major, self.minor) + } +} diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index a11ab20d9..19e9028c9 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -208,14 +208,20 @@ pub struct ProcessContainer { pub capabilities: Option>, /// Windows denial capture. When present, the runner records the sandboxed /// process's access attempts to a learning-mode ETL trace for later - /// inspection. Requires a host that exposes the learning-mode OS API. + /// inspection. Requires a host that exposes the complete official V2 + /// Learning Mode and process security-environment API set. Cannot be + /// combined with `leastPrivilege` or `network.proxy`; `filesystem.deniedPaths` + /// additionally requires the V2 deny-support capability. pub capture_denials: Option, /// BaseProcessContainer UI settings (Windows). pub ui: Option, } /// Windows denial-capture settings. The presence of the `captureDenials` -/// object enables capture; all fields are optional. +/// object enables capture; all fields are optional. Capture is incompatible +/// with `processContainer.leastPrivilege` and `network.proxy`. Explicit +/// `filesystem.deniedPaths` requires the host's V2 process security-environment +/// support query to advertise native deny enforcement. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schema-gen", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)]