Repository navigation
linux: one mount per destination, so bubblewrap 0.4 starts - #580
Merged
Merged
Conversation
One file can be named more than once: in filesystem.denyRead under two
spellings, in filesystem.denyRead and again in credentials.files, or twice
in credentials.files. Every spelling resolves to one place, and the read-deny
loop skipped an entry only where a denyRead tmpfs already hid it, which a
file mask is not — so each spelling emitted its own file mount at that one
destination, and the credential masks added theirs on top.
bubblewrap before 0.5.0 refuses to start on the second of them: its
ensure_file() returns early only for a regular file and creates a file over
anything else, so the mount lands on the character device the first
/dev/null mask left and creat() hits the read-only mount that mask just
made ("Can't create file at <dest>: Permission denied").
The credential masks are now collected first, keyed by where they land, so
two spellings of one file keep one fake: the last, which is the one
bubblewrap left in force. The read-deny loop skips a file whose destination
a mask already covers or that an earlier entry already masked. The mount
that used to win is the mount still emitted.
…ound Two things need 0.5.0, both of them changes to how bubblewrap prepares the mount point for a file bind. Before it, ensure_file() takes only a regular file for one and creates a file over anything else, so a denyRead entry or credential mask naming a fifo, socket or device node blocks or fails instead of binding over it; and it creates that file mode 0666 rather than 0444, so the recovery that keys off a read-only leftover does not recognise a mount point an interrupted sandbox left behind. checkDependencies() probes bwrap --version once per binary and reports both, naming the version found. A warning, not a refusal: every mount plan this library builds starts on 0.4.0 and later.
…tions A file mask is a --ro-bind of something other than the destination itself. Two of them at one destination with nothing between that mounts at or above it is a mount whose only effect is to replace the one before it, and the shape bubblewrap before 0.5.0 refuses to start on. The corpus spells one file through the routes a caller and the credentials block reach it by; a mask re-applied above a write deny that re-exposed it has that deny in between and is not one of these. Also covers the version warning: a stub bwrap that reports 0.4.1 draws one naming that version, a newer one draws none, and one that will not answer --version draws none either.
RHEL 8 and 9 ship 0.4.0 and 0.4.1, the oldest bubblewrap in current distribution use, and one of those refuses to start on a plan that mounts twice at one destination. Built with autotools, which that release predates meson for, and pinned by commit like the 0.12.0 job. stale-mount-point is the one mount-plan suite left out: it asserts the read-only mode bubblewrap gives a mount point it creates, which it does only from 0.5.0 on.
Every mount plan starts on 0.4.0 and later; a read deny or credential mask on a path that is not a regular file, and the recovery of a mount point an interrupted sandbox left behind, need 0.5.0.
bwrap applies mounts in order, so the second of two masks at one destination was the one in force before they were collapsed into one. Nothing else says that is the one kept.
Array.prototype.findLastIndex is ES2023 and the tests compile against the ES2020 library, so the last matching bind is found with map and lastIndexOf instead. One filter callback gets its parameter typed. No test changes what it checks.
This was referenced Sep 21, 2026
Closed
Comments only: the JavaScript emitted with comments removed is byte-identical for every file, and the test names are unchanged. Kept: every invariant and what could be done without it, the documentation of exported names, and the reasons for what is not done the obvious way. Cut: how the code came to be as it is, narration of the lines that follow, and explanations repeated at several sites.
…ersion again after a failed probe The test of one mount per destination only read argument lists, so the CI job on bubblewrap 0.4.1 passed with the fix reverted. It now also starts bubblewrap on every configuration of the corpus: on 0.4.x eight of them are refused without the fix. A `bwrap --version` probe that failed or timed out was remembered for the life of the process and silenced the warning for good. Only an answer is kept now. README: what 0.4.x supports, said without contradicting itself, and that the dependency check runs `bwrap --version`.
dylan-conway
approved these changes
Sep 28, 2026
This was referenced Sep 28, 2026
ronleizrowice-ant
added a commit
that referenced
this pull request
Oct 2, 2026
…ady holds main's squash is the head merged here before, so nothing changes: the four files that conflict are the bundle's own, with their joins.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Spellings of one file (a trailing slash, a symlinked directory, a denyRead entry that is also a credential mask) each put a file mask at the same destination. Before 0.5.0 bubblewrap takes only a regular file as a file bind's mount point, and the first mask leaves /dev/null there, so 0.4.0 and 0.4.1 (RHEL 8, 9) refused to start: "Can't create file at : Permission denied". Each destination now gets one mask: the credential mask where a read deny names the same file, and the last of two credential masks.
Who sees a difference: a config naming one file twice now starts on 0.4.x. checkDependencies() runs
bwrap --versiononce per process (5 s timeout; a failed probe is not cached) and warns, without refusing, below 0.5.0.Limits before 0.5.0 (README and warning say so): a mask on a fifo, socket or device node cannot be applied; a mount point left by an interrupted sandbox stays on the host.
Merge order: conflicts with #589 and #592 in integration-tests.yml (steps inserted at one spot; keep all), and with #594 (README insertion point).
Testing: linux-one-mount-per-destination.test.ts checks the argument lists of 15 configurations and starts the bubblewrap on PATH on each; CI's 0.4.1 leg runs it. Locally, on 0.4.1 built from the pinned commit, all 15 start; with the fix reverted 8 are refused. On 0.11.2 that arm passes either way, so only the 0.4.1 leg guards this.