Skip to content

Commit d0119fa

Browse files
committed
fix(plan): farm 的护栏补上 libc 轴,并且「不匹配」必须是被证明的
原护栏只看 os + arch。x86_64-linux-musl 两者都对得上,于是一个 glibc farm 会落到 musl 程序的搜索路径上 —— 正是 rule B 在防的载荷混用。今天那个目标是 static (实测:根本没有 dynamic section,flag 是惰性的),所以这是为它不再是 static 的那天准备的。 ⚠️ 第一版把「未声明」当成了「不匹配」:没有 subos_info 的 SubOS 没有 runtimeId, 于是它自己的库视图被拒绝了 —— e2e 220 的 half 0 当场变红。这是本次改动在别处正在 消灭的那个错误的又一次现形:**缺席不是矛盾**。改为只有两边都说了话且说的不一样才算 不匹配;任一侧未知,就交给 os/arch 护栏决定。 补一张护栏矩阵单测(host / 换 arch / 换 libc / 换格式 / 未声明),因为逐条端到端 验证每一格都要一整套交叉工具链,而它防的失败是静默的。
1 parent 6f51d5e commit d0119fa

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

src/build/plan.cppm

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -694,18 +694,38 @@ std::vector<mcpp::platform::search::Dir> runtime_search_closure(
694694
// format DT_RPATH exists on ELF only. Mach-O and PE get nothing rather
695695
// than a branch in every consumer — the same shape
696696
// `loader_contract` uses for the tag half of this contract.
697-
// host The farm belongs to THIS host's SubOS. A cross target
698-
// (aarch64-musl, mingw, wasm) would receive a path that is inert
699-
// at best and points at the wrong architecture's libraries at
700-
// worst.
697+
// host The farm belongs to THIS host's SubOS, and a SubOS is a
698+
// (os, arch, libc) triple's worth of libraries. A cross target
699+
// must match all three or the path is inert at best and points
700+
// at the wrong architecture's — or the wrong C library's —
701+
// objects at worst. `x86_64-linux-musl` is the case that makes
702+
// the libc axis load-bearing: same OS, same arch, and a glibc
703+
// farm on a musl program's search path is exactly the payload
704+
// mixing rule B exists to prevent. (Today that target is also
705+
// `linkage = "static"`, so the flag is inert — measured: no
706+
// dynamic section at all. The guard is for the day it is not.)
701707
const auto triple = [&] {
702708
auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple);
703709
return t ? *t : mcpp::toolchain::triple::Triple{};
704710
}();
705711
const bool elfTarget = triple.empty()
706712
? bool(mcpp::platform::is_linux)
707713
: (triple.os != "macos" && triple.os != "windows");
714+
// The binding names its libc as `<family>@<version>`; the triple names it
715+
// as an ABI env (`gnu` ⇒ glibc). A MISMATCH must be PROVEN, not assumed:
716+
// an undeclared SubOS has no runtime identity at all, and refusing its own
717+
// library view because it did not describe itself would be the same
718+
// absence-read-as-contradiction this change exists to remove. Unknown on
719+
// either side ⇒ no evidence of a mismatch ⇒ the os/arch guards decide.
720+
const auto bindingLibcFamily =
721+
binding.runtimeId.substr(0, binding.runtimeId.find('@'));
722+
const auto targetLibcFamily = triple.env.empty()
723+
? std::string{} : (triple.env == "gnu" ? "glibc" : triple.env);
724+
const bool libcMismatch = !bindingLibcFamily.empty()
725+
&& !targetLibcFamily.empty()
726+
&& targetLibcFamily != bindingLibcFamily;
708727
const bool hostTarget = binding.platform == "linux"
728+
&& !libcMismatch
709729
&& (triple.empty()
710730
|| (triple.os == "linux"
711731
&& (triple.arch.empty() || triple.arch == binding.arch)));

tests/unit/test_runtime_contract.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import mcpp.modgraph.scanner;
99
import mcpp.platform;
1010
import mcpp.platform.axis;
1111
import mcpp.platform.runtime_binding;
12+
import mcpp.platform.runtime_search;
1213
import mcpp.platform.xlings.subos_info;
1314

1415
namespace build = mcpp::build;
@@ -277,6 +278,8 @@ TEST(RuntimeContract, XlingsSelectedFactsPrecedeDescriptorFallbacks) {
277278
});
278279

279280
build::merge_runtime_binding_contract(plan, binding);
281+
ASSERT_EQ(plan.runtimeSearch.size(), 0u)
282+
<< "a binding with no SubOS library view contributes no farm entry";
280283
ASSERT_EQ(plan.runtimeProviders.size(), 2u);
281284
EXPECT_EQ(plan.runtimeProviders.front().provider.canonical(),
282285
"xim.selected@4.0.0");
@@ -287,3 +290,74 @@ TEST(RuntimeContract, XlingsSelectedFactsPrecedeDescriptorFallbacks) {
287290
}
288291

289292
} // namespace
293+
294+
// ── the farm's two guards, as a matrix ─────────────────────────────────────
295+
//
296+
// The farm belongs to THIS host's SubOS, so it may only reach an artifact that
297+
// will run here under this runtime. The guards are asserted directly because
298+
// each one costs a full cross toolchain to exercise end to end, and the failure
299+
// they prevent is silent: a path that is inert at best, and points at another
300+
// architecture's or another C library's objects at worst.
301+
namespace {
302+
303+
build::BuildPlan plan_for(std::string targetTriple) {
304+
build::BuildPlan plan;
305+
plan.toolchain.targetTriple = std::move(targetTriple);
306+
return plan;
307+
}
308+
309+
mcpp::platform::runtime::RuntimeBinding host_binding_with_farm() {
310+
mcpp::platform::runtime::RuntimeBinding b;
311+
b.platform = "linux";
312+
b.arch = "x86_64";
313+
b.declared = true;
314+
b.runtimeId = "glibc@2.39";
315+
b.searchDirs = {"/home/u/.mcpp/registry/subos/default/lib"};
316+
return b;
317+
}
318+
319+
std::size_t farm_entries(const std::vector<mcpp::platform::search::Dir>& dirs) {
320+
return static_cast<std::size_t>(std::ranges::count_if(dirs, [](auto const& d) {
321+
return d.origin == mcpp::platform::search::Origin::SubosFarm;
322+
}));
323+
}
324+
325+
} // namespace
326+
327+
TEST(RuntimeSearchClosure, HostTargetGetsTheFarm) {
328+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
329+
plan_for(""), host_binding_with_farm())), 1u);
330+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
331+
plan_for("x86_64-linux-gnu"), host_binding_with_farm())), 1u);
332+
}
333+
334+
TEST(RuntimeSearchClosure, CrossTargetGetsNoFarm) {
335+
// Another architecture: the farm holds x86_64 objects.
336+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
337+
plan_for("aarch64-linux-gnu"), host_binding_with_farm())), 0u);
338+
// Another C library: a glibc farm on a musl program's search path is the
339+
// payload mixing rule B exists to prevent.
340+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
341+
plan_for("x86_64-linux-musl"), host_binding_with_farm())), 0u);
342+
// Another format: DT_RPATH does not exist there at all.
343+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
344+
plan_for("x86_64-windows-gnu"), host_binding_with_farm())), 0u);
345+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
346+
plan_for("aarch64-macos"), host_binding_with_farm())), 0u);
347+
}
348+
349+
// A MISMATCH must be proven, not assumed. An undeclared SubOS has no runtime
350+
// identity, and refusing its own library view on that basis would be exactly
351+
// the absence-read-as-contradiction this release removes elsewhere. Caught by
352+
// e2e 220, which builds against a SubOS it creates and which therefore has no
353+
// `subos_info` to declare anything.
354+
TEST(RuntimeSearchClosure, UndeclaredBindingStillGetsItsOwnFarm) {
355+
auto b = host_binding_with_farm();
356+
b.declared = false;
357+
b.runtimeId.clear();
358+
EXPECT_EQ(farm_entries(build::runtime_search_closure(plan_for(""), b)), 1u);
359+
// …but the architecture guard still applies: unknown libc is not a licence
360+
// to ignore everything else.
361+
EXPECT_EQ(farm_entries(build::runtime_search_closure(
362+
plan_for("aarch64-linux-gnu"), b)), 0u);
363+
}

0 commit comments

Comments
 (0)