Skip to content

[Bubblewrap] Mount symlinked grants at their real paths - #1638

Open
Soham Das (SohamDas2021) wants to merge 1 commit into
sohamdas2021-msrc-170208-merged-usr-baseline-fixfrom
sohamdas2021-msrc-170208-caller-path-alias-fix
Open

Soham Das (SohamDas2021) wants to merge 1 commit into
sohamdas2021-msrc-170208-merged-usr-baseline-fixfrom
sohamdas2021-msrc-170208-caller-path-alias-fix

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

A readwritePaths or readonlyPaths entry named through a symlink was mounted at the path the caller wrote, while deniedPaths entries were already moved to their real paths. A denied or read-only path inside such a grant then sat in a different place in the sandbox from the grant, and did not apply to it. For example, with /home/alice/app -> /data/repos/app:

"filesystem": {
  "readwritePaths": ["/home/alice/app"],
  "deniedPaths": ["/home/alice/app/.env"]
}

.env stayed readable and writable at /home/alice/app/.env. Naming the denial, or a readonlyPaths exception, by its real path under /data/repos/app did not help: writes through /home/alice/app still changed the host file.

Grants are now mounted at their real paths too, so a grant and the rules inside it sit in one directory tree. Each symlink a grant is named through is recreated inside the sandbox as a relative link, so the caller's spelling still works, including as process.cwd, and the rules apply under both spellings. In the example, the sandbox mounts /data/repos/app, masks /data/repos/app/.env, and links /home/alice/app to it. This also fixes the case raised in review of #1624: a file granted through /lib, inside a directory denied as /lib/x or /usr/lib/x, is visible again instead of being hidden by the mask.

A link the sandbox cannot recreate, such as one under /dev or /proc, or inside a mount of a different host path, leaves that spelling unavailable, and MXC prints a warning naming the real path to use. Where the sandbox has its own directory at the link's path, such as its private /tmp on a host whose /tmp links to /var/tmp, the next part of the path is linked inside that directory instead, so a grant of /tmp/project still works as /tmp/project. A grant that cannot be resolved, such as a dangling symlink or a symlink loop, stops the sandbox from starting, with an error that names the entry.

Behavior changes:

  • Relative entries in readwritePaths, readonlyPaths and deniedPaths are resolved from the working directory of the process that starts the sandbox. Before, a relative grant did not appear at the path it names, and a relative denial starting with .. masked the wrong file: with /work granted and /work/sub as the working directory, ../shared/m masked /work/sub/shared/m and left /work/shared/m readable.

  • On a host whose /tmp is a symlink, a grant of /tmp itself now appears only at its real path, such as /var/tmp. The sandbox keeps its own private /tmp, and MXC prints the warning above.

Limitations: only symlinks are followed. A hard link, or a host bind mount that shows the same files under a second path, is still a separate path in the sandbox, so a rule on one does not reach the other (documented). The LXC backend is not covered here and will follow separately.

docs/backends/bwrap/bubblewrap-backend.md is updated, and the workaround #1624 added there for grants spelled through /lib is removed. No spec is needed: this changes how existing filesystem fields are enforced, with no schema or API change.

🔗 References

Stacked on #1624; review that first. Security report (reference to be added when published).

🔍 Validation

On WSL Ubuntu 24.04 with bwrap 0.9.0, run unprivileged, and on Windows:

  • cargo fmt --all -- --check: passed
  • cargo clippy -p mxc-sdk -p lxc --all-targets -- -D warnings (Linux): passed
  • cargo clippy -p mxc-sdk --all-targets -- -D warnings (Windows): passed
  • cargo test -p mxc-sdk --lib -- bubblewrap filesystem_symlink (Linux): 454 passed, 1 failed. probe_exec::tests::a_missing_command_reports_the_spawn_error fails the same way on [Bubblewrap] Recreate merged-/usr directory symlinks in the sandbox #1624 on this host, because WSL adds Windows directories to PATH, and searching them reports a permission error instead of not-found.
  • cargo test -p mxc-sdk --lib -- bwrap_command (Windows): 73 passed
  • MXC_BWRAP_TESTS_REQUIRE_EXECUTION=1 cargo test -p mxc-sdk --test streaming_bubblewrap: 15 passed
  • MXC_BWRAP_TESTS_REQUIRE_EXECUTION=1 cargo test -p mxc-sdk --test wxc_e2e_tests_e2e_bubblewrap_characterization: 15 passed
  • bash tests/scripts/run_bwrap_grant_alias_test.sh (new, in the nightly Bubblewrap suite): 23 cases passed. On [Bubblewrap] Recreate merged-/usr directory symlinks in the sandbox #1624 it fails: denied files stay readable or writable, and read-only files writable, through the symlinked spelling; a relative denial masks the wrong file; and the /lib grant stays hidden.
  • MXC_BWRAP_TESTS_REQUIRE_EXECUTION=1 bash tests/scripts/run_bwrap_all_tests.sh, with a release build and a sudo stand-in that runs as WSL's root user: 22 passed, 0 failed, 1 skipped (Inbound Deny, which needs root and is exempt in strict mode)
  • run_bwrap_grant_alias_test.sh and [Bubblewrap] Recreate merged-/usr directory symlinks in the sandbox #1624's run_bwrap_baseline_alias_test.sh inside Debian 13, Ubuntu 26.04 and AlmaLinux 10 root filesystems, run under bwrap on the same kernel: passed

The new test covers the cases above in a tmpfs directory and in the home directory, plus chains of links, a link already visible inside another grant, an aliased grant inside a denied directory, dangling and looping grants, and a host whose /tmp is a symlink, simulated by running lxc-exec inside an outer bwrap. On a host that cannot run bwrap inside bwrap, the script exits 77 at that point, which strict mode reports as a failure.

✅ Checklist

  • Signed the Contributor License Agreement
  • Linked to an issue
  • For features or changes requiring a spec: linked the issue with maintainer-applied feature-accepted
  • For implementation: linked the required approved and merged design/API spec, or explained why no spec is required
  • Feature issue remains open unless this PR completes all agreed implementation, validation, and documentation
  • Updated documentation (if applicable)
  • Updated Copilot instructions (if build, architecture, or conventions changed)
  • If this PR changes Cargo.lock, the dependency-feed-check check passes (see pull request builds)

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner November 3, 2026 00:40
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

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