Skip to content

[Seatbelt] Add custom Mach, IOKit, and shared memory lists - #1720

Open
Elliot (theelliotm) wants to merge 7 commits into
mainfrom
user/emichlin/mach-iokit-customizable
Open

Elliot (theelliotm) wants to merge 7 commits into
mainfrom
user/emichlin/mach-iokit-customizable

Conversation

@theelliotm

@theelliotm Elliot (theelliotm) commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

Adds 1.1.0-alpha Seatbelt settings that let a GUI app be granted only the Mach services, IOKit classes, and POSIX shared memory it actually needs.

1. Custom lists (feature)

New seatbelt fields in the 1.1.0-alpha contract:

Field Controls Shape
customMachLookups Mach services the app may look up default (deny/allow), allow, deny
customMachRegisters Mach services the app may register global and local blocks, each with the same shape
customIoKits IOKit user-client classes the app may open default, allow, deny
customPosixShm POSIX shared memory names default, read, readWrite, own, deny
  • An entry ending in * matches a prefix. deny entries win over everything else.
  • Each block that is present replaces the matching guiAccess grant. The blocks also work without guiAccess.
  • extraMachLookups is now documented as deprecated in favor of customMachLookups.
  • The schema and Node wire types are regenerated. Older contracts (1.0.0, 0.9.0-alpha) reject these fields.

2. Explicit settings win over guiAccess (bug fix)

Before this change, turning on guiAccess quietly undid some settings written in the config. Now, in every contract version:

  • nestedPty: false blocks PTY creation.
  • keychainAccess off (the default) blocks the Keychain servers, even though guiAccess allows all Mach lookups.
  • ui.clipboard: "none" blocks every com.apple.pasteboard.* service, not only com.apple.pasteboard.1.
  • readonlyPaths and deniedPaths entries inside /private/tmp and /private/var/folders stay enforced.

versioning.md records the nestedPty, Keychain, and path changes as a V1 servicing exception.

3. Validation

The config is rejected before the sandbox is created when:

  • a custom block is combined with profileOverride, or customMachLookups with extraMachLookups;
  • an entry is malformed: a bare *, a * that isn't at the end, an IOKit entry that isn't a class name, or a shared memory name over 31 bytes;
  • a customMachLookups entry uses MXC's reserved com.microsoft.mxc.run. prefix;
  • an allow entry names only something a setting denies (for example com.apple.SecurityServer without keychainAccess);
  • a deny entry would block something a setting grants (for example the pasteboard with ui.clipboard enabled, IOHIDLibUserClient with ui.injection: true, or com.apple.trustd with keychainAccess: true).

4. Learning mode (captureDenials)

  • In "allow" mode, every custom list acts as default: "allow", so the app runs and the report lists each name to add. deny entries still apply.
  • Settings omitted from the config allow and report. Settings written in the config, including nestedPty: false, stay enforced.
  • Accesses that only guiAccess's broad grants allow are reported in the verbose report, so you can see which names to move into the custom lists.

🔗 References

Test It Yourself Here:

I created a simple demo to show Microsoft Edge functioning inside MXC with just its required Mach/IOKit/POSIX grants. The grants were found using learning mode.

Download this: seatbelt_msft_edge 2.zip

Run  ./run_sandboxed_edge.sh <path-to-mxc-exec-mac>

You can either quit MXC or ctrl + click Edge on the dock and press Quit to end the demo.

🔍 Validation

Run on macOS (Apple silicon) at the head of this branch.

Command (from src/ unless noted) Result
cargo fmt --all -- --check ✅ passed
cargo clippy -p mxc-sdk -p mxc_darwin -p mxc_ffi --all-targets -- -D warnings ✅ passed
cargo clippy -p mxc-sdk --lib --tests --target x86_64-unknown-linux-gnu -- -D warnings ✅ passed
cargo clippy -p mxc-sdk --lib --tests --target x86_64-pc-windows-msvc -- -D warnings ✅ passed
cargo test -p mxc-sdk --lib ✅ 1961 passed
cargo test -p mxc-sdk --test mxc_contract_v1_1_0_alpha ✅ 382 passed
cargo test -p mxc-sdk --test mxc_contract_v1_0_0 ✅ 282 passed
cargo test -p mxc-sdk --test mxc_contract_v0_9_0_alpha ✅ 278 passed
cargo test -p mxc-sdk --test mxc_contract_version_boundaries ✅ 21 passed
cargo test -p mxc-sdk --test streaming ✅ 33 passed
node scripts/versioning/validate-configs.js (repo root) ✅ passed
npx tsc --noEmit -p . (sdk/node/) ✅ passed
./build-mac.sh (repo root) ✅ passed
bash tests/scripts/run_seatbelt_gui_access_test.sh ✅ 102 checks passed
bash tests/scripts/run_seatbelt_capture_denials_test.sh ✅ 125 checks passed
bash tests/scripts/run_seatbelt_options_test.sh ✅ 20 checks passed
bash tests/scripts/run_seatbelt_tty_test.sh ✅ passed
bash tests/scripts/run_seatbelt_ui_test.sh ✅ 5 checks passed
bash tests/scripts/run_seatbelt_examples_test.sh ✅ 28 checks passed

New end-to-end coverage, wired into the nightly Seatbelt run through run_seatbelt_all_tests.sh:

  • each custom block on its own, and all four together with several entries each, in block and allow mode;
  • each guiAccess override fix (nestedPty, keychainAccess, clipboard, /private/tmp read-only paths);
  • learning-mode reports for omitted and written clipboard, Keychain, and nestedPty settings.

The nestedPty: false allow-mode regression was confirmed to fail before its fix (PTY_OK) and pass after it (PTY_BLOCKED).

Not run: Windows and Linux runtime suites (this change is Seatbelt-only; both targets were checked with Clippy) and the .NET SDK tests (no .NET changes).

✅ Checklist

  • Signed the Contributor License Agreement
  • Linked to an issue
  • For features or changes requiring a spec: linked the issue with maintainer-applied feature-accepted
  • For implementation: linked the required approved and merged design/API spec, or explained why no spec is required
  • Feature issue remains open unless this PR completes all agreed implementation, validation, and documentation
  • Updated documentation (if applicable)
  • Updated Copilot instructions (if build, architecture, or conventions changed)
  • If this PR changes Cargo.lock, the dependency-feed-check check passes (see pull request builds)

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

Copilot AI balanced review requested due to automatic review settings November 10, 2026 02:45
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Previously valid V1 requests now fail validation without explicit maintainer acceptance of the compatibility break, and custom-policy validation inconsistencies remain unresolved.

3 open issues
What changed in this PR

Adds fine-grained Mach, IOKit, and POSIX shared memory controls to MXC’s macOS Seatbelt backend, building on the learning mode introduced in #1668.

Changes:

  • Adds four custom-list settings to the 1.1.0-alpha contract, with validation, schema, and generated Node types.
  • Makes explicit restrictions take precedence over broad guiAccess grants and distinguishes omitted settings in learning mode.
  • Expands contract and macOS functional coverage, with documentation of usage and V1 servicing changes.
File Description
tests/​scripts/​run_seatbelt_tty_test.sh Updates PTY expectations under guiAccess.
tests/​scripts/​run_seatbelt_options_test.sh Checks explicit Keychain deny rules.
tests/​scripts/​run_seatbelt_gui_access_test.sh Exercises custom lists and policy precedence.
tests/​scripts/​run_seatbelt_capture_denials_test.sh Extends learning-mode and reporting scenarios.
tests/​configs/​seatbelt_gui_access_readonly_tmp.json Adds read-only temporary-path regression fixture.
tests/​configs/​seatbelt_gui_access_nested_pty_off.json Adds GUI PTY-denial fixture.
tests/​configs/​seatbelt_gui_access_keychain_off.json Adds GUI Keychain-denial fixture.
tests/​configs/​seatbelt_gui_access_custom_registers.json Configures registration scopes and IOKit rules.
tests/​configs/​seatbelt_gui_access_custom_posix_shm.json Configures shared memory access levels.
tests/​configs/​seatbelt_gui_access_custom_lookups_deny.json Adds Mach lookup deny-list fixture.
tests/​configs/​seatbelt_gui_access_custom_lookups_allow.json Adds Mach lookup allow-list fixture.
tests/​configs/​seatbelt_gui_access_custom_clipboard_none.json Tests clipboard denial over custom grants.
tests/​configs/​seatbelt_gui_access_custom_all.json Combines all four custom-list settings.
tests/​configs/​seatbelt_capture_denials_gui_keychain.json Tests omitted Keychain settings in learning mode.
tests/​configs/​seatbelt_capture_denials_gui_app_custom_allow.json Runs a GUI app with empty learning-mode lists.
src/​mxc-sdk/​tests/​mxc_contract/​v1_1_0_alpha/​seatbelt.rs Tests custom-list contract shapes.
src/​mxc-sdk/​tests/​mxc_contract/​v1_0_0/​seatbelt.rs Verifies V1.0 rejects new fields.
src/​mxc-sdk/​src/​core/​mxc_contract/​dev/​stable.rs Defines development contract fields and types.
src/​mxc-sdk/​src/​core/​mxc_contract/​dev/​mod.rs Exports new contract types.
src/​mxc-sdk/​src/​core/​mxc_common/​wire.rs Adds intermediate custom-list representations.
src/​mxc-sdk/​src/​core/​mxc_common/​models.rs Adds runtime rules and setting-presence tracking.
src/​mxc-sdk/​src/​core/​mxc_common/​config_parser.rs Maps custom lists and preserves setting presence.
src/​mxc-sdk/​src/​core/​mxc_common/​config_contract_adapters/​v1_0/​one_shot.rs Leaves new fields absent for V1.0.
src/​mxc-sdk/​src/​core/​mxc_common/​config_contract_adapters/​v0_9/​one_shot.rs Leaves new fields absent for V0.9.
src/​mxc-sdk/​src/​core/​mxc_common/​config_contract_adapters/​dev/​one_shot.rs Converts development custom-list settings.
src/​mxc-sdk/​src/​backends/​seatbelt/​common/​seatbelt_runner.rs Invokes validation and updates PTY assertions.
src/​mxc-sdk/​src/​backends/​seatbelt/​common/​seatbelt_policy.rs Validates names, combinations, and setting conflicts.
src/​mxc-sdk/​src/​backends/​seatbelt/​common/​profile_builder.rs Generates custom rules and enforces precedence.
src/​mxc-sdk/​src/​backends/​seatbelt/​common/​capture_analysis.rs Reuses the pasteboard prefix for classification.
sdk/​node/​src/​generated/​v1_1_0_alpha/​wire.ts Adds generated TypeScript custom-list types.
schemas/​dev/​mxc-config.schema.1.1.0-alpha.json Adds generated custom-list schema definitions.
docs/​schema.md Documents new fields and learning-mode behavior.
docs/​development/​architecture/​versioning.md Records development-only fields and servicing exceptions.
docs/​development/​architecture/​designs/​1591-seatbelt-learning-mode/​README.md Clarifies omitted versus written settings.
docs/​containment-policy/​dev/​policy.md Summarizes development Seatbelt options.
docs/​backends/​seatbelt/​seatbelt-backend.md Explains custom lists, precedence, and learning-mode usage.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 10, 2026 22:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Policy hashes conflate settings with different access decisions, and report redaction hides leading-slash shared-memory names needed to configure custom grants.

1 open issue
2 previously missed issues

In code that hasn't changed since last review

Medium severity Preserve shared-memory names when redacting report targets

src/​mxc-sdk/​src/​backends/​seatbelt/​common/​profile_builder.rs:586

[P2] Shared-memory accesses allowed by these new :grant rules appear only in the verbose report, but capture_analysis::report_signature redacts any target containing /. A name such as /myapp.region is therefore unavailable in either JSON report, preventing callers from discovering names for customPosixShm; the custom default: "allow" grant has the same issue. Preserve shared-memory names when redacting filesystem paths, and add a regression test for a grant with a leading-slash name.

Medium severity Include effective settings in policy hash projection

src/​mxc-sdk/​src/​core/​mxc_common/​models.rs:821

[P2] In captureDenials allow mode, omitting ui.clipboard versus writing "none" produces the same mxc.PolicyHash, although one permits access and the other blocks it; omitted versus explicit keychainAccess: false has the same problem. policy_identity::policy_projection serializes these structs, so both new presence fields are excluded by #[serde(skip)]. Include the effective setting decisions explicitly in the hash projection, keep the fields out of transport serialization, and add hash regression tests for these pairs.

2 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 10, 2026 23:09
@theelliotm
Elliot (theelliotm) marked this pull request as ready for review November 10, 2026 23:10
@theelliotm
Elliot (theelliotm) requested a review from a team November 10, 2026 23:10
@theelliotm
Elliot (theelliotm) requested a review from a team as a code owner November 10, 2026 23:10
@theelliotm Elliot (theelliotm) self-assigned this Nov 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Maintainer review is needed because the new Mach, IOKit, and shared-memory controls lack a linked approved design, and their native enforcement was not independently validated on supported macOS hosts.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 10, 2026 23:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Maintainer review is needed to confirm the new macOS sandbox controls’ security semantics and grant/deny precedence, as no approved design is linked and runtime enforcement could not be reproduced in this environment.

No open issues 🎉

Review effort: Balanced

@richiemsft

Copy link
Copy Markdown
Contributor

Review findings

Versioning: Please keep parity with existing behavior for V1 and below, including 1.0.0 and 0.9.0-alpha. Start enforcing the stricter permissions in 1.1+ (1.1.0-alpha onward): deny by default and require callers to explicitly enable the access they need.

Medium findings

# Severity File Lines Vulnerability Confidence
1 🟡 MEDIUM profile_builder.rs 579–582 With GUI and injection enabled, a custom IOKit list removes the blanket grant without restoring IOHIDLibUserClient, silently disabling injection. Emit the enabled HID-specific grant independently of the custom list and add a behavioral regression. —
2 🟡 MEDIUM seatbelt_policy.rs 263–275 In learning mode, omitting keychainAccess permits and reports access, but adding com.apple.SecurityServer to customMachLookups.allow rejects the request. Align validation with enforcement's capture-mode and presence rules; keep explicit restrictions enforced. —
3 🟡 MEDIUM seatbelt_runner.rs 176–177 Custom-policy rejection is tested through helpers, not the backend entry point. Removing this validation call could leave the tests green. Add end-to-end rejection cases for malformed/conflicting policies and assert that the workload never executes. —
4 🟡 MEDIUM config_parser.rs 1734–1737 Parsed explicit ui.disable and ui.injection restrictions lack learning-mode regression coverage; manually setting presence flags in profile tests does not protect parsing. Test omitted fields against explicit disable: true and injection: false through parsing and enforcement. —
5 🟡 MEDIUM run_seatbelt_gui_access_test.sh 263–266 Shared-memory denial probes use O_RDWR and do not test unlink. Read denial can hide an unintended write grant. Precreate regions and independently assert read-only, write-only and unlink denial, including overlapping grants and learning mode. —
6 🟡 MEDIUM config_parser.rs 1332–1338 Shared normalization converts omitted custom-list defaults into explicit Deny, losing presence and placing backend policy defaulting in the parser. Preserve the optional value through normalization and resolve the default in Seatbelt's effective-policy construction. —
7 🟡 MEDIUM seatbelt-backend.md 423 The learning-mode promise that written settings remain enforced is too broad: an explicit network egress default of deny withholds an allow, so learning mode's allow default permits traffic. Enumerate preserved restrictions and explicitly document exceptions for network and custom-list denying defaults. —
8 🟡 MEDIUM seatbelt-backend.md; dev/stable.rs 579; 414 Describing custom default-deny as granting only the allowlist is inaccurate: baseline grants and enabled-setting grants remain. Clarify that each list replaces its corresponding broad GUI grant, not every grant in that category. —
9 🟡 MEDIUM seatbelt_policy.rs 297–300 The wildcard error recommends default: "allow" even for shared-memory read or deny entries. That can add write/create permissions or reverse the intended denial. Make the remedy list-specific and recommend concrete names or supported prefixes instead. —

Review scope: source inspection at 45044210; macOS runtime enforcement was not independently reproduced. Items 3–5 are regression-coverage gaps, and item 6 is an architectural/presence issue, not a separately demonstrated runtime failure. Numerical confidence scores for the Medium findings were not retained in the consolidated report.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants