Skip to content

Read a caller's path as a path where it exists, and let a caller mark one - #608

Closed
ronleizrowice-ant wants to merge 4 commits into
mainfrom
fix/literal-path-entries
Closed

ronleizrowice-ant wants to merge 4 commits into
mainfrom
fix/literal-path-entries

Conversation

@ronleizrowice-ant

@ronleizrowice-ant ronleizrowice-ant commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Superseded by three smaller stacked pull requests, to be read in this order: #620 (the anchor option on the glob walk), #621 (the { path, literal: true } marker), #622 (an entry is also read as the path it spells). Together they hold this change plus fixes: <bracketed>/** on Windows, tests for each { literal: true } argument, corrected testing claims, and two more known limits in the README.


A path inside a folder whose name holds [, ], * or ? was read as a pattern, so the caller's deny or allow was lost. An entry is now also read as the path it spells where that exists, and a caller can mark one as a path outright.

What changes

  • The pattern reading stays; a pattern beneath such a folder is also applied beneath it.
  • A deny takes every reading; an allow only a path that is there as itself (no symbolic link).
  • denyRead, allowRead, allowWrite and denyWrite take { "path": "...", "literal": true }: never a pattern, existing or not.
  • New exported type FilesystemPathEntry; the restriction configs gain optional literalDenyOnly, literalAllowWithinDeny, literalAllowOnly, literalDenyWithinAllow.

Who sees a difference

  • A project in such a folder: its entries are enforced. Otherwise nothing.
  • Embedders: the four lists are FilesystemPathEntry[], so reading an entry as a string stops compiling. Silently: pass the restriction configs on whole; an empty denyOnly no longer means nothing is read-denied.
  • Beneath an unsearchable directory a deny pattern is also denied as a path: more than on main.

Known limits

  • Linux write lists take no patterns.
  • Existence is checked at wrap time (Windows: at initialize()).
  • credentials.files takes no marker. More in the README.

Merge order

Late: conflicts with #569, #574, #575, #595, #607. A merge will not flag that readers of allowOnly added by #569, #594, #595 must use writeRootsOf(), nor that #607 gives expandReadDenyGlobLinux a different fourth parameter.

Testing

New test/sandbox/literal-path-entries.test.ts; its real bubblewrap runs fail on main. sandbox-exec and Windows cases run in CI only.

… one

Whether an entry of denyRead, allowRead, allowWrite or denyWrite is a
pattern is decided by looking for `*`, `?`, `[` and `]` in what the
caller wrote. A directory may have those characters in its name, and a
caller that lists absolute paths inside `[WIP] project` had every one of
them read as a pattern: on Linux a write entry was dropped and a read
entry expanded to nothing; on macOS the rule compiled to a regex that
covers `W project` and not the folder. The deny was lost on both.

An entry with those characters keeps the pattern reading it always had,
and is now also read as what it names wherever the part of it that
holds the characters exists on disk: as the path it spells when all of
it does, and as the rest of the pattern beneath the folder when a
pattern follows, the folder taken as the name it is. A deny takes every
reading, and counts a name as there when anything has it, a symbolic
link included, or when it cannot be looked at. An allow takes a reading
only for a path that is there as itself: no component from the first
with such a character on may be a symbolic link.

Each of the four lists also takes `{ path, literal: true }`, a path and
never a pattern whether or not it exists.

The decision is made in one new module, path-entries.ts, once per entry
per wrap, and carried with the entry: the restriction configs hold the
names in literalDenyOnly, literalAllowWithinDeny, literalAllowOnly and
literalDenyWithinAllow, the Linux wrapper takes every path it is handed
for a name, and the macOS builder emits a literal filter beside the
regex and escapes the anchor of a pattern beneath a named folder. A lint
rule keeps the sniffing form of normalizePathForSandbox out of the two
Linux files. An entry with no such folder on disk gives what it gave.
How an entry is read, for every shape of spelling, each glob character,
a link or a dangling link in the prefix and an unsearchable parent. By
real bubblewrap runs inside `[WIP] project`: the project writable when
allowed, a denyWrite enforced with the file on the host unchanged, a
denyRead file and directory unreadable, a carve-out working, a recursive
deny masking `.env` at two depths. An allow never follows a planted
link. The macOS profile for the same, with the anchor escaped. Both
readings at once. The marker through the schema, the getters,
updateConfig, a per-command config in all four lists and the violation
monitor. And that entries with no such folder on disk give
configurations, bubblewrap arguments and a macOS profile identical to
what they gave before.
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.
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