Skip to content

fix(macos): expand write denies in both spellings and check structural glob containment - #791

Open
inanna-malick wants to merge 2 commits into
anthropics:mainfrom
inanna-malick:fix/macos-seatbelt-symlink-write-deny-and-glob-carveout
Open

inanna-malick wants to merge 2 commits into
anthropics:mainfrom
inanna-malick:fix/macos-seatbelt-symlink-write-deny-and-glob-carveout

Conversation

@inanna-malick

@inanna-malick inanna-malick commented Oct 29, 2026 •

Copy link
Copy Markdown

Fixes #790

Summary

This PR resolves two macOS Seatbelt profile compilation bugs in src/sandbox/macos-sandbox-utils.ts following cc3d45c:

  1. Expand denyEntries in generateWriteRules through inBothSpellings('deny', entry):

    • resolveReadConfig expands read-deny entries through callerEntries(...).flatMap(entry => inBothSpellings('deny', entry)) so that symlinked deny paths emit both the link spelling and the resolved realpath spelling.
    • generateWriteRules previously omitted inBothSpellings('deny', entry), leaving the resolved target of an intra-workspace symlink (e.g., <workspace>/.mcp.json -> <workspace>/real_config/mcp.json or a symlink in denyWithinAllow) writable under allowOnly: [<workspace>].
    • Passing denyEntries through .flatMap(entry => inBothSpellings('deny', entry)) ensures both spellings are emitted in (deny file-write* ...) and move-blocking rules.
  2. Check structural glob containment in denyGlobCovers:

    • denyGlobCovers previously tested denyRegex.test(entrySamplePath(entry)) (where entrySamplePath substituted every glob wildcard with 'x'), causing denyGlobCovers to return true whenever a narrower denyOnly glob happened to match 'x' (such as /work/pub/x*, /work/pub/[u-z]*, /work/pub/[a-zA-Z]*, or /work/pub/?) under a broader allowWithinDeny glob (/work/pub/*).
    • Checking structural containment over the parsed glob atoms (globEntryAtoms + rangesCover) when entry.glob is true ensures denyGlobCovers only carves out an allow/writeRoot entry when the subtree-extended deny glob genuinely covers the allow glob's wildcard region.

Test Plan

  • Added regression tests in test/sandbox/trailing-slash-deny-spellings.test.ts covering:
    • Intra-workspace symlink in denyWithinAllow emitting both link and resolved target spellings in the ; File write section.
    • Narrower denyOnly globs (/work/pub/x*, /work/pub/[u-z]*, /work/pub/[a-zA-Z]*, /work/pub/?) under allowWithinDeny: ['/work/pub/*'] remaining un-carved and landing after the allow rule.
    • Covered character-class allow (/work/pub/[a-z]safe* under denyOnly: ['/work/pub/[a-z]*']) still being carved out.

Copilot AI balanced review requested due to automatic review settings October 29, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unsound glob containment checks can leave denied files readable and weaken unlink protection, while the write-deny regression is masked by read-deny rules and does not establish that the fix works.

3 open issues
What changed in this PR

This PR addresses #790 by updating macOS Seatbelt profile generation to protect symlinked write-deny targets and reduce incorrect glob carve-outs.

Changes:

  • Expand write-deny entries to include both original and resolved path spellings.
  • Add a second wildcard sample when checking deny-glob coverage.
  • Add regression tests for symlink write protection and narrower deny globs.
File Description
test/​sandbox/​trailing-slash-deny-spellings.test.ts Adds symlink and glob carve-out regression tests.
src/​sandbox/​macos-sandbox-utils.ts Expands write-deny path spellings and updates glob coverage probes.

Review effort: Balanced

Comment thread src/sandbox/macos-sandbox-utils.ts Outdated
Comment on lines +345 to +346
denyRegex.test(entrySamplePath(entry, 'x', 'x')) &&
denyRegex.test(entrySamplePath(entry, 'Q7_', '9'))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 42cc65c: replaced sample probing in denyGlobCovers with a sound structural containment check over parseGlob tokens (globEntryAtoms + rangesCover), and added /work/pub/[a-zA-Z]* under allowWithinDeny: ['/work/pub/*'] to the regression suite.

Comment thread src/sandbox/macos-sandbox-utils.ts Outdated
anchor +
entry.path
.slice(anchor.length)
.replace(/\[[^\]]*\]|\*+|\?/g, m => (m[0] === '*' ? starFill : charFill))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 42cc65c: character-class sets (GlobTokenKind.Set) are now compared by interval containment (rangesCover), and added the denyOnly: ['/work/pub/[a-z]*'] + allowWithinDeny: ['/work/pub/[a-z]safe*'] regression test.

Comment on lines +205 to +207
readConfig: {
denyOnly: [linkMcp],
},

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 42cc65c: set readConfig: undefined in the symlink write-deny test to isolate generateWriteRules from generateReadDenyUnlinkRules.

@inanna-malick inanna-malick changed the title fix(macos): expand write denies in both spellings and probe disjoint glob samples fix(macos): expand write denies in both spellings and check structural glob containment Oct 30, 2026

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.

macOS Seatbelt: intra-workspace symlink write-deny omission in generateWriteRules and single-probe 'x' glob carve-out in denyGlobCovers

2 participants