Skip to content

Commit c569152

Browse files
committed
fix(runtime): model ELF SONAME reuse
1 parent d008a21 commit c569152

2 files changed

Lines changed: 99 additions & 13 deletions

File tree

src/platform/elf_runtime.cppm

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ElfRuntimeFacts {
1717
std::filesystem::path artifact;
1818
std::uint16_t elfType = 0;
1919
std::string interp;
20+
std::string soname;
2021
std::vector<std::string> runpaths;
2122
std::vector<std::string> needed;
2223
std::vector<std::string> requiredGlibcVersions;
@@ -74,6 +75,7 @@ constexpr std::uint64_t kDtNeeded = 1;
7475
constexpr std::uint64_t kDtStrtab = 5;
7576
constexpr std::uint64_t kDtStrsz = 10;
7677
constexpr std::uint64_t kDtRpath = 15;
78+
constexpr std::uint64_t kDtSoname = 14;
7779
constexpr std::uint64_t kDtRunpath = 29;
7880
constexpr std::uint64_t kDtVerdef = 0x6ffffffc;
7981
constexpr std::uint64_t kDtVerdefnum = 0x6ffffffd;
@@ -382,6 +384,11 @@ inspect_elf_runtime(const std::filesystem::path& artifact) {
382384
if (!name || name->empty()) return std::unexpected(std::format(
383385
"artifact '{}' has an invalid DT_NEEDED", artifact.string()));
384386
out.needed.push_back(std::move(*name));
387+
} else if (tag == detail::kDtSoname) {
388+
auto name = dynstr(value);
389+
if (!name || name->empty()) return std::unexpected(std::format(
390+
"artifact '{}' has an invalid DT_SONAME", artifact.string()));
391+
out.soname = std::move(*name);
385392
} else if (tag == detail::kDtRpath || tag == detail::kDtRunpath) {
386393
auto path = dynstr(value);
387394
if (!path) return std::unexpected(std::format(
@@ -469,7 +476,9 @@ inspect_elf_runtime(const std::filesystem::path& artifact) {
469476
// Search order is loader physics, not presentation: sorting RUNPATH would
470477
// be capable of selecting a different libc than the process itself.
471478
detail::stable_unique(out.runpaths);
472-
detail::sort_unique(out.needed);
479+
// DT_NEEDED order is loader semantics. Reordering it can change which
480+
// payload wins when two dependency search paths contain the same SONAME.
481+
detail::stable_unique(out.needed);
473482
detail::sort_unique(out.requiredGlibcVersions);
474483
detail::sort_unique(out.definedGlibcVersions);
475484
return out;
@@ -492,16 +501,31 @@ RuntimeResolution resolve_runtime_closure(
492501
queue.push_back(resolution.artifact);
493502
std::set<std::filesystem::path> visited;
494503
visited.insert(detail::comparable_path(artifact));
504+
// The ELF loader maintains one process-global loaded-object namespace.
505+
// Once a SONAME has been mapped, a later requester reuses that object;
506+
// its own RUNPATH does not load a second file with the same SONAME.
507+
std::map<std::string, std::filesystem::path> loadedBySoname;
508+
if (!resolution.artifact.interp.empty()) {
509+
auto interp = detail::comparable_path(resolution.artifact.interp);
510+
loadedBySoname.emplace(interp.filename().string(), std::move(interp));
511+
}
495512
constexpr std::size_t kMaxClosureObjects = 512;
496513
while (!queue.empty() && resolution.objects.size() < kMaxClosureObjects) {
497514
auto requester = std::move(queue.front());
498515
queue.pop_front();
499516
for (auto const& soname : requester.needed) {
500-
auto path = detail::resolve_needed(
501-
soname, requester, binding, additionalSearchDirs);
502-
if (!path) {
503-
resolution.unresolved.push_back(soname);
504-
continue;
517+
std::optional<std::filesystem::path> path;
518+
if (auto loaded = loadedBySoname.find(soname);
519+
loaded != loadedBySoname.end()) {
520+
path = loaded->second;
521+
} else {
522+
path = detail::resolve_needed(
523+
soname, requester, binding, additionalSearchDirs);
524+
if (!path) {
525+
resolution.unresolved.push_back(soname);
526+
continue;
527+
}
528+
loadedBySoname.emplace(soname, *path);
505529
}
506530
if (soname == "libc.so.6") {
507531
if (resolution.artifact.resolvedLibc.empty())
@@ -518,6 +542,18 @@ RuntimeResolution resolve_runtime_closure(
518542
"{} ({})", soname, parsed.error()));
519543
continue;
520544
}
545+
if (!parsed->soname.empty()) {
546+
if (auto loaded = loadedBySoname.find(parsed->soname);
547+
loaded != loadedBySoname.end()
548+
&& !detail::same_path(loaded->second, *path)) {
549+
// The file reached through this request aliases a SONAME
550+
// that is already mapped. Model the loader's reuse and do
551+
// not add a second closure object.
552+
loadedBySoname[soname] = loaded->second;
553+
continue;
554+
}
555+
loadedBySoname.emplace(parsed->soname, *path);
556+
}
521557
queue.push_back(*parsed);
522558
resolution.objects.push_back(std::move(*parsed));
523559
}

tests/unit/test_elf_runtime.cpp

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@ std::uint32_t append_string(std::vector<unsigned char>& b,
5050
return offset;
5151
}
5252

53+
struct ElfFixtureSpec {
54+
std::string interp = "/store/glibc/2.44/lib64/ld-linux-x86-64.so.2";
55+
std::vector<std::string> needed = {"libc.so.6"};
56+
std::string runpath = "/host/z:/host/a";
57+
};
58+
5359
// One deliberately tiny ELF64-LE image. It has no executable code; the test
5460
// exercises the same program/dynamic/version tables real stripped binaries
5561
// retain, without depending on readelf, the host compiler or the host libc.
56-
std::filesystem::path write_elf_fixture(const std::filesystem::path& path) {
62+
std::filesystem::path write_elf_fixture(
63+
const std::filesystem::path& path,
64+
const ElfFixtureSpec& spec = {}) {
5765
constexpr std::uint64_t kVaddr = 0x400000;
5866
constexpr std::size_t kInterp = 0x200;
5967
constexpr std::size_t kDynamic = 0x300;
@@ -84,18 +92,23 @@ std::filesystem::path write_elf_fixture(const std::filesystem::path& path) {
8492
put64(b, p + 0x28, filesz);
8593
};
8694

87-
constexpr std::string_view interp = "/store/glibc/2.44/lib64/ld-linux-x86-64.so.2";
95+
const std::string_view interp = spec.interp;
8896
std::copy(interp.begin(), interp.end(), b.begin() + kInterp);
8997
b[kInterp + interp.size()] = 0;
9098
ph(0, 1, 0, b.size()); // PT_LOAD
9199
ph(1, 3, kInterp, interp.size() + 1); // PT_INTERP
92-
ph(2, 2, kDynamic, 11 * 16); // PT_DYNAMIC
100+
ph(2, 2, kDynamic, 24 * 16); // PT_DYNAMIC
93101

94102
std::size_t cursor = kDynstr;
95103
b[cursor++] = 0;
96-
auto libc = append_string(b, kDynstr, cursor, "libc.so.6");
104+
std::vector<std::uint32_t> needed;
105+
for (auto const& name : spec.needed)
106+
needed.push_back(append_string(b, kDynstr, cursor, name));
107+
auto versionOwner = needed.empty()
108+
? append_string(b, kDynstr, cursor, "libc.so.6")
109+
: needed.front();
97110
auto rpath = append_string(b, kDynstr, cursor, "/legacy/ignored");
98-
auto runpath = append_string(b, kDynstr, cursor, "/host/z:/host/a");
111+
auto runpath = append_string(b, kDynstr, cursor, spec.runpath);
99112
auto needVersion = append_string(b, kDynstr, cursor, "GLIBC_2.40");
100113
auto defVersion = append_string(b, kDynstr, cursor, "GLIBC_2.44");
101114
auto dynstrSize = cursor - kDynstr;
@@ -108,7 +121,7 @@ std::filesystem::path write_elf_fixture(const std::filesystem::path& path) {
108121
};
109122
dyn(5, kVaddr + kDynstr); // DT_STRTAB
110123
dyn(10, dynstrSize); // DT_STRSZ
111-
dyn(1, libc); // DT_NEEDED
124+
for (auto offset : needed) dyn(1, offset); // DT_NEEDED
112125
dyn(15, rpath); // DT_RPATH (ignored when RUNPATH exists)
113126
dyn(29, runpath); // DT_RUNPATH
114127
dyn(0x6ffffffe, kVaddr + kVerneed); // DT_VERNEED
@@ -120,7 +133,7 @@ std::filesystem::path write_elf_fixture(const std::filesystem::path& path) {
120133
// Elf64_Verneed + one Elf64_Vernaux.
121134
put16(b, kVerneed, 1);
122135
put16(b, kVerneed + 2, 1);
123-
put32(b, kVerneed + 4, libc);
136+
put32(b, kVerneed + 4, versionOwner);
124137
put32(b, kVerneed + 8, 16);
125138
put32(b, kVerneed + 12, 0);
126139
put32(b, kVerneed + 16, 0);
@@ -228,6 +241,43 @@ TEST(ElfRuntime, RejectsUnsupportedOrTruncatedElfWithoutGuessing) {
228241
EXPECT_FALSE(truncated.has_value());
229242
}
230243

244+
TEST(ElfRuntime, ReusesAnAlreadyLoadedSonameAcrossDependencyRunpaths) {
245+
if constexpr (!mcpp::platform::is_linux)
246+
GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux";
247+
Tmp t;
248+
auto payload = t.path / "store";
249+
auto glibc39 = payload / "2.39" / "lib64";
250+
auto glibc44 = payload / "2.44" / "lib64";
251+
auto shimDir = t.path / "shim";
252+
std::filesystem::create_directories(glibc39);
253+
std::filesystem::create_directories(glibc44);
254+
std::filesystem::create_directories(shimDir);
255+
256+
write_elf_fixture(glibc39 / "libc.so.6", {
257+
.needed = {"libc.so.6"},
258+
.runpath = glibc39.string(),
259+
});
260+
write_elf_fixture(glibc44 / "libc.so.6", {
261+
.needed = {"libc.so.6"},
262+
.runpath = glibc44.string(),
263+
});
264+
write_elf_fixture(shimDir / "libshim.so", {
265+
.needed = {"libc.so.6"},
266+
.runpath = glibc39.string(),
267+
});
268+
auto app = write_elf_fixture(t.path / "app", {
269+
.needed = {"libc.so.6", "libshim.so"},
270+
.runpath = shimDir.string(),
271+
});
272+
273+
auto resolution = elf::resolve_runtime_closure(app, binding_for(payload));
274+
ASSERT_TRUE(resolution.unresolved.empty());
275+
ASSERT_EQ(resolution.resolvedLibcs.size(), 1u)
276+
<< "a later dependency must reuse the process-global libc SONAME";
277+
EXPECT_EQ(resolution.resolvedLibcs.front(),
278+
std::filesystem::weakly_canonical(glibc44 / "libc.so.6"));
279+
}
280+
231281
TEST(RuntimePhysics, RuleBRejectsInterpreterAndLibcFromDifferentPayloads) {
232282
if constexpr (!mcpp::platform::is_linux)
233283
GTEST_SKIP() << "ELF/glibc runtime physics only apply on Linux";

0 commit comments

Comments
 (0)