Skip to content

linux: no placeholder where no mount point can be made - #688

Merged
dylan-conway merged 4 commits into
mainfrom
fix/linux-no-mount-point-where-none-can-be-made
Oct 13, 2026
Merged

dylan-conway merged 4 commits into
mainfrom
fix/linux-no-mount-point-where-none-can-be-made

Conversation

@ronleizrowice-ant

@ronleizrowice-ant ronleizrowice-ant commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

The fault

For an absent denyWrite path inside an allowed write path, bubblewrap makes a mount point on the host, as this user, before the command runs. Where it cannot, no sandboxed command starts: bwrap: Can't mkdir /mnt/c/Program Files/X: Permission denied on WSL 2 once /mnt/c is writable, or any absent deny path under /usr or /etc.

The change

mountPointFor decides what every absent deny path gets: nothing, a file or a directory.

Invariant: a placeholder is left out only where the sandboxed command cannot create the path either. On any doubt it stays. At worst bubblewrap cannot start, as today. No deny is lost.

  • New: the parent refuses, and the refusal is not this user's to lift (another's directory, a read-only file system, an immutable directory). One O_PATH descriptor answers every question, so a running command cannot swap the path between two. Ancestors are pinned as for any deny.
  • A directory this user owns keeps its placeholder, whatever its mode: the command could chmod it.
  • On a Windows drive (9p) only Windows knows, so the library makes the mount point itself at wrap time: the same object bubblewrap would make, with the same clean-up. See the README.
  • The kind of a missing component that two denies share does not depend on their order.

Not cured: running as root, where access() passes on a capability the sandbox lacks.

Cost

Six calls per placeholder: about 0.3 ms on a 3 ms wrap with 15 of them. None where #684 already leaves the placeholder out.

Testing

  • 44 new tests, 24 of which fail on the base's source. linux: bind a write deny that holds an allowed write path #684's 89 are unchanged. 54 mutations, each failing a test. bubblewrap 0.11.2 and 0.4.1.
  • 2,200 random configurations against the base in real sandboxes, with /usr, /etc, /, /proc, an own 0555 directory and a read-only bind among them: none that starts on the base fails to start, 668 start here only. No deny is lost, no read or write differs, nothing is written to the host at wrap time.
  • An ordinary project's bubblewrap arguments are the base's, word for word.
  • The 9p branch runs by injection only: no WSL 2 host was at hand.

Also here

Three comment lines in windows-sandbox-utils.ts. #683 brought a lint rule that names a call #686 makes on purpose, and the two met on main, where npx eslint . fails. #688 and #689 both carry the same lines, so either can merge first.

denyWrite takes precedence over allowWrite however the two are nested. A
deny is a read-only bind emitted after every allow bind: for a deny path
inside an allowed write path, equal to one, or holding one. The deny loop's
gate and its pre-pass share the predicate.

An absent deny path that only write paths beneath a deny directory hold gets
no mount point: nothing can create it there.
For an absent denyWrite path inside an allowed write path bubblewrap makes a
mount point on the host, as this user, before the command runs. Where it
cannot, no sandboxed command starts: a Windows drive's protected folder under
WSL 2, or any absent deny path under /usr.

mountPointFor decides what every absent deny path gets. A placeholder is left
out only where the sandboxed command cannot create the path either: the
parent refuses and the refusal is not this user's to lift. On any doubt it
stays. On a 9p file system only the other side knows, so the mount point is
made at wrap time.
Base automatically changed from fix/linux-write-deny-above-a-write-root to main October 12, 2026 14:14
With the lint comment main needs: the rule against direct realpath calls
and the one call that is made on purpose met there.
@dylan-conway
dylan-conway merged commit a1dfd43 into main Oct 13, 2026
16 checks passed
@dylan-conway
dylan-conway deleted the fix/linux-no-mount-point-where-none-can-be-made branch October 13, 2026 20:06
ronleizrowice-ant added a commit that referenced this pull request Oct 20, 2026
Joins that needed a decision:

- The placeholder planning of #688 and #705 (one decision per destination,
  asked of the record and not of the host; the kind settled beforehand by
  whether a deny lies beneath; a file above the path held in place; no mount
  point where none can be made) replaces this branch's upgrade in place. What
  this branch adds to it: an empty file a manifest names is covered like an
  absent path and named, and what the plan makes goes into the wrap's manifest
  instead of the set in memory. A held file that a manifest names is named by
  the look at everything the profile binds from.
- The count of wraps and the forced clean-ups of #671 are kept in this
  branch's terms: what the count protects is the manifest and the --args
  profile of a command that has not started.
- #673's cases on a file found at a deny path assert that a leftover stays;
  here a manifest says which is one. The suite is this branch's own.
- #675: one return value for the plan, with the mandatory denies' share.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEZ8LUnnuZdEgiFocuvVDm
Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
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.

2 participants