Skip to content

Deny contained reads of the home directory on macOS - #155

Open
fstubner wants to merge 4 commits into
mainfrom
fix/macos-deny-home-reads
Open

fstubner wants to merge 4 commits into
mainfrom
fix/macos-deny-home-reads

Conversation

@fstubner

@fstubner fstubner commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

On macOS a contained process could read every file in the home directory outside the credential stores, other projects included, and nvx's own home: grants, policy and tool_home credentials. Windows and Linux already deny reads of the home directory. This makes macOS match.

Change

After the blanket (allow file-read*) the Seatbelt profile now:

  1. denies file-read* on the real home and on nvx's home (NVX_HOME may be outside the home), each named as given and with links resolved
  2. allows file-read-metadata on both again, so tools can still stat ancestors of the project
  3. reopens file-read* on what Linux grants: the project, the guest home, every allow_read_exec root, and NVX_HOME/versions, bin and current
  4. keeps the credential-store denies last, so a project or allow_read_exec root that holds a store does not expose it

The runtime trees are one list shared with the Landlock rules. Both launch paths pass NvxHome and ReadExecRoots to the profile builder.

A runtime under the home outside nvx (nvm, the hosted runner's toolcache) now needs its directory in allow_read_exec to run contained, as it already does on Linux.

Evidence

  • Commit 1 (assertions only), run 37399750782: the macOS enforcement step failed with READ_OUTSIDE=ALLOWED, NVX_HOME_READ=ALLOWED, and the phase 4 read of a file in an NVX_HOME under /var/folders exiting 0. Every control passed, CONNECT=200 included. The run as a whole shows cancelled because the next push superseded its Windows job.
  • Commit 2 (profile), run 37400274341: READ_OUTSIDE=DENIED, NVX_HOME_READ=DENIED, phase 4 exit 3. The controls READ_INSIDE, READ_RUNTIME, READ_EXEC_ROOT, phase 3's project and runtime reads and CONNECT=200 all passed. The macOS smoke's contained npm install and nested-runtime check passed. The launch-escape probe reported all three vectors DENIED, with its node running from an allow_read_exec root.
  • A Go test asserts the home and nvx-home denies, their order against the blanket allow and the reopened roots, and that nothing reopened covers the home, nvx's home, another project, grants, tool_home, policy.json or another session's guest home. It fails against the old builder.

The probe and the smoke now install an nvx-managed runtime first, because the runner's node lives under the home. The enforcement probe's project, NVX_HOME and allow_read_exec fixture sit under $HOME, so the controls exercise the reopened paths.

The macOS enforcement probe now reads a file in the real home directory
outside the project, and a file in nvx's home outside its runtimes, from
inside the sandbox. Each read must be refused by the OS with EPERM or
EACCES. A fourth phase repeats the nvx home check with an NVX_HOME under
/var/folders, outside the home.

The controls sit under the home too, so a profile that denied all of it
fails them: a read of the project, of the runtime the process runs, and
of a directory the policy names in allow_read_exec. The project, NVX_HOME
and that directory move under $HOME for this reason.

The probe and the macOS smoke install an nvx-managed runtime first, as
the Linux probe does. The runner's own node is under
/Users/runner/hostedtoolcache, inside the home. The launch-escape probe
names that node's install directory in allow_read_exec instead.

The Seatbelt profile allows every read outside the credential stores, so
this commit fails on macOS. The next one narrows the profile.
The Seatbelt profile allowed every read and denied only the credential
stores. A contained process could read every other file in the home
directory, other projects included, and nvx's own home: grants, policy
and tool_home credentials. On the macOS runner the probe from the
previous commit read a file in the home outside the project, a file in
an NVX_HOME under the home, and a file in an NVX_HOME under
/var/folders. Windows and Linux already deny reads of the home.

After the blanket read allow the profile now denies reads of the real
home and of nvx's home, named as given and with links resolved. Metadata
stays readable. It then reopens what Linux grants: the project, the
guest home, every allow_read_exec root, and nvx's versions, bin and
current. The credential-store denies stay last, so a project or an
allow_read_exec root that holds a store does not expose it.

The runtime trees are now one list shared with the Landlock rules. A
runtime under the home outside nvx, such as one installed by nvm, needs
its directory in allow_read_exec, as it already does on Linux.
The enforcement matrix, SECURITY.md, README and PRODUCT.md said macOS
allowed every read outside the credential stores. They now say reads
under the home directory and nvx's home are denied apart from what a run
needs, and that reads elsewhere on the disk stay allowed. The matrix
records the two macOS runs: 37399750782 read all three files before the
profile change, 37400274341 refused them with every control passing.
…eads

# Conflicts:
#	CHANGELOG.md
#	docs/enforcement-matrix.md

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.

1 participant