-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathpost_install.cppm
More file actions
865 lines (809 loc) · 42.1 KB
/
Copy pathpost_install.cppm
File metadata and controls
865 lines (809 loc) · 42.1 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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
// mcpp.toolchain.post_install — toolchain payload post-install fixups (patchelf / specs / cfg)
//
// Extracted verbatim from cli.cppm (cli modularization, see
// .agents/docs/2026-06-10-cli-modularization.md). Zero behavior change:
// bodies are byte-identical moves; only the surrounding module/namespace
// changed (mcpp::cli::detail -> mcpp::cli).
module;
#include <cstdio>
#include <cstdlib>
export module mcpp.toolchain.post_install;
import std;
import mcpp.config;
import mcpp.libs.json;
import mcpp.log;
import mcpp.platform;
import mcpp.platform.runtime_binding;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.registry;
import mcpp.ui;
import mcpp.platform.xlings;
namespace mcpp::toolchain {
// ── #273 sandbox containment ─────────────────────────────────────────────
// patchelf_walk once escaped its sandbox through a symlinked payload
// directory (an e2e sandbox seeded `registry/data/xpkgs/xim-x-gcc` from the
// user's real ~/.xlings via `ln -s`) and rewrote PT_INTERP/RUNPATH of the
// REAL installation against loader paths inside a soon-deleted mktemp dir.
//
// Containment rule: every rewrite in this module is fenced by ONE explicit
// trust root — the owning sandbox's registry (`cfg.registryDir`), resolved
// once at the entry point via `containment_root` and threaded down as a
// parameter. The root is a first-class fact of the system; it is never
// re-derived from the payload path (canonicalizing the payload first would
// resolve the malicious symlink and collapse the fence into a tautology).
export std::filesystem::path
containment_root(const std::filesystem::path& registryDir) {
std::error_code ec;
auto canon = std::filesystem::weakly_canonical(registryDir, ec);
if (ec) return {}; // empty root → escapes_containment fails closed
return canon;
}
// True when `file`'s physical (symlink-resolved) location falls outside
// `rootCanon`. Fails CLOSED: an empty root or an unresolvable path counts
// as escaping — never patch what cannot be proven contained. Comparison is
// per path component (a `registry-evil` sibling sharing the string prefix
// is outside).
export bool escapes_containment(const std::filesystem::path& file,
const std::filesystem::path& rootCanon) {
if (rootCanon.empty()) return true;
std::error_code ec;
auto real = std::filesystem::weakly_canonical(file, ec);
if (ec) return true;
auto r = rootCanon.generic_string();
auto f = real.generic_string();
if (f == r) return false;
return !(f.size() > r.size() && f.starts_with(r) && f[r.size()] == '/');
}
// Run patchelf on every dynamic ELF in `dir` (recursively):
// - Set PT_INTERP to `loader` (the sandbox-local glibc loader).
// - Set RUNPATH to `rpath` (colon-separated list of sandbox lib dirs).
// Idempotent; skips static binaries and shared libs without PT_INTERP.
//
// TODO(xlings/libxpkg-upstream): xim 0.4.10's `elfpatch.auto({interpreter=...})`
// is supposed to do this in install hooks but currently scans 0 files for
// some packages (verified empirically: `binutils: elfpatch auto: 0 0 0`).
// Once the upstream legacy elfpatch path is fixed, this mcpp-side walker
// can be deleted.
export void patchelf_walk(const std::filesystem::path& dir,
const std::filesystem::path& loader,
const std::string& rpath,
const std::filesystem::path& patchelfBin,
const std::filesystem::path& fenceRoot)
{
if (!std::filesystem::exists(dir) || !std::filesystem::exists(patchelfBin))
return;
std::error_code ec;
for (auto it = std::filesystem::recursive_directory_iterator(dir, ec);
it != std::filesystem::recursive_directory_iterator{}; it.increment(ec))
{
if (ec) { ec.clear(); continue; }
if (!it->is_regular_file(ec)) continue;
auto path = it->path();
// Skip non-ELF (cheap magic check)
std::ifstream is(path, std::ios::binary);
char m[4]{};
is.read(m, 4);
if (!is || m[0] != 0x7f || m[1] != 'E' || m[2] != 'L' || m[3] != 'F')
continue;
is.close();
// #273 fence: never rewrite files that physically live outside the
// sandbox (payload reached through a symlink → foreign installation).
if (escapes_containment(path, fenceRoot)) {
mcpp::log::verbose("toolchain", std::format(
"patchelf_walk: skip (outside sandbox, #273 fence): {}",
path.string()));
continue;
}
// Probe PT_INTERP — skip static binaries (no interp).
auto probe = std::format("{} --print-interpreter {} 2>/dev/null",
mcpp::platform::shell::quote(patchelfBin.string()),
mcpp::platform::shell::quote(path.string()));
auto probeResult = mcpp::platform::process::capture(probe);
bool hasInterp = (probeResult.exit_code == 0 && !probeResult.output.empty());
// Patch a COPY and atomically rename it into place. The payload can
// contain libraries the CURRENT process has mmapped (a self-hosted
// mcpp links the sandbox glibc/libgcc_s, and since the fixup
// pipeline runs on every install path, the patching process may BE
// such a consumer). In-place patchelf rewrites the backing file of
// those live mappings and corrupts the running process — observed
// on CI as an exit-time SIGSEGV in _dl_fini jumping to an
// unrelocated address. rename() gives the patched content a fresh
// inode while live processes keep the old one.
auto tmp = path;
tmp += ".mcpp-patch.tmp";
{
std::error_code cec;
std::filesystem::copy_file(
path, tmp, std::filesystem::copy_options::overwrite_existing, cec);
if (cec) continue;
std::filesystem::permissions(
tmp, std::filesystem::status(path, cec).permissions(),
std::filesystem::perm_options::replace, cec);
}
bool patched = true;
if (hasInterp) {
patched = (mcpp::platform::process::run_silent(std::format(
"{} --set-interpreter {} {} 2>/dev/null",
mcpp::platform::shell::quote(patchelfBin.string()),
mcpp::platform::shell::quote(loader.string()),
mcpp::platform::shell::quote(tmp.string()))) == 0) && patched;
}
// Always set RUNPATH (works on .so too — they need to find deps).
if (!rpath.empty()) {
patched = (mcpp::platform::process::run_silent(std::format(
"{} --set-rpath {} {} 2>/dev/null",
mcpp::platform::shell::quote(patchelfBin.string()),
mcpp::platform::shell::quote(rpath),
mcpp::platform::shell::quote(tmp.string()))) == 0) && patched;
}
std::error_code rec;
if (patched) std::filesystem::rename(tmp, path, rec);
if (!patched || rec) std::filesystem::remove(tmp, rec);
}
}
// xim bakes the installing user's XLINGS_HOME into gcc specs at install
// time (as `--dynamic-linker` and `-rpath`). When mcpp uses its own
// isolated sandbox (MCPP_HOME/registry/), the baked-in paths point to
// xlings' home, not mcpp's sandbox glibc — binaries would fail to exec.
//
// Mcpp does a post-install spec rewrite:
// - Dynamically detects the baked-in loader path from the specs file
// - Replaces it with the sandbox glibc payload's loader
// - Replaces the rpath with <glibc_lib>:<gcc_lib64>
// NOT idempotent across homes -- and that is a defect, not a caveat. The
// needle is one path (the baked glibc dir) while the replacement is two
// (glibc + gccLib), so a second home patching the same shared payload leaves
// the first home's gccLib entry behind: it never appears in any later needle.
// One stale rpath per run, forever. Measured at 68 on a developer machine,
// all of them gccLib, none glibc -- a distribution only this mechanism
// produces. See 2026-08-08-payload-version-and-contract-drift-design.md §2.1;
// this function is slated for deletion, with the flags moving to the build.
// Extract the baked-in glibc loader path (".../ld-linux-<arch>.so.N") from a
// gcc specs file. xim bakes the installing user's XLINGS_HOME into specs at
// install time, so the DIR varies per machine, and the loader NAME varies
// per arch — detect both instead of hardcoding either.
// The runtime binding a toolchain was INSTALLED against, read back out of
// what mcpp itself wrote: gcc's specs, clang's cfg.
//
// Compatibility only. The authority is the subos's `subos_info.runtime`; this
// answers for machines whose subos predates that block, and it answers with
// the version the artifact will actually load -- so compile and run still
// agree, which is the invariant that matters.
//
// Returns "glibc@<ver>" or empty. The version comes from the payload path
// (.../xim-x-glibc/<ver>/lib64), which is the only place it is written down.
export std::string baked_runtime_binding(const std::filesystem::path& compilerBin);
// Write a specs file that REPLACES gcc's `*link:` with the pristine built-in
// one, and return its path (empty when not applicable or on failure).
//
// Why this exists: `-rpath` accumulates. mcpp emitting its own does not remove
// what the payload's specs already inject, and that file has been patched by
// every home that ever installed against this shared payload -- one stale
// entry per run, forever. Replacing the spec is the only way to stop them
// reaching a user's artifact, and it works on machines already polluted.
//
// The pristine `*link:` comes from `g++ -dumpspecs`, which prints the specs
// COMPILED INTO the binary and is unaffected by the file on disk. So the
// original is always recoverable and the payload is never touched -- an
// earlier plan to reinstall the toolchain was unnecessary.
//
// `-specs=<file>` whose definition does not begin with `+` replaces rather
// than appends; both properties were measured before this was written.
export std::filesystem::path write_clean_link_specs(
const std::filesystem::path& compilerBin,
const std::filesystem::path& outputDir);
export std::string detect_baked_loader(const std::string& specsContent) {
// Path-character whitelist. Specs embed loader paths inside %-spec
// syntax (`%{mmusl:...;:/baked/dir/ld-linux-x86-64.so.2}`), so scanning
// to "whitespace or :;" is NOT a valid boundary — it would swallow the
// closing braces, and replacing that string corrupts the spec grammar
// ("braced spec body ... is invalid" from every subsequent g++ run).
auto is_path_char = [](char c) {
return std::isalnum(static_cast<unsigned char>(c))
|| c == '/' || c == '.' || c == '-' || c == '_' || c == '+';
};
// The baked GNU loader is the ld-linux entry whose directory is NOT a
// standard /lib* location — specs also contain pristine defaults
// (/lib/ld-linux.so.2, /libx32/…) for other multilib branches that must
// never be rewritten.
constexpr std::string_view kLoaderMark = "/ld-linux-";
for (std::size_t pos = specsContent.find(kLoaderMark);
pos != std::string::npos;
pos = specsContent.find(kLoaderMark, pos + 1)) {
auto start = pos;
while (start > 0 && is_path_char(specsContent[start - 1])) --start;
auto end = pos + 1;
while (end < specsContent.size() && is_path_char(specsContent[end])) ++end;
auto loader = specsContent.substr(start, end - start);
if (loader.empty() || loader[0] != '/') continue;
auto dir = std::filesystem::path(loader).parent_path().string();
if (dir == "/lib" || dir == "/lib64" || dir == "/lib32" || dir == "/libx32")
continue; // pristine multilib default, not a baked path
return loader;
}
return "";
}
// fixup_gcc_specs is GONE.
//
// It wrote the loader and the rpath into the gcc payload's `specs` at install
// time. Both are now emitted per build by mcpp.toolchain.linkmodel, which
// removes an answerer rather than adding a path:
//
// - the RUN side was a per-toolchain-install decision while the COMPILE
// side was per-build, and they named different glibc versions the moment
// a second one was installed;
// - the substitution had a one-path needle and a two-path replacement, so
// every home that patched the SHARED payload left one entry behind. 68 on
// a developer machine, all pointing at deleted mktemp directories, in
// every gcc artifact it produced.
//
// The payload is no longer written to at all, which restores what R6 asks of
// it: immutable, and independent of any particular home. A build that needs
// a clean `*link:` gets one generated into its own build directory (see
// write_clean_link_specs) -- the pristine spec comes from `g++ -dumpspecs`,
// which prints what is compiled into the binary and is therefore always
// recoverable.
//
// patchelf_walk stays: making the compiler itself runnable is a different
// question from what the compiler produces.
// Regenerate the clang driver cfg files after the LLVM payload landed in the
// sandbox. The cfg xlings authored at install time is a per-machine,
// per-install-path artifact (its content depended on what existed when the
// package was installed); mcpp's builds bypass it entirely
// (--no-default-config), so its only remaining job is to make a HUMAN
// running `clang++` directly get a working, hermetic compiler. We therefore
// regenerate it deterministically from the same link model the builds use,
// instead of line-patching whatever a given install produced:
// C + C++: -B/-L glibc payload, payload dynamic linker + rpath,
// lld / compiler-rt / libunwind
// C++ only: -nostdinc++ -stdlib=libc++ + payload libc++ headers/libs
// On macOS the C library comes from the SDK: --sysroot=<sdk> + libc++ headers.
export void fixup_clang_cfg(const std::filesystem::path& payloadRoot,
const std::filesystem::path& glibcLibDir,
const std::filesystem::path& fenceRoot) {
// #273 fence — same rule the removed gcc specs fixup used.
if (escapes_containment(payloadRoot, fenceRoot)) {
mcpp::log::verbose("toolchain",
"fixup_clang_cfg: skip (payload outside sandbox, #273 fence)");
return;
}
auto binDir = payloadRoot / "bin";
if (!std::filesystem::exists(binDir)) return;
// Target triple from the payload layout (lib/<triple>), used for the
// loader lookup and the per-target libc++ include/lib dirs.
std::string triple;
std::error_code ec;
for (auto it = std::filesystem::directory_iterator(payloadRoot / "lib", ec);
!ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) {
auto name = it->path().filename().string();
if (it->is_directory(ec) && name.find("-linux-") != std::string::npos) {
triple = name;
break;
}
}
std::string common, cxxOnly, cHdr;
auto cxxInclude = payloadRoot / "include" / "c++" / "v1";
if constexpr (mcpp::platform::is_macos) {
// macOS keeps its historical cfg semantics: the C library and the
// C++ runtime LINK both come from the SDK; only the libc++ HEADERS
// come from the payload. Do NOT add -nostdinc++/-stdlib=libc++
// here — a bare cfg-driven link has no libc++abi handling (that
// lives in the main build's needs_explicit_libcxx path) and dies
// with undefined __cxa_* / __gxx_personality_v0.
if (auto sdk = mcpp::platform::macos::sdk_path())
common += "--sysroot=" + sdk->string() + "\n";
if (std::filesystem::exists(cxxInclude))
cxxOnly += "-isystem " + cxxInclude.string() + "\n";
} else {
if (!glibcLibDir.empty()) {
auto loader = resolve_loader(glibcLibDir, triple);
common += "-B" + glibcLibDir.string() + "\n";
common += "-L" + glibcLibDir.string() + "\n";
if (!loader.empty())
common += "-Wl,--dynamic-linker=" + loader.string() + "\n";
common += "-Wl,--enable-new-dtags,-rpath," + glibcLibDir.string() + "\n";
}
common += "-fuse-ld=lld\n--rtlib=compiler-rt\n--unwindlib=libunwind\n";
// HEADER axis (C and C++ drivers alike): the C library and kernel
// headers come from the same payloads the link axis uses. Without
// these, a direct `clang hello.c` only works when the HOST happens
// to ship /usr/include — silently non-hermetic, broken on
// header-less machines. For C++ they must come AFTER the libc++
// block (its C-header wrappers reach libc via #include_next), so
// they are collected separately and appended in order below —
// byte-consistent with what llvm.lua's install hook generates.
if (!glibcLibDir.empty()) {
auto glibcInclude = glibcLibDir.parent_path() / "include";
if (std::filesystem::exists(glibcInclude / "features.h"))
cHdr += "-isystem " + glibcInclude.string() + "\n";
constexpr std::string_view kLinuxLimits = "include/linux/limits.h";
auto linuxHeaders = mcpp::xlings::paths::find_sibling_package(
payloadRoot / "bin" / "clang++", "linux-headers", kLinuxLimits);
if (!linuxHeaders)
linuxHeaders = mcpp::xlings::paths::find_home_tool(
"linux-headers", kLinuxLimits);
if (linuxHeaders)
cHdr += "-isystem " + (*linuxHeaders / "include").string() + "\n";
}
if (std::filesystem::exists(cxxInclude)) {
cxxOnly += "-nostdinc++\n-stdlib=libc++\n";
cxxOnly += "-isystem " + cxxInclude.string() + "\n";
}
if (!triple.empty()) {
auto tripleInclude = payloadRoot / "include" / triple / "c++" / "v1";
if (std::filesystem::exists(tripleInclude))
cxxOnly += "-isystem " + tripleInclude.string() + "\n";
}
cxxOnly += cHdr;
if (!triple.empty()) {
auto tripleLib = payloadRoot / "lib" / triple;
if (std::filesystem::exists(tripleLib)) {
cxxOnly += "-L" + tripleLib.string() + "\n";
cxxOnly += "-Wl,-rpath," + tripleLib.string() + "\n";
}
}
}
// Regenerate every existing cfg in bin/ (clang.cfg, clang++.cfg, and any
// versioned clang-<major>.cfg xlings created), classified C vs C++ by
// whether the driver name contains "++".
for (auto it = std::filesystem::directory_iterator(binDir, ec);
!ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) {
auto name = it->path().filename().string();
if (!name.ends_with(".cfg")) continue;
const bool isCxx = name.find("++") != std::string::npos;
std::ofstream os(it->path());
os << common << (isCxx ? cxxOnly : cHdr);
}
}
// Resolve one glibc payload from the RuntimeBinding identity. This is a
// semantic exact lookup, never a directory-order choice: glibc@2.44 means the
// `2.44` directory and no other. Exported so the #392 regression stays pinned
// by a pure unit test without installing a toolchain.
export std::expected<std::filesystem::path, std::string>
select_glibc_payload_lib(const std::filesystem::path& glibcRoot,
std::string_view runtimeId) {
constexpr std::string_view prefix = "glibc@";
if (!runtimeId.starts_with(prefix)) {
// Name the PROVIDER that was found, not just the one that was
// wanted. `ucrt@10.0.26100.0` is a perfectly valid runtime identity
// that this fixup has nothing to do with (there is no ucrt payload to
// bind to — it is an OS component), and "not a glibc payload
// identity" reads as "malformed" for it.
auto provider = mcpp::platform::runtime::runtime_provider(runtimeId);
return std::unexpected(provider.empty()
? std::format(
"selected RuntimeBinding '{}' is not a glibc payload identity "
"(expected glibc@<version>)", runtimeId)
: std::format(
"selected RuntimeBinding '{}' names the '{}' runtime "
"provider; the glibc payload fixup does not apply to it",
runtimeId, provider));
}
auto version = runtimeId.substr(prefix.size());
if (version.empty() || version == "." || version == ".."
|| version.find('/') != std::string_view::npos
|| version.find('\\') != std::string_view::npos
|| version.find('@') != std::string_view::npos) {
return std::unexpected(std::format(
"selected RuntimeBinding '{}' has an invalid glibc version",
runtimeId));
}
auto payload = glibcRoot / std::string(version);
std::error_code ec;
if (!std::filesystem::is_directory(payload, ec)) {
return std::unexpected(std::format(
"selected RuntimeBinding {} requires payload '{}', but it is not "
"installed; mcpp will not fall back to another directory entry",
runtimeId, payload.string()));
}
auto lib = payload_lib_dir_with_loader(payload);
if (lib.empty()) {
return std::unexpected(std::format(
"selected RuntimeBinding {} payload '{}' is stale/incomplete: no "
"dynamic loader was found under lib64/ or lib/",
runtimeId, payload.string()));
}
return lib;
}
std::expected<std::filesystem::path, std::string>
find_sandbox_glibc_lib(const mcpp::xlings::Env& xlEnv,
std::string_view runtimeId) {
return select_glibc_payload_lib(
mcpp::xlings::paths::xim_tool_root(xlEnv, "glibc"), runtimeId);
}
// Post-install fixup for a freshly-installed GNU gcc payload: patchelf
// PT_INTERP/RUNPATH for gcc/binutils binaries + linker-specs wiring against
// the sandbox glibc — without it a fresh-sandbox glibc gcc cannot find the
// C library (stdlib.h not found).
void gcc_post_install_fixup(const mcpp::config::GlobalConfig& cfg,
const std::filesystem::path& payloadRoot,
const std::filesystem::path& glibcLibDir) {
auto xlEnv = mcpp::config::make_xlings_env(cfg);
auto gccLibDir = payloadRoot / "lib64";
auto patchelfBin = mcpp::xlings::paths::xim_tool(xlEnv, "patchelf",
mcpp::xlings::pinned::kPatchelfVersion) / "bin" / "patchelf";
if (!glibcLibDir.empty() && std::filesystem::exists(gccLibDir)
&& std::filesystem::exists(patchelfBin))
{
auto loader = resolve_loader(glibcLibDir, /*targetTriple=*/{});
auto rpath = std::format("{}:{}",
glibcLibDir.string(), gccLibDir.string());
mcpp::log::verbose("toolchain", std::format(
"gcc fixup: patchelf_walk rpath='{}'", rpath));
// Single trust root for every walk below — including the binutils
// SIBLING payloads, which the entry-point ownership guard does not
// cover (it only vets the gcc payload itself).
auto fence = containment_root(cfg.registryDir);
auto binutilsRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "binutils");
if (std::filesystem::exists(binutilsRoot)) {
for (auto& v : std::filesystem::directory_iterator(binutilsRoot))
patchelf_walk(v.path(), loader, rpath, patchelfBin, fence);
}
patchelf_walk(payloadRoot, loader, rpath, patchelfBin, fence);
} else {
mcpp::ui::warning(
"could not locate sandbox glibc/gcc/patchelf paths; "
"gcc-built binaries may have unresolved PT_INTERP/RUNPATH");
}
}
// LLVM payload fixup: RUNPATH for the bundled runtime shared libraries
// (libc++.so / libunwind.so need to find siblings like libatomic.so.1 after
// the payload moved) + deterministic cfg regeneration. Only lib/ dirs are
// walked — NOT bin/: the clang++ binary's own RUNPATH (zlib, libxml2, …) was
// set by xlings and must be preserved.
void llvm_post_install_fixup(const mcpp::config::GlobalConfig& cfg,
const std::filesystem::path& payloadRoot,
const std::filesystem::path& glibcLibDir) {
auto xlEnv = mcpp::config::make_xlings_env(cfg);
auto patchelfBin = mcpp::xlings::paths::xim_tool(xlEnv, "patchelf",
mcpp::xlings::pinned::kPatchelfVersion) / "bin" / "patchelf";
if (!glibcLibDir.empty() && std::filesystem::exists(patchelfBin)) {
auto loader = resolve_loader(glibcLibDir, /*targetTriple=*/{});
auto llvmLib = payloadRoot / "lib";
std::string rpath;
std::error_code ec;
for (auto it = std::filesystem::directory_iterator(llvmLib, ec);
!ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) {
if (it->is_directory(ec)
&& it->path().filename().string().find("-linux-") != std::string::npos)
rpath += it->path().string() + ":";
}
rpath += llvmLib.string() + ":" + glibcLibDir.string();
mcpp::log::verbose("toolchain", std::format(
"llvm fixup: patchelf_walk lib/ rpath='{}'", rpath));
patchelf_walk(llvmLib, loader, rpath, patchelfBin,
containment_root(cfg.registryDir));
}
mcpp::log::verbose("toolchain", "llvm fixup: fixup_clang_cfg");
fixup_clang_cfg(payloadRoot, glibcLibDir, containment_root(cfg.registryDir));
}
// ── the single fixup pipeline entry ──────────────────────────────────────
//
// Called from the payload-resolution seam shared by ALL toolchain install
// paths (explicit `mcpp toolchain install`, default-toolchain auto-install,
// and manifest `[toolchain]` auto-install). Previously each path remembered
// (or forgot) its own subset of fixups: the manifest path ran none, which is
// how a fresh llvm install kept a stale install-time cfg and unpatched
// runtime libs. Idempotent via a content-fingerprinted marker.
//
// Bump when the fixup logic changes so existing installs re-run it.
constexpr std::string_view kFixupRev = "hermetic-4-exact-runtime";
// What the fixup DID, so the caller can decide how loud to be about it.
//
// mcpp#427: this used to be `expected<void>`, and "the runtime identity is
// unknown" was an ERROR inside this function. That made one severity for every
// caller, and it was the wrong one: `mcpp build` died on a machine whose
// default SubOS predates xlings' `subos_info` block — an absence, not a
// contradiction — while the very function this gate protects already degrades
// correctly on an empty `glibcLibDir` (it warns and skips patching). The
// degradation existed and was unreachable.
//
// Severity belongs to the caller. `build` continues; `toolchain install`,
// which the user asked for explicitly, says so at warning level.
export struct FixupOutcome {
bool applied = false; // patching actually ran
std::string skippedReason; // non-empty ⇒ degraded, and why
};
export std::expected<FixupOutcome, std::string>
ensure_post_install_fixup(const mcpp::config::GlobalConfig& cfg,
const std::filesystem::path& payloadRoot,
const XimToolchainPackage& pkg,
std::string_view runtimeId = {},
const std::filesystem::path& selectedRuntimeLibDir = {}) {
std::string kind;
if (pkg.needsGccPostInstallFixup) kind = "gcc";
else if (pkg.ximName == "llvm") kind = "llvm";
else return FixupOutcome{};
if constexpr (mcpp::platform::is_windows)
return FixupOutcome{}; // PE world: no fixups
// Ownership guard: payloads inherited via symlink from another MCPP_HOME
// are not ours to patch — their owner already ran the fixup, and patching
// through the symlink would rewrite the canonical files against OUR
// (possibly ephemeral) paths, bricking the owner's toolchain.
// Rewritten on the shared containment predicate (#273): the old inline
// check reused one error_code across both canonicalizations (the second
// success cleared the first failure) and compared raw strings without a
// component boundary (a `registry-evil` sibling passed). The predicate
// fails closed on any resolution error.
if (escapes_containment(payloadRoot, containment_root(cfg.registryDir))) {
mcpp::log::verbose("toolchain", std::format(
"skip {} fixup: payload '{}' resolves outside this home — "
"inherited payload, owner is responsible for its fixup",
kind, payloadRoot.string()));
return FixupOutcome{};
}
auto xlEnv = mcpp::config::make_xlings_env(cfg);
std::filesystem::path glibcLibDir;
std::string skipped;
if constexpr (mcpp::platform::is_linux) {
// ⚠️ ONE DERIVATION, AND IT IS NOT THIS FUNCTION'S.
//
// The caller resolves a RuntimeBinding first and hands the identity in;
// `prepare.cppm` says so where it does it ("The fixup is itself a
// consumer of RuntimeBinding: doing it first would recreate #392 by
// letting directory order choose a libc"). This function used to ALSO
// read `<xlings home>/subos/default` when the identity was empty —
// a second derivation that (a) contradicted that architecture, (b)
// consulted a HARDCODED SubOS even when the project selected another
// one, and (c) turned an absent description into a fatal error.
//
// Unknown degrades. A CONTRADICTION — an identity that is declared but
// cannot be honoured — still fails, below.
std::string selected(runtimeId);
if (selected.empty()) {
skipped = "the selected SubOS declares no runtime identity, so "
"there is no C runtime to bind this toolchain to";
} else if (!selectedRuntimeLibDir.empty()) {
constexpr std::string_view prefix = "glibc@";
auto version = selected.starts_with(prefix)
? std::string_view(selected).substr(prefix.size())
: std::string_view{};
auto payload = selectedRuntimeLibDir.parent_path();
auto discovered = payload_lib_dir_with_loader(payload);
std::error_code dec, sec;
auto discoveredReal = std::filesystem::weakly_canonical(discovered, dec);
auto selectedReal = std::filesystem::weakly_canonical(
selectedRuntimeLibDir, sec);
if (version.empty() || payload.filename() != version
|| discovered.empty() || dec || sec
|| discoveredReal != selectedReal) {
return std::unexpected(std::format(
"selected RuntimeBinding {} points at incompatible/stale "
"runtime directory '{}'; expected the exact payload version",
selected, selectedRuntimeLibDir.string()));
}
glibcLibDir = selectedRuntimeLibDir;
} else {
auto exact = find_sandbox_glibc_lib(xlEnv, selected);
if (!exact) return std::unexpected(exact.error());
glibcLibDir = std::move(*exact);
}
}
// Nothing to bind to. Report it and change nothing — patching a payload
// against a runtime we cannot name would be a guess, and a guess here is
// an ABI. NO MARKER IS WRITTEN: "we did nothing" must never be able to
// read back as "already applied", so the day the SubOS learns to describe
// itself the fixup runs on the next invocation.
if (!skipped.empty())
return FixupOutcome{.applied = false, .skippedReason = std::move(skipped)};
// Content-fingerprinted marker: a marker whose INPUTS drifted (different
// glibc payload, newer fixup logic) re-runs the fixup — "a process once
// exited 0" is not evidence the current inputs were ever applied.
auto markerPath = payloadRoot / ".mcpp-fixup.json";
nlohmann::json expected;
expected["schema"] = 1;
expected["kind"] = kind;
expected["rev"] = std::string(kFixupRev);
expected["glibcLib"] = glibcLibDir.generic_string();
{
std::ifstream is(markerPath);
if (is) {
try {
nlohmann::json actual;
is >> actual;
if (actual == expected) // fixup already applied
return FixupOutcome{.applied = true, .skippedReason = {}};
} catch (...) { /* corrupt marker → re-run */ }
}
}
if (kind == "gcc") gcc_post_install_fixup(cfg, payloadRoot, glibcLibDir);
else llvm_post_install_fixup(cfg, payloadRoot, glibcLibDir);
std::ofstream os(markerPath);
os << expected.dump(2) << "\n";
return FixupOutcome{.applied = true, .skippedReason = {}};
}
// PT_INTERP of an ELF, read directly.
//
// Not via `patchelf --print-interpreter`: this runs during prepare, where
// patchelf is not guaranteed to be resolved, and a dependency on an external
// tool for four fields of a header is a dependency that will be missing on
// exactly the machine that needs the answer.
std::string read_elf_interp(const std::filesystem::path& bin) {
std::ifstream is(bin, std::ios::binary);
if (!is) return {};
unsigned char ident[16]{};
is.read(reinterpret_cast<char*>(ident), sizeof ident);
if (!is || ident[0] != 0x7f || ident[1] != 'E'
|| ident[2] != 'L' || ident[3] != 'F') return {};
const bool is64 = ident[4] == 2;
const bool le = ident[5] == 1;
if (!is64 || !le) return {}; // the only shape payloads ship
auto u16 = [&](std::streamoff off) -> std::uint16_t {
is.seekg(off); unsigned char b[2]{};
is.read(reinterpret_cast<char*>(b), 2);
return static_cast<std::uint16_t>(b[0] | (b[1] << 8));
};
auto u64 = [&](std::streamoff off) -> std::uint64_t {
is.seekg(off); unsigned char b[8]{};
is.read(reinterpret_cast<char*>(b), 8);
std::uint64_t v = 0;
for (int i = 7; i >= 0; --i) v = (v << 8) | b[i];
return v;
};
const auto phoff = u64(0x20);
const auto phentsize = u16(0x36);
const auto phnum = u16(0x38);
if (!is || phoff == 0 || phentsize < 0x38 || phnum == 0) return {};
constexpr std::uint32_t kPtInterp = 3;
for (std::uint16_t i = 0; i < phnum; ++i) {
const auto ph = static_cast<std::streamoff>(phoff)
+ static_cast<std::streamoff>(i) * phentsize;
is.seekg(ph);
unsigned char t[4]{};
is.read(reinterpret_cast<char*>(t), 4);
if (!is) return {};
const std::uint32_t type =
static_cast<std::uint32_t>(t[0]) | (t[1] << 8)
| (t[2] << 16) | (static_cast<std::uint32_t>(t[3]) << 24);
if (type != kPtInterp) continue;
const auto offset = u64(ph + 0x08);
const auto filesz = u64(ph + 0x20);
if (filesz == 0 || filesz > 4096) return {};
std::string str(static_cast<std::size_t>(filesz), '\0');
is.seekg(static_cast<std::streamoff>(offset));
is.read(str.data(), static_cast<std::streamsize>(filesz));
if (!is) return {};
if (auto z = str.find('\0'); z != std::string::npos) str.resize(z);
return str;
}
return {};
}
std::string baked_runtime_binding(const std::filesystem::path& compilerBin) {
if (compilerBin.empty()) return {};
std::error_code ec;
auto from_text = [](const std::string& text) -> std::string {
auto loader = detect_baked_loader(text);
if (loader.empty()) return {};
// The recorded path may name a subos VIEW (`<home>/subos/default/lib/
// ld-linux-...`), which is a symlink into the payload. R6: artifacts
// bind the payload, never the mutable view -- so resolve to the
// payload before reading a version off it. A view path has no version
// component at all, and parsing one yields nothing.
std::error_code cec;
if (auto real = std::filesystem::canonical(loader, cec); !cec)
loader = real.string();
// .../xim-x-glibc/<ver>/lib64/ld-linux-... — the version is the
// grandparent of the lib dir.
auto dir = std::filesystem::path(loader).parent_path(); // lib64
auto ver = dir.parent_path().filename().string(); // <ver>
auto pkg = dir.parent_path().parent_path().filename().string();
if (ver.empty() || pkg.find("glibc") == std::string::npos) return {};
return "glibc@" + ver;
};
// Which glibc payloads are actually installed. Every source below is
// checked against this, because all of them are RECORDS of a past state:
// specs and cfg were written at some install, PT_INTERP at some fixup. A
// payload can be replaced afterwards -- upgraded, garbage-collected -- and
// the record keeps naming what used to be there.
//
// That is not hypothetical. CI resolved `glibc@2.39` from a record while
// the only payload on disk was 2.44, so the exact-match probe correctly
// refused, no payload paths resolved, and the artifact fell through to the
// host loader. A fossil that names something no longer present is not an
// authority; it is just old.
std::vector<std::string> installed;
if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin)) {
for (auto it = std::filesystem::directory_iterator(*xpkgs / "xim-x-glibc", ec);
!ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) {
if (!it->is_directory(ec)) continue;
auto v = it->path().filename().string();
if (!v.empty() && v.front() != '.') installed.push_back(v);
}
}
auto still_there = [&](const std::string& binding) {
if (binding.empty()) return false;
const auto at = binding.find('@');
if (at == std::string::npos) return false;
auto ver = binding.substr(at + 1);
return std::ranges::find(installed, ver) != installed.end();
};
auto read_file = [&](const std::filesystem::path& p) -> std::string {
if (!std::filesystem::exists(p, ec)) return {};
std::ifstream is(p);
std::stringstream ss; ss << is.rdbuf();
return ss.str();
};
// clang: the sibling <driver>.cfg.
auto cfg = compilerBin.parent_path()
/ (compilerBin.stem().string() + ".cfg");
if (auto r = from_text(read_file(cfg)); still_there(r)) return r;
// gcc: lib/gcc/<triple>/<ver>/specs, one level of globbing each.
auto gccRoot = compilerBin.parent_path().parent_path();
for (auto t = std::filesystem::directory_iterator(gccRoot / "lib" / "gcc", ec);
!ec && t != std::filesystem::directory_iterator{}; t.increment(ec)) {
for (auto v = std::filesystem::directory_iterator(t->path(), ec);
!ec && v != std::filesystem::directory_iterator{}; v.increment(ec)) {
if (auto r = from_text(read_file(v->path() / "specs")); still_there(r))
return r;
}
}
// The compiler's OWN interpreter.
//
// The two sources above are files mcpp used to write, and mcpp no longer
// writes them -- so on a machine where the toolchain was installed after
// that change they simply are not there, and the compatibility path that
// depends on them answers nothing. That is not hypothetical: it is what
// turned CI red while every existing developer machine, which still has
// the files from before, stayed green.
//
// PT_INTERP is produced by the patchelf walk, which still runs on every
// install, and it names the glibc payload this toolchain was aligned to --
// the same fact the specs held, from a mechanism that has not gone away.
if (auto r = from_text(read_elf_interp(compilerBin)); still_there(r))
return r;
// Last: the installed payload set, but ONLY when it is a singleton.
//
// This is not the rule this design removed. That rule asked a directory
// for "the glibc" and took whatever `readdir` yielded first -- a CHOICE,
// made by something with no bearing on what the artifact would load, and
// silently wrong the moment a second payload appeared. Where exactly one
// payload is installed there is no choice to make: it is the only glibc
// any artifact from this toolchain could bind, and declining would refuse
// a question that has a single answer.
//
// Two or more and this stays silent. That is the case the incident was,
// and it is the case the subos must answer.
if (installed.size() == 1) {
mcpp::log::verbose("probe", std::format(
"runtime binding glibc@{} — the only glibc payload installed, so "
"there is nothing to choose between", installed[0]));
return "glibc@" + installed[0];
}
if (installed.size() > 1)
mcpp::log::verbose("probe", std::format(
"{} glibc payloads installed and nothing still present declares "
"which one this build binds; declining rather than picking",
installed.size()));
return {};
}
std::filesystem::path write_clean_link_specs(
const std::filesystem::path& compilerBin,
const std::filesystem::path& outputDir)
{
if (compilerBin.empty() || outputDir.empty()) return {};
std::error_code ec;
auto out = outputDir / "mcpp-clean-link.specs";
// Idempotent: the pristine spec is a property of the compiler binary, so
// regenerating it every build would spawn a process for a constant.
if (std::filesystem::exists(out, ec)) return out;
auto r = mcpp::platform::process::capture(std::format(
"{} -dumpspecs 2>/dev/null",
mcpp::platform::shell::quote(compilerBin.string())));
if (r.exit_code != 0 || r.output.empty()) return {};
// `*link:` is a section header on its own line; its body is the next line.
constexpr std::string_view kHead = "*link:\n";
auto pos = r.output.find(kHead);
if (pos == std::string::npos) return {};
auto bodyStart = pos + kHead.size();
auto bodyEnd = r.output.find('\n', bodyStart);
if (bodyEnd == std::string::npos) return {};
auto body = r.output.substr(bodyStart, bodyEnd - bodyStart);
if (body.empty()) return {};
std::filesystem::create_directories(outputDir, ec);
std::ofstream os(out);
if (!os) return {};
os << "*link:\n" << body << "\n\n";
os.close();
return out;
}
} // namespace mcpp::toolchain