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
115 changes: 98 additions & 17 deletions docs/backends/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ combined with `runtimeConfig.networkProxy`.
| Policy | Generated rule |
|---|---|
| `ui.disable: true` (default) | Denies mach-lookup of `com.apple.windowserver.active`, `com.apple.windowserver.session`, and `com.apple.coreservices.launchservicesd` |
| `ui.clipboard: "none"` (default) | Denies mach-lookup of `com.apple.pasteboard.1` |
| `ui.clipboard: "none"` (default) | Denies mach-lookup of every name starting with `com.apple.pasteboard.` |
| `ui.injection: false` (default) | Denies `iokit-open` of `IOHIDLibUserClient` |

## Seatbelt-specific options
Expand All @@ -361,21 +361,28 @@ Set under a top-level `"seatbelt"` key.
| `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.** MXC still appends the two [process cleanup](#process-cleanup) rules, and rejects an override that contains their `com.microsoft.mxc.run.` names. Last resort. |
| `extraMachLookups` | string[] | `[]` | Additional Mach services the sandbox may look up, as exact `global-name` values. The escape hatch for an app that needs one XPC service without resorting to `profileOverride`. Names starting with `com.microsoft.mxc.run.` are rejected. |
| `extraMachLookups` | string[] | `[]` | **Deprecated in `1.1.0-alpha`: use `customMachLookups.allow`.** Additional Mach services the sandbox may look up, as exact `global-name` values. Names starting with `com.microsoft.mxc.run.` are rejected. |
| `customMachLookups` | object | unset | **Requires config version `1.1.0-alpha`.** Mach services the sandbox may look up. See [Custom Mach, IOKit, and shared memory lists](#custom-mach-iokit-and-shared-memory-lists). |
| `customMachRegisters` | object | unset | **Requires config version `1.1.0-alpha`.** Mach services the sandbox may register, split into `global` and `local` names. See [Custom Mach, IOKit, and shared memory lists](#custom-mach-iokit-and-shared-memory-lists). |
| `customIoKits` | object | unset | **Requires config version `1.1.0-alpha`.** IOKit user-client classes the sandbox may open. See [Custom Mach, IOKit, and shared memory lists](#custom-mach-iokit-and-shared-memory-lists). |
| `customPosixShm` | object | unset | **Requires config version `1.1.0-alpha`.** POSIX shared memory regions the sandbox may read, write, or create. See [Custom Mach, IOKit, and shared memory lists](#custom-mach-iokit-and-shared-memory-lists). |
| `captureDenials` | object | unset | **Requires config version `1.1.0-alpha`.** Records what the sandbox blocked into a JSON report. See [Learning mode](#learning-mode-capturedenials). |

<details>
<summary><code>guiAccess</code> β€” exactly what it opens</summary>

All `mach-lookup` (an allowlist would be fragile β€” the services GUI frameworks
need vary by macOS release), plus `mach-register`, `iokit-open` for GPU/Metal,
`pseudo-tty` with `/dev/ptmx` and sandbox-authorized `/dev/ttys*` devices, and
POSIX shared memory. PTY allocation remains enabled even when `nestedPty` is
`false`.
All `mach-lookup`, plus `mach-register`, `iokit-open` for GPU/Metal,
and POSIX shared memory. A `customMachLookups`, `customMachRegisters`,
`customIoKits`, or `customPosixShm` block replaces the matching grant.

It also grants read **and** write across all of `/private/tmp` and
`/private/var/folders`, regardless of your `filesystem` policy. Deny rules are
emitted last, so a `deniedPaths` entry still overrides this grant.
`/private/var/folders`. Your `filesystem` policy is applied after this grant, so
`readonlyPaths` and `deniedPaths` entries inside those directories still apply.

Settings always win over `guiAccess`, whether written or left at their default:
`nestedPty` alone controls PTY creation, `keychainAccess` controls the
Keychain, and the `ui` settings control WindowServer, the pasteboard, and HID
input.

</details>

Expand Down Expand Up @@ -413,7 +420,7 @@ the V1.Dev JSON API for [Rust](../../api-reference/rust/v1/dev.md),

| Field | Default | Meaning |
|---|---|---|
| `mode` | `"block"` | `"block"`: blocked accesses stay blocked and are recorded. `"allow"`: accesses the policy would block by default are allowed and recorded, and MXC prints a warning. Explicit denies, such as `deniedPaths` and `ui` denies, are still enforced. |
| `mode` | `"block"` | `"block"`: blocked accesses stay blocked and are recorded. `"allow"`: accesses the policy would block by default are allowed and recorded, and MXC prints a warning. Settings left out of the config, such as `keychainAccess` or `ui.clipboard`, also don't block. `deniedPaths`, custom `deny` entries, and settings written in the config are still enforced. |
| `outputPath` | a temp file | Absolute path, or one starting with `~/`. The parent directory must exist. |
| `retainEtl` | `false` | Keeps the raw `log stream` output next to the report as `.seatbelt.log`, readable only by you. The pointer line's `etlPath` names it. The field shares its name with Windows; the file isn't an ETL. |

Expand All @@ -435,15 +442,16 @@ with these macOS differences:

- Verbose signatures use `providerGuid` `com.apple.security.sandbox`, event
ID `0`, and the Seatbelt operation (for example `file-read-data`) as
`eventName`. Any `target` that contains `/` becomes `<REDACTED>`, other
values are kept, and values over 256 characters are cut short. There is no
account-name replacement or digest.
`eventName`. Any `target` that contains `/` becomes `<REDACTED>`, except
shared memory names, which are kept so you can use them in
`customPosixShm`. Other values are kept, and values over 256 characters are
cut short. There is no account-name replacement or digest.

`deniedResourcesTruncated: true` means some denials may be missing. Delete the
report, its `.verbose.json` file, and any `.seatbelt.log` when you're done.

Requests that `guiAccess` grants, such as Mach lookups and IOKit opens, aren't
denials. They appear only in the verbose file, with `"decision": "allowGrant"`.
**Mach, IOKit, and shared memory requests**: Names granted by `guiAccess` or a custom `default: "allow"` aren't
denials; they appear only in the verbose file as `"decision": "allowGrant"`.

`mxc-exec-mac --audit config.json` is a shortcut that runs the
config with `captureDenials` in `"allow"` mode and writes the report to a temp
Expand Down Expand Up @@ -538,6 +546,78 @@ out-of-policy `cwd` makes callers that resolve relative paths (`git`, Python's
fail with `Operation not permitted`. Grant the working directory in
`readwritePaths` or `readonlyPaths`.

## Custom Mach, IOKit, and shared memory lists

These `1.1.0-alpha` settings let you grant only the Mach services, IOKit
classes, and POSIX shared memory regions an app needs.

```json
"seatbelt": {
"customMachLookups": {
"default": "deny",
"allow": ["com.apple.CARenderServer", "com.apple.fonts*"],
"deny": []
},
"customMachRegisters": {
"global": { "allow": ["com.example.MyApp.*"] },
"local": { "default": "allow" }
},
"customIoKits": {
"allow": ["AGXDeviceUserClient", "IOSurfaceRootUserClient"]
},
"customPosixShm": {
"read": ["apple.shm.notification_center"],
"own": ["/com.example.MyApp.*"]
}
}
```

| Field | Meaning |
|---|---|
| `default` | `"deny"` (default) grants only the `allow` list. `"allow"` grants every name except the `deny` list; avoid it unless absolutely necessary. |
| `allow` | Names to grant. An entry ending in `*` matches every name with that prefix. A bare `*` is rejected; use `default: "allow"`. |
| `deny` | Names to block. Deny entries win over `allow`, over `default: "allow"`, and over MXC's built-in grants. |

`customMachRegisters` has two optional blocks. `global` covers names published
through launchd for other processes to look up. `local` covers names visible
only inside the process; denial reports show them as `(per-pid)`.
`customIoKits` entries must be class names (letters, digits, and `_`).

`customPosixShm` replaces `allow` with three lists:

| List | Grants |
|---|---|
| `read` | Open read-only. |
| `readWrite` | Open read-write. Opening with `O_CREAT` needs `own`, even if the region exists. |
| `own` | Read, write, create, and delete. |

Its `default: "allow"` grants read, write, and create for every name, but not
delete. Write each name exactly as the app passes it to `shm_open`, including
any leading `/`. Names are limited to 31 bytes.

MXC rejects a config when:

- a custom block is combined with `profileOverride`;
- `customMachLookups` is combined with `extraMachLookups`;
- a `customMachLookups` entry starts with `com.microsoft.mxc.run.`, which MXC
reserves for [process cleanup](#process-cleanup);
- an `allow` entry names only something an explicit setting denies (for
example `com.apple.SecurityServer` without `keychainAccess`, or
`IOHIDLibUserClient` with `ui.injection: false`). Change that setting instead.
This check also applies to `extraMachLookups`;
- a `deny` entry would block something an explicit setting grants (for example
the pasteboard with `ui.clipboard` enabled, `IOHIDLibUserClient` with
`ui.injection: true`, or a Keychain helper such as `com.apple.trustd` with
`keychainAccess: true`).

To find the names an app needs, run it once with
[`captureDenials`](#learning-mode-capturedenials) in `"allow"` mode. In that
mode every custom list acts as `default: "allow"`, so the app isn't blocked,
while `deny` entries and settings written in the config still apply. The report lists each
name the lists don't grant, such as `mach-lookup:com.apple.CARenderServer` or
`ipc-posix-shm-read-data:apple.shm.notification_center`. Add those names to the
matching list.

## Process cleanup

A run ends when its main process (the `/bin/sh` that runs your command) exits,
Expand Down Expand Up @@ -571,8 +651,9 @@ profile too.

`--debug` prints these rules after the end of the generated profile, not inside
it, so the profile can be copied into `profileOverride` without them. MXC
rejects a `profileOverride` or `extraMachLookups` entry that uses the
`com.microsoft.mxc.run.` prefix, because the rules MXC adds would override it.
rejects a `profileOverride`, `extraMachLookups`, or `customMachLookups` entry
that uses the `com.microsoft.mxc.run.` prefix, because the rules MXC adds would
override it.

MXC freezes every matching process before killing any, so a process cannot
restart one that was just killed. It scans again until two scans in a row find
Expand Down
2 changes: 1 addition & 1 deletion docs/containment-policy/dev/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ experimental authorization and suitable host/build support.
| `processContainer` | `leastPrivilege` (boolean), `capabilities` (array of single capability names, no commas), `learningMode` (boolean), `captureDenials` (`mode`: `block` or `allow`, default `block`; optional `outputPath` and `retainEtl`), `filesystem.enumeratePaths` (array of paths), `network.allowedProxyPeer` (proxy peer identity), and `ui` (below). `enumeratePaths` needs host support and cannot combine with `leastPrivilege`. `captureDenials.mode: "allow"` permits and records otherwise denied access. |
| `processContainer.ui` | Optional `isolation` (`container`, `desktop`, `handles`, `atoms`), `desktopSystemControl` and `ime` booleans, and `systemSettings` (`none`, `parameters`, `display`, `all`). |
| `lxc` | `distribution` and `release` strings, both required if this section is present. |
| `seatbelt` | Optional `profileOverride` (raw profile), `guiAccess`, `nestedPty`, `keychainAccess` (booleans), `extraMachLookups` (string array), and `captureDenials` (`mode`: `block` or `allow`, default `block`; optional `outputPath` and `retainEtl`). `captureDenials` cannot combine with `profileOverride`. |
| `seatbelt` | Optional `profileOverride` (raw profile), `guiAccess`, `nestedPty`, `keychainAccess` (booleans), `extraMachLookups` (string array, deprecated), `customMachLookups` and `customIoKits` (objects with optional `default`: `deny` or `allow`, default `deny`; `allow` and `deny` string arrays), `customMachRegisters` (optional `global` and `local` objects of the same shape), `customPosixShm` (optional `default`; `read`, `readWrite`, `own`, and `deny` string arrays), and `captureDenials` (`mode`: `block` or `allow`, default `block`; optional `outputPath` and `retainEtl`). `captureDenials` and the `custom*` objects cannot combine with `profileOverride`. |
| `wslc` | Optional `image`, `imageTarPath`, `storagePath`, `targetOs` (strings), `cpuCount` and `memoryMb` (non-negative integers), `gpu` (boolean), and `portMappings` (array of `windowsPort` / `containerPort`, 1-65535, optional `protocol`: TCP only). These are create-and-run settings. The contract accepts any `targetOs` string, but the WSLc backend rejects values other than `"linux"`; omit it or use `"linux"`. |
| `windowsSandbox` | Optional `idleTimeoutMs` (non-negative integer milliseconds), legacy `idleTimeout` (non-negative integer), and `daemonPipeName` (string). Compatibility fields, not live controls of the current execution path. |
| `hyperlight` | Optional `runtime`: `agent` (default), `python`, `python-shell`, `node`, `bash`, or `dotnet-jit`. The command line supplies source for that guest runtime. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ temp report afterward.

| Field | Default | Behavior |
|---|---|---|
| `mode` | `"block"` | `block`: denied accesses stay denied and are recorded. `allow`: accesses the default policy would deny are allowed and recorded. Explicit denies (`deniedPaths`, `ui` denies) stay enforced. MXC prints a warning. |
| `mode` | `"block"` | `block`: denied accesses stay denied and are recorded. `allow`: accesses the default policy would deny are allowed and recorded. Settings the request omits don't block either. `deniedPaths` and written settings stay enforced. MXC prints a warning. |
| `outputPath` | Temp file | Absolute or `~/` path. The parent directory must exist. Empty, relative, root, and directory paths are rejected. A name without an extension gets `.json`. |
| `retainEtl` | `false` | Keep the run's filtered raw log as `.seatbelt.log` next to the report, and return its path in `etlPath`. The Windows name is reused; see [compatibility](#contracts-apis-and-compatibility). |

Expand Down Expand Up @@ -189,7 +189,7 @@ capture after parsing.
| Component | Change |
|---|---|
| `mxc_contract::dev` | New Seatbelt field |
| Seatbelt `profile_builder` | Adds a per-run tag to deny rules. `allow` mode uses a reporting allow baseline and keeps explicit denies. |
| Seatbelt `profile_builder` | Adds a per-run tag to deny rules. `allow` mode uses a reporting allow baseline, reports omitted settings' denies, and keeps written ones. |
| Seatbelt `denial_capture` | Runs `log stream` filtered on the tag, and uses ready and drain markers. Enforces the limits above. |
| Seatbelt `capture_analysis` | Converts log events into shared report records. Uses the Windows fast path once verbose groups fill up. |
| `learning_mode_core` | Shared report writer, moved from `process_container` |
Expand Down
31 changes: 29 additions & 2 deletions docs/development/architecture/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,9 @@ The v1.0 contract preserves the v0.9 request roots and canonical field/value
spellings, but removes the legacy `appcontainer`, `appContainer`, and
`macos_sandbox` aliases. They remain rejected throughout the v1 contract line.
Features that exist only in mutable v1.1 development, including Windows Sandbox
provision, Seatbelt `captureDenials`, and the `vm`, `microvm`, and `hyperlight`
one-shot surfaces, are not accepted by v1.0.
provision, Seatbelt `captureDenials`, `customMachLookups`,
`customMachRegisters`, `customIoKits`, and `customPosixShm`, and the `vm`,
`microvm`, and `hyperlight` one-shot surfaces, are not accepted by v1.0.

### Trust boundary vs schema defaults

Expand Down Expand Up @@ -356,6 +357,32 @@ field or set it to `"linux"`.
This fail-closed exception applies within V1 and does not require a new
versioned API surface.

### Servicing exception: Seatbelt settings take precedence over `guiAccess`

In every contract version, Seatbelt settings now take precedence over
`seatbelt.guiAccess`, whether they are written in the config or omitted and
left at a denying default. Previously `guiAccess` silently overrode them; this
change is intentional.

- `nestedPty: false` blocks PTY creation. `nestedPty` defaults to `true`.
- `keychainAccess: false` or an omitted `keychainAccess` blocks the Keychain
servers.
- `ui.clipboard: "none"` or an omitted `ui.clipboard` blocks every
`com.apple.pasteboard.` server.
- `readonlyPaths` and `deniedPaths` entries inside `/private/tmp` and
`/private/var/folders` apply.

In `captureDenials` `"allow"` mode, omitted settings allow and report access
instead, so the report shows what they would block.

The Seatbelt backend also rejects an `extraMachLookups` entry that names only
a service a setting denies, written or by default, such as
`com.apple.SecurityServer` without `keychainAccess: true`. Change that setting
instead.

This exception applies within V1 and does not require a new versioned API
surface.

### Shipped vs Experimental

Development features use their intended permanent top-level locations in the
Expand Down
Loading
Loading