Summary
At the end of rustc_compile_action (rust/private/rustc.bzl, lines 1799-1808 on main @ 6d8595965412b9ca37704ed53e289c4c4be2a0bf), the CrateInfo provider is re-created with rustc_env = env:
if crate_info_dict != None:
crate_info_dict.update({
"rustc_env": env,
})
crate_info = rust_common.create_crate_info(
deps = depset(deps),
proc_macro_deps = depset(proc_macro_deps),
srcs = depset(srcs),
**crate_info_dict
)
env at this point was built at lines 1528-1531 as:
env = dict(ctx.configuration.default_shell_env)
env.update(env_from_args)
So env is a union of Bazel's default_shell_env (a host-level concern, re-injected by Bazel at action time) and env_from_args (the rustc-specific env assembled in construct_arguments). Persisting that union into CrateInfo.rustc_env means every arbitrary key the host happens to export — PATH, LIB, INCLUDE, LIBPATH, etc. — becomes part of the provider's advertised env for downstream consumers.
Why this breaks cc_toolchain link_env
construct_arguments pulls link_env from cc_toolchain via cc_common.get_environment_variables(...) (line 465), and for link-emitting targets applies it at line 1183:
env.update(link_env) # step 1 — cc_toolchain wins
...
env.update(crate_info.rustc_env) # step 2 — applied AFTER link_env
For a standalone rust_binary, step 1 correctly seeds PATH/LIB/INCLUDE from cc_toolchain, and step 2 is a no-op (empty rustc_env).
For a rust_test(crate = ":some_lib"), _rust_test_impl copies crate.rustc_env into the test's own rustc_env. Because the lib was typically built as an rlib (skips the link branch), its published rustc_env contains PATH (and LIB, INCLUDE, …) sourced from default_shell_env — not from cc_toolchain. Step 2 then overwrites the correct link_env values that step 1 just set.
Net effect: cc_toolchain's env_set / link_env is silently clobbered by default_shell_env values that hitch-hiked through a transitive rlib CrateInfo. The cc_toolchain contract — per-action env provided by the toolchain author — is violated, with no way for the toolchain author to opt out.
Who is affected
Any cc_toolchain that uses env_set on link actions with values diverging from default_shell_env:
- MSVC (Windows):
link.exe needs LIB / INCLUDE / LIBPATH from vcvars via env_set (no -sysroot equivalent). Without --action_env=PATH,LIB,INCLUDE,... aligning both envs, rust_test(crate = lib) fails with LNK1104 / LNK1181. Most MSVC users already set --action_env for cache-key stability, which masks the bug.
- MinGW (Windows-GNU):
gcc.exe → collect2.exe needs mingw64/bin on PATH to resolve MinGW runtime DLLs. Without it, rust_test(crate = lib) fails linking.
GCC-based Linux toolchains (including hermetic sysroot ones) don't trip this because they configure the linker via flags, not env_set — cc_common.get_environment_variables(...) returns {}, so there's nothing to clobber. The leak still exists in the CrateInfo, it just has no observable effect. Any future cc_toolchain that opts into env_set on link actions — Linux or otherwise — would hit the same bug.
Reproduction (toolchain-agnostic)
- Any Windows
cc_toolchain (MSVC auto-configured, or MinGW via env_set) whose link_env["PATH"] (or LIB/INCLUDE) differs from ctx.configuration.default_shell_env.
rust_library(name = "foo", ...) with no explicit rustc_env.
rust_test(name = "foo_test", crate = ":foo").
bazel build //:foo_test without a global --action_env=PATH,LIB,INCLUDE,... propagating the toolchain values into default_shell_env.
Observed: linker fails because the action's final PATH/LIB is whatever default_shell_env happens to contain (inherited through foo's CrateInfo.rustc_env), not what cc_toolchain declared.
Expected: cc_toolchain.link_env wins over host default_shell_env during rules_rust link actions. No downstream target should be able to shadow toolchain env merely by being depended on.
Diagnostic output
Probes placed in rustc.bzl confirm the chain (label some_lib is the rlib dep, some_test is rust_test(crate = ":some_lib")):
# rustc_compile_action for the rlib — BEFORE the re-publish at line 1799:
post-create label=...some_lib type=rlib rustc_env.keys=[]
# _rust_test_impl reading the published provider later:
crate.rustc_env.keys=["CARGO_*...", "PATH", "REPOSITORY_NAME"]
crate.rustc_env.PATH=<default_shell_env value — NOT cc_toolchain's>
# During rust_test's own rustc action:
step01 after link_env PATH=<cc_toolchain value> # correct
step06 after rustc_env PATH=<default_shell_env> # CLOBBERED
The env.update(crate_info.rustc_env) at step06 is the point of corruption; the root cause is the re-publish at line 1801 which persisted default_shell_env into CrateInfo.
Proposed fix
rust/private/rustc.bzl lines 1799-1802 — save env_from_args (captured at line 1474) instead of the merged env:
if crate_info_dict != None:
crate_info_dict.update({
- "rustc_env": env,
+ "rustc_env": env_from_args,
})
env_from_args is already the rustc-specific env (CARGO_*, REPOSITORY_NAME, link_env contents for linking targets, extra_rustc_env, etc.) — exactly what a CrateInfo should advertise. default_shell_env stays where it belongs: injected by Bazel at action execution, not encoded into provider state.
Conservative alternative, if maintainers are worried about unknown consumers relying on the leak:
persistent_env = {k: v for k, v in env.items() if k not in ctx.configuration.default_shell_env}
crate_info_dict.update({"rustc_env": persistent_env})
Workaround (until fixed)
Setting --@rules_rust//rust/settings:extra_rustc_env=PATH=... scoped via .bazelrc unbreaks this, because extra_rustc_env is applied after crate_info.rustc_env in construct_arguments and wins the final merge. Unlike --action_env=PATH, it's scoped to rustc actions, so it doesn't invalidate C/C++/other-language cache keys. Still a band-aid: the root issue is that CrateInfo shouldn't carry default_shell_env in the first place.
Environment
rules_rust main at 6d8595965412b9ca37704ed53e289c4c4be2a0bf
- Bazel 9.1.0 (bzlmod)
- Reproduced on MinGW (
x86_64-pc-windows-gnu) with a custom cc_toolchain using env_set for c++-link-executable. MSVC exhibits the same failure mode whenever default_shell_env and cc_toolchain.link_env disagree. Not observed on GCC-based Linux toolchains (including hermetic ones) because they don't use env_set on link actions — the leak exists but has nothing to clobber.
Happy to send a PR with the one-line fix plus a regression test that asserts CrateInfo.rustc_env doesn't contain any key from ctx.configuration.default_shell_env for a simple rlib target.
Summary
At the end of
rustc_compile_action(rust/private/rustc.bzl, lines 1799-1808 onmain@6d8595965412b9ca37704ed53e289c4c4be2a0bf), theCrateInfoprovider is re-created withrustc_env = env:envat this point was built at lines 1528-1531 as:So
envis a union of Bazel'sdefault_shell_env(a host-level concern, re-injected by Bazel at action time) andenv_from_args(the rustc-specific env assembled inconstruct_arguments). Persisting that union intoCrateInfo.rustc_envmeans every arbitrary key the host happens to export —PATH,LIB,INCLUDE,LIBPATH, etc. — becomes part of the provider's advertised env for downstream consumers.Why this breaks
cc_toolchainlink_envconstruct_argumentspullslink_envfromcc_toolchainviacc_common.get_environment_variables(...)(line 465), and for link-emitting targets applies it at line 1183:For a standalone
rust_binary, step 1 correctly seedsPATH/LIB/INCLUDEfromcc_toolchain, and step 2 is a no-op (emptyrustc_env).For a
rust_test(crate = ":some_lib"),_rust_test_implcopiescrate.rustc_envinto the test's ownrustc_env. Because the lib was typically built as anrlib(skips the link branch), its publishedrustc_envcontainsPATH(andLIB,INCLUDE, …) sourced fromdefault_shell_env— not fromcc_toolchain. Step 2 then overwrites the correctlink_envvalues that step 1 just set.Net effect:
cc_toolchain'senv_set/link_envis silently clobbered bydefault_shell_envvalues that hitch-hiked through a transitive rlibCrateInfo. Thecc_toolchaincontract — per-action env provided by the toolchain author — is violated, with no way for the toolchain author to opt out.Who is affected
Any
cc_toolchainthat usesenv_seton link actions with values diverging fromdefault_shell_env:link.exeneedsLIB/INCLUDE/LIBPATHfromvcvarsviaenv_set(no-sysrootequivalent). Without--action_env=PATH,LIB,INCLUDE,...aligning both envs,rust_test(crate = lib)fails withLNK1104/LNK1181. Most MSVC users already set--action_envfor cache-key stability, which masks the bug.gcc.exe→collect2.exeneedsmingw64/binonPATHto resolve MinGW runtime DLLs. Without it,rust_test(crate = lib)fails linking.GCC-based Linux toolchains (including hermetic sysroot ones) don't trip this because they configure the linker via flags, not
env_set—cc_common.get_environment_variables(...)returns{}, so there's nothing to clobber. The leak still exists in theCrateInfo, it just has no observable effect. Any futurecc_toolchainthat opts intoenv_seton link actions — Linux or otherwise — would hit the same bug.Reproduction (toolchain-agnostic)
cc_toolchain(MSVC auto-configured, or MinGW viaenv_set) whoselink_env["PATH"](orLIB/INCLUDE) differs fromctx.configuration.default_shell_env.rust_library(name = "foo", ...)with no explicitrustc_env.rust_test(name = "foo_test", crate = ":foo").bazel build //:foo_testwithout a global--action_env=PATH,LIB,INCLUDE,...propagating the toolchain values intodefault_shell_env.Observed: linker fails because the action's final
PATH/LIBis whateverdefault_shell_envhappens to contain (inherited throughfoo'sCrateInfo.rustc_env), not whatcc_toolchaindeclared.Expected:
cc_toolchain.link_envwins over hostdefault_shell_envduringrules_rustlink actions. No downstream target should be able to shadow toolchain env merely by being depended on.Diagnostic output
Probes placed in
rustc.bzlconfirm the chain (labelsome_libis the rlib dep,some_testisrust_test(crate = ":some_lib")):The
env.update(crate_info.rustc_env)at step06 is the point of corruption; the root cause is the re-publish at line 1801 which persisteddefault_shell_envintoCrateInfo.Proposed fix
rust/private/rustc.bzllines 1799-1802 — saveenv_from_args(captured at line 1474) instead of the mergedenv:env_from_argsis already the rustc-specific env (CARGO_*,REPOSITORY_NAME,link_envcontents for linking targets,extra_rustc_env, etc.) — exactly what aCrateInfoshould advertise.default_shell_envstays where it belongs: injected by Bazel at action execution, not encoded into provider state.Conservative alternative, if maintainers are worried about unknown consumers relying on the leak:
Workaround (until fixed)
Setting
--@rules_rust//rust/settings:extra_rustc_env=PATH=...scoped via.bazelrcunbreaks this, becauseextra_rustc_envis applied aftercrate_info.rustc_envinconstruct_argumentsand wins the final merge. Unlike--action_env=PATH, it's scoped to rustc actions, so it doesn't invalidate C/C++/other-language cache keys. Still a band-aid: the root issue is thatCrateInfoshouldn't carrydefault_shell_envin the first place.Environment
rules_rustmainat6d8595965412b9ca37704ed53e289c4c4be2a0bfx86_64-pc-windows-gnu) with a customcc_toolchainusingenv_setforc++-link-executable. MSVC exhibits the same failure mode wheneverdefault_shell_envandcc_toolchain.link_envdisagree. Not observed on GCC-based Linux toolchains (including hermetic ones) because they don't useenv_seton link actions — the leak exists but has nothing to clobber.Happy to send a PR with the one-line fix plus a regression test that asserts
CrateInfo.rustc_envdoesn't contain any key fromctx.configuration.default_shell_envfor a simple rlib target.