-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtest_symbol_provision.cpp
More file actions
295 lines (264 loc) · 13.4 KB
/
Copy pathtest_symbol_provision.cpp
File metadata and controls
295 lines (264 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// One library, one provider — evaluated on a produced image (issue #519).
//
// Two of these are regression tests for predicates that were WRONG before
// they were measured, and both would have failed silently rather than loudly:
//
// * matching copy relocations by NAME reports `environ` as hijacked in
// every dynamically linked executable, because it is a weak alias of
// `__environ` at the same address and only the latter is in the
// relocation table;
// * reporting on stage one alone flags mcpp's OWN `kind = "shared"`
// arrangement, where a shared dependency's static dependency legitimately
// lands in the consumer's executable with exactly one copy in the process.
#include <gtest/gtest.h>
import std;
import mcpp.build.symbol_provision;
import mcpp.runtime.elf;
namespace sp = mcpp::build::symbol_provision;
namespace elf = mcpp::platform::elf;
namespace {
elf::DynamicSymbols image() {
elf::DynamicSymbols s;
s.present = true;
s.copyRelocationsKnown = true;
return s;
}
elf::DynamicSymbol func(std::string name, std::uint64_t at = 0x1000) {
return elf::DynamicSymbol{ .name = std::move(name), .isFunc = true, .value = at };
}
elf::DynamicSymbol object(std::string name, std::uint64_t at) {
return elf::DynamicSymbol{ .name = std::move(name), .isFunc = false, .value = at };
}
std::vector<std::string> names(const std::vector<sp::Export>& exports) {
std::vector<std::string> out;
for (auto const& e : exports) out.push_back(e.name);
return out;
}
} // namespace
// ── stage one ──────────────────────────────────────────────────────────────
TEST(SymbolProvision, ADefinedFunctionIsAlwaysAnExport) {
auto s = image();
s.defined.push_back(func("inflate"));
// Even at an address that carries a copy relocation: a copy relocation
// moves DATA, so a function sharing that address is not one.
s.copyRelocations.insert(0x1000);
auto exports = sp::exported_definitions(s);
ASSERT_TRUE(exports.has_value());
EXPECT_EQ(names(*exports), std::vector<std::string>{"inflate"});
}
TEST(SymbolProvision, CopyRelocatedDataIsNotAnExport) {
auto s = image();
s.defined.push_back(object("stdout", 0xb1d888));
s.copyRelocations.insert(0xb1d888);
auto exports = sp::exported_definitions(s);
ASSERT_TRUE(exports.has_value());
EXPECT_TRUE(exports->empty());
}
TEST(SymbolProvision, CopyRelocationsMatchByAddressNotByName) {
// MEASURED, on every mcpp binary: glibc's `environ` is a WEAK alias of
// `__environ` at one address, and only `__environ` appears in `.rela.dyn`.
// A name-keyed filter reports `environ` as a hijacked symbol in every
// dynamically linked executable ever built.
auto s = image();
s.defined.push_back(object("__environ", 0xb1d840));
s.defined.push_back(object("environ", 0xb1d840)); // same address
s.copyRelocations.insert(0xb1d840); // only one entry
auto exports = sp::exported_definitions(s);
ASSERT_TRUE(exports.has_value());
EXPECT_TRUE(exports->empty()) << "environ must not be reported";
}
TEST(SymbolProvision, AnUninitialisedDataSymbolWithNoCopyRelocationIsAnExport) {
// /usr/bin/ls's `obstack_alloc_failed_handler` — a function-pointer
// variable that gnulib defines and glibc also defines. It lives in .bss
// like a copy relocation would, and it is NOT one.
auto s = image();
s.defined.push_back(object("obstack_alloc_failed_handler", 0x2000));
auto exports = sp::exported_definitions(s);
ASSERT_TRUE(exports.has_value());
EXPECT_EQ(names(*exports),
std::vector<std::string>{"obstack_alloc_failed_handler"});
}
TEST(SymbolProvision, AnUnknownMachineDeclinesRatherThanGuessing) {
// Without the machine's COPY relocation type every data symbol looks like
// an export. "Could not evaluate" must not be spelled the same way as
// "evaluated and clean".
auto s = image();
s.copyRelocationsKnown = false;
s.defined.push_back(object("stdout", 0x10));
EXPECT_FALSE(sp::exported_definitions(s).has_value());
}
// ── stage two ──────────────────────────────────────────────────────────────
TEST(SymbolProvision, AnExportWithNoSecondProviderIsNotAConflict) {
// mcpp's OWN arrangement. A `kind = "shared"` dependency's link unit
// takes only its own objects, so its static dependency lands in the
// consumer's executable and the shared library binds back to it. One copy
// in the process, entirely benign — and stage one alone would warn about
// it on every build that uses compat.x11 with a real static dependency.
std::vector<sp::Export> exports{ sp::Export{"shared_answer", true} };
std::vector<sp::Provider> closure{
sp::Provider{"/lib/libc.so.6", {"printf", "malloc"}},
};
EXPECT_TRUE(sp::conflicting_exports(exports, closure).empty());
}
TEST(SymbolProvision, AnExportWithASecondProviderIsAConflictAndNamesIt) {
std::vector<sp::Export> exports{ sp::Export{"inflate", true},
sp::Export{"deflate", true} };
std::vector<sp::Provider> closure{
sp::Provider{"/pkg/lib/libz.so.1", {"deflate", "inflate", "crc32"}},
sp::Provider{"/lib/libc.so.6", {"printf"}},
};
auto conflicts = sp::conflicting_exports(exports, closure);
ASSERT_EQ(conflicts.size(), 2u);
EXPECT_EQ(conflicts[0].name, "inflate");
ASSERT_EQ(conflicts[0].alsoProvidedBy.size(), 1u);
EXPECT_EQ(conflicts[0].alsoProvidedBy[0], "/pkg/lib/libz.so.1");
}
TEST(SymbolProvision, TheReportNamesEveryProviderAndCapsTheSymbolList) {
sp::Report report;
report.status = sp::Status::Conflict;
report.total = 217;
for (int i = 0; i < 20; ++i)
// DESIGNATED, not positional. A field added to `Conflict` between
// `isFunc` and `alsoProvidedBy` bound the provider list to a bool
// here -- a string literal converts to one, so it compiled, and the
// provider list silently became empty.
report.conflicts.push_back(sp::Conflict{
.name = std::format("sym{}", i),
.isFunc = true,
.alsoProvidedBy = {"/pkg/lib/libz.so.1"}});
report.exported = report.conflicts.size();
auto text = report.explain("consumer");
EXPECT_NE(text.find("20 symbols"), std::string::npos);
EXPECT_NE(text.find("and 14 more"), std::string::npos);
EXPECT_NE(text.find("/pkg/lib/libz.so.1"), std::string::npos);
// The three ways out are the point of the message, and their ORDER is
// load-bearing: switching the form removes this finding while leaving two
// copies loaded unless the SONAMEs also match, so it must not be first.
auto stop = text.find("stop one side");
auto soname = text.find("SONAME");
auto form = text.find("dependency_linkage");
ASSERT_NE(stop, std::string::npos);
ASSERT_NE(soname, std::string::npos);
ASSERT_NE(form, std::string::npos);
EXPECT_LT(stop, soname);
EXPECT_LT(soname, form);
}
TEST(SymbolProvision, OnlyAConflictIsActionable) {
EXPECT_FALSE(sp::not_applicable("static").actionable());
EXPECT_FALSE(sp::not_evaluated("unknown machine").actionable());
sp::Report clean; clean.status = sp::Status::Clean;
EXPECT_FALSE(clean.actionable());
EXPECT_TRUE(clean.explain("x").empty());
}
TEST(SymbolProvision, ANonAnswerCarriesItsReason) {
// "Not checked" and "checked and clean" must never render the same.
auto na = sp::not_applicable("statically linked");
EXPECT_EQ(na.status, sp::Status::NotApplicable);
EXPECT_EQ(na.reason, "statically linked");
EXPECT_EQ(sp::to_string(sp::Status::NotApplicable), "not-applicable");
EXPECT_EQ(sp::to_string(sp::Status::NotEvaluated), "not-evaluated");
EXPECT_NE(sp::to_string(sp::Status::Clean),
sp::to_string(sp::Status::NotApplicable));
}
// ── the precondition ───────────────────────────────────────────────────────
TEST(SymbolProvision, AnAuthorRequestedExportSurfaceVoidsThePredicate) {
// /usr/bin/bash exports 2339 symbols on purpose, for loadable builtins.
// Detecting exactly the thing the author asked for is not a finding.
EXPECT_TRUE(sp::export_dynamic_requested(
std::vector<std::string>{"-O2", "-rdynamic"}));
EXPECT_TRUE(sp::export_dynamic_requested(
std::vector<std::string>{"-Wl,--export-dynamic"}));
EXPECT_TRUE(sp::export_dynamic_requested(std::vector<std::string>{"-Wl,-E"}));
EXPECT_TRUE(sp::export_dynamic_requested(
std::vector<std::string>{"-Wl,--dynamic-list=syms.txt"}));
EXPECT_TRUE(sp::export_dynamic_requested(
std::vector<std::string>{"-Wl,--export-dynamic-symbol=foo"}));
}
TEST(SymbolProvision, OrdinaryLinkFlagsDoNotVoidThePredicate) {
// A substring sweep would fire on any of these.
EXPECT_FALSE(sp::export_dynamic_requested(std::vector<std::string>{
"-O2", "-Wl,-rpath,$ORIGIN", "-lz", "-Wl,--as-needed",
"-Wl,--enable-new-dtags", "-static-libstdc++", "-shared"}));
}
// ── vague linkage is not a second provider ─────────────────────────────────
//
// A template instantiation, an inline function or a vtable is emitted into
// every image that needs it and the loader keeps one. That is the C++ ABI
// working, and reporting it names a correct build.
//
// Measured on the SYCL example once the real findings were repaired: of the
// thirty-nine symbols the image still shared with `libsycl.so.9`,
// thirty-seven were `sycl::queue` and `sycl::buffer` instantiations from the
// same headers libsycl was built from -- and the remaining two were the
// island's own `extern "C"` entry points, which libsycl does not define. A
// check that could not tell binding from name reported all of them.
TEST(SymbolProvision, AWeakDefinitionIsCarriedThroughAsWeak) {
auto s = image();
auto weak = func("_ZN4sycl3_V15queueD2Ev");
weak.isWeak = true;
s.defined.push_back(weak);
s.defined.push_back(func("saxpy_device"));
auto exports = sp::exported_definitions(s);
ASSERT_TRUE(exports.has_value());
ASSERT_EQ(exports->size(), 2u);
// Sorted by name: the mangled one first.
EXPECT_TRUE((*exports)[0].isWeak);
EXPECT_FALSE((*exports)[1].isWeak);
}
TEST(SymbolProvision, AConflictRemembersWhetherItsDefinitionIsWeak) {
std::vector<sp::Export> exports{
{ .name = "_ZN4sycl3_V15queueD2Ev", .isFunc = true, .isWeak = true },
{ .name = "inflate", .isFunc = true, .isWeak = false },
};
std::vector<sp::Provider> closure{
{ .label = "libsycl.so.9",
.defines = {"_ZN4sycl3_V15queueD2Ev", "inflate"} },
};
auto conflicts = sp::conflicting_exports(exports, closure);
ASSERT_EQ(conflicts.size(), 2u);
// Both are shared; only the binding separates them, and the caller is what
// decides which one is a finding. Asserted here rather than in the caller
// so the DATA carries the distinction even if a future caller forgets it.
EXPECT_TRUE(conflicts[0].isWeak);
EXPECT_FALSE(conflicts[1].isWeak);
}
// #646 F3. GCC emits the static data of an inline entity with STB_GNU_UNIQUE
// (10) so that the loader keeps one copy across RTLD_LOCAL. It is vague linkage
// exactly as STB_WEAK is; reading only the latter reported seven libstdc++
// objects as a program displacing the library.
TEST(SymbolProvision, TheUniqueBindingIsVagueLinkageAsTheWeakOneIs) {
EXPECT_TRUE(elf::is_vague_linkage_binding(2)); // STB_WEAK
EXPECT_TRUE(elf::is_vague_linkage_binding(10)); // STB_GNU_UNIQUE
EXPECT_FALSE(elf::is_vague_linkage_binding(1)); // STB_GLOBAL
EXPECT_FALSE(elf::is_vague_linkage_binding(0)); // STB_LOCAL
}
// #646 F3. The std module's initialiser is linked into every C++ image that
// imports `std`, from one object. A provider that defines a name only through
// an object this build linked into both images is removed from that conflict.
TEST(SymbolProvision, ADefinitionBothImagesTakeFromOneObjectIsNotAConflict) {
std::vector<sp::Conflict> conflicts{
{ .name = "_ZGIW3std", .isFunc = true,
.alsoProvidedBy = {"bin/liblib.so"} },
{ .name = "inflate", .isFunc = true,
.alsoProvidedBy = {"bin/liblib.so", "/usr/lib/libz.so.1"} },
};
const std::map<std::string, std::set<std::string>> shared{
{ "bin/liblib.so", {"_ZGIW3std"} },
};
EXPECT_EQ(sp::drop_shared_plan_definitions(conflicts, shared), 1u);
ASSERT_EQ(conflicts.size(), 1u);
EXPECT_EQ(conflicts[0].name, "inflate");
// The library still defines `inflate` from its OWN object, so it stays a
// provider: attribution is by object, never by the library as a whole.
EXPECT_EQ(conflicts[0].alsoProvidedBy,
(std::vector<std::string>{"bin/liblib.so", "/usr/lib/libz.so.1"}));
}
// The rule is provenance, not a name pattern: an initialiser-shaped name that
// no shared object defines is still a finding.
TEST(SymbolProvision, AnInitialiserShapedNameWithoutSharedProvenanceIsStillReported) {
std::vector<sp::Conflict> conflicts{
{ .name = "_ZGIW5other", .isFunc = true, .alsoProvidedBy = {"bin/libother.so"} },
};
EXPECT_EQ(sp::drop_shared_plan_definitions(conflicts, {}), 0u);
EXPECT_EQ(conflicts.size(), 1u);
}