0.9: Honor inheritDefaultEnv on LXC, Bubblewrap, and Seatbelt - #1195
Elliot (theelliotm) wants to merge 12 commits into
Conversation
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>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 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, andTERMdefaults 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.
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>
There was a problem hiding this comment.
🟡 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.cwdis allowed to be relative. Forcwd: "work", this setsHOME=work, whilebuild_argssubsequently changes intowork; inside the child,$HOMEthen resolves towork/workrather than the working directory promised by this default. DeriveHOMEfrom 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.cwdis copied intoHOMEunchanged, butlxc-attachlater executescd workbefore starting the workload. Consequently$HOME=workresolves to a nestedwork/workpath 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_runnerseparately expands~and (for the open path) anchors relative cwd values. Thus a cwd such asworkor a~/...policy fallback can leaveHOMErelative/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
…ATH explicitly sets always overwrites the baseline
There was a problem hiding this comment.
🔵 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.cwdis copied directly intoHOME. After Bubblewrap applies--chdir work,HOME=workresolves relative to the new directory (for example, aswork/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_directorycan return a relativeprocess.cwd, so this setsHOMEto that raw relative string. The attach wrapper first changes into that directory; from inside the child,HOME=workthen refers to a nestedwork/workpath rather than the actual working directory. Resolve the cwd against the same container-side base used byattach_run(or setHOMEafter the wrapper'scd) before building the default environment.
src/backends/seatbelt/common/src/seatbelt_runner.rs:244- A relative
process.cwdis passed through unchanged here.Command::current_dir("work")resolves it against the launcher directory, but the child receivesHOME=work; after thechdir, that value resolves to<launcher>/work/work, not the directory where the child is running. This also makes the exec path differ fromspawn_open, which already anchors relative paths. Anchor the resolved cwd once before using it for bothcurrent_dirandresolved_env.
- Files reviewed: 30/31 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 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.cwdis copied directly intoHOME, but--chdirchanges the process directory before launch. Forcwd: "work",HOME=workthen 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
HOMEdoes not make HOME name the directory entered by the wrapper. Withcwd: "work", the wrapper first changes intowork, after whichHOME=workrefers to a nestedwork/workpath. 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.cwdmakes the newHOMEpoint somewhere different from the actual working directory. For example,cwd: "work"is resolved byCommand::current_diragainst the launcher directory, but this passesHOME=work; once the child is in<launcher>/work,HOMEresolves as<launcher>/work/work. Anchor the value before passing it toresolved_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_environmenthandlesenv: []or a supplied block, this unconditionally addsPWDand overwrites any caller-providedPWD. That contradicts the stated empty/verbatim contract. Either gate this legacy workaround for the 0.9 explicit modes or explicitly reserve and documentPWDand 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
There was a problem hiding this comment.
🟢 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
…it also has a bash-supplied fallback.
There was a problem hiding this comment.
🔵 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.cwdis copied directly intoHOME. Bubblewrap resolves--chdirwithin the sandbox, but the resulting child cwd is absolute; leavingHOMErelative means shell~expansion points below that cwd instead of back to it. Normalize the sandbox start directory to the absolute path used by--chdirbefore using it asHOME, and test a relative cwd.
src/backends/lxc/common/src/lxc_runner.rs:66 start_directorymay be relative, so this sets a relativeHOMEeven thoughlxc-attachresolvescd "$1"from its container start directory. Forcwd: "work", the process starts in/workbutHOME=work, causing~to resolve as/work/work. Resolve the in-container cwd to the same absolute path used by the attach command before derivingHOME, and add a relative-cwd test.
src/backends/seatbelt/common/src/seatbelt_runner.rs:243- A relative
process.cwdis passed directly as the defaultHOME. The exec path resolves that cwd relative to the launcher whencurrent_dirruns, so the child starts in an absolute directory but receives a relativeHOME(for example,cwd: "work"yieldsHOME=work, making~resolve below the cwd rather than to it). Anchor the cwd with the existingabsolute_working_directoryhelper before passing it to both environment resolution andcurrent_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
|
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>
There was a problem hiding this comment.
🟡 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
Fix typographical errors in documentation regarding Homebrew and parser migration. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 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.envwithout a schema gate, and the v0.8 adapter preservesNoneversusSome([]), 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
📖 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:
process.envgets a default block ofPATH,HOME, andTERM.process.envstays empty, distinct from omitted.process.envis used verbatim.inheritDefaultEnvlayers a supplied environment over the default, with a caller entry replacing the same-named default rather than duplicating it.PATHis per-OS: Seatbelt keeps its existing value, Linux gets thesbindirectories.HOMEis 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_envmodule, sinceseatbelt_runneristarget_os = "macos"and would otherwise be untested off a Mac.IsolationSession and WSLc are not covered — see #1165.
🔗 References
Resolves #1153
Related:
inheritDefaultEnvon IsolationSession and WSLc, still open🔍 Validation
run_bwrap_environment_test.shandrun_lxc_env_09_test.sh, wired into the suite runners;run_seatbelt_environment_test.shextendedCargo.lockchanges only becauselxc_commonandseatbelt_commonnow depend onsemverto parse the schema version. No new crate enters the graph —semver = "1"is already a direct dependency ofwxc_common,bwrap_common,mxc_ffi, andisolation_session_bindings.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow