microsoft / microsoft/mxc

[Cross-backend] process.cwd is silently discarded or resolved against host state

Open
#902 4 comments 0 reactions 1 assignee View on GitHub

Nobody has claimed this yet.

Area-Executor-Schema Container-WSLC Issue-Bug OS-Linux OS-MacOS OS-Windows Priority1
Dominant language
Rust
Stars
1.3k
Forks
79
Avg merge
2d 7h
Merged PRs (30d)
117

Description

Both defects in how process.cwd is handled: one drops the value on WSLc one-shot, the other resolves a relative path against host state across every backend. Both fail silently.

This consolidates 2 separately filed issues so the backlog carries one entry per area instead of one per finding. Every original report is reproduced below in full, unedited. The originals are closed and point here.

Folded from Status Title
#872 Open WSLc one-shot silently ignores process.cwd when it is not a Windows drive path
#873 Open Relative process.cwd is resolved against the host process's working directory

WSLc one-shot silently ignores process.cwd when it is not a Windows drive path

Originally #872.

Relevant area(s)

Windows

Brief description of your issue

The WSLc one-shot runner silently discards process.cwd when the value is not a Windows drive path, so the sandboxed process runs in the container's default working directory with no error and no warning.

wsl_container_runner.rs:1429 maps the cwd through policy_mapping::windows_path_to_container_path inside an if let Some(...) with no else branch:

let _cwd_cstr;
if !request.working_directory.is_empty() {
    if let Some(container_cwd) =
        policy_mapping::windows_path_to_container_path(&request.working_directory)
    {
        // ... WslcSetProcessSettingsWorkingDirectory ...
    }
    // no else: an untranslatable cwd is dropped on the floor
}

windows_path_to_container_path returns None for anything that is not <drive>: followed by \, /, or end-of-string (backends/wslc/common/src/policy_mapping.rs:49). An absolute in-container path such as /workspace therefore fails translation and is dropped.

This contradicts the documented contract in docs/schema.md ("Working Directory"):

process.cwd is optional. When it is set, it is passed to the backend verbatim — an unusable value fails the launch rather than being silently replaced.

It also contradicts the WSLc state-aware surface, which handles the same field in the opposite direction and explicitly documents the behavior this code violates (backends/wslc/common/src/container_steps.rs:422):

// Working directory (an absolute in-container path, e.g. `/work`; empty =
// container default). It is passed straight to the SDK; a non-absolute
// value is rejected rather than silently ignored.

The two WSLc surfaces accept disjoint path shapes for the same config field:

process.cwd WSLc one-shot WSLc state-aware
/workspace silently ignored → wrong cwd accepted
C:\work translated to a container path rejected (must be an absolute in-container path)

Reachable from the Rust SDK (mxc-sdk), which supports WSLc streaming:

let mut req = mxc_sdk::build_request_with_containment(&policy, &Containment::Wslc, None)?;
req.set_experimental(true);
req.set_script("pwd").set_working_directory("/workspace");
let out = mxc_sdk::run(req)?;

...and equally from a JSON config with "process": { "cwd": "/workspace" } on the one-shot WSLc path.

Steps to reproduce
  1. On a Windows host with WSL2 + wslcsdk.dll, build with --features wslc.
  2. Run a one-shot WSLc execution with process.commandLine = "pwd" and process.cwd = "/workspace" (via wxc-exec --experimental with a JSON config, or via mxc_sdk::build_request_with_containment(..., &Containment::Wslc, ...) + set_working_directory("/workspace")).
  3. Observe the printed working directory.
Expected behavior

Either the cwd is honored, or the launch fails with a policy_validation / malformed-request error naming the unusable value — matching the documented "an unusable value fails the launch rather than being silently replaced" and the state-aware surface's explicit "rejected rather than silently ignored".

Actual behavior

The cwd is silently dropped and the process runs in the container's default working directory. No error, no warning, nothing in the logs.


Two things to decide when fixing:

  1. The minimal fix is to turn the if let into a match with an Err(...) arm so an untranslatable cwd fails the launch.
  2. Whether one-shot and state-aware WSLc should agree on the accepted path shape (Windows host path vs. absolute in-container path) is a separate design call — one-shot maps volumes from Windows paths, state-aware does not. Worth resolving deliberately rather than as part of the fail-loudly fix.

Relative process.cwd is resolved against the host process's working directory

Originally #873.

Relevant area(s)

Linux, macOS, Windows

Brief description of your issue

A relative process.cwd is never validated, so it is resolved against the host process's current directory. The sandboxed child then runs in a directory chosen by host state that the policy never granted — silently, with a successful exit.

Nothing in the config pipeline checks the shape of the value:

  • config_parser.rs:802 takes process.cwd verbatim (process.cwd.unwrap_or_default()).
  • validator.rs has no rule for working_directory at all (it only appears in a test fixture).
  • Backends hand it straight to the OS: Seatbelt via Command::current_dir (seatbelt_runner.rs:226), Bubblewrap via --chdir (bwrap_command.rs:260), Windows ProcessContainer via CreateProcessW (working_directory.rs:71).

For an absolute path this is correct and matches the documented contract. For a relative path, current_dir / --chdir resolve it against the launching process's cwd, so the effective sandbox cwd depends on where the host binary happened to be invoked from.

This defeats the reason the resolution logic exists in the first place (core/wxc_common/src/models.rs:771):

Backends that must not let the child inherit the host process's cwd use this to fall back to a policy-granted path.

An explicit relative cwd re-introduces exactly that inheritance through the back door.

Steps to reproduce

On macOS (same shape applies to Bubblewrap on Linux and ProcessContainer on Windows):

  1. mkdir -p /tmp/mxccwd/sub
  2. Build a policy granting only /tmp/mxccwd as readwritePaths, then:
    let mut request = build_request(&policy, None)?;
    request.set_script("pwd").set_working_directory("sub");
    let output = run(request)?;
    
  3. Run the resulting binary from /tmp/mxccwd.

Equivalently via a JSON config with "process": { "cwd": "sub" }.

Expected behavior

A relative process.cwd is rejected with a malformed-request / policy_validation error naming the value. The sandboxed child's working directory should never depend on the host process's cwd.

Actual behavior

Observed output from the reproduction above:

host_cwd="/private/tmp/mxccwd" requested="sub" -> "/private/tmp/mxccwd/sub" (Exited(0))

The child ran in a host-relative directory, exit code 0, no error and no warning. Invoking the same binary from a different directory silently changes the sandbox's working directory.


Note for whoever picks this up: the check is not a shared one-liner. "Absolute" is backend-relative — C:\x is not absolute per Path::is_absolute on Unix, and /x is a valid in-container path for WSLc but not a valid Windows host path. The rule likely belongs at each backend's resolution point (or as a per-backend predicate passed into the shared helper), not as a single check in validator.rs.

Related: #872 (WSLc one-shot silently drops an untranslatable process.cwd) — same field, same class of "wrong working directory, no diagnostic" failure, but a separate root cause and fix.


Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.