Skip to content

rustc_compile_action leaks default_shell_env into published CrateInfo.rustc_env, clobbering cc_toolchain link_env in downstream rust_test(crate = ...) #3989

@JSGette

Description

@JSGette

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.execollect2.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_setcc_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)

  1. 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.
  2. rust_library(name = "foo", ...) with no explicit rustc_env.
  3. rust_test(name = "foo_test", crate = ":foo").
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions