Skip to content

Commit 1edbc59

Browse files
committed
feat(runtime): 运行期搜索闭包 —— SubOS 库视图进 DT_RPATH,末位
mcpp 在编译与链接两条线上都发 --sysroot=<subos>,所以 subos 提供的库零 flag 就能 解析。运行期的搜索路径却是另一套独立推导(只有工具链载荷目录)。同一个决策 「subos 是我的根」推导了两次,而第二次的输入集合小了一整个库视图: $ mcpp build rc=0 $ ./bin/app libGL.so.1: cannot open shared object file (127) $ mcpp why runtime validation: pass ← 假绿 新增 mcpp.platform.runtime_search:一条搜索目录的来源、次序、是否机器本地。 放 platform/ 是因为它描述的是加载器怎么搜(物理),而 elf_runtime 必须读它 —— 放 build/ 会让 platform 反向依赖 build。它只 import std,四个消费者共读一份。 次序 = 不可变性递减,farm 在最后。载荷目录装一次不再动,<subos>/lib 每次 xlings install 都重写。载荷在前 ⇒ libc/libm/libstdc++ 永远从被 pin 的载荷解析, farm 只补没人提供的;farm 在前 ⇒ 一次安装能在事后换掉一个已链接产物的 libc。 今天 farm 的 libc.so.6 就是指向那个载荷的符号链接,所以两种次序行为相同 —— 这正是它必须被写下来并断言的理由。 假绿是独立缺陷,且必须先修:resolve_needed 搜完 rpath 后回落到宿主默认目录, 而宿主通常自带 libGL.so.1 ⇒ 模型认为解析到了。但产物跑在私有加载器下,它的默认 路径里没有宿主目录 —— 模型模拟的是宿主加载器,产物用的是私有加载器。现在宿主 默认目录只在非 hermetic binding 下参与。 第四种判决 Unresolvable:hermetic 下一个解析不到的 DT_NEEDED 是可证的失败 (整条搜索路径由 mcpp 算出,没有 ld.so.cache 兜底),把它归到 Inconclusive 是 把可证的事说成没查过。失败门改问 verdict.blocking(),而不是枚举状态。 护栏两条,与加载器标签契约同一判据:非 ELF 不发(Mach-O/PE 没有这个概念), 交叉目标不发(farm 属于宿主 subos)。 farm 不进 linkIntent.runtimeSearchDirs —— 那个字段会流向 runtimeLibraryDirs, 再变成 mcpp run 的 LD_LIBRARY_PATH,污染它拉起的每个子进程包括宿主二进制。 farm 只按对象可达(DT_RPATH),这是上一轮图形设计里明确立下的规矩。 载荷目录向发出它们的同一个函数要(resolve_link_model(...).libDirs): plan.toolchain.linkRuntimeDirs 只有 clang 会填,第一版记录只有一条 farm 而产物 DT_RPATH 有三条 —— 记录与产物不一致,正是这套东西要消灭的形状。 不读 $XLINGS_SUBOS_LIB。实测它指向当前 shell 的 subos,而 mcpp 有自己的 registry home,两者由不同物理 glibc 载荷支撑 —— 继承它等于给产物挂第二套 libc。 改为主动声明 XLINGS_SUBOS_LD_PATHS=0(xlings#540 的退出),今天是无操作。 观测:resolution.json 的 runtime.search.closure(保序,带 origin 与 machine_local),mcpp why runtime 按加载器次序打印。 测试:219 断言产物 DT_RPATH 末位是 farm、首位载荷目录真的有 libc,并要求记录与 产物逐条一致;220 从「farm 有而载荷没有」的差集里取一个库,--no-as-needed 让它 成为 DT_NEEDED,然后真的 exec —— 只有跑起来才能戳破假绿,因为宿主也有 libGL; 另一半用一个 SONAME 谁都提供不了的库断言构建变红并指名。215 的机器本地前缀从 data/xpkgs 扩到整个 mcpp home(farm 在 registry/subos 下,原来会漏过去),并补 --mode system 档 —— 它是唯一不重写 rpath 的档,此前没有任何用例检查过它。
1 parent 797b4f5 commit 1edbc59

13 files changed

Lines changed: 1114 additions & 22 deletions

src/build/flags.cppm

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import mcpp.build.plan;
1717
import mcpp.manifest.types;
1818
import mcpp.modgraph.scanner;
1919
import mcpp.platform;
20+
import mcpp.platform.runtime_search;
2021
import mcpp.toolchain.clang;
2122
import mcpp.toolchain.detect;
2223
import mcpp.toolchain.dialect;
@@ -503,6 +504,26 @@ CompileFlags compute_flags(const BuildPlan& plan) {
503504
}();
504505
const std::string link_intent_ld =
505506
render_link_intent_flags(plan.linkIntent, linkIntentFlavor);
507+
508+
// The SubOS farm tail — the only origin in `plan.runtimeSearch` with no
509+
// other producer, appended after everything else so it is LAST in the
510+
// artifact's DT_RPATH (see `runtime_search_closure`).
511+
//
512+
// RUNPATH ONLY, never `-L`. Link-time resolution already works: mcpp
513+
// passes `--sysroot=<subos>`, which makes `<subos>/lib` the linker's
514+
// default library directory. Emitting `-L` as well would be redundant on
515+
// a link line that has a hard 128KiB ceiling real workspaces already spend
516+
// 43% of. This is the same rule `runtimeSearchDirs` states for package
517+
// dirs, applied to the origin that needed it most.
518+
std::string farm_ld;
519+
if (linkIntentFlavor == LinkIntentFlavor::Elf) {
520+
for (auto const& dir : plan.runtimeSearch) {
521+
if (dir.origin != mcpp::platform::search::Origin::SubosFarm) continue;
522+
farm_ld += ' ';
523+
farm_ld += shell_quote_arg(escape_ninja_chars(
524+
"-Wl,-rpath," + dir.path.string()));
525+
}
526+
}
506527
std::filesystem::path binutilsBin;
507528
if (!isMuslTc && !isMingwTc && caps.stdlib_id == "libstdc++") {
508529
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
@@ -972,9 +993,9 @@ CompileFlags compute_flags(const BuildPlan& plan) {
972993
// actually being present (see atomic_link_flag).
973994
std::string atomic_ld = atomic_link_flag(plan.toolchain.linkRuntimeDirs,
974995
!full_static.empty());
975-
f.ld = std::format("{}{}{}{}{}{}{}{}{}", full_static,
996+
f.ld = std::format("{}{}{}{}{}{}{}{}{}{}", full_static,
976997
link_toolchain_flags, b_flag, runtime_dirs,
977-
link_intent_ld, atomic_ld, payload_ld,
998+
link_intent_ld, farm_ld, atomic_ld, payload_ld,
978999
user_ldflags, link_extra);
9791000
}
9801001

src/build/ninja_backend.cppm

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import mcpp.toolchain.detect;
3737
import mcpp.toolchain.dialect;
3838
import mcpp.toolchain.provider;
3939
import mcpp.toolchain.registry;
40-
import mcpp.xlings;
40+
import mcpp.platform.xlings;
4141
import mcpp.platform;
4242
import mcpp.ui;
4343

@@ -1791,7 +1791,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
17911791
for (auto const& checked : runtimeReport.artifacts) {
17921792
using Status = mcpp::platform::elf::RuntimeVerdict::Status;
17931793
auto explanation = checked.verdict.explain();
1794-
if (checked.verdict.status == Status::ProvenMismatch) {
1794+
// `blocking()` rather than a state list: a proven payload mismatch
1795+
// and a DT_NEEDED the artifact's own loader will never find are
1796+
// both "this artifact is known bad", and enumerating them here is
1797+
// how one of them gets forgotten.
1798+
if (checked.verdict.blocking()) {
17951799
if (runtimeFailure.empty()) {
17961800
runtimeFailureArtifact = checked.artifact;
17971801
runtimeFailure = std::move(explanation);
@@ -1807,7 +1811,7 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
18071811
}
18081812
if (!runtimeFailure.empty()) {
18091813
return std::unexpected(BuildError{
1810-
"runtime closure validation failed (proven Linux ELF mismatch)",
1814+
"runtime closure validation failed (proven Linux ELF defect)",
18111815
runtimeFailureArtifact, std::move(runtimeFailure)});
18121816
}
18131817
// Rule E — the loader-tag contract, checked on what actually landed

src/build/plan.cppm

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import mcpp.toolchain.cppfly;
1616
import mcpp.toolchain.detect;
1717
import mcpp.toolchain.dialect;
1818
import mcpp.toolchain.fingerprint;
19+
import mcpp.toolchain.linkmodel;
1920
import mcpp.toolchain.triple;
2021
import mcpp.platform;
2122
import mcpp.platform.runtime_binding;
2223
import mcpp.platform.runtime_env_contract;
23-
import mcpp.xlings.subos_info;
24+
import mcpp.platform.runtime_search;
25+
import mcpp.platform.xlings.subos_info;
2426

2527
export namespace mcpp::build {
2628

@@ -202,6 +204,22 @@ struct BuildPlan {
202204
std::vector<mcpp::manifest::RuntimeRequirement> runtimeRequirements;
203205
std::vector<mcpp::manifest::RuntimeArtifact> runtimeArtifacts;
204206
mcpp::manifest::LinkIntent linkIntent;
207+
// The complete run-time search closure of the artifacts this plan will
208+
// produce, in loader order and tagged with where each directory came from
209+
// (`mcpp.platform.runtime_search`).
210+
//
211+
// This is the RECORD — what resolution.json publishes and `mcpp why
212+
// runtime` explains. Emission is still owned by each origin's existing
213+
// producer (payloads by the toolchain link model, package dirs by
214+
// LinkIntent), with one exception: SubosFarm entries have no other
215+
// producer, so `flags.cppm` renders them from here, appended last.
216+
//
217+
// ⚠️ The farm deliberately does NOT enter `runtimeLibraryDirs`. That
218+
// vector becomes LD_LIBRARY_PATH for `mcpp run`, which is inherited by
219+
// every child process including host binaries — measured to kill
220+
// `xdg-open`/`notify-send` outright when a private libc is on it. The farm
221+
// is reachable PER OBJECT (DT_RPATH) and must stay that way.
222+
std::vector<mcpp::platform::search::Dir> runtimeSearch;
205223
// Windows runtime-DLL deployment. On PE (`supports_rpath` is false) a
206224
// directly-launched .exe cannot RUNPATH-locate a dependency's DLL, so each
207225
// *.dll found in a dependency's [runtime] library_dir is copied beside the
@@ -233,6 +251,13 @@ void merge_runtime_binding_contract(
233251
BuildPlan& plan,
234252
const mcpp::platform::runtime::RuntimeBinding& binding);
235253

254+
// The run-time search closure for this plan, in loader order and tagged with
255+
// provenance. Exported so a test can exercise the guards (cross target, non-ELF
256+
// format, undeclared SubOS) without linking a binary for each.
257+
std::vector<mcpp::platform::search::Dir> runtime_search_closure(
258+
const BuildPlan& plan,
259+
const mcpp::platform::runtime::RuntimeBinding& binding);
260+
236261
// Is `p` inside one of `roots`, judged LEXICALLY?
237262
//
238263
// Lexical is the whole point (mcpp#344). std::filesystem::relative() runs
@@ -621,6 +646,76 @@ ResolvedRuntimeContract resolve_runtime_contract(
621646
return out;
622647
}
623648

649+
// The run-time search closure of everything this plan will link, in the order
650+
// the loader will consult it, tagged with where each directory came from.
651+
//
652+
// ONE assembly, three producers. Payload directories come from the toolchain
653+
// link model, package directories from the resolved LinkIntent, and the SubOS
654+
// farm from the RuntimeBinding — and the farm is the addition that closes the
655+
// gap this whole change exists for: mcpp already passes `--sysroot=<subos>` on
656+
// the compile AND link lines, so `-lGL` resolves out of `<subos>/lib` with no
657+
// flags from the user, while the RUN-time path was derived from payload
658+
// directories alone. Link succeeded, the artifact could not start.
659+
//
660+
// FARM LAST, and it is the only invariant here. `<subos>/lib` is a symlink
661+
// view rewritten by every `xlings install`; payload directories are written
662+
// once and never touched. Payload-first keeps libc / libm / libstdc++ resolving
663+
// from the pinned payload and leaves the farm to supply only what nothing else
664+
// does. Farm-first would let a later install silently change which libc an
665+
// ALREADY LINKED artifact loads. `search::ordered` is what enforces it, and
666+
// e2e 219 asserts it on the produced ELF rather than on this code.
667+
std::vector<mcpp::platform::search::Dir> runtime_search_closure(
668+
const BuildPlan& plan,
669+
const mcpp::platform::runtime::RuntimeBinding& binding) {
670+
using mcpp::platform::search::Dir;
671+
using mcpp::platform::search::Origin;
672+
673+
// PAYLOAD DIRECTORIES COME FROM THE SAME FUNCTION THAT EMITS THEM.
674+
//
675+
// `resolve_link_model` is a pure function of the toolchain and is what
676+
// `flags.cppm` renders as `-L`/`-Wl,-rpath` for the C runtime; asking it
677+
// here is how the record and the artifact stay the same list. Deriving
678+
// them a second way is what made the first version of this record show a
679+
// one-entry closure while the artifact carried three — `linkRuntimeDirs`
680+
// is populated for CLANG ONLY, so on GCC it is simply empty and the
681+
// payloads arrive through the link model instead.
682+
//
683+
// Both are read, in the order `flags.cppm` concatenates them.
684+
std::vector<Dir> closure;
685+
for (auto const& dir : mcpp::toolchain::resolve_link_model(plan.toolchain).libDirs)
686+
closure.push_back({dir, Origin::Payload});
687+
for (auto const& dir : plan.toolchain.linkRuntimeDirs)
688+
closure.push_back({dir, Origin::Payload});
689+
for (auto const& dir : plan.linkIntent.runtimeSearchDirs)
690+
closure.push_back({dir, Origin::Package});
691+
692+
// TWO GUARDS, both about "will this artifact ever run here".
693+
//
694+
// format DT_RPATH exists on ELF only. Mach-O and PE get nothing rather
695+
// than a branch in every consumer — the same shape
696+
// `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.
701+
const auto triple = [&] {
702+
auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple);
703+
return t ? *t : mcpp::toolchain::triple::Triple{};
704+
}();
705+
const bool elfTarget = triple.empty()
706+
? bool(mcpp::platform::is_linux)
707+
: (triple.os != "macos" && triple.os != "windows");
708+
const bool hostTarget = binding.platform == "linux"
709+
&& (triple.empty()
710+
|| (triple.os == "linux"
711+
&& (triple.arch.empty() || triple.arch == binding.arch)));
712+
if (elfTarget && hostTarget)
713+
for (auto const& dir : binding.searchDirs)
714+
closure.push_back({dir, Origin::SubosFarm});
715+
716+
return mcpp::platform::search::ordered(std::move(closure));
717+
}
718+
624719
void merge_runtime_binding_contract(
625720
BuildPlan& plan,
626721
const mcpp::platform::runtime::RuntimeBinding& binding) {
@@ -676,6 +771,8 @@ void merge_runtime_binding_contract(
676771
}))
677772
plan.runtimeArtifacts.push_back(std::move(value));
678773
}
774+
775+
plan.runtimeSearch = runtime_search_closure(plan, binding);
679776
}
680777

681778
// True if `src` defines a top-level `int main(` / `auto main(` entry point.

src/build/runtime_validation.cppm

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ struct ValidatedArtifact {
3838
struct ValidationReport {
3939
std::vector<ValidatedArtifact> artifacts;
4040

41-
bool has_proven_mismatch() const {
41+
// Any artifact PROVEN bad — payloads mixed, or a DT_NEEDED that the
42+
// artifact's own loader will not find. Asks the verdict rather than
43+
// enumerating states here, so a fifth state cannot be added without this
44+
// gate deciding what it means.
45+
bool has_blocking_failure() const {
4246
return std::ranges::any_of(artifacts, [](auto const& artifact) {
43-
return artifact.verdict.status
44-
== mcpp::platform::elf::RuntimeVerdict::Status::ProvenMismatch;
47+
return artifact.verdict.blocking();
4548
});
4649
}
4750
};
@@ -150,16 +153,35 @@ std::string status_name(mcpp::platform::elf::RuntimeVerdict::Status status) {
150153
switch (status) {
151154
case Status::Pass: return "pass";
152155
case Status::ProvenMismatch: return "proven_mismatch";
156+
case Status::Unresolvable: return "unresolvable";
153157
case Status::Inconclusive: return "inconclusive";
154158
}
155159
return "inconclusive";
156160
}
157161

162+
// How bad each state is, for rolling many artifacts into one summary.
163+
// `Unresolvable` sits above `Inconclusive` (it is proven, not unknown) and
164+
// below `ProvenMismatch` (mixing payloads is the more fundamental error, and
165+
// it is usually the CAUSE of anything unresolvable alongside it).
166+
int status_severity(mcpp::platform::elf::RuntimeVerdict::Status status) {
167+
using Status = mcpp::platform::elf::RuntimeVerdict::Status;
168+
switch (status) {
169+
case Status::Pass: return 0;
170+
case Status::Inconclusive: return 1;
171+
case Status::Unresolvable: return 2;
172+
case Status::ProvenMismatch: return 3;
173+
}
174+
return 1;
175+
}
176+
158177
mcpp::platform::elf::RuntimeVerdict::Status
159178
parse_status(std::string_view value) {
160179
using Status = mcpp::platform::elf::RuntimeVerdict::Status;
161180
if (value == "pass") return Status::Pass;
162181
if (value == "proven_mismatch") return Status::ProvenMismatch;
182+
if (value == "unresolvable") return Status::Unresolvable;
183+
// Anything unknown reads as `inconclusive`, never as `pass`: a record
184+
// written by a newer mcpp must not be mistaken for a clean bill of health.
163185
return Status::Inconclusive;
164186
}
165187

@@ -273,8 +295,10 @@ void sync_resolution_verdict(const mcpp::build::BuildPlan& plan,
273295
if (!it.value().is_object()) continue;
274296
any = true;
275297
auto status = parse_status(it.value().value("status", "inconclusive"));
276-
if (status == Status::ProvenMismatch
277-
|| (status == Status::Inconclusive && summary == Status::Pass))
298+
// Worst wins, by an explicit severity order rather than a chain of
299+
// pairwise comparisons that has to be re-derived every time a
300+
// state is added.
301+
if (status_severity(status) > status_severity(summary))
278302
summary = status;
279303
checked.push_back({
280304
{"path", (plan.outputDir / it.key()).lexically_normal().generic_string()},

src/cli.cppm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import mcpp.pm.commands;
3030
import mcpp.toolchain.fingerprint; // MCPP_VERSION
3131
import mcpp.wire;
3232
import mcpp.platform.env; // --offline → MCPP_OFFLINE
33+
import mcpp.platform.runtime_search; // linker-wrapper path-injection opt-out
3334
import mcpp.ui;
3435
import mcpp.log;
3536

@@ -114,6 +115,28 @@ int run(int argc, char** argv) {
114115
// it makes `MCPP_OFFLINE=1` and `--offline` literally the same switch.
115116
else if (a == "--offline") mcpp::platform::env::set("MCPP_OFFLINE", "1");
116117
}
118+
// Decline xlings' linker-wrapper path injection, for this process and
119+
// everything it spawns (openxlings/xlings#540).
120+
//
121+
// That wrapper appends `-rpath "$XLINGS_SUBOS_LIB"` to every link it sees.
122+
// mcpp wants the TAG half of what it does and must refuse the PATH half:
123+
// `$XLINGS_SUBOS_LIB` names the ACTIVE SHELL's SubOS, which is measurably
124+
// not the one mcpp resolved — mcpp keeps its own xlings home under
125+
// `<mcpp home>/registry`, so on an ordinary developer machine the variable
126+
// points at a different farm backed by a DIFFERENT PHYSICAL glibc payload.
127+
// Inheriting it would put a second libc on the artifact's search path,
128+
// which is the one thing rule B exists to prevent. mcpp emits its own farm
129+
// entry, derived from the binding it actually selected.
130+
//
131+
// Set here rather than per link command: the link line has a hard 128KiB
132+
// ceiling that real workspaces already spend 43% of, and children inherit
133+
// the environment for free. Declared BEFORE the wrapper ships, because
134+
// "the exit must be declared, not inferred" is the rule that whole
135+
// negotiation established — today this is a no-op.
136+
mcpp::platform::env::set(
137+
std::string(mcpp::platform::search::kLinkerPathInjectionOptOut),
138+
std::string(mcpp::platform::search::kLinkerPathInjectionOptOutValue));
139+
117140
// Env override (observability, esp. CI): MCPP_VERBOSE=<non-empty, not "0">
118141
// turns on verbose logging for EVERY mcpp invocation — including the ones
119142
// nested inside e2e test scripts that call $MCPP without flags. Lets a

src/doctor.cppm

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import mcpp.toolchain.registry;
3535
import mcpp.toolchain.stdmod;
3636
import mcpp.toolchain.abi;
3737
import mcpp.ui;
38-
import mcpp.xlings;
38+
import mcpp.platform.xlings;
3939

4040
namespace mcpp::doctor {
4141

@@ -281,6 +281,9 @@ export int doctor_report() {
281281
} else if (stored->verdict.status == Status::Inconclusive) {
282282
warn(std::format("{}: inconclusive{}{}", subject,
283283
detail.empty() ? "" : "\n", detail));
284+
} else if (stored->verdict.status == Status::Unresolvable) {
285+
err(std::format("{}: unresolvable runtime closure{}{}", subject,
286+
detail.empty() ? "" : "\n", detail));
284287
} else {
285288
err(std::format("{}: proven mismatch{}{}", subject,
286289
detail.empty() ? "" : "\n", detail));
@@ -677,6 +680,22 @@ int print_stored_runtime_resolution() {
677680
search->value("format", "?"), search->value("link_library", "?"),
678681
search->value("transitive_needed", "?"),
679682
search->value("runtime", "?"));
683+
// The closure IN ORDER, with where each directory came from. Order is
684+
// the answer to "why does my GL program find its driver" and to "why
685+
// is my libc still the pinned one" — both invisible when the report
686+
// says only which mechanism is used.
687+
if (auto closure = search->find("closure");
688+
closure != search->end() && closure->is_array() && !closure->empty()) {
689+
std::println(" runtime search closure (loader order):");
690+
for (auto const& dir : *closure) {
691+
if (!dir.is_object()) continue;
692+
std::println(" {:<12} {}{}",
693+
dir.value("origin", "?"), dir.value("path", "?"),
694+
dir.value("machine_local", false) ? " [machine-local]" : "");
695+
}
696+
std::println(" note: the mutable SubOS farm is LAST on purpose — "
697+
"libc stays pinned to its payload");
698+
}
680699
}
681700
if (auto validation = runtime->find("validation");
682701
validation != runtime->end() && validation->is_object()) {

0 commit comments

Comments
 (0)