Skip to content

Commit 7515f6f

Browse files
committed
fix(runtime): DT_RPATH 被整条链继承 —— 闭包检查拒掉了一个加载器能起来的产物
1 parent 35e9d5c commit 7515f6f

4 files changed

Lines changed: 156 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@
4747
工具);单测 `test_xlings_address_set.cpp``xpkg_payload_at` 的范围/不可解析版本两组。
4848
规范:SPEC-001 §10、SPEC-004 §4.5。
4949

50+
### 运行期闭包检查漏掉了 `DT_RPATH` 的继承,于是拒绝了一个能跑起来的产物
51+
52+
`DT_RPATH` 被整条依赖链继承,`DT_RUNPATH` 不被继承。而闭包模型只查**发起请求的那个
53+
对象自己**的搜索路径列表 —— 于是一个厂商工具包的形状(库与库之间按裸 SONAME 互相
54+
依赖、都不带搜索路径,而它们所在的目录只写在**可执行文件**`DT_RPATH` 里)整批读成
55+
「找不到」。
56+
57+
`examples/09-heterogeneous/cann` 上实测:mcpp 拒绝构建并点名**八个**库,而它刚刚链接
58+
出来的那个产物解析掉了其中七个,只在第八个上失败 —— 而那一个属于这台机器没有的驱动。
59+
**模型与加载器给的是相反的读数,而模型赢了报告、加载器赢了现实。**
60+
61+
抑制的那一半同样是规则的一部分:带 `DT_RUNPATH` 的对象不使用任何 RPATH,自己的和
62+
继承的都不用。两条腿各有单测,而正向那条在修复被拿掉时当场变红。
63+
5064
### 四处新拒绝有了名字
5165

5266
本轮之前新增的三处拒绝(设备源没到达 action、glob 命名了本包未声明的后端、`build.mcpp`

examples/09-heterogeneous/cann/app/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,18 @@ Measured on an x86_64 machine with **no Ascend hardware and no Ascend driver**:
2727
| the kernel compiles | `bisheng -x asc --cce-aicore-arch=dav-c220` |
2828
| the object joins the ordinary link | mixed mode: an x86-64 object carrying the device binary |
2929
| the host half links | ACL, plus the six-library closure the rule names |
30+
| the runtime-closure check | names `libascend_hal.so` and nothing else (2026.9.6.6+) |
3031
| the artifact starts | **no** -- `libascend_hal.so` is missing |
3132
| `--no-accel` | builds and runs: `12 24 36 48`, `device: cpu` |
3233

34+
The closure row is a measurement that once disagreed with the loader. Before
35+
2026.9.6.6 mcpp named **eight** libraries, and the artifact it had just linked
36+
resolved seven of them: the toolkit's shared libraries depend on each other by
37+
bare SONAME and carry no search path, while the directory holding them is named
38+
once, in the executable's `DT_RPATH`. The model searched only the requesting
39+
object's own list, and `DT_RPATH` is inherited down the whole chain. The engine
40+
now models that, so the refusal names exactly what the loader will fail on.
41+
3342
`libascend_hal.so` belongs to the **driver**, not the toolkit, and is the role
3443
`libcuda.so.1` plays for CUDA: in ABI lockstep with the kernel module, not
3544
redistributable, and absent on a machine with no NPU. A device build of this

src/runtime/elf.cppm

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ std::optional<std::filesystem::path> resolve_needed(
395395
std::string_view soname,
396396
const ElfRuntimeFacts& requester,
397397
const mcpp::platform::runtime::RuntimeBinding& binding,
398-
std::span<const std::filesystem::path> additionalSearchDirs) {
398+
std::span<const std::filesystem::path> additionalSearchDirs,
399+
std::span<const std::string> inheritedRpaths) {
399400
std::filesystem::path named(soname);
400401
std::error_code ec;
401402
if (named.has_parent_path()) {
@@ -409,6 +410,26 @@ std::optional<std::filesystem::path> resolve_needed(
409410
std::vector<std::filesystem::path> dirs;
410411
for (auto const& raw : requester.runpaths)
411412
append_unique_path(dirs, expand_origin(raw, requester.artifact));
413+
// DT_RPATH IS INHERITED DOWN THE DEPENDENCY CHAIN; DT_RUNPATH IS NOT.
414+
//
415+
// The loader searches the DT_RPATH of every object on the chain that
416+
// loaded this one, not just this object's own. Modelling only the
417+
// requester's list reported a library as unfindable whenever a payload's
418+
// shared libraries depend on each other and the RPATH naming their
419+
// directory sits on the EXECUTABLE -- which is the ordinary shape for a
420+
// vendor toolkit. Measured on examples/09-heterogeneous/cann: mcpp refused
421+
// the build naming eight libraries, and the artifact it had just linked
422+
// resolved seven of them and failed on the eighth, which belongs to a
423+
// driver that machine does not have.
424+
//
425+
// The suppression is a property of the SEARCHING object: an object that
426+
// carries DT_RUNPATH uses no RPATH at all, its own or inherited. `Both` is
427+
// that case too, because glibc ignores DT_RPATH whenever DT_RUNPATH is
428+
// present.
429+
if (requester.searchPathTag != SearchPathTag::Runpath
430+
&& requester.searchPathTag != SearchPathTag::Both)
431+
for (auto const& raw : inheritedRpaths)
432+
append_unique_path(dirs, expand_origin(raw, requester.artifact));
412433
for (auto const& dir : additionalSearchDirs) append_unique_path(dirs, dir);
413434
for (auto const& dir : binding.libraryDirs) append_unique_path(dirs, dir);
414435
// NOTE: the SubOS farm is NOT read from the binding here.
@@ -910,8 +931,16 @@ RuntimeResolution resolve_runtime_closure(
910931
resolution.artifact = std::move(*root);
911932
resolution.artifactIsElf = true;
912933

913-
std::deque<ElfRuntimeFacts> queue;
914-
queue.push_back(resolution.artifact);
934+
// Each queued object carries the DT_RPATHs it inherited from the chain
935+
// that loaded it. Kept beside the facts rather than inside them: it is a
936+
// property of HOW this object was reached, and the same file reached twice
937+
// is one loaded object with the first arrival's chain.
938+
struct Pending {
939+
ElfRuntimeFacts facts;
940+
std::vector<std::string> inheritedRpaths;
941+
};
942+
std::deque<Pending> queue;
943+
queue.push_back({resolution.artifact, {}});
915944
std::set<std::filesystem::path> visited;
916945
visited.insert(detail::comparable_path(artifact));
917946
// The ELF loader maintains one process-global loaded-object namespace.
@@ -924,16 +953,27 @@ RuntimeResolution resolve_runtime_closure(
924953
}
925954
constexpr std::size_t kMaxClosureObjects = 512;
926955
while (!queue.empty() && resolution.objects.size() < kMaxClosureObjects) {
927-
auto requester = std::move(queue.front());
956+
auto pending = std::move(queue.front());
928957
queue.pop_front();
958+
const auto& requester = pending.facts;
959+
// What this object hands to the objects it loads: its own DT_RPATH
960+
// when that is the tag it carries, on top of whatever it inherited.
961+
// DT_RUNPATH never propagates, and a `Both` object's DT_RPATH is dead
962+
// to the loader, so neither contributes.
963+
std::vector<std::string> childRpaths = pending.inheritedRpaths;
964+
if (requester.searchPathTag == SearchPathTag::Rpath)
965+
for (auto const& raw : requester.runpaths)
966+
if (std::ranges::find(childRpaths, raw) == childRpaths.end())
967+
childRpaths.push_back(raw);
929968
for (auto const& soname : requester.needed) {
930969
std::optional<std::filesystem::path> path;
931970
if (auto loaded = loadedBySoname.find(soname);
932971
loaded != loadedBySoname.end()) {
933972
path = loaded->second;
934973
} else {
935974
path = detail::resolve_needed(
936-
soname, requester, binding, additionalSearchDirs);
975+
soname, requester, binding, additionalSearchDirs,
976+
pending.inheritedRpaths);
937977
if (!path) {
938978
resolution.unresolved.push_back(soname);
939979
resolution.unresolvedSonames.push_back(soname);
@@ -968,7 +1008,7 @@ RuntimeResolution resolve_runtime_closure(
9681008
}
9691009
loadedBySoname.emplace(parsed->soname, *path);
9701010
}
971-
queue.push_back(*parsed);
1011+
queue.push_back({*parsed, childRpaths});
9721012
resolution.objects.push_back(std::move(*parsed));
9731013
}
9741014
}

tests/unit/test_elf_runtime.cpp

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ struct ElfFixtureSpec {
5555
std::string interp = "/store/glibc/2.44/lib64/ld-linux-x86-64.so.2";
5656
std::vector<std::string> needed = {"libc.so.6"};
5757
std::string runpath = "/host/z:/host/a";
58+
// Emit the search path as DT_RPATH and nothing else. The default image
59+
// carries both tags, which is the common shape and the one glibc reads as
60+
// DT_RUNPATH -- so a test about DT_RPATH's reach cannot use it.
61+
bool rpathOnly = false;
62+
// No search path at all: the object that has to reach its dependencies
63+
// through someone else's DT_RPATH.
64+
bool noSearchPath = false;
5865
};
5966

6067
// One deliberately tiny ELF64-LE image. It has no executable code; the test
@@ -108,7 +115,8 @@ std::filesystem::path write_elf_fixture(
108115
auto versionOwner = needed.empty()
109116
? append_string(b, kDynstr, cursor, "libc.so.6")
110117
: needed.front();
111-
auto rpath = append_string(b, kDynstr, cursor, "/legacy/ignored");
118+
auto rpath = append_string(b, kDynstr, cursor,
119+
spec.rpathOnly ? spec.runpath : "/legacy/ignored");
112120
auto runpath = append_string(b, kDynstr, cursor, spec.runpath);
113121
auto needVersion = append_string(b, kDynstr, cursor, "GLIBC_2.40");
114122
auto defVersion = append_string(b, kDynstr, cursor, "GLIBC_2.44");
@@ -123,8 +131,10 @@ std::filesystem::path write_elf_fixture(
123131
dyn(5, kVaddr + kDynstr); // DT_STRTAB
124132
dyn(10, dynstrSize); // DT_STRSZ
125133
for (auto offset : needed) dyn(1, offset); // DT_NEEDED
126-
dyn(15, rpath); // DT_RPATH (ignored when RUNPATH exists)
127-
dyn(29, runpath); // DT_RUNPATH
134+
if (!spec.noSearchPath) {
135+
dyn(15, rpath); // DT_RPATH (ignored when RUNPATH exists)
136+
if (!spec.rpathOnly) dyn(29, runpath); // DT_RUNPATH
137+
}
128138
dyn(0x6ffffffe, kVaddr + kVerneed); // DT_VERNEED
129139
dyn(0x6fffffff, 1); // DT_VERNEEDNUM
130140
dyn(0x6ffffffc, kVaddr + kVerdef); // DT_VERDEF
@@ -283,6 +293,80 @@ TEST(ElfRuntime, ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths) {
283293
std::filesystem::weakly_canonical(glibc44 / "libc.so.6"));
284294
}
285295

296+
// DT_RPATH REACHES THE WHOLE CHAIN; DT_RUNPATH REACHES ONE OBJECT.
297+
//
298+
// A vendor toolkit's shared libraries depend on each other by bare SONAME and
299+
// carry no search path of their own; the directory holding them is named once,
300+
// in the EXECUTABLE's DT_RPATH. The model searched only the requesting
301+
// object's own list, so every such library read as unfindable.
302+
//
303+
// Measured on examples/09-heterogeneous/cann before this: mcpp refused the
304+
// build naming eight libraries, and the artifact it had just linked resolved
305+
// seven of them -- failing only on the one that belongs to a driver the
306+
// machine does not have.
307+
TEST(ElfRuntime, ADependencyInheritsTheExecutablesRpath) {
308+
if constexpr (!mcpp::platform::is_linux)
309+
GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux";
310+
Tmp t;
311+
auto payload = t.path / "store";
312+
auto glibc = payload / "2.44" / "lib64";
313+
auto toolkit = t.path / "toolkit" / "lib64";
314+
std::filesystem::create_directories(glibc);
315+
std::filesystem::create_directories(toolkit);
316+
317+
write_elf_fixture(glibc / "libc.so.6", { .needed = {}, .runpath = glibc.string() });
318+
// The leaf, reachable only through the executable's DT_RPATH.
319+
write_elf_fixture(toolkit / "libdeep.so", { .needed = {}, .noSearchPath = true });
320+
// The middle object: names its dependency and says nothing about where it
321+
// lives, which is what a vendor library does.
322+
write_elf_fixture(toolkit / "libtop.so",
323+
{ .needed = {"libdeep.so"}, .noSearchPath = true });
324+
auto app = write_elf_fixture(t.path / "app", {
325+
.needed = {"libtop.so", "libc.so.6"},
326+
.runpath = toolkit.string() + ":" + glibc.string(),
327+
.rpathOnly = true,
328+
});
329+
330+
auto resolution = elf::resolve_runtime_closure(app, binding_for(payload));
331+
EXPECT_TRUE(resolution.unresolvedSonames.empty())
332+
<< "unresolved: " << (resolution.unresolvedSonames.empty()
333+
? std::string{} : resolution.unresolvedSonames.front());
334+
}
335+
336+
// …and the suppression, which is the half that makes the rule a rule. An
337+
// object carrying DT_RUNPATH uses no RPATH at all -- its own or inherited --
338+
// so a test with only the leg above would also pass on an implementation that
339+
// inherited unconditionally.
340+
TEST(ElfRuntime, ADependencyWithItsOwnRunpathDoesNotInheritOne) {
341+
if constexpr (!mcpp::platform::is_linux)
342+
GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux";
343+
Tmp t;
344+
auto payload = t.path / "store";
345+
auto glibc = payload / "2.44" / "lib64";
346+
auto toolkit = t.path / "toolkit" / "lib64";
347+
auto elsewhere = t.path / "elsewhere";
348+
std::filesystem::create_directories(glibc);
349+
std::filesystem::create_directories(toolkit);
350+
std::filesystem::create_directories(elsewhere);
351+
352+
write_elf_fixture(glibc / "libc.so.6", { .needed = {}, .runpath = glibc.string() });
353+
write_elf_fixture(toolkit / "libdeep.so", { .needed = {}, .noSearchPath = true });
354+
// Same graph as above, except this middle object carries DT_RUNPATH. It
355+
// names a directory that does not hold `libdeep.so`, and glibc will not
356+
// fall back to the executable's DT_RPATH for it.
357+
write_elf_fixture(toolkit / "libtop.so",
358+
{ .needed = {"libdeep.so"}, .runpath = elsewhere.string() });
359+
auto app = write_elf_fixture(t.path / "app", {
360+
.needed = {"libtop.so", "libc.so.6"},
361+
.runpath = toolkit.string() + ":" + glibc.string(),
362+
.rpathOnly = true,
363+
});
364+
365+
auto resolution = elf::resolve_runtime_closure(app, binding_for(payload));
366+
ASSERT_EQ(resolution.unresolvedSonames.size(), 1u);
367+
EXPECT_EQ(resolution.unresolvedSonames.front(), "libdeep.so");
368+
}
369+
286370
TEST(RuntimePhysics, RuleBRejectsInterpreterAndLibcFromDifferentPayloads) {
287371
if constexpr (!mcpp::platform::is_linux)
288372
GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux";

0 commit comments

Comments
 (0)