feat(toolchain): hermetic link model — CRT/loader from one resolver, fixup pipeline, hermeticity check (v0.0.83, fixes #195)#196
Merged
Conversation
ToolchainLinkModel (CRT dir / lib dirs / loader / system includes, payload-first with --sysroot fallback) + ClangDriverModel (cfg-bypass driver flags), replacing the four divergent copies of this knowledge. Loader names come from data: declared payload exports (.xpkg-exports.json) -> per-arch triple map -> glob, never a hardcoded x86_64 string. Part of the hermetic toolchain link model (.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md, issue #195).
…#195) flags.cppm, stdmod.cppm and build_program.cppm host_base_flags now derive their sysroot/payload flags from the shared link model. The clang-with-cfg payload link path previously emitted only -L/-rpath/--dynamic-linker; the driver resolves Scrt1.o/crti.o/crtn.o through -B prefixes and sysroot paths only, so on hosts without a system toolchain it passed bare CRT names that lld cannot open (issue #195), and on hosts with one it silently linked the host's CRT. The model's link_flags carry -B<glibc-payload-lib>, so CRT objects now resolve inside the payload everywhere. build.mcpp host compiles stop trusting the sibling clang cfg (an install-time-generated, per-machine artifact) and use the same explicit flags as the main build.
…cfg regeneration ensure_post_install_fixup(cfg, payloadRoot, pkg) is now the single entry for toolchain post-install fixups (gcc: patchelf + specs; llvm: patchelf(lib) + cfg), called from all three install paths — explicit `toolchain install`, default-toolchain auto-install, and manifest [toolchain] auto-install. The manifest path previously ran NO fixup, so a fresh llvm auto-install kept its stale install-time cfg and unpatched runtime libs (how the #195 reporter's cfg still carried an install-time --sysroot while other installs didn't). Idempotent via a content-fingerprinted marker (<payload>/.mcpp-fixup.json: schema + kind + fixup rev + glibc lib) — drifted inputs re-run the fixup. The ownership guard (inherited/symlinked payloads are not ours to patch) now covers llvm too. fixup_clang_cfg no longer line-patches whatever a given install produced: it regenerates the cfg deterministically from the link model, so the same payload yields byte-identical cfgs on every machine and install path — and a human running clang++ directly now gets hermetic CRT discovery (-B) too. All loader paths come from resolve_loader (no ld-linux-x86-64 hardcodes).
…only) The cfg-mined sysroot was dead trust: mcpp's fixup pipeline regenerates the cfg without a --sysroot line (the C library comes from the payload link model), so the mined value existed only on never-fixed-up installs and pointed at an environment directory the payload doesn't own. Builds derive everything from the link model; the cfg serves humans running clang++ directly. The parse is kept as a debug log.
…he sandbox Dry-run the driver (-###) with the exact ldflags the build will use and assert every CRT object plus the EFFECTIVE dynamic linker (last occurrence wins — the driver emits its built-in default before the -Wl override) resolve under the sandbox's xpkgs registry or the toolchain sysroot. This converts two previously silent failure modes into one actionable build-time diagnostic: on hosts WITH a system toolchain the driver would quietly link the host's CRT (contamination that made CI green a false signal), and on hosts WITHOUT one it passed bare CRT names lld cannot open (issue #195). Sandbox toolchains only — a PATH/system compiler is the user's explicit choice of the host world. Verdict cached per flag-set (.mcpp-hermetic-ok). Escape hatches: [build] allow_host_libs = true or MCPP_ALLOW_HOST_LIBS=1.
…codes fixup_gcc_specs detects the baked loader name from the specs content and replaces it with resolve_loader()'s answer (any glibc arch); the specs dir is discovered instead of assuming x86_64-linux-gnu. mcpp pack derives the BundleProject PT_INTERP distro path from the loader soname ldd resolved for the binary being packed (LSB /lib64 vs /lib), instead of a hardcoded x86_64 string. The only remaining literal loader names live in linkmodel.cppm's per-arch triple map.
86 asserts, via a -### dry-run with the exact ldflags mcpp generated, that every CRT object and the effective dynamic linker resolve inside the sandbox (an xpkgs path) — failing on both regression modes of issue #195: bare CRT names (link failure on hosts without a system toolchain) and host-CRT contamination (silent on hosts with one). The llvm e2e scripts (36-41, 47, 65) source the new _llvm_env.sh instead of hardcoding llvm@20.1.7: MCPP_E2E_LLVM_VERSION overrides, default is the newest installed payload — so new toolchain versions get coverage the day they're installed instead of never.
debian:stable-slim with NO compiler and NO host Scrt1.o: the only environment class that faithfully reproduces issue #195 (standard runners ship libc6-dev, so a sandbox toolchain leaking to the host CRT still links green there). Bootstraps xlings + released mcpp, builds the PR code with the sandbox gcc only, then runs the #195 manifest-llvm reproduction plus the hermetic e2e subset.
…ed payloads mcpp passes symlink-view paths on the command line, but the clang driver reports its own resource dir (clang_rt.crt*) through the canonical path; with payloads symlink-inherited from another MCPP_HOME (the e2e isolation pattern) that canonical registry must be allowed too.
This was referenced Jul 7, 2026
…st semantics
Two regressions the first CI round caught (exactly the environments local
verification can't fake):
1. gcc specs corruption (linux): the rewritten detect walked the loader
path to 'whitespace/:;' — but specs embed the baked loader inside
%-spec conditionals (%{mmusl:...;:/baked/ld-linux-x86-64.so.2}), so the
scan swallowed closing braces and the rewrite corrupted the spec
grammar ('braced spec body ... is invalid' from every g++ run after).
Now: path-character whitelist scan, skip pristine /lib* multilib
defaults, unit-tested against the real spec grammar (incl. aarch64
loader names). detect_baked_loader is exported for the tests.
Fixup rev bumped (hermetic-2) so payloads stamped by the broken pass
re-run the fixup.
2. macOS host/cfg semantics: the cfg-bypass host flags and the
-nostdinc++/-stdlib=libc++ cfg regeneration are LINUX semantics; a bare
macOS link has no libc++abi handling (that lives in the main build's
needs_explicit_libcxx path) and died with undefined __cxa_* /
__gxx_personality_v0. build.mcpp host compiles keep trusting the cfg
off-Linux, and the macOS cfg keeps its historical shape
(--sysroot=<sdk> + payload libc++ headers only).
…ings The fixup pipeline now runs on every toolchain install path, which means the process executing it can itself be linked against the very libraries being patched (a self-hosted mcpp loads the sandbox glibc/libgcc_s). In-place patchelf rewrites the backing file of those live mappings and corrupts the running process — reproduced deterministically on fresh- sandbox CI as an exit-time SIGSEGV in _dl_fini jumping to an unrelocated address (0x45a0), while a control run of main on the same fresh sandbox passed (the old code only ever patched from the statically-linked bootstrap mcpp, so the hazard was structural but unexposed). patchelf now operates on a copy and atomically rename()s it into place: the patched content gets a fresh inode, live processes keep the old one.
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the architecture in
.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md, fixing #195 at the root instead of patching the symptom.What was broken (#195)
The clang-with-cfg payload link path emitted only
-L/-rpath/--dynamic-linker— no-B. The driver resolvesScrt1.o/crti.o/crtn.othrough-Bprefixes and sysroot paths only, so:cannot open Scrt1.o;Root architecture defect: the "how to link against the payload glibc" knowledge lived in four divergent mcpp copies (flags / stdmod / build_program / cfg-fixup) plus the install-time-generated cfg, with no owner and no verification.
Changes
mcpp.toolchain.linkmodel— single resolver for the C-library axis (CRT dir, lib dirs, loader, system includes; payload-first,--sysrootfallback) + the clang cfg-bypass driver model. All four consumers converge on it; the payload link gains-B(the Linux 下 llvm 工具链链接阶段传入 --no-default-config,丢失 clang++.cfg 中的 --sysroot,导致 cannot open Scrt1.o/crti.o/crtn.o #195 fix, as a model field). Loader names come from data: declared payload exports → per-arch triple map → glob.grep ld-linux-x86-64 src/now hits only the triple map.ensure_post_install_fixup(content-fingerprinted marker, ownership guard) called from all three install paths; the manifest[toolchain]path previously ran no fixup (why the reporter's cfg still carried an install-time--sysroot). The clang cfg is now regenerated deterministically from the model — same payload ⇒ same cfg on every machine, and a human runningclang++directly gets hermetic CRT discovery too.-###dry-run before the build asserts every CRT object + the effective dynamic linker resolve inside the sandbox; hard error naming the leak, escape hatches[build] allow_host_libs = true/MCPP_ALLOW_HOST_LIBS=1, verdict cached per flag-set. Converts both silent failure modes into one diagnostic.--sysroot(dead trust — the fixup regenerates the cfg without it); kept as a debug log.86_llvm_hermetic_link.sh(-###sandbox-prefix assertion — fails on both regression modes on any machine); llvm e2e unpinned from 20.1.7 (MCPP_E2E_LLVM_VERSION, default newest installed); new hermetic CI job ondebian:stable-slimwith no host toolchain — the only environment class that faithfully reproduces Linux 下 llvm 工具链链接阶段传入 --no-default-config,丢失 clang++.cfg 中的 --sysroot,导致 cannot open Scrt1.o/crti.o/crtn.o #195 (the standard e2e job never installs llvm, so the llvm suite always skipped on CI).Verification
[toolchain] linux = "llvm@22.1.8"+mcpp run) passes;-###shows all three CRT objects + loader resolving inside the glibc payload.clang++ hello.cpplinks hermetically (CRT from payload, runs).Companion PRs: xim-pkgindex (deterministic
llvm.luacfg + packaging admission gate) follows after this merges + releases.