Skip to content

About

Instrument the macOS Seatbelt sandbox at runtime without hand-waving

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

PolicyWitness

PolicyWitness is a witness harness for macOS sandbox policies: it applies an SBPL profile to a disposable process, records what the sandbox predicted for each probe and what happened when the probe was attempted, and prints the evidence as one JSON envelope.

Querying sandbox_check about a process and attempting an operation in that process under the same policy requires managing process lifecycles. sandbox_check answers for an existing PID, and sandbox application is one-way — a process gets exactly one sandbox. Evaluating a policy therefore means a fresh process per evaluation: compile and apply the policy to it once, aim both the query and the attempted operation at that PID while it lives, and carry the answer out through a channel the policy under test cannot sever.

The user guide has the specimen format, the output envelope and the operating guidance.

Flow

PolicyWitness operates on specimens: an SBPL policy plus a probe plan. The controller hands each specimen to a fresh runner, an unsandboxed XPC host that spawns a C worker that applies the policy to itself once. An optional file-extension declaration first runs a separate issuer outside that policy; bounded token bytes travel through a transient channel to the worker. Explicit lifecycle actions consume the token and release its native handle in that same worker. A simple probe_plan runs as one phase; an explicit phases sequence divides actions into ordered intervals in that same worker. At each phase arrival, the host plans queries afresh and, when eligible queries exist, starts a fresh validator against the worker's PID. Collection closes before that phase's actions are released, including on collection failure. The host joins the observations into one reply and exits; the controller wraps the reply in the envelope it prints.

Each ordinary step carries three records: the prediction, the attempt, and a comparison that relates the two. The comparison says what was observed and in what order; it never says whether prediction and enforcement agree, and no failure is attributed to the sandbox without evidence. Kernel denial records from the unified log are attached out of band, as candidates rather than verdicts, and cannot change the execution result. The guide's reading rules say how to use each record. Extension preparation, issuance and delivery have their own records; consume/release events carry native results and publication evidence without a comparison. Fresh reads retain descriptor identity and bounded bytes. None of these records declares effective permission or successful revocation.

How one run unfolds in time, the boundaries it crosses and the checks that pin each of them: the architecture tour.

Entitlements + SBPL

macOS sandboxing isn't just SBPL: a process's effective sandbox is its SBPL profile applied on top of the entitlements its binary was codesigned with. The same SBPL can yield different kernel behavior depending on which entitlements are granted, so a specimen has to describe both halves to be a faithful witness.

By default the policy is applied to a worker holding no entitlements. To observe another combination, install an external runner (BYOXPC, bring your own XPC service): a copy of the bundled service that policy-witness runner install signs with your own entitlements plist. The policy is then applied to a worker that holds those entitlements, the registry records what each binary carries, and a specimen can require keys the worker must hold. The guide's external runners section has the install recipe and the signing constraints.

Try it

Download a notarized release from GitHub Releases, or build one as described under Build and bundle. Releases and builds target macOS 26, the version this repository is tested on; Info.plist and meson.build declare that minimum together. Run it from a normal Terminal; a sandboxed automation harness can refuse the XPC lookup.

PW="/Applications/PolicyWitness.app/Contents/MacOS/policy-witness"

cat > /tmp/file_read_deny.json <<'JSON'
{
  "schema_version": 4,
  "specimen_id": "file_read_deny",
  "policy": {
    "format": "sbpl",
    "sbpl_source": "(version 1) (allow default) (deny file-read-data)"
  },
  "probe_plan": [
    {
      "step_id": "fr1",
      "sandbox_check": {
        "operation": "file-read-data",
        "filter": { "kind": "path", "value": "/etc/hosts" }
      },
      "attempt": { "kind": "file", "action": "open_read", "target": "/etc/hosts" }
    }
  ]
}
JSON

$PW run /tmp/file_read_deny.json > /tmp/result.json

The envelope's one step records a deny prediction, an attempt that failed with EPERM, and a comparison stating that the query preceded the attempt; the rest of the envelope records who ran it, what was run and what the host observed.

Short answers to common questions, including who needs this and what it can attribute, are in QUESTIONS.md.

Build and bundle

Building PolicyWitness requires Rust with Cargo, Xcode Command Line Tools (or Xcode), and Meson and Ninja on PATH (brew install meson ninja). The Developer ID Application signing identity must already be in your login keychain. From the repository root:

make build IDENTITY='Developer ID Application: YOUR NAME (TEAMID)'

The build writes dist/PolicyWitness.app, a ZIP ready for notarization at dist/PolicyWitness.zip, and the standalone user guide at dist/PolicyWitness.md.

build.sh compiles the Rust components with Cargo and the Swift and C components with Meson, assembles them into one app bundle, and signs it with the supplied identity. The app carries a version derived from git and generated evidence recording its binaries' hashes and entitlements.

The app contains:

  • dist/PolicyWitness.app
    • Contents/MacOS/policy-witness (Rust controller)
    • Contents/MacOS/pw-runner-client (Swift NSXPCConnection wrapper)
    • Contents/MacOS/sandbox-log-observer (Rust unified-log capture helper)
    • Contents/MacOS/sbpl-check (SBPL compile-check helper)
    • Contents/XPCServices/PWRunner.xpc (Swift XPC host; one host and worker per specimen, with a fresh validator per eligible phase)
      • Contents/MacOS/pw-probe-runner (bundle-local C worker that applies the policy and runs probe attempts)
      • Contents/MacOS/sb_api_validator (bundle-local validator launched per eligible phase for sandbox_check verdicts)
      • Contents/MacOS/pw-extension-issuer (ordinary-authority file-extension issuer with a transient token channel)
    • Contents/Resources/Evidence/* (generated manifests: hashes/entitlements, symbols.json)

The build tour describes the toolchain, build settings and signing checks. Building produces a signed app; the release procedure covers notarization, acceptance testing and publication. The distribution directory describes the deliverables and preserved release archives.

Documentation

About

Instrument the macOS Seatbelt sandbox at runtime without hand-waving

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages