From 29ae154edac6deb60104cac7337c2a9e101ad03b Mon Sep 17 00:00:00 2001 From: Apoorv Kothari Date: Wed, 28 Oct 2026 13:59:18 -0700 Subject: [PATCH 1/4] ci(rust-musl): check that musl targets link aws-lc Signed-off-by: Apoorv Kothari --- .github/Dockerfile-rust-musl | 34 ++++++++++++++++++++++++++++++ .github/workflows/devcontainer.yml | 12 +++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/Dockerfile-rust-musl diff --git a/.github/Dockerfile-rust-musl b/.github/Dockerfile-rust-musl new file mode 100644 index 0000000..fde72cd --- /dev/null +++ b/.github/Dockerfile-rust-musl @@ -0,0 +1,34 @@ +# Checks that the rust-musl image links a C dependency for both musl targets. +# aws-lc-sys compiled against glibc >= 2.38 headers emits __isoc23_* symbols +# that musl lacks; calling into it keeps the linker from dropping its archive. +# CI passes the freshly built rust-musl image so the check tests this PR's image. +ARG RUST_MUSL_IMAGE=ghcr.io/linkerd/dev:ci-rust-musl +FROM ${RUST_MUSL_IMAGE} +# aws-lc-rs pulls in aws-lc-sys, a C library sensitive to which libc headers it sees. +ARG AWS_LC_RS_VERSION=1.18.2 # repo=aws/aws-lc-rs,prefix=v +WORKDIR /musl-link +RUN cargo init --quiet --name musl-link && \ + cargo add --quiet "aws-lc-rs@=${AWS_LC_RS_VERSION}" +# Calls into aws-lc so the linker must resolve its symbols; an unused dependency would be dropped. +COPY <&2 ; \ + exit 1 ; \ + fi ; \ + done diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index 078d7fd..08c2f45 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -4,7 +4,9 @@ on: pull_request: paths: - .devcontainer/** + - .github/Dockerfile-rust-musl - .github/workflows/devcontainer.yml + - bin/just-cargo - Dockerfile - justfile @@ -19,6 +21,16 @@ jobs: - uses: ./actions/setup-tools - run: just build + # Builds the rust-musl image, then links a C dependency with it for both musl + # targets (see .github/Dockerfile-rust-musl). + dev-musl-link: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: ./actions/setup-tools + - run: just load=true version=ci targets=rust-musl build + - run: docker build --build-arg RUST_MUSL_IMAGE=ghcr.io/linkerd/dev:ci-rust-musl -f .github/Dockerfile-rust-musl .github + dev-shellcheck: runs-on: ubuntu-latest steps: From 550a6228aacb49aca1b76cf8b5f5690be0750e7a Mon Sep 17 00:00:00 2001 From: Apoorv Kothari Date: Wed, 28 Oct 2026 14:56:42 -0700 Subject: [PATCH 2/4] fix(rust-musl): compile musl C dependencies against musl headers Signed-off-by: Apoorv Kothari --- Dockerfile | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Dockerfile b/Dockerfile index a96d5b9..8db8ec3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -347,6 +347,26 @@ ENV CARGO_INCREMENTAL=0 \ RUSTUP_MAX_RETRIES=10 ENTRYPOINT ["/usr/local/bin/just-cargo"] +# musl C headers matching the musl that this Rust toolchain's musl targets link +# (its self-contained `libc.a`), so C dependencies compile against the same libc +# they link against. The version is read from the toolchain, so a RUST_TAG bump +# picks up matching headers without a separate pin. +FROM rust as rust-musl-patch +# Only needed to read the bundled musl version; both targets ship the same release. +RUN rustup target add x86_64-unknown-linux-musl +# musl embeds its release string (e.g. 1.2.3) in libc.a; there is no rustc flag for it. +RUN libc="$(rustc --print target-libdir --target x86_64-unknown-linux-musl)/self-contained/libc.a" && \ + version=$(strings "$libc" | grep -m1 -xE '[0-9]+\.[0-9]+\.[0-9]+') && \ + for arch in x86_64 aarch64 ; do \ + # One source tree per arch: `bits/alltypes.h` is generated per arch and a shared tree would reuse a stale one. + mkdir -p "/src/$arch" && \ + scurl "https://musl.libc.org/releases/musl-${version}.tar.gz" | tar xzf - --strip-components=1 -C "/src/$arch" && \ + # Installs only headers (copy plus sed-generated bits/*.h); nothing is compiled. + make -C "/src/$arch" ARCH="$arch" prefix="/musl/$arch" install-headers || exit 1 ; \ + done && \ + # Records the header version so CI can check it against the toolchain's libc.a. + echo "$version" > /musl/VERSION + FROM rust as rust-musl RUN rustup target add \ aarch64-unknown-linux-musl \ @@ -359,6 +379,15 @@ RUN --mount=type=cache,from=apt-base,source=/etc/apt,target=/etc/apt,sharing=loc g++-aarch64-linux-gnu \ gcc-aarch64-linux-gnu \ libc6-dev-arm64-cross +COPY --link --from=rust-musl-patch /musl /usr/local/musl +# C compiled for musl targets must see musl's headers, not the host glibc's: +# since glibc 2.38 those redirect `sscanf`/`strtol` to `__isoc23_*` symbols that +# musl lacks, so crates like `aws-lc-sys` fail to link. cc-rs reads these +# per-target variables. +# `-isystem` searches musl's headers before /usr/include, so musl's declarations win. +ENV CFLAGS_x86_64_unknown_linux_musl="-isystem /usr/local/musl/x86_64/include" +# Same for aarch64, with the aarch64 musl headers. +ENV CFLAGS_aarch64_unknown_linux_musl="-isystem /usr/local/musl/aarch64/include" ## ## Devcontainer From 4eb1b815bccb0cd359090d95a68563d8d4f9cc27 Mon Sep 17 00:00:00 2001 From: Apoorv Kothari Date: Wed, 28 Oct 2026 14:56:42 -0700 Subject: [PATCH 3/4] build(rust-musl): fetch musl headers once and document why Signed-off-by: Apoorv Kothari --- Dockerfile | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8db8ec3..e47d67d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -347,22 +347,32 @@ ENV CARGO_INCREMENTAL=0 \ RUSTUP_MAX_RETRIES=10 ENTRYPOINT ["/usr/local/bin/just-cargo"] -# musl C headers matching the musl that this Rust toolchain's musl targets link -# (its self-contained `libc.a`), so C dependencies compile against the same libc -# they link against. The version is read from the toolchain, so a RUST_TAG bump -# picks up matching headers without a separate pin. +# Builds musl C headers that match the musl release this Rust toolchain links. +# +# Why: musl targets compile C (e.g. `aws-lc-sys`) with clang, which otherwise +# uses the host glibc headers. Since glibc 2.38 those redirect `sscanf`/`strtol` +# to `__isoc23_*` symbols that musl lacks, so links fail. Debian's `musl-dev` +# doesn't help: clang ignores its include dir and its release can differ from +# the musl Rust links (trixie 1.2.5 vs Rust 1.90's 1.2.3). +# +# Caveats: +# - The version is read with `strings` from the toolchain's `libc.a`; if musl +# stops embedding it, `version` is empty and the fetch fails loudly. +# - Headers are fetched from musl.libc.org at build time, like other tools here. +# - Both musl targets are assumed to ship the same musl release. FROM rust as rust-musl-patch # Only needed to read the bundled musl version; both targets ship the same release. RUN rustup target add x86_64-unknown-linux-musl # musl embeds its release string (e.g. 1.2.3) in libc.a; there is no rustc flag for it. RUN libc="$(rustc --print target-libdir --target x86_64-unknown-linux-musl)/self-contained/libc.a" && \ version=$(strings "$libc" | grep -m1 -xE '[0-9]+\.[0-9]+\.[0-9]+') && \ + mkdir -p /src && \ + scurl "https://musl.libc.org/releases/musl-${version}.tar.gz" | tar xzf - --strip-components=1 -C /src && \ for arch in x86_64 aarch64 ; do \ - # One source tree per arch: `bits/alltypes.h` is generated per arch and a shared tree would reuse a stale one. - mkdir -p "/src/$arch" && \ - scurl "https://musl.libc.org/releases/musl-${version}.tar.gz" | tar xzf - --strip-components=1 -C "/src/$arch" && \ # Installs only headers (copy plus sed-generated bits/*.h); nothing is compiled. - make -C "/src/$arch" ARCH="$arch" prefix="/musl/$arch" install-headers || exit 1 ; \ + make -C /src ARCH="$arch" prefix="/musl/$arch" install-headers && \ + # bits/*.h are generated per arch under obj/; clear them so the next arch regenerates its own. + rm -rf /src/obj || exit 1 ; \ done && \ # Records the header version so CI can check it against the toolchain's libc.a. echo "$version" > /musl/VERSION From 6faa455acaa01483db39001a15366314c27906b9 Mon Sep 17 00:00:00 2001 From: Apoorv Kothari Date: Wed, 28 Oct 2026 15:48:38 -0700 Subject: [PATCH 4/4] refactor --- Dockerfile | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index e47d67d..c4e1154 100644 --- a/Dockerfile +++ b/Dockerfile @@ -357,25 +357,29 @@ ENTRYPOINT ["/usr/local/bin/just-cargo"] # # Caveats: # - The version is read with `strings` from the toolchain's `libc.a`; if musl -# stops embedding it, `version` is empty and the fetch fails loudly. +# stops embedding it, `grep` finds no match and the build fails before the fetch. # - Headers are fetched from musl.libc.org at build time, like other tools here. # - Both musl targets are assumed to ship the same musl release. FROM rust as rust-musl-patch # Only needed to read the bundled musl version; both targets ship the same release. RUN rustup target add x86_64-unknown-linux-musl # musl embeds its release string (e.g. 1.2.3) in libc.a; there is no rustc flag for it. -RUN libc="$(rustc --print target-libdir --target x86_64-unknown-linux-musl)/self-contained/libc.a" && \ - version=$(strings "$libc" | grep -m1 -xE '[0-9]+\.[0-9]+\.[0-9]+') && \ - mkdir -p /src && \ - scurl "https://musl.libc.org/releases/musl-${version}.tar.gz" | tar xzf - --strip-components=1 -C /src && \ - for arch in x86_64 aarch64 ; do \ - # Installs only headers (copy plus sed-generated bits/*.h); nothing is compiled. - make -C /src ARCH="$arch" prefix="/musl/$arch" install-headers && \ - # bits/*.h are generated per arch under obj/; clear them so the next arch regenerates its own. - rm -rf /src/obj || exit 1 ; \ - done && \ - # Records the header version so CI can check it against the toolchain's libc.a. - echo "$version" > /musl/VERSION +# Saved to /musl/VERSION for the steps below and for CI to check against libc.a. +# grep exits non-zero if no version is found, so a format change fails here. +RUN musl_target_libdir=$(rustc --print target-libdir --target x86_64-unknown-linux-musl) && \ + mkdir -p /musl && \ + strings "$musl_target_libdir/self-contained/libc.a" | grep -m1 -xE '[0-9]+\.[0-9]+\.[0-9]+' > /musl/VERSION +# Fetch the musl source for that release. +RUN musl_version=$(cat /musl/VERSION) && \ + scurl -o /musl.tar.gz "https://musl.libc.org/releases/musl-${musl_version}.tar.gz" +RUN mkdir -p /src && \ + tar xzf /musl.tar.gz --strip-components=1 -C /src +# Installs only headers (copy plus sed-generated bits/*.h); nothing is compiled. +RUN make -C /src ARCH=x86_64 prefix=/musl/x86_64 install-headers +# bits/*.h are generated per arch under obj/; clear x86_64's so aarch64 regenerates its own. +RUN rm -rf /src/obj +# Installs the aarch64 headers. +RUN make -C /src ARCH=aarch64 prefix=/musl/aarch64 install-headers FROM rust as rust-musl RUN rustup target add \ @@ -390,11 +394,7 @@ RUN --mount=type=cache,from=apt-base,source=/etc/apt,target=/etc/apt,sharing=loc gcc-aarch64-linux-gnu \ libc6-dev-arm64-cross COPY --link --from=rust-musl-patch /musl /usr/local/musl -# C compiled for musl targets must see musl's headers, not the host glibc's: -# since glibc 2.38 those redirect `sscanf`/`strtol` to `__isoc23_*` symbols that -# musl lacks, so crates like `aws-lc-sys` fail to link. cc-rs reads these -# per-target variables. -# `-isystem` searches musl's headers before /usr/include, so musl's declarations win. +# cc-rs reads these per-target flags; `-isystem` puts musl's headers ahead of glibc's (see rust-musl-patch). ENV CFLAGS_x86_64_unknown_linux_musl="-isystem /usr/local/musl/x86_64/include" # Same for aarch64, with the aarch64 musl headers. ENV CFLAGS_aarch64_unknown_linux_musl="-isystem /usr/local/musl/aarch64/include"