Repository navigation
Links over a graph-supplied C library still search the host's /usr/lib: -lm breaks aarch64-linux-musl and can link glibc code into x86_64-linux-musl #696
Description
When the C library comes from the dependency graph (openkal-musl), the link line mcpp builds still lets clang add the build machine's library directories. -nostdlib removes the host's startup files and default libraries, but not these, which is what clang 22.1.8 puts on the aarch64-linux-musl line on my machine:
-L/lib/../lib64 -L/usr/lib64 -L/lib -L/usr/lib
So a -l that the graph does not answer is looked up on the host. I ran into it through a package descriptor that links -lm on linux, which many descriptors do, and it shows up in two ways.
On aarch64-linux-musl the link fails. Here /usr/lib/libm.a is glibc's linker script:
/* GNU ld script
*/
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /usr/lib/libm-2.44.a /usr/lib/libmvec.a )
Once lld has read that OUTPUT_FORMAT, it reports the aarch64 objects on the line as "incompatible with elf64-x86-64", including ones that have nothing to do with libm. It is that line in particular: a script holding only OUTPUT_FORMAT(elf64-x86-64) gives the same error, and one holding only the GROUP gives /usr/lib/libm-2.44.a(fclrexcpt.o) is incompatible with aarch64linux instead. With -lm taken off mcpp's own link command, it links and the binary runs under qemu-aarch64.
On x86_64-linux-musl the link succeeds, and lld opens the host's libm.a, libm-2.44.a and libmvec.a. Usually it takes nothing from them, because openkal-musl already defines what the program calls. When it does not, glibc's object is linked in without a word. fmaximum (C23) is in glibc 2.44's libm and not in musl 1.2.5. A program that calls it builds, prints the right answer, and --why-extract shows where the definition came from:
reference extracted symbol
obj/main.o /usr/lib/libm-2.44.a(s_fmaximum.o) fmaximum
Without -lm the same program stops at undefined symbol: fmaximum, which is the graph's real answer. So whether the build succeeds, and what goes into the static image, depends on what the build machine has in /usr/lib. That is the question #662 raised for headers, here on the link side, and the image that was meant to carry only musl carries a glibc object.
Reproduction
mcpp.toml:
[package]
name = "lmcross"
version = "0.1.0"
[toolchain]
default = "llvm@22.1.8"
[target.'cfg(all(os = "linux", env = "musl"))'.dependencies]
openkal-llvm-runtime = "0.15.1"
[build]
ldflags = ["-lm"]
[targets.lmcross]
kind = "bin"
main = "src/main.c"src/main.c:
#include <math.h>
#include <stdio.h>
int main(void) {
volatile double x = 2.5;
printf("%g\n", floor(x));
return 0;
}$ mcpp build --offline --target aarch64-linux-musl
Resolving toolchain
Resolved llvm@22.1.8 → aarch64-linux-musl → @mcpp/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++
Resolved host toolchain for build.mcpp: clang 22.1.8 (x86_64-unknown-linux-gnu)
build.mcpp compiling
build.mcpp running
Target aarch64-linux-musl → aarch64-unknown-linux-musl
compiler-runtime compiler-rt (openkal-llvm-runtime@0.15.1, graph)
kernel-abi openkal (openkal-linux@0.15.0, graph)
c-abi musl (openkal-musl@0.19.1, graph)
c++-abi libc++ (openkal-llvm-runtime@0.15.1, graph)
Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}]
Compiling lmcross v0.1.0 (.)
Cached openkal-llvm-runtime v0.15.1 (353 units)
error: build failed
failed: bin/lmcross
ld.lld: error: obj/mcpplibs_openkal/src/types.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/version.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/time.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/task.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/stream.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/terminal.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/net.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/random.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/memory.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/macros.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/fs.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/process.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/space.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/exec.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/env.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/datagram.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/timeout.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal/src/abort.m.o is incompatible with elf64-x86-64
ld.lld: error: obj/main.o is incompatible with elf64-x86-64
ld.lld: error: obj/mcpplibs_openkal-linux/src/version.o is incompatible with elf64-x86-64
ld.lld: error: too many errors emitted, stopping now (use --error-limit=0 to see all errors)
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
The same project for x86_64 builds and runs:
$ mcpp build --offline --target x86_64-linux-musl
...
Compiling lmcross v0.1.0 (.)
Cached openkal-llvm-runtime v0.15.1 (244 units)
Finished dev [unoptimized + debuginfo] in 4.40s
$ ./target/x86_64-linux-musl/*/bin/lmcross
2
The link command mcpp generated for aarch64 (ninja -t commands bin/lmcross in the target directory):
clang++ @bin/lmcross.rsp -o bin/lmcross -static --target=aarch64-unknown-linux-musl --no-default-config -fuse-ld=lld -lm -nostdlib -static -Wl,--no-dynamic-linker -Wl,--gc-sections -nostdlib++ -Wl,--disable-new-dtags
and the ld.lld call clang makes from it (-###, quotes and the 1733 object files left out):
ld.lld -EL --hash-style=gnu --eh-frame-hdr -m aarch64linux -static -o bin/lmcross -L/lib/../lib64 -L/usr/lib64 -L/lib -L/usr/lib -lm --no-dynamic-linker --gc-sections --disable-new-dtags
For the x86_64 extraction, the program is
#include <stdio.h>
/* C23; glibc's libm has it, musl 1.2.5 does not. */
double fmaximum(double, double);
int main(void) {
volatile double a = 1.0, b = 2.0;
printf("%g\n", fmaximum(a, b));
return 0;
}with ldflags = ["-lm", "-Wl,--why-extract=why-extract.txt"]. mcpp build --offline --target x86_64-linux-musl finishes, the binary prints 2, and why-extract.txt is the table above. With ldflags removed:
ld.lld: error: undefined symbol: fmaximum
>>> referenced by main.c:8 (<project>/src/main.c:8)
>>> obj/main.o:(main)
The same thing without mcpp, on an empty object, with the clang mcpp resolves for llvm@22.1.8. --no-default-config keeps the payload's config file out, as mcpp does on this line. [rc=N] is the exit status:
$ printf 'void _start(void) {}\n' > start.c
$ clang --no-default-config --target=aarch64-unknown-linux-musl -c start.c -o start.o
$ clang --no-default-config --target=aarch64-unknown-linux-musl -fuse-ld=lld -nostdlib -static start.o -o a.out
[rc=0]
$ clang --no-default-config --target=aarch64-unknown-linux-musl -fuse-ld=lld -nostdlib -static start.o -lm -o a.out -Wl,--verbose
ld.lld: start.o
ld.lld: /lib/../lib64/libm.a
ld.lld: /usr/lib/libm-2.44.a
ld.lld: /usr/lib/libmvec.a
ld.lld: error: start.o is incompatible with elf64-x86-64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
[rc=1]
$ clang --no-default-config --target=x86_64-unknown-linux-musl -c start.c -o start.o
$ clang --no-default-config --target=x86_64-unknown-linux-musl -fuse-ld=lld -nostdlib -static start.o -lm -o a.out -Wl,--verbose
ld.lld: start.o
ld.lld: /usr/lib64/gcc/x86_64-pc-linux-gnu/16/../../../../lib64/libm.a
ld.lld: /usr/lib/libm-2.44.a
ld.lld: /usr/lib/libmvec.a
[rc=0]
Where it comes from
The graph branch in src/build/flags.cppm (lines 1898-2043 at 82867ad) replaces the payload's link line, and it already closes two ways of reaching the host: --no-default-config drops the payload config's -L and -B, and ldRuntimeFallback is cleared because "a search path belongs to whoever supplies the libraries". What is left are the directories clang derives on its own from its default sysroot, /, and on x86_64 from the host's GCC installation, because nothing on the line names another root. #664 closed the compile-side twin of this with -nostdlibinc. Its title says the link side had already dropped the host's C library, and that is true of the startup files and -lc, but not of the search directories.
The hermetic link check does not see it either. src/build/hermetic.cppm checks the startup objects and the dynamic linker, and with -nostdlib on the line there are none, so it passes. It wrote its .mcpp-hermetic-ok marker for all three links above, the failing aarch64 one included.
Suggested direction
For clang, give the graph link line a --sysroot that holds nothing of the host's, either an empty directory or one the C library package owns. I tried this by hand on the aarch64 command above. With --sysroot=<empty dir> and -lm removed, the link succeeds and the binary runs under qemu-aarch64 and prints 2. On the empty object above, the same --sysroot leaves no -L on the ld.lld line at all. With -lm kept, the error becomes unable to find library -lm. That is honest, but on its own it would break every descriptor that links libm on linux. compat.brotli, compat.nanosvg and compat.libwebp are three in the index today.
musl has its own answer to that. Its Makefile installs empty archives for m, rt, pthread, crypt, util, xnet, resolv and dl next to libc.a, since everything they would hold is already in libc. With --sysroot=<empty dir> plus -L to a directory holding one empty libm.a, the aarch64 command with -lm links and runs, and that directory is the only -L in -###. So I think the complete fix has two halves. The C library package publishes those eight empty archives, or mcpp treats those names as answered when the C library is musl from the graph. And mcpp stops the driver from adding host directories. The first half has to land no later than the second, or every -lm descriptor breaks on openkal targets at once. If you would rather track the openkal-musl half in that repository, I can open it there.
It may also be worth having the hermetic check report -L directories outside the store on graph links, so a later change to this line cannot bring the host back without a diagnostic.
What I have not tried: GCC over the graph (#664 handled it separately), the Mach-O and PE graph links, which carry their own anchors, and other host layouts. The directories are whatever clang derives from / and the host's GCC for the triple, so the exact outcome will differ between distributions; I have only measured the host below. mcpp-index measures its openkal set on x86_64-linux-gnu and x86_64-windows-musl (tests/openkal/pins.toml), which is probably why the aarch64 case has not shown up there.
Environment
- mcpp 2026.9.21.3, the engine mcpp-index's openkal measurement pins. I have not run 2026.9.25.1, but the graph branch of
flags.cppmandhermetic.cppmread the same on main (82867ad). - llvm@22.1.8 (clang 22.1.8, ca7933e47d3a3451d81e72ac174dcb5aa28b59d1).
- openkal-llvm-runtime 0.15.1, openkal-musl 0.19.1, openkal-linux 0.15.0. The first two are the versions in their repositories'
mcpp.tomlon main. - CachyOS (Arch-based), x86_64, Linux 7.2.6,
ldd (GNU libc) 2.44./lib64and/usr/lib64are symlinks to/usr/lib. - qemu-aarch64 runs the aarch64 binaries.
mcpp self env, with the home directory shortened:
MCPP_HOME = <MCPP_HOME>
xlings binary = <MCPP_HOME>/registry/bin/xlings
xlings pinned = 2026.9.16.1
xlings home = <MCPP_HOME>/registry
config = <MCPP_HOME>/config.toml
build cache = <MCPP_HOME>/build-cache/v1
meta cache = <MCPP_HOME>/cache
default toolchain = (none — run `mcpp toolchain install gcc 16.1.0`)
Index repos:
mcpplibs https://github.com/mcpplibs/mcpp-index.git [artifact] (default)
Toolchain = gcc 20260810 (x86_64-pc-linux-gnu)
std module src = /usr/include/c++/16/bits/std.cc
The project pins llvm@22.1.8 in its own [toolchain], so the Toolchain line above (the host's gcc on PATH) is not the one that builds it.
Activity
0.19.2: musl's empty archives, so -lm is answered by the C library an…
register openkal-musl 0.19.2 (#467)
0.15.2: openkal-musl 0.19.2 (#30)
register openkal-llvm-runtime 0.15.2 (#468)
2026.9.26.1: paths in UTF-8, a C standard per package, a graph link w…
mcpp 2026.9.26.1 as the measured client, and the graph's own Linux ta…
docs: #693-#696 records landed, with the release and ecosystem readin…
speak-agent commentedon Sep 25, 2026
Fixed for the clang ELF graph link by #698, released as 2026.9.26.1 (commit
f61b46632), together with openkal-musl 0.19.2 and openkal-llvm-runtime 0.15.2.There were two causes with two owners. The graph branch replaced the payload's
--sysrootwith nothing, and clang without a sysroot searches the host's/(engine). And openkal-musl did not install the eight empty archives that musl's ownmake installprovides (m,rt,pthread,crypt,util,xnet,resolv,dl), so nothing inside the graph answered-lm(data).The fix, in release order:
-L; its CI asserts that-lmresolves inside the package. 0.15.2: openkal-musl 0.19.2 mcpplibs/openkal-llvm-runtime#30 (0.15.2) pins it, and register openkal-musl 0.19.2 mcpplibs/mcpp-index#467 and feat(target): aarch64-none-elf,零 libc 档的第二个裸机架构 #468 register both.--sysroot=<outputDir>/graph-sysroot, an empty directory, so the driver derives no host directory (src/build/flags.cppm:563-565and:2069-2070; the directory is created atsrc/build/ninja_backend.cppm:3568). The hermetic check holds every-Lof a graph link to the store, the build directory and the graph's package roots, and refuses and names anything else;allow_host_libslifts the refusal.-lfrom those eight names the openkal-musl version that answers it (0.19.2), because a runtime pins its C library exactly, so an older runtime keeps the older openkal-musl.Criteria: the unit tests
HermeticGraphLink.*(four), andtests/e2e/778_a_graph_link_searches_no_host_directory.sh, run byopenkal-cross.ymlin four legs: 0.19.2 with runtime 0.15.2 links and runs; the older pair fails with the note; aarch64 runs under qemu;-L/usr/libis refused. Its leg B fails on 2026.9.25.1. mcpp-index#469 addsx86_64-linux-muslandaarch64-linux-muslrows to the openkal measurement, where no published label went lower. The published-artefact sandbox checks (the empty graph sysroot on the link line;-lmanswered by openkal-musl 0.19.2; the older pair's note) pass on 2026.9.26.1, re-run today.Outside this issue, as its own text says: the Mach-O and PE graph links. PE through the MinGW driver waits for an inventory of the import libraries that the index's
x86_64-windows-muslbuilds take from a host MinGW. One cell this fix made visible is tracked with its owner:-latomichas no answer in the openkal graph on linux-musl targets (mcpplibs/openkal-llvm-runtime#31).Update with
xlings update mcpp -y.Record:
.agents/docs/2026-09-25-issues-693-696-triage-and-repair-plan.md§5.Closing.