Skip to content
Merged
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
10 changes: 7 additions & 3 deletions docs/backends/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ The `/dev/*` entries are writable because shell redirections (`>/dev/null`,
`</dev/urandom`) need both directions. Writes to `/dev/null` and `/dev/zero` are
discarded; writes to the entropy devices are harmless.

MXC grants exact device paths for inherited terminals or the caller-owned PTY
assigned to the workload. Explicit filesystem grants or `profileOverride` can widen access.

The developer grant resolves from the `xcode-select` symlink, and only when root
owns both the link and every directory above it, so an unprivileged process
cannot point it elsewhere. `DEVELOPER_DIR` is ignored for the same reason. When
Expand Down Expand Up @@ -349,7 +352,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 and use its own ptys, without granting access to unrelated host terminals. Needed by anything that spawns a shell β€” test runners, `git`, `gh`, REPLs, agent tools. Set `false` for a tighter sandbox. |
| `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 All @@ -360,8 +363,9 @@ Set under a top-level `"seatbelt"` key.

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 the `/dev/ttys*` and `/dev/ptmx` devices, and POSIX shared
memory.
`pseudo-tty` with `/dev/ptmx` and sandbox-authorized `/dev/ttys*` devices, and
POSIX shared memory. PTY allocation remains enabled even when `nestedPty` is
`false`.

It also grants read **and** write across all of `/private/tmp` and
`/private/var/folders`, regardless of your `filesystem` policy. Deny rules are
Expand Down
48 changes: 28 additions & 20 deletions src/mxc-sdk/src/backends/seatbelt/common/profile_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ use crate::seatbelt_common::seatbelt_policy;
pub fn build_profile_with_proxy(
request: &ExecutionRequest,
proxy_address: Option<&ProxyAddress>,
) -> Result<String, String> {
build_profile_with_terminals(request, proxy_address, &[])
}

/// Bind terminal paths discovered by the runner, never supplied by the workload.
pub(crate) fn build_profile_with_terminals(
request: &ExecutionRequest,
proxy_address: Option<&ProxyAddress>,
terminal_paths: &[PathBuf],
) -> Result<String, String> {
if let Some(override_profile) = request
.seatbelt
Expand All @@ -69,12 +78,18 @@ pub fn build_profile_with_proxy(
out.push_str(SYSTEM_READ_ALLOW);
write_developer_dir_rule(&mut out);

// Pseudo-terminal access β€” when the executor binary runs under a pty
// the sandboxed shell inherits that TTY, so it sees a real terminal
// and calls `isatty()` / `tcgetattr()` / `ttyname()` against it.
// Without these rules, those calls fail with EPERM because the
// kernel calls block on the secondary fd.
// Keep the controlling terminal usable without granting other host PTYs.
out.push_str(TTY_ALLOW);
for path in terminal_paths {
let path = path
.to_str()
.ok_or_else(|| "Seatbelt: assigned terminal path is not valid UTF-8".to_string())?;
let _ = writeln!(
out,
"(allow file-read* file-write* file-ioctl (literal {}))",
quote_scheme(path)
);
}

// Policy-derived allow rules.
let resolved = ResolvedPaths::from_policy(&request.policy)?;
Expand Down Expand Up @@ -250,18 +265,14 @@ fn push_developer_dir_rule(out: &mut String, dir: &Path) {
let _ = writeln!(out, "(allow file-read* (subpath {}))", quote_scheme(path));
}

/// Pseudo-terminal device access required by the inner shell when the
/// runner attaches it to a pty. The secondary fd we hand the child as
/// stdin/stdout/stderr lives at `/dev/ttysNNN`, and the shell calls
/// `isatty()` (β†’ `tcgetattr` β†’ ioctl) plus `ttyname()` against it. We
/// also need read access to `/dev/tty` because most shells re-open it
/// at startup, and read access to `/dev/fd` for the `/dev/stdout` etc.
/// indirection some tools use.
/// Controlling-terminal access and kernel-scoped access to newly allocated PTYs.
const TTY_ALLOW: &str = "\
;; --- pseudo-terminal access (inherited TTY when run under a pty) ---
;; --- controlling terminal and sandbox-allocated pseudo-terminals ---
(allow file-read* file-write* file-ioctl (literal \"/dev/tty\"))
(allow file-read* file-write* file-ioctl
(literal \"/dev/tty\")
(regex #\"^/dev/ttys[0-9]+$\"))
(require-all
(regex #\"^/dev/ttys[0-9]+$\")
(extension \"com.apple.sandbox.pty\")))
(allow file-read* (subpath \"/dev/fd\"))
";

Expand Down Expand Up @@ -560,8 +571,7 @@ fn write_ui_rules(out: &mut String, request: &ExecutionRequest) {
out.push_str(";; --- guiAccess: allow pseudo-TTY for shell sessions ---\n");
out.push_str("(allow pseudo-tty)\n");
out.push_str("(allow file-read* file-write* file-ioctl\n");
out.push_str(" (regex #\"/dev/ttys[0-9]+\")\n");
out.push_str(" (regex #\"/dev/ptmx\"))\n");
out.push_str(" (literal \"/dev/ptmx\"))\n");

// POSIX shared memory and IPC β€” required by Terminal.app and
// other apps that use notification center or shared memory.
Expand Down Expand Up @@ -606,9 +616,7 @@ fn write_nested_pty_rules(out: &mut String, request: &ExecutionRequest) {
}
out.push_str(";; --- nestedPty: allow inner process to allocate its own pty ---\n");
out.push_str("(allow pseudo-tty)\n");
// /dev/ptmx is the primary multiplexer; opening it is what posix_openpt
// does under the hood. The TTY_ALLOW baseline already grants access to
// /dev/ttysNNN (the secondary side).
// TTY_ALLOW restricts secondary-device access to the kernel's PTY extension.
out.push_str("(allow file-read* file-write* file-ioctl\n");
out.push_str(" (literal \"/dev/ptmx\"))\n");
}
Expand Down
Loading
Loading