Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Microsoft eXecution Container (MXC)

MXC is a **sandboxed code execution system** for running untrusted code
MXC is a **containment-based code execution system** for running untrusted code
(model output, plugins, and tools) on Windows, Linux, and macOS. It provides
multiple containment backends, from OS-native process sandboxes to full VMs,
behind a unified containment model and typed SDKs.
Expand All @@ -13,7 +13,7 @@ behind a unified containment model and typed SDKs.
security policies
- **Multiple containment backends**: ProcessContainer, Windows Sandbox, LXC,
Bubblewrap, Seatbelt, MicroVM (Nanvix), Hyperlight, IsolationSession, and WSLC
- **Policy-driven sandboxing**:
- **Policy-driven containment**:
- **Filesystem policy**: Read-only, read-write, and denied path lists
- **Network policy**: Proxy support, outbound controls, and backend-dependent
host filtering
Expand Down Expand Up @@ -189,6 +189,7 @@ build.bat --all # Release build for current architecture
|---|---|---|
| SDK samples | [`samples/`](samples/README.md) | Runnable Rust, .NET, and Node scenarios |
| SDK API reference | [`docs/api-reference/`](docs/api-reference/README.md) | Supported V1 operations and types |
| Containment policy | [`docs/containment-configuration/`](docs/containment-configuration/README.md) | Supported policy contracts by schema version |
| Container lifecycle | [`docs/container-lifecycle.md`](docs/container-lifecycle.md) | Persistent container lifecycle overview |
| Logging access denied | [`docs/logging-access-denied.md`](docs/logging-access-denied.md) | Diagnose blocked accesses and author policy |
| Telemetry | [`docs/telemetry.md`](docs/telemetry.md) | Consent and administrative controls |
Expand Down
92 changes: 46 additions & 46 deletions docs/backends/bwrap/bubblewrap-backend.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/backends/lxc/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ kill. `StdioMode::Inherit` remains unsupported.
**`kill()` stops the container,** not just the workload: the workload runs under
container init, where nothing aimed at the host `lxc-attach` process reaches it.

**One live sandbox per container name, per process.** A second sandbox naming a
**One live container per container name, per process.** A second container naming a
`containerId` this process already holds is refused rather than queued, because
LXC reads a run's network section only when the container starts. Omit
`containerId` for a generated name. The claim is released when the handle drops.
Expand Down
4 changes: 2 additions & 2 deletions docs/backends/process-container/UIPolicy_Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The `"ui"` section of the MXC container configuration controls how a contained p
- **Job Object UI Restrictions**
- **Process Mitigation: Win32k System Call Disable** (`PROCESS_MITIGATION_SYSTEM_CALL_DISABLE_POLICY`)

Developers declare *what the process is allowed to do* — the OS-side sandbox layer translates that into the correct kernel flags and mitigations.
Developers declare *what the process is allowed to do* — the OS-side containment layer translates that into the correct kernel flags and mitigations.

### Design Principles

Expand Down Expand Up @@ -223,7 +223,7 @@ All fields default to the most restrictive value. **`"ui": {}` = total lockdown.

## Examples

### Example 1: Sandboxed App — GUI enabled, everything else locked down
### Example 1: Contained App — GUI enabled, everything else locked down

The process can create and manage its own windows but is fully isolated from other applications and the system.

Expand Down
4 changes: 2 additions & 2 deletions docs/backends/process-container/host-prep.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

`wxc-host-prep.exe` is a Windows-only, privileged-by-manifest binary
that owns the one-time host setup steps MXC requires before
AppContainer- and other sandboxed workloads can run reliably. It is
AppContainer- and other contained workloads can run reliably. It is
shipped alongside `wxc-exec.exe` inside the SDK bin payload.

The binary has `requireAdministrator` baked into its embedded
application manifest in release builds. The Windows loader prompts
for UAC at process start (or, when launched under SYSTEM — e.g. from
a scheduled task — satisfies the requirement trivially). The
sandbox launcher `wxc-exec.exe` never elevates itself; all
container launcher `wxc-exec.exe` never elevates itself; all
privilege-requiring setup work lives in `wxc-host-prep.exe` instead.

> **Migrated from `wxc-exec --prepare-system-drive`.** Earlier
Expand Down
2 changes: 1 addition & 1 deletion docs/backends/process-container/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ for unpackaged profile creation.

- **Capabilities:** none; no host or peer loopback exemptions.
- **Enforcement:** no proxy; external outbound and inbound are dropped.
Intra-sandbox loopback is not part of the shared external-network policy.
Intra-container loopback is not part of the shared external-network policy.

When no runtime proxy or backend proxy peer is configured, deny-all is the default and model 3 is also the result of
providing no network policy at all: the explicit form, an omitted network block, and an empty `"network": {}` are
Expand Down
46 changes: 23 additions & 23 deletions docs/backends/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ ingress on just to run a build. These rules are path-scoped, so they never
widen IP networking.

> ⚠️ **`connect()` is a capability `file-write*` alone didn't grant.** A broad
> `readwritePaths` root lets the sandbox talk to any pre-existing listener
> `readwritePaths` root lets the contained process talk to any pre-existing listener
> underneath it — and a Docker, `ssh-agent`, or `gpg-agent` socket is a control
> plane. Keep the read-write root narrow, and put sensitive sockets in
> `deniedPaths`.

### Always-on baseline

Every sandbox gets these regardless of policy, so the dynamic linker, shells,
Every workload gets these regardless of policy, so the dynamic linker, shells,
and standard tools work:

| Access | Paths |
Expand All @@ -163,8 +163,9 @@ and standard tools work:
| Read **+ write** | `/dev/null`, `/dev/zero`, `/dev/random`, `/dev/urandom` |
| Read-data only | `/` itself — the loader can't resolve path lookups without it |

Every sandbox also gets an unfiltered `(allow file-read-metadata)`, because the
kernel reads metadata on each ancestor directory while resolving a path.
Each generated Seatbelt profile also includes an unfiltered
`(allow file-read-metadata)`, because the kernel reads metadata on each
ancestor directory while resolving a path.
`deniedPaths` names that operation explicitly so it still outranks the grant.

The `/dev/*` entries are writable because shell redirections (`>/dev/null`,
Expand Down Expand Up @@ -192,9 +193,8 @@ Anything it hasn't declared is rejected up front.

### Fields (supported schema 0.9+)

This is the cross-backend directional shape accepted by the registered exact
contracts. Schema 0.8 is no longer accepted; see the
[supported network fields](../../schema.md#directional-networking-supported-contracts).
This is the cross-backend directional shape described by the
[supported containment policy](../../containment-configuration/1.0.0/policy.md).

> **Omitting `network` entirely denies all IP networking.** Every field below
> defaults to `deny`, so a config with no `network` block behaves exactly like
Expand All @@ -206,14 +206,14 @@ contracts. Schema 0.8 is no longer accepted; see the
| `egress.default` | `"deny"` → no *general* outbound rule; baseline `(deny default)` blocks IP sockets, except for the host-loopback path (`ingress.hostLoopback`) and a `runtimeConfig.networkProxy` endpoint, which are carved out of it. `"allow"` → `(allow network-outbound)`, `(allow network-bind (local ip))`, `(allow system-socket)`. Only the first of those three is egress. |
| `egress.allow` / `egress.deny` | **Rejected** if non-empty — no CIDR/port/protocol primitive exists |
| `ingress.default` | `"allow"` → `(allow network-inbound (local ip))`. This single rule is what permits **both `bind()` and `listen()`**; `network-bind` alone grants `bind()` but not `listen()`. |
| `ingress.hostLoopback` | Controls sandbox → host loopback. May be `"deny"` under `ingress.default: "allow"`; `"allow"` under `ingress.default: "deny"` is rejected. **Defaults to `"deny"`.** |
| `ingress.hostLoopback` | Controls workload → host loopback. May be `"deny"` under `ingress.default: "allow"`; `"allow"` under `ingress.default: "deny"` is rejected. **Defaults to `"deny"`.** |
| `runtimeConfig.networkProxy` | Loopback `http`/`https` URL with an explicit port |

### The `hostLoopback` trap

> ⚠️ **`ingress.hostLoopback` defaults to `"deny"`.**

This config looks like "let the sandbox use the network":
This config looks like "let the workload use the network":

```json
{ "network": { "egress": { "default": "allow" } } }
Expand All @@ -227,7 +227,7 @@ generated profile is:
(deny network-outbound (remote ip "localhost:*")) ;; last match wins
```

**Your sandbox can reach the whole internet but not your own machine** — no
**Your workload can reach the whole internet but not your own machine** — no
`localhost:3000` dev server, no local model endpoint. It passes validation
silently, because `ingress.default` defaulted to `deny` too and the two agree.

Expand Down Expand Up @@ -255,7 +255,7 @@ Two more things to know about `hostLoopback`:

`hostLoopback` is bidirectional, but Seatbelt can only enforce the outbound
half. There's no way to scope an inbound grant by peer: `(local ip)` filters on
the sandbox's *own* bind address, and a `remote ip` inbound filter is a no-op
the workload's *own* bind address, and a `remote ip` inbound filter is a no-op
because the peer isn't known at bind time.

**`hostLoopback: "allow"` under `ingress.default: "deny"` is rejected**, because
Expand All @@ -266,7 +266,7 @@ the only rule that could carry the promised inbound grant is the blanket
its container→host half *is* expressible — by the `(deny default)` baseline
under a denied egress default, and by the explicit `localhost:*` deny under an
allowed one. Its host→container half is not, so a host process can still reach
the sandbox's listeners. That residual grant is strictly narrower than the
the workload's listeners. That residual grant is strictly narrower than the
alternative it replaces: reaching a listener through `hostLoopback: "allow"`
gives up the container→host direction as well.

Expand All @@ -275,7 +275,7 @@ gives up the container→host direction as well.
> profile syntax error, `host must be * or localhost`, exactly as for `remote`.
> So a workload that binds `0.0.0.0` rather than `127.0.0.1` is reachable from
> **the LAN**, not only from this host. `--bind 127.0.0.1` is a convention the
> workload follows, not one the sandbox can enforce. Prefer a backend with a
> workload follows, not one the containment mechanism can enforce. Prefer a backend with a
> private network namespace when that is not acceptable.

If you only need *outbound* loopback, `egress.default: "deny"` plus a loopback
Expand All @@ -287,7 +287,7 @@ This distinction matters, and it's easy to get backwards.

| Question | Enforced? |
|---|---|
| Can the sandbox reach anything *other than* the proxy? | **No — kernel-enforced**, provided `ingress.hostLoopback` stays `"deny"` (see the caveat below). |
| Can the workload reach anything *other than* the proxy? | **No — kernel-enforced**, provided `ingress.hostLoopback` stays `"deny"` (see the caveat below). |
| Will a client actually *speak to* the proxy? | Not enforced — cooperative. |
| Is traffic transparently redirected into the proxy? | No. |
| Can the proxy contain *inbound* traffic? | No — a proxy confines egress only. Inbound is governed solely by `ingress.default`, and Seatbelt cannot scope that grant by peer or by address. |
Expand All @@ -301,20 +301,20 @@ recommended `hostLoopback: "deny"` the profile ends up as:
(allow network-outbound (remote ip "localhost:<proxy-port>"))
```

That single port is the sandbox's entire outbound universe. The kernel enforces
That single port is the workload's entire outbound universe. The kernel enforces
it. A client that opens raw sockets and ignores `HTTP_PROXY` **cannot** reach
the internet or any other host-local service — it simply fails to connect.

> ⚠️ **`ingress.hostLoopback: "allow"` widens outbound** to *every port on this
> host*, not just the proxy port, and the confinement claim above no longer
> holds. Keep `hostLoopback: "deny"` whenever the proxy is meant to be the only
> way out. This won't prevent the proxy's TCP responses from reaching the
> sandbox.
> workload.
>
> `ingress.default` is a separate decision: it grants inbound only and never
> widens outbound, so `{"default": "allow", "hostLoopback": "deny"}` keeps
> proxy-only egress while permitting a listener. The tradeoff is inbound — that
> grant cannot be scoped by peer or address, so the sandbox's listeners are
> grant cannot be scoped by peer or address, so the workload's listeners are
> reachable from this host and, if the workload binds `0.0.0.0`, from the LAN.

**Proxy usage is cooperative.** MXC injects `HTTP_PROXY` / `HTTPS_PROXY` /
Expand All @@ -326,7 +326,7 @@ way Windows can.

**What the profile does not control is where the proxy then connects.** A
caller-managed proxy applies its own destination policy; MXC does not supply
one. Configure hostname allow/block lists on that proxy, not in the sandbox
one. Configure hostname allow/block lists on that proxy, not in the containment
request. `egress.allow` / `egress.deny` describe direct traffic and cannot be
combined with `runtimeConfig.networkProxy`.

Expand All @@ -349,7 +349,7 @@ Set under a top-level `"seatbelt"` key.

| Option | Type | Default | What it does |
|---|---|---|---|
| `nestedPty` | bool | `true` | Lets the inner process allocate its own ptys. Needed by anything that spawns a shell — test runners, `git`, `gh`, REPLs, agent tools. Set `false` for a tighter sandbox. |
| `nestedPty` | bool | `true` | Lets the inner process allocate its own ptys. Needed by anything that spawns a shell — test runners, `git`, `gh`, REPLs, agent tools. Set `false` for tighter containment. |
| `guiAccess` | bool | `false` | Adds Mach/IOKit rules so GUI apps can create windows, and widens the filesystem — see below. **Requires UI to be enabled**, which is spelled `ui.disable: false` (there is no `ui.enable`). |
| `keychainAccess` | bool | `false` | Opens the sandbox enough for `keytar` / Security.framework to reach the Keychain. Opt in only if genuinely needed. |
| `profileOverride` | string | unset | Replaces the generated profile with raw TinyScheme. **All `filesystem`/`network`/`ui` policy is ignored for profile generation.** Last resort. |
Expand Down Expand Up @@ -414,7 +414,7 @@ The child gets a default block of `PATH` (`/usr/bin:/bin:/usr/sbin:/sbin`),
`PWD` sits outside the table: it is always exported, set to the resolved
working directory. It is applied *after* everything above. It exists so the
child's `getcwd()` takes its fast `$PWD` path
instead of walking parent directories the sandbox may not let it read, which
instead of walking parent directories the containment policy may not let it read, which
would otherwise leak a "getcwd: … Operation not permitted" line onto stderr.

The table is the environment MXC hands the child. macOS `/bin/sh` assigns its
Expand Down Expand Up @@ -443,9 +443,9 @@ Tools installed outside the default `PATH` need both an env entry **and** a
`PATH` defaults to `/usr/bin:/bin:/usr/sbin:/sbin` and each `process.env` entry
adds to or overrides that baseline. `inheritDefaultEnv` is rejected.

> ⚠️ **`$HOME` and `TERM` are unset inside the sandbox unless you set them.**
> ⚠️ **`$HOME` and `TERM` are unset in the workload unless you set them.**
> Policy paths still accept `~` (expanded against the *host's* `$HOME` when the
> config is parsed), but a script running inside the sandbox cannot use `~` —
> config is parsed), but a script running under containment cannot use `~` —
> the shell expands it against an unset `HOME`. `getpwuid()` doesn't help
> either, since directory services aren't reachable. Pass `"HOME=…"` in
> `process.env` if your command needs it.
Expand Down Expand Up @@ -549,7 +549,7 @@ Follow the `PATH` instructions it prints (`/opt/homebrew/bin` on Apple silicon).
| Node.js | `brew install node` | building/testing the TypeScript SDK |

> On Apple silicon Homebrew lives at `/opt/homebrew`, so example configs that
> run Python include `"readonlyPaths": ["/opt/homebrew"]` to let the sandbox
> run Python include `"readonlyPaths": ["/opt/homebrew"]` to let the workload
> reach the interpreter and its libraries.

**4. Verify**
Expand Down
4 changes: 2 additions & 2 deletions docs/backends/wslc/wsl-container-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ cost once per image, not once per run.

> **Bring-up reaches the network.** A cache miss makes the host fetch
> from the image's registry before the container starts. That fetch is
> outside the sandbox's own network policy, so a config declaring
> outside the container's own network policy, so a config declaring
> `network.egress.default: "deny"` is **refused** rather than pulled —
> warm the cache first, or set `wslc.imageTarPath`. A config that
> allows egress pulls on a miss.
Expand Down Expand Up @@ -592,7 +592,7 @@ container to persist — its daemon holds the session open across phase processe
policy-persistence primitive, so there is nothing for the flag to select.

Note the state-aware surface differs: it rejects the whole `lifecycle` section
at parse time, because a multi-invocation sandbox's lifetime is driven by the
at parse time, because a multi-invocation container's lifetime is driven by the
explicit `provision` / `deprovision` phases rather than by per-run flags.

## Supported workloads
Expand Down
Loading
Loading