[Bubblewrap/LXC/WSLC] Add filesystem-policy delegation check#598
[Bubblewrap/LXC/WSLC] Add filesystem-policy delegation check#598SohamDas2021 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a cross-backend “filesystem-policy delegation” gate so Linux (LXC/Bubblewrap) and WSLC runs reject readonlyPaths/readwritePaths entries that the invoking user cannot access, preventing the sandbox from being granted more filesystem access than the caller has.
Changes:
- Introduces
wxc_common::filesystem_accesswith a per-platform access probe (access(2)/CreateFileW) andcheck_delegation(ContainerPolicy). - Wires the delegation check into Bubblewrap, LXC, and WSLC runners close to mount/enforcement.
- Adds unit tests for common allow/skip/reject cases.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core/wxc_common/src/lib.rs | Exposes the new filesystem_access module from wxc_common. |
| src/core/wxc_common/src/filesystem_access.rs | Implements the delegation check and associated unit tests. |
| src/backends/wslc/common/src/wsl_container_runner.rs | Runs delegation check early in WSLC runner to reject inaccessible policy paths. |
| src/backends/lxc/common/src/lxc_runner.rs | Runs delegation check at start of LXC execution path. |
| src/backends/bubblewrap/common/src/bwrap_runner.rs | Runs delegation check before spawning bwrap. |
| fn user_can_access(path: &str, mode: AccessMode) -> Option<bool> { | ||
| use std::ffi::CString; | ||
|
|
||
| if std::fs::metadata(path).is_err() { | ||
| return None; | ||
| } | ||
| let c_path = CString::new(path).ok()?; | ||
| let mask = match mode { | ||
| AccessMode::Read => libc::R_OK, | ||
| AccessMode::ReadWrite => libc::R_OK | libc::W_OK, | ||
| }; | ||
| // SAFETY: `c_path` is a valid NUL-terminated C string for the duration of the call. | ||
| let rc = unsafe { libc::access(c_path.as_ptr(), mask) }; | ||
| Some(rc == 0) | ||
| } |
| //! This does file I/O (`access(2)` / `CreateFileW`), so — like the object-based | ||
| //! normalization in [`crate::filesystem_object`] and per design review — it runs | ||
| //! in each backend runner **close to the point of enforcement**, NOT in | ||
| //! `config_parser` (which stays string-only). Two reasons: |
| //! When both this and object normalization are wired into a runner, | ||
| //! [`crate::filesystem_object::normalize_object_conflicts`] must run **first**, | ||
| //! so delegation is checked against the already-tightened intents (a path moved | ||
| //! `rw → denied` must not then be required to have write access). |
3381a65 to
e1623d1
Compare
| Some(true) | ||
| } | ||
| _ => { | ||
| // Only an explicit access denial is a delegation failure. Any other |
There was a problem hiding this comment.
Copilot tells me - Unix uses a skip-list (only genuine "missing" is skipped; the default is reject). Windows uses a reject-list (only explicit access-denied is rejected; the default is skip). A ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION, or transient IO error on a real, existing file hits the Windows default and is silently skipped — the exact opposite of what the same situation does on Unix.
So on Unix "non-existent is surfaced separately" is achieved by listing the missing errnos and rejecting everything else; on Windows the author achieves it by skipping everything except one error. Those are not the same posture.
There was a problem hiding this comment.
Thanks for the catch, seems to be a valid concern, will address.
The delegation access-probe used asymmetric error handling across platforms: Unix skipped only genuine-missing paths and rejected everything else (fail-closed), while Windows rejected only ERROR_ACCESS_DENIED and skipped every other failure (fail-open). A sharing/lock violation or transient I/O error on a real, existing file was therefore silently allowed on Windows but rejected on Unix — the opposite posture for a security delegation gate. Align Windows to the Unix posture: skip only ERROR_FILE_NOT_FOUND / ERROR_PATH_NOT_FOUND (surfaced separately by the existence warning); treat every other failure, including access-denied and ambiguous errors, as 'cannot confirm access' and reject. Add a Windows test locking the fail-closed behavior for an ambiguous (ERROR_INVALID_NAME) error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📖 Description
Enforce that the container is never granted more filesystem access than the invoking user already holds: every readwritePaths entry requires the user to have read+write access, and every readonlyPaths entry requires read access. deniedPaths are unbounded (denying needs no access) and are not checked. A path the user cannot access is rejected, so a sandboxed process can't reach files the caller themselves couldn't.
🔗 References
🔍 Validation
✅ Checklist
📋 Issue Type
GitHub Actions runs the PR validation build automatically. The ADO pipeline
(
MXC-PR-Build) is the official build pipeline that signs the binaries; itruns on merge to
mainand nightly, and Microsoft reviewers can trigger iton a PR with
/azp run. See docs/pull-requests.md.Microsoft Reviewers: Open in CodeFlow