-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathsymbol_provision.cppm
More file actions
316 lines (286 loc) · 14.8 KB
/
Copy pathsymbol_provision.cppm
File metadata and controls
316 lines (286 loc) · 14.8 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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// mcpp.build.symbol_provision — is every symbol in this image provided ONCE?
//
// THE INVARIANT
//
// One library, one provider, one form.
//
// mcpp enforces it at two altitudes, because it has two kinds of knowledge.
// `mcpp.build.linkage_form` enforces it over what mcpp DECIDED — packages,
// targets, forms — at plan time, on every platform. This module enforces it
// over what the linker actually PRODUCED, and that is the only altitude that
// can see a library mcpp never knew about: one vendored inside a prebuilt
// package, one arriving through pkg-config, one belonging to the host.
//
// WHY AN IMAGE CAN HIJACK A LIBRARY IT DID NOT MEAN TO
//
// ELF has one flat symbol namespace and the executable is searched first. When
// an executable statically contains `inflate` and a shared object it links
// declares an undefined `inflate`, the linker adds the executable's definition
// to `.dynsym` so that reference will bind — no `-rdynamic` required, and mcpp
// passes none. At run time the shared object then calls the STATIC copy, and
// whatever `libz.so.1` sits beside it is dead weight. The link is silent, the
// loader is silent, and the program usually works.
//
// The mechanism is not a defect; it is what makes `malloc` interposition work.
// What was missing is anyone asking whether it happened on purpose.
//
// THE PREDICATE, IN TWO STAGES
//
// EXPORTED = defined entries of .dynsym, minus copy relocations
// CONFLICT = EXPORTED ∩ ⋃ defines(closure)
//
// Stage one is cheap and runs on every changed image; it is empty for a normal
// mcpp binary (measured: 0 of 217 dynamic symbols). Stage two runs only when
// stage one is not, and it is the stage that makes the report TRUE.
//
// STAGE TWO IS NOT OPTIONAL, and the reason is mcpp's own doing. A
// `kind = "shared"` dependency's link unit receives only ITS OWN objects
// (mcpp.build.plan), so a static package underneath it lands in the CONSUMER's
// executable instead, and the shared library binds back to it at run time.
// That is the shape above — arranged by mcpp, with exactly one copy of the
// code in the process, and completely benign. Reporting stage one alone would
// warn about a correct build that the user cannot do anything about, which is
// precisely the noise `mcpp.build.distribution` refuses to emit.
//
// Design: .agents/docs/2026-08-28-issue519-dependency-linkage-form.md §2.
export module mcpp.build.symbol_provision;
import std;
import mcpp.runtime.elf;
export namespace mcpp::build::symbol_provision {
// A definition this image contributes to the process-wide namespace.
struct Export {
std::string name;
bool isFunc = false;
// A vague-linkage definition (STB_WEAK): a template instantiation, an
// inline function, a vtable. Every translation unit that needs one emits
// it and the loader unifies them; that is the C++ ABI working, not an
// image displacing a library's own copy.
bool isWeak = false;
};
// One object that could also supply a symbol, as the report will name it.
struct Provider {
std::string label; // a path, or "<pkg> (static, merged)"
std::vector<std::string> defines; // symbol names it defines
};
// A symbol with more than one provider in one image.
struct Conflict {
std::string name;
bool isFunc = false;
bool isWeak = false;
std::vector<std::string> alsoProvidedBy;
};
// FOUR-VALUED, and the last two are why.
//
// Clean the predicate applies, was evaluated, and held
// Conflict a symbol has two providers and the static one wins
// NotApplicable the predicate does not apply here (a static link, a shared
// library, an image whose author asked for exports)
// NotEvaluated it applies but could not be computed
//
// `NotApplicable` and `NotEvaluated` exist separately from `Clean` for the
// reason this repository keeps rediscovering: a check that reports "no
// findings" when it never ran is a check that goes green forever.
enum class Status { Clean, Conflict, NotApplicable, NotEvaluated };
std::string_view to_string(Status status);
struct Report {
Status status = Status::NotApplicable;
// |EXPORTED| and the size of the whole dynamic symbol table. Both are
// reported, always: "0" means nothing without the denominator beside it.
std::size_t exported = 0;
std::size_t total = 0;
std::vector<Conflict> conflicts;
// Shared vague-linkage definitions, counted and not listed.
//
// They are NOT a finding: the C++ ABI emits a template instantiation into
// every image that needs it and expects the loader to keep one. Counting
// them is still worth doing -- a reader who runs `nm -D` sees them and has
// to be told which ones this check decided about, or "clean" reads as
// "did not look".
std::size_t sharedWeak = 0;
// Why, for the two non-answers. Empty for Clean and Conflict.
std::string reason;
bool actionable() const { return status == Status::Conflict; }
// Human-readable body. Empty unless there is something to say.
std::string explain(std::string_view artifact) const;
};
// Did this link ask for its symbols to be exported? Then the whole predicate
// is void: the author requested exactly the thing it detects.
//
// A FUNCTION over the flags rather than a boolean threaded from far away, so
// the vocabulary is written down once and can be tested without a linker.
bool export_dynamic_requested(std::span<const std::string> flags);
// Stage one. Every defined FUNC counts; a defined OBJECT counts unless a copy
// relocation sits at its address (libc data moved into this image).
//
// Returns nullopt when `symbols.copyRelocationsKnown` is false: without the
// machine's COPY relocation type every data symbol would look like a
// conflict, and answering wrongly is worse than declining.
std::optional<std::vector<Export>>
exported_definitions(const mcpp::platform::elf::DynamicSymbols& symbols);
// Stage two. `closure` is every object the image's loader will consult.
std::vector<Conflict> conflicting_exports(std::span<const Export> exports,
std::span<const Provider> closure);
// The report an image with no dynamic symbol table gets, and the one an image
// whose author asked for exports gets. Named constructors rather than raw
// struct literals so every non-answer carries its reason.
Report not_applicable(std::string reason);
Report not_evaluated(std::string reason);
} // namespace mcpp::build::symbol_provision
namespace mcpp::build::symbol_provision {
std::string_view to_string(Status status) {
switch (status) {
case Status::Clean: return "clean";
case Status::Conflict: return "conflict";
case Status::NotApplicable: return "not-applicable";
case Status::NotEvaluated: return "not-evaluated";
}
return "not-evaluated";
}
bool export_dynamic_requested(std::span<const std::string> flags) {
// Spellings, not a substring sweep: `--export-dynamic-symbol=foo` and
// `--dynamic-list=x.txt` restrict the export set rather than requesting
// one wholesale, but either way the author has taken over the decision.
static constexpr std::string_view kExact[] = {
"-rdynamic", "-export-dynamic", "--export-dynamic",
"-Wl,--export-dynamic", "-Wl,-export-dynamic", "-Wl,-E",
};
static constexpr std::string_view kPrefixes[] = {
"-Wl,--dynamic-list", "--dynamic-list",
"-Wl,--export-dynamic-symbol", "--export-dynamic-symbol",
};
for (auto const& flag : flags) {
for (auto exact : kExact) if (flag == exact) return true;
for (auto prefix : kPrefixes) if (flag.starts_with(prefix)) return true;
}
return false;
}
std::optional<std::vector<Export>>
exported_definitions(const mcpp::platform::elf::DynamicSymbols& symbols) {
if (!symbols.copyRelocationsKnown) return std::nullopt;
std::vector<Export> out;
for (auto const& symbol : symbols.defined) {
// A copy relocation is never a function, so the address lookup is
// only asked about data — which also keeps a FUNC that happens to
// share an address with relocated data from being excused.
if (!symbol.isFunc && symbols.copyRelocations.contains(symbol.value))
continue;
out.push_back(Export{ .name = symbol.name, .isFunc = symbol.isFunc,
.isWeak = symbol.isWeak });
}
std::ranges::sort(out, {}, &Export::name);
return out;
}
std::vector<Conflict> conflicting_exports(std::span<const Export> exports,
std::span<const Provider> closure) {
std::vector<Conflict> out;
for (auto const& exported : exports) {
Conflict conflict{ .name = exported.name, .isFunc = exported.isFunc,
.isWeak = exported.isWeak };
for (auto const& provider : closure) {
if (std::ranges::find(provider.defines, exported.name)
!= provider.defines.end())
conflict.alsoProvidedBy.push_back(provider.label);
}
if (!conflict.alsoProvidedBy.empty()) out.push_back(std::move(conflict));
}
return out;
}
Report not_applicable(std::string reason) {
return Report{ .status = Status::NotApplicable, .reason = std::move(reason) };
}
Report not_evaluated(std::string reason) {
return Report{ .status = Status::NotEvaluated, .reason = std::move(reason) };
}
std::string Report::explain(std::string_view artifact) const {
if (status != Status::Conflict || conflicts.empty()) return {};
// Cap the list. The names are evidence, not an inventory — a zlib pulled
// in twice contributes 86 of them, and a diagnostic nobody finishes
// reading is a diagnostic nobody acts on.
constexpr std::size_t kShown = 6;
std::string body = std::format(
"{}: {} symbol{} in this image {} also provided by a library it loads.\n",
artifact, conflicts.size(), conflicts.size() == 1 ? "" : "s",
conflicts.size() == 1 ? "is" : "are");
std::set<std::string> providers;
for (std::size_t i = 0; i < conflicts.size(); ++i) {
auto const& conflict = conflicts[i];
for (auto const& label : conflict.alsoProvidedBy) providers.insert(label);
if (i < kShown)
body += std::format(" {}{}\n", conflict.name,
conflict.isFunc ? "()" : "");
else if (i == kShown)
body += std::format(" ... and {} more\n", conflicts.size() - kShown);
}
body += " Also provided by:\n";
for (auto const& label : providers) body += std::format(" {}\n", label);
if (sharedWeak > 0)
body += std::format(
" ({} vague-linkage definition{} -- template instantiations, inline\n"
" functions, vtables -- {} also shared and are NOT part of this\n"
" finding: the C++ ABI emits one per image and the loader keeps one.)\n",
sharedWeak, sharedWeak == 1 ? "" : "s",
sharedWeak == 1 ? "is" : "are");
// WHY it matters, then what to do — IN THE ORDER THAT ACTUALLY WORKS.
//
// `dependency_linkage = "shared"` is deliberately NOT first, and that
// ordering was corrected against a measurement rather than reasoned. On a
// real graph (a package staging glib, whose libgio pulls libz.so.1, plus a
// statically built compat.zlib) switching the form does remove the
// executable's 88 exported symbols — and then TWO zlibs load, because the
// library mcpp builds is `libzlib.so` while the reference is to
// `libz.so.1`. That is a worse state than the one being reported: two
// copies instead of one, and no diagnostic at all, since the check
// described above only looks at executables.
//
// So the first suggestion is the one that is always correct, and the form
// switch is offered with the condition that makes it work.
body +=
" The executable is searched first, so the copy inside it wins for\n"
" every symbol both provide — the library's own copy is never called,\n"
" and code inside that library now runs against a build it was not\n"
" linked against.\n";
// THE UNWINDER FAMILY IS NOT ONE MORE DUPLICATE SYMBOL.
//
// For every other name the sentence above is the whole consequence: one
// implementation is called instead of another, and the two are usually
// interchangeable. For `_Unwind_*` the consequence is that a throw is
// processed by TWO unwinders and no `catch` runs.
//
// A static archive contributes only the members something references, so
// the interposition is PARTIAL by construction. Measured on a SYCL
// artifact (mcpp#596): 10 of libgcc_s's 18 entry points came from the
// executable's libunwind and 8 stayed in libgcc_s, including the context
// accessors — so libstdc++'s personality routine read an LLVM libunwind
// `_Unwind_Context` through libgcc's `_Unwind_GetIPInfo`, recovered a
// meaningless IP, found no landing pad, and `__cxa_call_terminate` ran
// past a handler three frames up. The program aborted with exit 134 and
// printed nothing, because `__verbose_terminate_handler` rethrows to name
// the exception's type and that rethrow terminated as well.
//
// Said separately rather than folded into the list below, because none of
// the three ways out addresses it: the fix is one unwinder in the process,
// which is a link-line question rather than a packaging one.
if (std::ranges::any_of(conflicts, [](auto const& c) {
return c.name.starts_with("_Unwind_"); })) {
body +=
" These include the unwinder's entry points (`_Unwind_*`), and\n"
" for those the consequence is stronger: an exception is then\n"
" raised by one unwinder and inspected by the other, no `catch`\n"
" matches, and the program calls std::terminate past a handler\n"
" that should have run. A static archive contributes only the\n"
" members something references, so the split is partial and the\n"
" program is correct until something throws.\n";
}
body +=
" Ways out, in the order they apply:\n"
" 1. stop one side from providing it — usually the package that\n"
" ships a copy of a library the graph already builds;\n"
" 2. make both resolve to ONE file: declare the library's real\n"
" SONAME on its target, so the shared copy and the built one are\n"
" the same name;\n"
" 3. `[build] dependency_linkage` / a per-dependency `linkage`\n"
" changes which form mcpp builds — it removes THIS finding, but\n"
" it only unifies the two providers when (2) holds as well.";
return body;
}
} // namespace mcpp::build::symbol_provision