Skip to content

Commit 0987b16

Browse files
committed
fix(build): a subos declaring a payload that is gone cannot be honoured
The stale value was not a fossil after all -- it was the subos's own `subos_info`, saying `glibc@2.39` on a machine whose only payload is 2.44. The subos description is written once; the payloads beneath it are replaced independently. Same staleness, a layer up, and the previous commit only guarded the layer below. Two things kept it alive: 1. The declaration was returned unchecked. Honouring it is not the conservative choice -- the exact-match probe finds nothing, no loader reaches the link line, and the artifact lands on the HOST loader, outside the sandbox entirely. Worse than any substitution. 2. The second resolution pass was gated on `runtimeBinding.empty()`. A stale declaration is not empty, so the pass that has the compiler in hand -- the only one that can check whether the payload exists -- never ran. The gate is now `!tc->payloadPaths`, which is what both failures (undiscoverable binding, uninstallable binding) actually look like. Reproduced locally by injecting `runtime: glibc@2.99` into this machine's subos: the declaration is passed over, resolution carries on to the 2.39 that is installed, and the artifact takes the payload loader and runs. 66 unit tests; e2e 201/86/200/103/155.
1 parent d99d1e3 commit 0987b16

1 file changed

Lines changed: 35 additions & 7 deletions

File tree

‎src/build/prepare.cppm‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -929,14 +929,36 @@ std::string resolve_runtime_binding(const mcpp::manifest::Manifest& m,
929929
auto runtime_of = [](const std::filesystem::path& dir) -> std::string {
930930
return mcpp::xlings::subos::read(dir).runtime;
931931
};
932+
// A declaration can only be honoured while the payload it names is still
933+
// installed. Subos descriptions are written once and the payloads beneath
934+
// them are replaced independently -- upgraded, garbage-collected -- so a
935+
// subos can go on saying `glibc@2.39` on a machine that now has only 2.44.
936+
//
937+
// Taking that at face value is not conservative, it is worse than any
938+
// substitution: the exact-match probe finds nothing, no loader reaches the
939+
// link line, and the artifact ends up on the HOST loader -- outside the
940+
// sandbox entirely. Measured on CI, three rounds running.
941+
//
942+
// So a declaration naming an absent payload is passed over rather than
943+
// returned, and resolution carries on to something that can be honoured.
944+
auto usable = [&](const std::string& binding) {
945+
if (binding.empty()) return false;
946+
if (compilerBin.empty()) return true; // cannot check yet; caller re-asks
947+
if (mcpp::toolchain::probe_payload_paths(compilerBin, binding))
948+
return true;
949+
mcpp::log::verbose("probe", std::format(
950+
"subos declares runtime {}, but that payload is not installed here "
951+
"— looking for one that is", binding));
952+
return false;
953+
};
932954
if (auto active = mcpp::xlings::paths::subos_dir_of(compilerBin)) {
933955
// 1 — the project names a subos; it is a sibling of the active one.
934956
if (!m.xlings.subos.empty()) {
935957
auto named = active->parent_path() / m.xlings.subos;
936-
if (auto r = runtime_of(named); !r.empty()) return r;
958+
if (auto r = runtime_of(named); usable(r)) return r;
937959
}
938960
// 2 — whatever is active.
939-
if (auto r = runtime_of(*active); !r.empty()) return r;
961+
if (auto r = runtime_of(*active); usable(r)) return r;
940962
}
941963

942964
// 3 — COMPATIBILITY: the value this toolchain already has baked in.
@@ -1705,12 +1727,18 @@ prepare_build(bool print_fingerprint,
17051727
auto runtimeBinding = resolve_runtime_binding(*m, {});
17061728
auto tc = mcpp::toolchain::detect(explicit_compiler, runtimeBinding);
17071729
if (!tc) return std::unexpected(tc.error().message);
1708-
// The binding may only be discoverable from the compiler's own home
1709-
// (an inherited toolchain resolves into its owner). Re-resolve once we
1710-
// know where the compiler is, then re-probe if that changed the answer.
1711-
if (runtimeBinding.empty()) {
1730+
// The first pass has no compiler, so it can neither discover a binding
1731+
// that only the compiler's own home knows (an inherited toolchain resolves
1732+
// into its owner) nor check that a declared one is still installed. Both
1733+
// failures look identical from here: no payload paths. Re-ask with the
1734+
// compiler in hand whenever that is what happened.
1735+
//
1736+
// The old condition was `runtimeBinding.empty()`, which meant a subos
1737+
// naming a payload that had since been upgraded away won on the first pass
1738+
// and the second pass never ran.
1739+
if (!tc->payloadPaths) {
17121740
auto fromCompiler = resolve_runtime_binding(*m, tc->binaryPath);
1713-
if (!fromCompiler.empty()) {
1741+
if (!fromCompiler.empty() && fromCompiler != runtimeBinding) {
17141742
tc = mcpp::toolchain::detect(explicit_compiler, fromCompiler);
17151743
if (!tc) return std::unexpected(tc.error().message);
17161744
}

0 commit comments

Comments
 (0)