Skip to content

0.9: Honor inheritDefaultEnv on LXC, Bubblewrap, and Seatbelt - #1195

Open
Elliot (theelliotm) wants to merge 12 commits into
mainfrom
user/emichlin/consistent-PATH-behavior
Open

Elliot (theelliotm) wants to merge 12 commits into
mainfrom
user/emichlin/consistent-PATH-behavior

Conversation

@theelliotm

@theelliotm Elliot (theelliotm) commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

📖 Description

The Linux and MacOS backends had no standard default environment. This PR gives all three the same schema-0.9 contract the Windows process container got in #1120, implemented per-backend:

  • An omitted process.env gets a default block of PATH, HOME, and TERM.
  • An explicitly empty process.env stays empty, distinct from omitted.
  • A supplied process.env is used verbatim.
  • inheritDefaultEnv layers a supplied environment over the default, with a caller entry replacing the same-named default rather than duplicating it.

PATH is per-OS: Seatbelt keeps its existing value, Linux gets the sbin directories. HOME is the resolved working directory (else /tmp), since the child runs as the launching uid and that user's real home is not reachable under the sandbox's filesystem policy.

Scope

Behavior below 0.9 is unchanged on all three. IsolationSession and WSLc are not covered and are tracked separately.

Seatbelt's resolution lives in a new host-agnostic default_env module, since seatbelt_runner is target_os = "macos" and would otherwise be untested off a Mac.

IsolationSession and WSLc are not covered — see #1165.

🔗 References

Resolves #1153

Related:

🔍 Validation

  • Unit tests per backend for all four states plus the sub-0.9 passthrough
  • run_bwrap_environment_test.sh and run_lxc_env_09_test.sh, wired into the suite runners; run_seatbelt_environment_test.sh extended
  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo clippy for x86_64-unknown-linux-gnu and aarch64-apple-darwin
  • cargo test for the touched crates
  • node scripts/versioning/validate-configs.js

Cargo.lock changes only because lxc_common and seatbelt_common now depend on semver to parse the schema version. No new crate enters the graph — semver = "1" is already a direct dependency of wxc_common, bwrap_common, mxc_ffi, and isolation_session_bindings.

✅ Checklist

📋 Issue Type

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

Elliot (theelliotm) and others added 3 commits September 14, 2026 17:40
The Unix backends had no default environment: Bubblewrap supplied none at all,
so `PATH` came from the shell's compiled-in default and a nightly run failed on
RHEL 10 where that value omits the `sbin` directories. LXC and Seatbelt each
had their own unrelated behavior.

Gives all three the same schema-0.9 contract the Windows process container got
in #1120, implemented per-backend:

* An omitted `process.env` gets a default block of `PATH`, `HOME`, and `TERM`.
* An explicitly empty `process.env` stays empty, distinct from omitted.
* A supplied `process.env` is used verbatim.
* `inheritDefaultEnv` layers a supplied environment over the default, with a
  caller entry replacing the same-named default rather than duplicating it.

`PATH` is per-OS: Seatbelt keeps its existing value, Linux gets the `sbin`
directories. `HOME` is the resolved working directory (else `/tmp`), since the
child runs as the launching uid and that user's real home is not reachable
under the sandbox's filesystem policy.

Behavior below 0.9 is unchanged on all three. IsolationSession and WSLc are not
covered and are tracked separately.

Seatbelt's resolution lives in a new host-agnostic `default_env` module, since
`seatbelt_runner` is `target_os = "macos"` and would otherwise be untested off
a Mac.

Tests

* Unit tests per backend for all four states plus the sub-0.9 passthrough
* `run_bwrap_environment_test.sh` and `run_lxc_env_09_test.sh`, wired into the
  suite runners; `run_seatbelt_environment_test.sh` extended
* cargo fmt --all -- --check
* cargo clippy --workspace --all-targets -- -D warnings
* cargo clippy for x86_64-unknown-linux-gnu and aarch64-apple-darwin
* cargo test for the touched crates
* node scripts/versioning/validate-configs.js

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Conflict resolutions:
- lxc_runner.rs: keep resolved_env(request) for the exec environment on top of
  main's ContainerFirewall/plan_network changes; take main's trimmed comments.
- config_parser.rs: differential corpus inventory is (379, 355, 14) -- main's
  converged baseline plus the twelve 0.9 inheritDefaultEnv fixtures.
- version-specific-parser-migration-inventory.md: match the new counts.
- lxc-backend.md: keep main's rewrite and re-apply the 0.9 default-env section.
- run_lxc_all_tests.sh: keep both new suites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The merge resolution rewrote the file with LF, which made it diff as 180
changed lines instead of the 18 that were actually added. main stores this
file with CRLF; match it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 17, 2026 00:46
@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.

🟡 Changes recommended

Default HOME can disagree with the actual working directory, and Seatbelt still injects or overwrites PWD for supposedly empty or verbatim environments.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Standardizes schema-0.9 default environment handling across LXC, Bubblewrap, and Seatbelt.

Changes:

  • Adds backend-specific PATH, HOME, and TERM defaults with inheritance handling.
  • Adds unit and end-to-end coverage for all environment modes.
  • Documents the new behavior and updates parser fixtures.
File summaries
File Description
tests/scripts/run_seatbelt_environment_test.sh Extends Seatbelt environment tests.
tests/scripts/run_lxc_env_09_test.sh Adds LXC environment tests.
tests/scripts/run_lxc_all_tests.sh Registers the LXC test.
tests/scripts/run_bwrap_environment_test.sh Adds Bubblewrap environment tests.
tests/scripts/run_bwrap_all_tests.sh Registers the Bubblewrap test.
tests/configs/seatbelt_env_09_verbatim.json Tests verbatim Seatbelt environment.
tests/configs/seatbelt_env_09_inherit.json Tests Seatbelt inheritance.
tests/configs/seatbelt_env_09_empty.json Tests empty Seatbelt environment.
tests/configs/seatbelt_env_09_default_block.json Tests Seatbelt defaults.
tests/configs/lxc_env_09_verbatim.json Tests verbatim LXC environment.
tests/configs/lxc_env_09_inherit.json Tests LXC inheritance.
tests/configs/lxc_env_09_empty.json Tests empty LXC environment.
tests/configs/lxc_env_09_default_block.json Tests LXC defaults.
tests/configs/bwrap_env_09_verbatim.json Tests verbatim Bubblewrap environment.
tests/configs/bwrap_env_09_inherit.json Tests Bubblewrap inheritance.
tests/configs/bwrap_env_09_empty.json Tests empty Bubblewrap environment.
tests/configs/bwrap_env_09_default_block.json Tests Bubblewrap defaults.
src/core/wxc_common/src/models.rs Updates environment semantics documentation.
src/core/wxc_common/src/config_parser.rs Updates parser inventory counts.
src/Cargo.lock Records added semver dependencies.
src/backends/seatbelt/common/src/seatbelt_runner.rs Applies resolved Seatbelt environments.
src/backends/seatbelt/common/src/lib.rs Exposes the environment module.
src/backends/seatbelt/common/src/default_env.rs Implements Seatbelt defaults and merging.
src/backends/seatbelt/common/Cargo.toml Adds semver.
src/backends/lxc/common/src/lxc_runner.rs Implements LXC defaults and merging.
src/backends/lxc/common/Cargo.toml Adds semver.
src/backends/bubblewrap/common/src/bwrap_command.rs Implements Bubblewrap defaults and merging.
docs/version-specific-parser-migration-inventory.md Updates fixture totals.
docs/seatbelt/seatbelt-backend.md Documents Seatbelt environment behavior.
docs/lxc-support/lxc-backend.md Documents LXC environment behavior.
docs/bwrap-support/bubblewrap-backend.md Documents Bubblewrap environment behavior.
Review details
  • Files reviewed: 30/31 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/backends/bubblewrap/common/src/bwrap_command.rs Outdated
Comment thread src/backends/lxc/common/src/lxc_runner.rs Outdated
Comment thread src/backends/seatbelt/common/src/default_env.rs Outdated
Comment thread src/backends/seatbelt/common/src/seatbelt_runner.rs
Elliot (theelliotm) and others added 2 commits September 17, 2026 11:28
The three 0.9 environment suites asserted PATH=[] for the two cases that
supply no PATH. A shell started without a PATH assigns its own compiled-in
fallback, so \ reports the shell's default rather than what MXC
passed and is never empty. On Debian that fallback is byte-for-byte the
Linux default block, and lxc-attach injects the same string into every
process it attaches, so neither its presence nor its absence proved
anything. macOS /bin/sh fabricates TERM=dumb the same way.

Assert HOME and TERM instead -- neither is fabricated, and both would be
set had the default block been applied. That resolved_env is exactly empty
is already asserted directly by the per-backend unit tests, which read the
environment MXC builds rather than the child's view of it.

Also make the host-leak assertions real: the 0.9 fixtures never echoed
MXC_LEAK_PROBE, so grepping the output for its value could never match and
the assertions passed vacuously. The fixtures now print it, which also
distinguishes a shell-fabricated TERM from an inherited one. The inherit
fixtures override TERM with vt100 rather than dumb so the override is
distinguishable from the macOS shell default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 17, 2026 20:08

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.

🟡 Changes recommended

Relative working directories can produce incorrect HOME values across all three updated backends.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

src/backends/bubblewrap/common/src/bwrap_command.rs:592

  • process.cwd is allowed to be relative. For cwd: "work", this sets HOME=work, while build_args subsequently changes into work; inside the child, $HOME then resolves to work/work rather than the working directory promised by this default. Derive HOME from the effective absolute sandbox cwd (or pass the final chdir path into this resolver), and cover the relative-cwd case.
    let home = request
        .resolved_working_directory()
        .map(|dir| dir.path.to_string())
        .unwrap_or_else(|| FALLBACK_HOME.to_string());

src/backends/lxc/common/src/lxc_runner.rs:55

  • A relative process.cwd is copied into HOME unchanged, but lxc-attach later executes cd work before starting the workload. Consequently $HOME=work resolves to a nested work/work path from the child's actual cwd instead of naming that cwd as documented. Resolve the effective absolute container cwd once and use it for both the attach command and this default.
    let home = request
        .resolved_working_directory()
        .map(|dir| dir.path.to_string())
        .unwrap_or_else(|| FALLBACK_HOME.to_string());

src/backends/seatbelt/common/src/default_env.rs:42

  • This uses the generic, unnormalized resolver, while seatbelt_runner separately expands ~ and (for the open path) anchors relative cwd values. Thus a cwd such as work or a ~/... policy fallback can leave HOME relative/unexpanded even though the child runs in a different normalized directory, breaking tools that use $HOME. Pass the runner's effective normalized cwd into environment resolution so both values agree.
    let home = request
        .resolved_working_directory()
        .map(|dir| dir.path.to_string())
        .unwrap_or_else(|| FALLBACK_HOME.to_string());
  • Files reviewed: 30/31 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread docs/lxc-support/lxc-backend.md
…ATH explicitly sets always overwrites the baseline
Copilot AI review requested due to automatic review settings September 17, 2026 20:41

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.

🔵 Needs a closer look

Relative process.cwd values produce an incorrect relative HOME across all three updated backends.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

src/backends/bubblewrap/common/src/bwrap_command.rs:603

  • A relative process.cwd is copied directly into HOME. After Bubblewrap applies --chdir work, HOME=work resolves relative to the new directory (for example, as work/work) instead of naming the directory where the child actually runs. Resolve the cwd to the same absolute target used by --chdir, or reject relative cwd values before constructing the default block.
    src/backends/lxc/common/src/lxc_runner.rs:66
  • start_directory can return a relative process.cwd, so this sets HOME to that raw relative string. The attach wrapper first changes into that directory; from inside the child, HOME=work then refers to a nested work/work path rather than the actual working directory. Resolve the cwd against the same container-side base used by attach_run (or set HOME after the wrapper's cd) before building the default environment.
    src/backends/seatbelt/common/src/seatbelt_runner.rs:244
  • A relative process.cwd is passed through unchanged here. Command::current_dir("work") resolves it against the launcher directory, but the child receives HOME=work; after the chdir, that value resolves to <launcher>/work/work, not the directory where the child is running. This also makes the exec path differ from spawn_open, which already anchors relative paths. Anchor the resolved cwd once before using it for both current_dir and resolved_env.
  • Files reviewed: 30/31 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 17, 2026 20:50

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.

🔵 Needs a closer look

Relative working directories produce incorrect HOME values, and Seatbelt still injects or overwrites PWD in supposedly empty or verbatim environments.

Review details

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

src/backends/bubblewrap/common/src/bwrap_command.rs:603

  • A relative process.cwd is copied directly into HOME, but --chdir changes the process directory before launch. For cwd: "work", HOME=work then resolves beneath the new directory rather than naming it, contrary to this function's contract. Normalize the actual target to an absolute path or reject relative cwd before building the environment.
    src/backends/lxc/common/src/lxc_runner.rs:66
  • Using the raw relative cwd as HOME does not make HOME name the directory entered by the wrapper. With cwd: "work", the wrapper first changes into work, after which HOME=work refers to a nested work/work path. Resolve the container-relative target to an absolute path, or reject relative cwd before constructing the default environment.
    src/backends/seatbelt/common/src/seatbelt_runner.rs:243
  • A relative process.cwd makes the new HOME point somewhere different from the actual working directory. For example, cwd: "work" is resolved by Command::current_dir against the launcher directory, but this passes HOME=work; once the child is in <launcher>/work, HOME resolves as <launcher>/work/work. Anchor the value before passing it to resolved_env, or reject relative cwd before applying this contract.

src/backends/seatbelt/common/src/seatbelt_runner.rs:256

  • The final Seatbelt environment is still not verbatim for schema 0.9: after apply_clean_environment handles env: [] or a supplied block, this unconditionally adds PWD and overwrites any caller-provided PWD. That contradicts the stated empty/verbatim contract. Either gate this legacy workaround for the 0.9 explicit modes or explicitly reserve and document PWD and adjust the contract/tests.
    let cwd = resolved_cwd.unwrap_or_else(|| UNRESOLVED_WORKING_DIRECTORY.to_string());
    command.current_dir(&cwd);
    command.env("PWD", &cwd);
  • Files reviewed: 30/31 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 17, 2026 21:11

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.

🟢 Approval recommended

The implementation consistently preserves version boundaries, applies the four-state contract across all scoped backends, and includes focused unit, integration, and documentation updates.

Review details
  • Files reviewed: 30/31 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 17, 2026 22:54

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.

🔵 Needs a closer look

Relative working directories produce incorrect relative HOME values across all three backends.

Review details

Suppressed comments (4)

Previously missed (4) — in code that hasn't changed since the last review.

src/backends/bubblewrap/common/src/bwrap_command.rs:603

  • A relative process.cwd is copied directly into HOME. Bubblewrap resolves --chdir within the sandbox, but the resulting child cwd is absolute; leaving HOME relative means shell ~ expansion points below that cwd instead of back to it. Normalize the sandbox start directory to the absolute path used by --chdir before using it as HOME, and test a relative cwd.
    src/backends/lxc/common/src/lxc_runner.rs:66
  • start_directory may be relative, so this sets a relative HOME even though lxc-attach resolves cd "$1" from its container start directory. For cwd: "work", the process starts in /work but HOME=work, causing ~ to resolve as /work/work. Resolve the in-container cwd to the same absolute path used by the attach command before deriving HOME, and add a relative-cwd test.
    src/backends/seatbelt/common/src/seatbelt_runner.rs:243
  • A relative process.cwd is passed directly as the default HOME. The exec path resolves that cwd relative to the launcher when current_dir runs, so the child starts in an absolute directory but receives a relative HOME (for example, cwd: "work" yields HOME=work, making ~ resolve below the cwd rather than to it). Anchor the cwd with the existing absolute_working_directory helper before passing it to both environment resolution and current_dir, and cover the relative-cwd case.
    docs/seatbelt/seatbelt-backend.md:442
  • Correct the typo “ons” to “on”.
  • Files reviewed: 30/31 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@theelliotm
Elliot (theelliotm) marked this pull request as ready for review September 18, 2026 00:14
@theelliotm
Elliot (theelliotm) requested a review from a team September 18, 2026 00:14
@theelliotm
Elliot (theelliotm) requested a review from a team as a code owner September 18, 2026 00:14
@azure-pipelines

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

Resolved the differential corpus inventory in config_parser.rs: both sides
added corpus fixtures, so the expected totals combine to (380, 356, 14).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 18, 2026 18:55

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.

🟡 Changes recommended

Relative working directories produce incorrect HOME values across all three updated backends.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 30/31 changed files
  • Comments generated: 6
  • Review effort level: Balanced

Comment thread src/backends/bubblewrap/common/src/bwrap_command.rs
Comment thread src/backends/lxc/common/src/lxc_runner.rs
Comment thread src/backends/seatbelt/common/src/seatbelt_runner.rs
Comment thread docs/lxc-support/lxc-backend.md
Comment thread docs/seatbelt/seatbelt-backend.md Outdated
Comment thread docs/version-specific-parser-migration-inventory.md Outdated
Fix typographical errors in documentation regarding Homebrew and parser migration.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 18, 2026 19:10

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.

🔵 Needs a closer look

The shared model documentation incorrectly describes pre-0.9 Windows environment behavior.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/core/wxc_common/src/models.rs:1020

  • This incorrectly says pre-0.9 requests flatten omitted and explicitly empty environments for every backend. The Windows process-container paths match on request.env without a schema gate, and the v0.8 adapter preserves None versus Some([]), so Windows honors the distinction at older supported versions too. Limit the pre-0.9 statement to the non-Windows backends.
  • Files reviewed: 30/31 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 22, 2026 00:24

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.

Copilot review overview

🟢 Approval recommended

The implementation consistently preserves version gates, backend isolation, and the documented four-state contract with comprehensive coverage.

Review effort: Balanced
Findings: None

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.

Default PATH behavior is inconsistent across Linux and macOS backends

2 participants