Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4e2a788
Refactor PLM to use canonical analyzer
richiemsft Aug 6, 2026
a1bf155
Preserve PLM integration semantics
richiemsft Aug 7, 2026
e8a9f15
Normalize PLM source line endings
richiemsft Aug 7, 2026
cd62e41
Harden PLM denial compatibility
richiemsft Aug 7, 2026
8af915f
Close PLM collision edge cases
richiemsft Aug 7, 2026
db90996
Harden PLM path and capability handling
richiemsft Aug 10, 2026
cf1a4ad
Preserve volume identity in PLM filtering
richiemsft Aug 10, 2026
d366e09
Fail closed on dangling filesystem links
richiemsft Aug 10, 2026
1809faf
Scope dangling-link handling to Windows
richiemsft Aug 10, 2026
0474961
Handle negative audit exit codes
richiemsft Aug 10, 2026
041948f
Restrict PLM elevation boundary
richiemsft Aug 11, 2026
1eaa71d
Document PLM privilege boundary
richiemsft Aug 11, 2026
fab6573
Harden PLM lifecycle handoff
richiemsft Aug 11, 2026
adca097
Remove forgeable PLM singleton bypass
richiemsft Aug 11, 2026
c8012e0
Disarm PLM at WPR stop milestone
richiemsft Aug 11, 2026
6835e59
Consolidate PLM lifecycle ownership
richiemsft Aug 11, 2026
2c63544
Report guarded PLM cleanup failures
richiemsft Aug 11, 2026
1ddb90a
Harden guarded PLM pipe lifecycle
richiemsft Aug 11, 2026
aa2f343
Harden PLM launch and audit timeout
richiemsft Aug 11, 2026
d6ec62d
Harden PLM failure recovery
richiemsft Aug 11, 2026
90195e4
Fix PLM supervision regressions
richiemsft Aug 11, 2026
3f145ae
Add guarded WPR capture foundation
richiemsft Aug 11, 2026
acfc03d
Add guarded WPR capture fallback
richiemsft Aug 11, 2026
c8324d4
Fix guarded capture recovery paths
richiemsft Aug 11, 2026
e9bae80
Format guarded capture integration
richiemsft Aug 11, 2026
2914e04
Fix fallback capability validation
richiemsft Aug 12, 2026
cd538bc
Package PLM with Windows executors
richiemsft Aug 12, 2026
ae48f83
Route incompatible capture policies to fallback
richiemsft Aug 12, 2026
47fca66
Harden guarded capture fallback lifecycle
richiemsft Aug 12, 2026
a577ae5
Stabilize inherited output handle regression
richiemsft Aug 12, 2026
873befa
Remove timing-sensitive output assertion
richiemsft Aug 12, 2026
b6990d8
Attest guarded capture process scope
richiemsft Aug 12, 2026
3c02a47
Decode kernel process IDs directly
richiemsft Aug 12, 2026
0839737
Keep inherited handle test behavioral
richiemsft Aug 12, 2026
9c59623
Filter denials by attested process lifetime
richiemsft Aug 12, 2026
f2c4c62
Use retained handles for capture scope
richiemsft Aug 12, 2026
711b77a
Harden guarded capture teardown
richiemsft Aug 12, 2026
e2e7e7f
Fix guarded capture review findings
richiemsft Aug 12, 2026
011478b
Split PLM packaging from guarded fallback
richiemsft Aug 12, 2026
4142e9c
Merge main into guarded WPR fallback
richiemsft Aug 12, 2026
7cabfd1
Harden guarded capture review findings
richiemsft Aug 12, 2026
16505b2
Close adversarial guarded capture findings
richiemsft Aug 12, 2026
3723449
Merge main into guarded capture fallback
richiemsft Aug 13, 2026
9cefcb9
Restrict external denied-path assertion
richiemsft Aug 13, 2026
959361d
Fail closed on guarded tracker errors
richiemsft Aug 13, 2026
15bcc69
Merge main into guarded capture fallback
richiemsft Aug 13, 2026
1d0831d
Merge main into guarded capture fallback
Copilot Aug 13, 2026
c7620e3
Keep enforcement until guarded capture release
richiemsft Aug 13, 2026
5f46d42
Harden guarded release confirmation retries
richiemsft Aug 13, 2026
2463a29
Bound guarded release confirmation
richiemsft Aug 13, 2026
a1f067b
Address guarded capture review findings
richiemsft Aug 14, 2026
8d1dafd
Harden elevated PLM helper trust
richiemsft Aug 15, 2026
c126863
Validate DACL pointer bounds
richiemsft Aug 15, 2026
4a27b4e
Parse trusted ACLs from owned buffers
richiemsft Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the `
| 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` — prefers `CreateProcessSecurityEnvironment` with PSEC whenever its runtime probe succeeds and the requested policy is compatible, independent of schema version. It temporarily falls back to `Experimental_CreateProcessInSandbox` with the SBOX FlatBuffer contract when PSEC is unavailable or policy-incompatible, 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. |
| BaseContainer (OS sandbox API) | `wxc-exec.exe` | Windows | `backends/appcontainer/common/src/base_container_runner.rs` — prefers `CreateProcessSecurityEnvironment` with PSEC whenever its runtime probe succeeds and the requested policy is compatible, independent of schema version. It temporarily falls back to `Experimental_CreateProcessInSandbox` with the SBOX FlatBuffer contract when PSEC is unavailable or policy-incompatible, 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` prefers the complete compatible PSEC + V2 Learning Mode path; when that path cannot fully honor a request, MXC retains the highest compatible legacy tier and pairs it with guarded WPR using exact handle-attested process scope. |
| 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` `<MappedFolder>` 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` 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 |
Expand Down Expand Up @@ -206,7 +206,7 @@ The workspace is organized into six top-level directories under `src/`:
- 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`.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only the hidden fixed WPR start operation; the retained elevated guardian accepts authenticated attach and stop/discard controls over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and returns bounded analysis or trace bytes to the unelevated parent. Successful authenticated stop/discard disarms the child before releasing the PLM singleton. Owner death, pipe break, or another uncertain control failure preserves the recovery marker and deliberately leaves WPR untouched for administrator recovery.
- `wxc`, `lxc`, and `mxc_darwin` are thin binary crates (`wxc-exec` / `lxc-exec` / `mxc-exec-mac`) that wire up CLI args (`clap`), load/validate config, handle maintenance modes (`--probe`, `--delete`, `--setup-*`, `--audit`), and **delegate all backend dispatch to `mxc_engine`**. They contain no `match request.containment` of their own. `wxc-exec` additionally owns the Windows Ctrl-C / DACL-cleanup / `--audit` PLM-trace / telemetry orchestration around the engine call.
- `mxc_engine` is the **single execution engine** — the one home for "given an `ExecutionRequest`, run it". It owns: run-to-completion backend selection (`run` / `resolve_runner`, covering **all** backends, incl. the Windows ProcessContainer BaseContainer/AppContainer BFS/DACL fallback tiers via `appcontainer_common::dispatcher::dispatch_with_fallback`, and every experimental backend, feature-gated); streaming (`spawn` → `Box<dyn SandboxProcess>`); state-aware lifecycle dispatch (`run_state_aware`, including Windows Sandbox and IsolationSession); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request` / `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`.
Expand Down
37 changes: 20 additions & 17 deletions docs/learning-mode/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,34 +127,35 @@ Windows-only `captureDenials` config switch drives collecting those events and
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
> **Host selection.** MXC prefers native capture on a feature-enabled Windows
> 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`.
> `CloseProcessSecurityEnvironment`. When that set is unavailable or cannot
> fully honor the requested policy, MXC retains the highest compatible legacy
> containment tier (SBOX, AppContainer+BFS, or AppContainer+DACL) and pairs it
> with the guarded WPR capture provider. Unsupported hosts return
> `backend_unavailable` only when neither path can preserve the full policy.
>
> 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.
> Native PSEC capture cannot represent `processContainer.leastPrivilege`
> because the process security-environment API does not expose an LPAC token
> option. MXC therefore retains a compatible legacy containment tier and uses
> guarded WPR instead of weakening or rejecting 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.
> Native PSEC capture also cannot currently represent `network.proxy` without a
> separate proxy AppContainer peer identity. Compatible requests use guarded
> WPR with the legacy tier that can enforce the proxy contract.
>
> `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.
> Native capture uses `filesystem.deniedPaths` only when
> `QueryProcessSecurityEnvironmentSupport` advertises `PSE_SUPPORT_FS_DENY`.
> Otherwise MXC selects a compatible legacy SBOX, AppContainer+BFS, or
> AppContainer+DACL tier and uses guarded WPR.

- `mode: "block"` (default) maps onto `learningModeLogging`
(deny-and-record) — the app / user-configurable flow.
Expand Down Expand Up @@ -234,7 +235,9 @@ C# SDK exposes it through `RunResult.OutputMetadata` and
By default, the intermediate ETW `.etl` trace is an internal, runner-managed
file in a protected per-run temporary directory that MXC deletes after
analysis. Set `captureDenials.retainEtl` to `true` to preserve the sealed trace
for diagnostics after a terminal wait. Retention-enabled captures begin under
for diagnostics after a terminal wait when native PSEC/V2 capture is selected.
Guarded-WPR fallback rejects `retainEtl: true` with `backend_unavailable`
rather than returning its raw host-wide trace. Retention-enabled captures begin under
`%LOCALAPPDATA%\Microsoft\MXC\capture-denials\working` and move to a protected
per-run directory under `capture-denials\retained` only after sealing succeeds.
Abandoning or disposing a process without a terminal wait deletes the internal
Expand Down
42 changes: 31 additions & 11 deletions docs/process-container/os-version-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,42 @@ The PSEC probe requires:
- `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:
When `processContainer.captureDenials` is present, MXC treats PSEC plus the
official V2 Learning Mode exports as one native capture capability 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 legacy SBOX execution remains
functional when PSEC is unavailable, 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.
When that complete set is available, MXC uses PSEC with native V2 capture.
Otherwise it retains the highest legacy containment tier that can fully honor
the request (SBOX, AppContainer+BFS, or AppContainer+DACL) and pairs it with
the guarded WPR capture provider. The elevated guardian filters the host-wide
trace to OS-observed process lifetime windows: before the suspended sandbox
child resumes, the authenticated owner sends its job and still-owned root
process HANDLE values. The guardian duplicates both from that authenticated
process, verifies the duplicated process belongs to the duplicated job, and
retains the stable process handle. The root generation uses exact kernel
creation/exit FILETIMEs read from that handle (with the exit time read only
after WPR stops). For every descendant new-process notification, the guardian
opens and retains a process handle, verifies membership in the duplicated job,
and reads exact creation/exit FILETIMEs. Denial filtering uses those
handle-attested lifetimes directly; it does not infer process generations from
host-wide ETL lifecycle timestamps. At finish, job accounting
`TotalProcesses` must equal the retained unique root-plus-descendant
generations, so missing or inconsistent membership notifications fail closed.
Guarded capture tracks at most 4096 root-plus-descendant process generations
per execution. Exceeding that bound fails capture teardown and emits no denial
output rather than continuing with an incomplete process scope.
The owner never supplies PID/time scopes. Only bounded canonical denial data
returns; raw ETL does not cross into the SDK result. If no containment tier can
honor the policy, or the guarded PLM helper is unavailable, the request fails
as `backend_unavailable`.

Internal validation confirmed the earlier contract on build `26657.1002` uses
legacy containment rather than native capture, while 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
requests using that option use the transitional SBOX contract instead of
Expand Down
7 changes: 5 additions & 2 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ production configs and the dev schema when working on experimental features:
// path in output metadata. Defaults to false.
// Retention requires a terminal wait; abandoning the
// process handle deletes the internal trace.
// Requires native PSEC/V2 capture; guarded-WPR fallback
// rejects retention rather than exposing a host-wide ETL.
}
// Omit outputPath for a managed JSON output file.
// captureDenials cannot be combined with leastPrivilege.
// captureDenials cannot currently be combined with network.proxy.
// Native PSEC/V2 capture cannot combine with leastPrivilege
// or network.proxy. Hosts without that complete native set
// retain an eligible legacy containment tier and use guarded WPR.
},

"lxc": { // LXC-specific
Expand Down
Loading
Loading