You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An entry ending in * matches a prefix. deny entries win over everything else.
customPosixShm uses access levels: read, readWrite, create (read, write, and create), and own (adds delete). Each listed name gets exactly its list's level, even under a broader default, and when entries overlap the longer one wins.
With ui.injection: true, the IOHIDLibUserClient grant stays when customIoKits replaces guiAccess's IOKit grant.
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" (the default) 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, clipboard, 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 * (the error names the list-specific fix), 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;
a shared memory name is in two access lists;
an allow entry names only something a setting denies, whether written or omitted (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 and customPosixShm list levels still apply.
Settings that are omitted from the config will allow and report. Written deniedPaths, ingress.hostLoopback: "deny", nestedPty, keychainAccess, and ui settings stay enforced; a written network default: "deny" does not.
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.
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.
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, and allow-mode enforcement of written ui.disable and ui.injection;
customPosixShm read, write, create, delete, and new-region creation checked separately for every level, overlapping entries, and allow mode;
rejection of malformed and conflicting custom configs through the executor, asserting the workload never starts.
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).
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.
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.
[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.
Include effective settings in policy hash projection
[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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- Keep the ui.injection HID grant when customIoKits replaces guiAccess's IOKit grant.
- Keep omitted custom-list defaults unset in the parser; Seatbelt resolves them.
- Add leveled customPosixShm defaults (deny/read/readWrite/create/own), a create
list, and exact per-entry levels; reject a name in two level lists.
- Give list-specific remedies for bare "*" entries.
- Add rejection, learning-mode UI, and shared memory level e2e coverage.
- Clarify learning-mode and custom default-deny docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Maintainer confirmation is needed because no approved design is linked for the new containment controls, and their native macOS enforcement precedence could not be independently validated.
Versioning: Not keeping V1 parity: most of the old behavior was buggy (e.g. guiAccess overriding keychainAccess), so the fixes apply to all versions.
Fixed: with ui.injection: true, the IOHIDLibUserClient grant is now written even when customIoKits replaces guiAccess's IOKit grant (unit test, since CI hosts have no guaranteed HID device).
Intended: an omitted setting counts as deny, so a custom entry that contradicts it is rejected in every mode, and the docs now say so.
Added e2e rejection cases through the executor that assert the workload never starts: bare *, profileOverride, extraMachLookups, reserved name, setting conflicts both ways, a shm name in two lists, and a name over 31 bytes.
Added a parser test for omitted vs explicit ui.disable/ui.injection, plus learning-mode e2e checks for ui.disable (true/omitted) and ui.injection (false/omitted).
Shm probes now pre-create regions and check read, write, create, delete, and new-region creation separately for every level, overlapping entries, and learning mode.
Fixed: omitted custom-list defaults stay unset through parsing and Seatbelt resolves them to deny.
The learning-mode table now lists exactly what "allow" mode still enforces and names the network and custom-list default exceptions.
Reworded: each list replaces only guiAccess's matching broad grant; baseline and other settings' grants remain.
Bare * now gets a list-specific remedy (e.g. set customPosixShm.default to that list's level) instead of always suggesting default: "allow".
Also changed: customPosixShm levels
Levels are read, readWrite, create (read/write/create), and own (adds delete); default accepts deny or any level, and create is a new list for apps that create regions but shouldn't delete them.
Each listed name gets exactly its list's level, even under a broader default: MXC adds a deny for the operations above that level.
When entries overlap, the longer one wins (e.g. own: ["/app.*"] with read: ["/app.config"] makes /app.config read-only).
The same entry in two level lists is rejected, and deny wins over every level.
List levels and deny entries still apply in captureDenials"allow" mode.
Shared-memory write-denial coverage :The level probe now tests unlink independently, but the write check still uses O_RDWR. A read denial can make that fail even if write access is accidentally allowed. Please add an O_WRONLY probe against a precreated denied region, with a successful write-only control, in both normal and learning modes.
nestedPty must be explicit in 1.1+:Normalization still defaults omitted nestedPty to true, including for 1.1.0-alpha. For 1.1+ (1.1.0-alpha onward), default it to false and require explicit nestedPty: true to grant nested PTY allocation. Keep the existing default for V1 and below. Please update the documentation and add version-boundary tests for omitted, explicit false, and explicit true.
Regarding point 1, macOS rejects 'O_WRONLY'. Not possible to test that.
Regarding point 2, could that be in a separate PR? Open a separate issue and tag me on it.
Elliot (@theelliotm) Thanks for the clarification. You're right about O_WRONLY: Apple's macOS shm_open documentation requires exactly one of O_RDONLY or O_RDWR. The write-only probe recommendation was incorrect; withdrawing point 1 as an outstanding test request.
Tracked the nestedPty default change separately in #1735 and tagged you there. That follow-up requests explicit opt-in starting with 1.1.0-alpha, while preserving the defaults for V1 and below; it can be handled in a separate PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📖 Description
Adds
1.1.0-alphaSeatbelt 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
seatbeltfields in the1.1.0-alphacontract:customMachLookupsdefault(deny/allow),allow,denycustomMachRegistersglobalandlocalblocks, each with the same shapecustomIoKitsdefault,allow,denycustomPosixShmdefault(deny/read/readWrite/create/own),read,readWrite,create,own,deny*matches a prefix.denyentries win over everything else.customPosixShmuses access levels:read,readWrite,create(read, write, and create), andown(adds delete). Each listed name gets exactly its list's level, even under a broaderdefault, and when entries overlap the longer one wins.ui.injection: true, theIOHIDLibUserClientgrant stays whencustomIoKitsreplacesguiAccess's IOKit grant.guiAccessgrant. The blocks also work withoutguiAccess.extraMachLookupsis now documented as deprecated in favor ofcustomMachLookups.1.0.0,0.9.0-alpha) reject these fields.2. Explicit settings win over
guiAccess(bug fix)Before this change, turning on
guiAccessquietly undid some settings written in the config. Now, in every contract version:nestedPty: falseblocks PTY creation.keychainAccessoff (the default) blocks the Keychain servers, even thoughguiAccessallows all Mach lookups.ui.clipboard"none"(the default) blocks everycom.apple.pasteboard.*service, not onlycom.apple.pasteboard.1.readonlyPathsanddeniedPathsentries inside/private/tmpand/private/var/foldersstay enforced.versioning.mdrecords thenestedPty, Keychain, clipboard, and path changes as a V1 servicing exception.3. Validation
The config is rejected before the sandbox is created when:
profileOverride, orcustomMachLookupswithextraMachLookups;*(the error names the list-specific fix), a*that isn't at the end, an IOKit entry that isn't a class name, or a shared memory name over 31 bytes;customMachLookupsentry uses MXC's reservedcom.microsoft.mxc.run.prefix;allowentry names only something a setting denies, whether written or omitted (for examplecom.apple.SecurityServerwithoutkeychainAccess);denyentry would block something a setting grants (for example the pasteboard withui.clipboardenabled,IOHIDLibUserClientwithui.injection: true, orcom.apple.trustdwithkeychainAccess: true).4. Learning mode (
captureDenials)"allow"mode, every custom list acts asdefault: "allow", so the app runs and the report lists each name to add.denyentries andcustomPosixShmlist levels still apply.deniedPaths,ingress.hostLoopback: "deny",nestedPty,keychainAccess, anduisettings stay enforced; a written networkdefault: "deny"does not.guiAccess's broad grants allow are reported in the verbose report, so you can see which names to move into the custom lists.🔗 References
docs/backends/seatbelt/seatbelt-backend.md("Custom Mach, IOKit, and shared memory lists"),docs/schema.md,docs/development/architecture/versioning.md.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.
src/unless noted)cargo fmt --all -- --checkcargo clippy -p mxc-sdk -p mxc_darwin -p mxc_ffi --all-targets -- -D warningscargo clippy -p mxc-sdk --lib --tests --target x86_64-unknown-linux-gnu -- -D warningscargo clippy -p mxc-sdk --lib --tests --target x86_64-pc-windows-msvc -- -D warningscargo test -p mxc-sdk --libcargo test -p mxc-sdk --test mxc_contract_v1_1_0_alphacargo test -p mxc-sdk --test mxc_contract_v1_0_0cargo test -p mxc-sdk --test mxc_contract_v0_9_0_alphacargo test -p mxc-sdk --test mxc_contract_version_boundariescargo test -p mxc-sdk --test streamingnode scripts/versioning/validate-configs.js(repo root)npx tsc --noEmit -p .(sdk/node/)./build-mac.sh(repo root)bash tests/scripts/run_seatbelt_gui_access_test.shbash tests/scripts/run_seatbelt_capture_denials_test.shbash tests/scripts/run_seatbelt_rejections_test.shbash tests/scripts/run_seatbelt_options_test.shbash tests/scripts/run_seatbelt_tty_test.shbash tests/scripts/run_seatbelt_ui_test.shbash tests/scripts/run_seatbelt_examples_test.shNew end-to-end coverage, wired into the nightly Seatbelt run through
run_seatbelt_all_tests.sh:guiAccessoverride fix (nestedPty,keychainAccess, clipboard,/private/tmpread-only paths);nestedPtysettings, and allow-mode enforcement of writtenui.disableandui.injection;customPosixShmread, write, create, delete, and new-region creation checked separately for every level, overlapping entries, and allow mode;The
nestedPty: falseallow-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
feature-acceptedCargo.lock, thedependency-feed-checkcheck passes (see pull request builds)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow