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
4 changes: 3 additions & 1 deletion docs/bwrap-support/bubblewrap-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ Common consequences of this default:
- `/opt` and `/usr/local` tooling is not on PATH; list either path under
`readonlyPaths` if the script depends on it.
- `working_directory` must live under the baseline or a policy path — a
`cwd` of `~/project` without a matching `readonlyPaths` entry will fail.
`cwd` of `/home/alice/project` without a matching `readonlyPaths` entry
will fail. From `0.9.0-alpha` it must also be absolute: `--chdir` does not
expand `~`.
- DNS works on systemd-resolved, NetworkManager, and resolvconf hosts
because the corresponding `/run/...` directories are bound. The common
symlink targets *outside* `/run` are covered too: `/var/run/...`-routed
Expand Down
5 changes: 3 additions & 2 deletions docs/isolation-session/oneshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ versions and stating that the bindings must be regenerated.
gated by `--experimental`.
- `process.commandLine` (the script command, wrapped via `cmd.exe /c "..."`
— the same pattern the LXC runner uses with `/bin/sh -c`).
- `process.cwd` (working directory inside the session).
- `process.cwd` (working directory inside the session; must be an absolute
Windows path).
- `process.env` (environment variables forwarded via the OS-side
`IsoSessionProcessOptions`).
- `process.timeout` (forwarded to the OS-side per-process timeout
Expand Down Expand Up @@ -244,7 +245,7 @@ the rationale for each disposition, and the error mapping live in
| Field | one-shot disposition |
|---|---|
| `process.commandLine` | **honored** (required) |
| `process.cwd` / `process.env` / `process.timeout` | **honored** |
| `process.cwd` / `process.env` / `process.timeout` | **honored** — `cwd` must be an absolute Windows path |
| `filesystem.{readwritePaths,readonlyPaths,deniedPaths}` | rejected — no host-folder-sharing primitive |
| `network` — directional all-allow (`egress.default`, `ingress.default`, and `ingress.hostLoopback` all `allow`, no rules) | **required** |
| `network` — legacy fields, absent, empty, restrictive, mixed, rule-bearing, or proxy-bearing | rejected |
Expand Down
3 changes: 2 additions & 1 deletion docs/isolation-session/state-aware-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ also structurally refused as `malformed_request`.
- `process.commandLine` — required for one-shot and for state-aware exec;
rejected structurally at non-exec state-aware phases.
- `process.cwd`, `process.env`, `process.timeout` — optional in both modes,
honoured per-process (each exec receives its own block).
honoured per-process (each exec receives its own block). A supplied `cwd`
must be an absolute Windows path.

### Policy fields and mode parity

Expand Down
8 changes: 8 additions & 0 deletions docs/lxc-support/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ Note the required field lxc.

Any combination that lxc-create supports.

### Working directory

`process.cwd` is applied inside the container, so it names a path in the
container's filesystem rather than the host's. From schema `0.9.0-alpha` on, a
set value must be an absolute Linux path; earlier versions resolve a relative
value against the `lxc-exec` process's directory. An empty value keeps the
container default.

### Preventing environment variables from leaking into LXC

If `process.env` has a value, `lxc-attach` is run with `--clear-env` so host
Expand Down
27 changes: 21 additions & 6 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,8 @@ that can be executed independently.

"process": {
"commandLine": "python app.py", // Required: command to execute
"cwd": "C:\\workspace", // Working directory (optional; when omitted each
// backend substitutes a granted directory rather
// than inheriting the launcher's — see
// "Working Directory" below)
"cwd": "C:\\workspace", // Working directory (optional; must be absolute on
// 0.9.0-alpha+ — see "Working Directory" below)
"env": ["MY_VAR=value"], // Omitted: backend default; supplied: used verbatim
"inheritDefaultEnv": true, // Layer env on the backend default (0.9.0-alpha+)
"timeout": 30000 // Timeout in ms (0 = no timeout)
Expand Down Expand Up @@ -269,7 +267,24 @@ that can be executed independently.

`process.cwd` is optional. When it is set, it is passed to the backend
verbatim — an unusable value fails the launch rather than being silently
replaced. When it is **omitted**, backends do not simply inherit the launcher's
replaced. The one exception is the WSL Container backend's one-shot surface,
which reads the value as a Windows host path and translates it to the matching
in-container path (`C:\workspace` → `/mnt/c/workspace`); a value with no such
equivalent is rejected.

**From schema `0.9.0-alpha` on, a set value must be absolute**, since a relative
path would resolve against the launching process's working directory. Absolute
means absolute for the target the path reaches, not for the host MXC runs on:

| Backend | Absolute form |
|---------|---------------|
| Windows ProcessContainer / Windows Sandbox / IsolationSession | `C:\workspace`, `C:/workspace`, or a UNC path. `C:workspace` and `\workspace` are relative. |
| WSL Container | One-shot: the Windows host path (`C:\workspace`), which the backend translates. State-aware `exec`: the in-container path (`/workspace`). |
Comment thread
MGudgin marked this conversation as resolved.
| Seatbelt (macOS) | `/workspace`. A `~` path is rejected: MXC would expand it from the launching host's `HOME`, and falls back to a literal `~` when that is unset. |
| LXC / Bubblewrap | `/workspace`. A `~` path is rejected; it is not expanded. |
| MicroVM (NanVix) / Hyperlight | n/a — these backends reject any working directory. |

When `process.cwd` is **omitted**, backends do not simply inherit the launcher's
working directory: under a deny-by-default sandbox that directory is usually
unreadable, and the result ranges from a confusing silent relocation (Windows
restarts the child at the drive root) to `getcwd()` errors on the child's
Expand All @@ -280,7 +295,7 @@ use:
|---------|----------------------------------------|
| Windows ProcessContainer (AppContainer / BaseContainer) | First `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root (`%SystemDrive%\`). Never `NULL`. |
| Seatbelt (macOS) | Same precedence, with `~` expanded as the profile expands it; falls back to `/`. |
| LXC / WSL Container | The container root — see [`docs/lxc-support/lxc-backend.md`](lxc-support/lxc-backend.md). |
| LXC / WSL Container | The container root. |
| MicroVM (NanVix) / Hyperlight | Not applicable — these backends reject a working directory outright. |

Policy entries that are blank, name a file, or do not exist yet are skipped:
Expand Down
8 changes: 6 additions & 2 deletions docs/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,12 @@ paths are. `PWD` is exported to the resolved directory.

Both launch methods apply it: `exec` sets it on the child process, while `open`
performs the `cd` and the `PWD` export inside the generated helper script,
since Terminal would otherwise start the workload in its own directory. A
relative `cwd` is resolved against the MXC process's directory on both paths.
since Terminal would otherwise start the workload in its own directory.

From schema `0.9.0-alpha` on, a set `cwd` must be absolute. A `~` path is
rejected too — it is expanded from the launching host's `HOME`, and an unset
`HOME` leaves a literal `~` that `chdir`s against the launcher. Earlier
versions resolve a relative value against the MXC process's directory.

**Note:** `getcwd()` only succeeds when the directory *itself* is readable under the profile. An
out-of-policy `cwd` makes callers that resolve relative paths (`git`, Python's
Expand Down
7 changes: 4 additions & 3 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1586,8 +1586,9 @@ Exact structural errors carry the full field path and source coordinates before
the successful request is constructed. Semantic errors remain backend-owned.
`validate_exec_common`
is a free function in `validator.rs` that checks cross-backend per-phase invariants
(e.g., `request.script_code` non-empty); other phases have no cross-backend common
checks today and skip directly to the backend's `validate_<phase>` hook.
(`request.script_code` non-empty, and an absolute `process.cwd` from `0.9.0-alpha`);
other phases have no cross-backend common checks today and skip directly to the
backend's `validate_<phase>` hook.

Helper functions for handle-validation, envelope wrapping, and
empty-envelope construction are mechanical and elided. The executor's outer driver
Expand Down Expand Up @@ -1630,7 +1631,7 @@ shapes.
|---|---|---|
| SDK (TypeScript) | Recognised `containment` (provision); branded `SandboxId<C>` (other phases); required cross-backend fields (`process.commandLine` for exec); typed config shape (autocompletion + compile-time check) | Thrown at the call site, before any subprocess runs |
| MXC parser (Rust) | Exact registered version and closed request root; required phase fields; phase-inappropriate, unknown, and recursively unknown experimental fields | `error.code: malformed_request`, `unsupported_phase`, `unsupported_containment` |
| MXC dispatch common (Rust) | Cross-backend per-phase invariants (e.g., `validate_exec_common` checks `process.commandLine` non-empty) | `error.code: malformed_request`, `policy_validation` |
| MXC dispatch common (Rust) | Cross-backend per-phase invariants (`validate_exec_common` checks `process.commandLine` non-empty and an absolute `process.cwd`) | `error.code: malformed_request`, `policy_validation` |
| Backend `validate_<phase>` hooks (Rust) | Per-backend per-phase invariants: config field values, cross-cutting policy honor (per the matrix in §10.3), id format checks beyond prefix matching | `error.code: policy_validation`, `malformed_id`, `stale_id`, `backend_error`, `backend_unavailable` |

The native CLI template form is resolved before these layers: a trailing
Expand Down
11 changes: 11 additions & 0 deletions docs/wsl/wsl-container-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,17 @@ Paths in `filesystem.readwritePaths` and `filesystem.readonlyPaths` are mounted
into the container. Host path `C:\workspace` becomes `/mnt/c/workspace` inside
the container.

### `process.cwd`

One-shot takes a **Windows host path** and translates it the way mounts are
translated: `C:\workspace` runs the process in `/mnt/c/workspace`. A value that
cannot be translated — a UNC path, or an in-container path such as
`/workspace` — is **rejected**. It was previously dropped without a diagnostic,
leaving the process in the container's default directory.

State-aware `exec` is the reverse: it takes the in-container path (`/workspace`)
directly and rejects anything that does not start with `/`.

### `ui` is not supported

A `ui` section is **rejected** — the backend has no mechanism to enforce UI
Expand Down
2 changes: 1 addition & 1 deletion schemas/dev/mxc-config.schema.0.9.0-alpha.json
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@
"description": "The non-empty command line to execute."
},
"cwd": {
"description": "Optional working directory.",
"description": "Optional working directory. A supplied value must be absolute for the target the path reaches — `C:\\workspace` or a UNC path for the Windows backends, `/workspace` for the Unix ones. WSL Container reads it as a Windows host path one-shot and as an in-container path on a state-aware `exec`. A relative path — including a `~` path, which MXC would have to expand from the launching host's environment — is rejected because it would resolve against the launching process's working directory.",
"type": "string"
},
"env": {
Expand Down
2 changes: 1 addition & 1 deletion schemas/dev/mxc-config.schema.0.9.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@
]
},
"cwd": {
"description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a working directory outright. See `docs/schema.md` (\"Working Directory\").",
"description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a working directory outright. See `docs/schema.md` (\"Working Directory\").\n\nFrom schema 0.9 on, a supplied value must be absolute for the target the path reaches — `C:\\workspace` or a UNC path for the Windows backends, `/workspace` for the Unix ones. WSL Container reads it as a Windows host path one-shot and as an in-container path on a state-aware `exec`. A relative path — including a `~` path, which MXC would have to expand from the launching host's environment — is rejected because it would resolve against the launching process's working directory.",
"type": [
"string",
"null"
Expand Down
5 changes: 4 additions & 1 deletion sdk/dotnet/Microsoft.Mxc.Sdk/SandboxRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ public SandboxRequest(SandboxPolicy policy, string command)
[JsonPropertyName("containerName")]
public string? ContainerName { get; set; }

/// <summary>An optional initial working directory.</summary>
/// <summary>
/// An optional initial working directory. Must be absolute from schema
/// <c>0.9.0-alpha</c> on.
/// </summary>
Comment thread
theelliotm marked this conversation as resolved.
[JsonPropertyName("workingDirectory")]
public string? WorkingDirectory { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion sdk/dotnet/Microsoft.Mxc.Sdk/StateAwareTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public class StateAwarePhaseOptions
/// <summary>Process and schema options for a state-aware exec phase.</summary>
public class StateAwareExecOptions : StateAwarePhaseOptions
{
/// <summary>Working directory inside the sandbox.</summary>
/// <summary>Working directory inside the sandbox. Must be absolute.</summary>
public string? WorkingDirectory { get; set; }

/// <summary>Environment variables encoded as <c>KEY=VALUE</c> strings.</summary>
Expand Down
4 changes: 2 additions & 2 deletions sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,9 @@ You must set `config.process!.commandLine = '…'` before calling `spawnSandboxF

No `network` field → no network. No `readwritePaths` → process can't write `%TEMP%`. No `ui` → no GUI. Use the discovery helpers to compose a sensible baseline.

### `process.cwd` doesn't grant filesystem access
### `process.cwd`

Setting `cwd` (or the `workingDirectory` argument) does **not** add that path to the policy. Add it to `readonlyPaths` / `readwritePaths` explicitly.
Setting `cwd` (or the `workingDirectory` argument) does **not** add that path to the policy. Add it to `readonlyPaths` / `readwritePaths` explicitly. On `0.9.0-alpha` it must also be absolute — `C:\workspace` for the Windows backends, `/workspace` for the Unix ones. WSL Container splits the two: a one-shot run takes the Windows host path (`C:\workspace`, which the backend translates), while a state-aware `exec` takes the in-container path (`/workspace`).

For Windows ProcessContainer requests using schema `0.9.0-alpha`,
`processContainer.filesystem.enumeratePaths` permits directory listing without
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/src/generated/v0_9_0_alpha/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ export interface Process {
*/
commandLine: NonEmptyString;
/**
* Optional working directory.
* Optional working directory. A supplied value must be absolute for the target the path reaches — `C:\workspace` or a UNC path for the Windows backends, `/workspace` for the Unix ones. WSL Container reads it as a Windows host path one-shot and as an in-container path on a state-aware `exec`. A relative path — including a `~` path, which MXC would have to expand from the launching host's environment — is rejected because it would resolve against the launching process's working directory.
*/
cwd?: string;
/**
Expand Down
2 changes: 2 additions & 0 deletions sdk/node/src/generated/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ export interface Process {
commandLine?: string | null;
/**
* Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a working directory outright. See `docs/schema.md` ("Working Directory").
*
* From schema 0.9 on, a supplied value must be absolute for the target the path reaches — `C:\workspace` or a UNC path for the Windows backends, `/workspace` for the Unix ones. WSL Container reads it as a Windows host path one-shot and as an in-container path on a state-aware `exec`. A relative path — including a `~` path, which MXC would have to expand from the launching host's environment — is rejected because it would resolve against the launching process's working directory.
*/
cwd?: string | null;
/**
Expand Down
6 changes: 5 additions & 1 deletion sdk/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
export interface ProcessConfig {
/** Complete command line to execute (e.g., "python -c \"print('hello')\"") */
commandLine: string;
/** Working directory for the process */
/**
* Working directory for the process. From schema `0.9.0-alpha` it must be
* absolute for the backend that receives it — see `docs/schema.md`
* ("Working Directory").
*/
Comment thread
theelliotm marked this conversation as resolved.
cwd?: string;
/**
* Environment variables as KEY=VALUE strings.
Expand Down
29 changes: 29 additions & 0 deletions sdk/node/tests/integration/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';
import os from 'os';
import { MxcError } from '@microsoft/mxc-sdk';
import {
sdk,
supportedVersions,
Expand Down Expand Up @@ -104,3 +105,31 @@ for (const schemaVersion of platformVersions) {
});
});
}

// A working directory MXC cannot honour is caller-fixable, so it must reach an
// SDK caller as `policy_validation` — the classification the state-aware and
// native surfaces already give the same refusal — rather than the
// infrastructure-failure `backend_error`. The refusal happens in shared
// validation before any sandbox is created, so this needs no backend
// prerequisites and cannot run the command.
describe('Working directory (schema 0.9.0-alpha)', {
skip: !platformSupport.isSupported ? `Platform not supported: ${platformSupport.reason}` : undefined,
}, () => {
it('should reject a relative cwd as policy_validation', async () => {
await assert.rejects(
() => sdk.spawnSandboxAsync(
'echo unreachable',
{ version: '0.9.0-alpha' },
{ ...debugSpawnOptions },
'relative-subdir',
'cwd-relative',
),
(error: unknown) => {
assert.ok(error instanceof MxcError, `expected an MxcError, got ${String(error)}`);
assert.strictEqual(error.code, 'policy_validation', `got ${error.code}: ${error.message}`);
assert.match(error.message, /process\.cwd must be an absolute path/);
return true;
},
);
});
});
Loading
Loading