Skip to content

Build drivers (xcodebuild/gradle/swift) can't escape the seatbelt: expects_sandbox_block is ignored for "contained" commands #786

Description

@jonathanKingston

Summary

A command that needs ~/Library (build/SPM caches, DerivedData) or a system XPC service — e.g. xcodebuild … build — has no working path to run outside the sandbox, even when the agent sets expects_sandbox_block: true. It runs under the workspace seatbelt, which denies exactly the paths and services the build needs, and the up-front escalation never fires.

Root cause

expects_sandbox_block: true only pulls the escalation prompt forward when the command's scope verdict is 'ambiguous':

// src/main/services/security/permission-policy.ts:309 (shellExpectedBlockEscalation)
if (analysis.verdict !== 'ambiguous') return { eligible: false, reasons: [] }

A command with no network/outside-path markers in its text is classified 'sandbox' (contained):

// src/main/services/security/shell-scope.ts:390
return { verdict: 'sandbox', reasons: ['no network or outside-path signals detected'] }

xcodebuild -workspace … build reads/writes ~/Library/Caches/org.swift.swiftpm, ~/Library/Developer/Xcode/DerivedData, the system $TMPDIR (/var/folders/.../T), and talks to com.apple.CoreSimulator.CoreSimulatorService — but none of that is visible in the command string, so it's classified 'sandbox', the expects_sandbox_block hint is silently ignored (shell-tool.ts:288), and it runs under seatbelt. The seatbelt overlay allows only the workspace + workspace $TMPDIR, so the build fails with permissionDenied on the SPM cache, DerivedData, and the result bundle, plus CoreSimulatorService connection invalid.

The only built-in escape for such a command is the reactive maybeRetryUnsandboxed offer (shell-tool.ts:324-340), which requires a non-zero exit + recorded sandbox violations. In practice that path is fragile here (see the sibling | tail issue) and the agent never gets an unsandboxed run.

Not a hardware/OS limitation

The observing agent concluded CoreSimulator was "unreachable even outside the sandbox." That's wrong: Copse ships with hardenedRuntime only, not the macOS App Sandbox (build/entitlements.mac.plist has no com.apple.security.app-sandbox), so a genuinely unsandboxed child can reach CoreSimulator. In the one run where DerivedData was redirected into the workspace, xcodebuild got past the cache denials and successfully enumerated installed simulators — proving the block is the seatbelt, not the OS.

Suggested fixes (options)

  • Recognize known build drivers (xcodebuild, gradle, swift build, cargo) as 'ambiguous' so expects_sandbox_block / the escalation prompt can fire for them, and/or
  • Add the common build-cache paths (~/Library/Caches/org.swift.swiftpm, ~/Library/Developer/Xcode/DerivedData) and the CoreSimulator service to the seatbelt overlay so contained builds work without escaping, and/or
  • Let expects_sandbox_block: true escalate a 'sandbox'-verdict command too (with a clear prompt), since today it's a no-op for the exact commands most likely to need it.

Context

Surfaced while investigating a failed DuckDuckGo iOS build. Sibling issues: the 300s timeout_ms cap, and | tail masking the exit code (which defeats the reactive retry offer above).

Related prior art: #623 (ACP unsandboxed retry), #481 (tmp dir sandbox escape), #104 (spoofable sandbox-failure — why the reactive path is runner-signal-only).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:securitySecurity boundaries, permissions, secrets, and hardeningarea:shellShell execution, command routing, and sandbox behaviorbugSomething isn't workingpriority:p2Important issue to address soon

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions