Skip to content

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

@yspbwx2010

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.cppm and hermetic.cppm read 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.toml on main.
  • CachyOS (Arch-based), x86_64, Linux 7.2.6, ldd (GNU libc) 2.44. /lib64 and /usr/lib64 are 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions