Skip to content

[WSLC] Reject nested deniedPaths overlaps in WSLC policy mapping#650

Merged
SohamDas2021 merged 3 commits into
mainfrom
user/sodas/wslc-denied-path-overlap-validation
Jul 17, 2026
Merged

[WSLC] Reject nested deniedPaths overlaps in WSLC policy mapping#650
SohamDas2021 merged 3 commits into
mainfrom
user/sodas/wslc-denied-path-overlap-validation

Conversation

@SohamDas2021

@SohamDas2021 SohamDas2021 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

📖 Description

WSLC mounts host paths as flat WSL volumes and has no overlay primitive, so a deniedPaths entry nested under a mounted
(readwrite/readonly) parent cannot be masked and would remain accessible through the parent mount. Add validate_denied_path_overlap, which rejects such configs at policy-mapping time (component-wise, case- and separator-insensitive, drive-letter aware) instead of silently leaving the subtree exposed.

🔗 References

🔍 Validation

✅ Checklist

📋 Issue Type

  • Bug fix
  • Feature
  • Task

GitHub Actions runs the PR validation build automatically. The ADO pipeline
(MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHub
Actions build; it runs on merge to main, and Microsoft reviewers with write access can trigger it
on a PR with /azp run. See docs/pull-requests.md.

If the dependency-feed-check check fails on a new dependency, the crate must be added to
the feed before the PR can pass. See docs/pull-requests.md
for the steps.

Microsoft Reviewers: Open in CodeFlow

Copilot AI review requested due to automatic review settings July 14, 2026 21:06
@azure-pipelines

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds WSLC-specific validation to reject configs where a deniedPaths entry is nested under a mounted readwritePaths/readonlyPaths parent, since WSLC’s flat volume-mount model cannot mask subtrees the way LXC/Bubblewrap can.

Changes:

  • Add a validate_denied_path_overlap pre-check in WSLC policy mapping to reject unenforceable nested denies.
  • Invoke the new validation during WSLC runner setup (after object-identity normalization and delegation checks).
  • Add unit tests covering overlap detection across path casing/separator variants and list combinations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/backends/wslc/common/src/wsl_container_runner.rs Calls the new denied-path overlap validator during WSLC request setup to fail fast on unenforceable configs.
src/backends/wslc/common/src/policy_mapping.rs Implements denied-path overlap validation logic and adds targeted unit tests for the new behavior.

Comment thread src/backends/wslc/common/src/policy_mapping.rs Outdated
@SohamDas2021
SohamDas2021 marked this pull request as ready for review July 14, 2026 22:31
@SohamDas2021
SohamDas2021 requested a review from a team as a code owner July 14, 2026 22:31
@azure-pipelines

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

/// check and fails closed on unresolvable paths when `deniedPaths` are present.
/// Treat this function as defense-in-depth for the flat-mount overlay gap, not
/// a complete traversal-safe deny on its own.
pub fn validate_denied_path_overlap(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the compare is textual, not path-aware

THE BUG

normalize_path_components lowercases + splits on / and ; is_strict_descendant does a
component-wise prefix compare. Nothing is canonicalized, so spellings that resolve into
a mounted tree pass the check:

  1. .. traversal (main one). Mount C:\project\sub.. (resolves to C:\project) while
    denying C:\project\secret. Textually mount = ["c:","project","sub",".."], not a prefix
    of ["c:","project","secret"] -> accepted, but secret is exposed. Same for denying
    C:\outside..\project\secrets against a C:\project mount.

  2. Bare root mount (line 197). \ or / normalizes to an empty vec, so
    if mounted_components.is_empty() { continue; } skips that mount entirely; anything
    denied under it is accepted. (C:\ -> ["c:"] is fine/tested; only the drive-less root
    spelling leaks.)

  3. Drive-relative (line 127). C:secrets splits into one fused token ["c:secrets"], which
    doesn't prefix-match a C:\ mount ["c:"] -> escapes.

  4. Aliases: \?\ prefixes, 8.3 short names, junctions/symlinks, trailing dots/spaces all
    differ textually but resolve into the mounted tree.

D6 does NOT cover these: normalize_object_conflicts only reconciles paths resolving to the
SAME object and only fails closed on UNRESOLVABLE paths. A mount vs a denied child are
different (parent/child) objects, and a resolvable .. spelling isn't unresolvable - so
neither layer catches it. The doc-comment claiming D6 handles this is wrong and should be
corrected.

Bonus false-positive (Medium): stripping leading separators makes \project\x (absolute)
and project\x (relative) identical, so a valid config can be wrongly rejected.

THE FIX (one change closes cases 1-3 + the false positive)

  1. Parse structurally instead of splitting strings: std::path::Path::components() on
    Windows (or typed-path for cross-build parsing) so Prefix and RootDir stay distinct -
    fixes C:secrets vs C:\ and \ vs project.

  2. Lexically fold . and .. on BOTH the mounted and denied sides before comparing, so
    C:\project\sub.. becomes C:\project. Pure lexical, no disk access.

  3. Where paths exist, canonicalize (std::fs::canonicalize / GetFinalPathNameByHandle) to
    resolve 8.3/junctions/symlinks. If deniedPaths are present and a path can't be
    resolved unambiguously, FAIL CLOSED rather than falling back to the textual compare.

  4. Use the same canonical form for the actual SDK mount, so you don't validate one path
    and mount another.

  5. Drop the empty-components continue: after structural parsing a root mount is an
    ancestor of everything on that drive - treat it as such (or reject when denies present).

TESTS TO ADD

  • .. on the denied side and on the mounted side -> rejected.
  • bare \ / root mount with a nested denied path -> rejected.
  • drive-relative C:secrets under C:\ -> rejected.
  • absolute \project\x vs relative project\x -> not a false rejection.
  • integration test at the runner call site (wsl_container_runner.rs:899): runner returns
    the overlap error and starts no container. The 11 unit tests only cover the pure
    function; a wiring/ordering regression would pass them all.

@SohamDas2021 SohamDas2021 Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch!

I am fixing the lexical/structural half in this branch, will do the full alias canonicalization in a follow-up right after, hope that is alright?

@SohamDas2021
SohamDas2021 merged commit fd7e906 into main Jul 17, 2026
22 checks passed
@SohamDas2021
SohamDas2021 deleted the user/sodas/wslc-denied-path-overlap-validation branch July 17, 2026 19:59
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.

3 participants