Summary
build_env_cache sorts its names by StringId handle, and env_cache_find binary-searches the result by name:
// src/exec/scheduler.cpp:62 — handle order, i.e. interning order
std::sort(names.begin(), names.end());
// src/exec/scheduler.cpp:44 — a name comparator over that same range
auto pos = std::lower_bound(cache.begin(), cache.end(), key,
[&pool](auto const& p, std::string_view k) { return pool.get(p.first) < k; });
lower_bound requires the range to be partitioned with respect to the comparator it is given. The range is ordered by handle and searched by name, so the lookup returns nullptr for entries that are present. prepare_job_launch (src/exec/scheduler.cpp:129-137) then omits the variable, and since base_child_env() contributes only PATH=, the variable is absent from the child process entirely.
Interning order here is the order the export directives were parsed, so the bug is triggered by the order the exports are written in the Tupfile.
Reproduced
Two projects, identical except for the order of three export lines. Rule is : |> sh -c "env | sort > %o" |> out.txt, both run with ZVAR=zzz MVAR=mmm AVAR=aaa:
export AVAR / export MVAR / export ZVAR -> 3 of 3 present: AVAR=aaa MVAR=mmm ZVAR=zzz
export ZVAR / export MVAR / export AVAR -> 0 of 3 present
Both builds exit 0 and report success. The command simply runs with a silently wrong environment.
A single exported variable always works, because lower_bound over a one-element range cannot be wrong — which is why test/unit/test_exec.cpp:437 ("Scheduler exported_vars", one variable) passes today.
Severity
Silent wrong build output, no diagnostic, exit 0. It contradicts what docs/reference.md:1099-1101 promises for an exported variable.
Fix
One comparator must serve both the sort and the search: sort with the same name projection the lookup uses, or give EnvCache a lookup that owns its ordering. Note that job.exported_vars is a SortedIdVec (handle-ordered), so the std::sort at :62 is nearly a no-op today — fixing only that line is not enough unless the projection is applied there.
The general enforcement point is the deleted-relational-operator change proposed separately: a container ordered by handle and searched by name is a mistake the compiler can catch.
Provenance
Found by adversarial multi-agent review of PR #176, whose body wrongly cleared this site as "order-agnostic by construction". Pre-existing on main — not introduced by any open PR. Reproduced independently before filing.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TLob4Ef3qyMd3Dnp9q3XmA
Summary
build_env_cachesorts its names byStringIdhandle, andenv_cache_findbinary-searches the result by name:lower_boundrequires the range to be partitioned with respect to the comparator it is given. The range is ordered by handle and searched by name, so the lookup returnsnullptrfor entries that are present.prepare_job_launch(src/exec/scheduler.cpp:129-137) then omits the variable, and sincebase_child_env()contributes onlyPATH=, the variable is absent from the child process entirely.Interning order here is the order the
exportdirectives were parsed, so the bug is triggered by the order the exports are written in the Tupfile.Reproduced
Two projects, identical except for the order of three
exportlines. Rule is: |> sh -c "env | sort > %o" |> out.txt, both run withZVAR=zzz MVAR=mmm AVAR=aaa:Both builds exit 0 and report success. The command simply runs with a silently wrong environment.
A single exported variable always works, because
lower_boundover a one-element range cannot be wrong — which is whytest/unit/test_exec.cpp:437("Scheduler exported_vars", one variable) passes today.Severity
Silent wrong build output, no diagnostic, exit 0. It contradicts what
docs/reference.md:1099-1101promises for an exported variable.Fix
One comparator must serve both the sort and the search: sort with the same name projection the lookup uses, or give
EnvCachea lookup that owns its ordering. Note thatjob.exported_varsis aSortedIdVec(handle-ordered), so thestd::sortat :62 is nearly a no-op today — fixing only that line is not enough unless the projection is applied there.The general enforcement point is the deleted-relational-operator change proposed separately: a container ordered by handle and searched by name is a mistake the compiler can catch.
Provenance
Found by adversarial multi-agent review of PR #176, whose body wrongly cleared this site as "order-agnostic by construction". Pre-existing on main — not introduced by any open PR. Reproduced independently before filing.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TLob4Ef3qyMd3Dnp9q3XmA