[Cross-backend] process.cwd is silently discarded or resolved against host state
Nobody has claimed this yet.
- 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.cwdis 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
- On a Windows host with WSL2 +
wslcsdk.dll, build with--features wslc. - Run a one-shot WSLc execution with
process.commandLine = "pwd"andprocess.cwd = "/workspace"(viawxc-exec --experimentalwith a JSON config, or viamxc_sdk::build_request_with_containment(..., &Containment::Wslc, ...)+set_working_directory("/workspace")). - 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:
- The minimal fix is to turn the
if letinto amatchwith anErr(...)arm so an untranslatable cwd fails the launch. - 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:802takesprocess.cwdverbatim (process.cwd.unwrap_or_default()).validator.rshas no rule forworking_directoryat 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 viaCreateProcessW(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):
mkdir -p /tmp/mxccwd/sub- Build a policy granting only
/tmp/mxccwdasreadwritePaths, then:let mut request = build_request(&policy, None)?; request.set_script("pwd").set_working_directory("sub"); let output = run(request)?; - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.