From 1299bb85b0b597644b2ea51d572ff93405f58ecc Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Mon, 14 Sep 2026 11:57:14 +0200 Subject: [PATCH 1/9] ci: drop the archived buster base default, bump the amd64 build image The x64-2.17 image's Dockerfile defaulted to openjdk:11-slim-buster. Debian 10 buster left even LTS support in June 2024 and its repositories are archived, so `apt-get update` against them now fails with "does not have a Release file" -- and rebuild-images.sh builds without --build-arg BASE_IMAGE whenever BASE_IMAGE_LIBC_2_17 is unset, which reaches that default. The body of the file is yum-based, so a Debian base cannot work there at all; the default now names what CI actually passes (centos:7), with the glibc 2.17 rationale for staying on an EOL distro recorded next to the vault mirror URLs it already needs. The amd64 build image moves to Debian 13 trixie, the current stable release. It compiles no shipped native code -- build:x64 uses the glibc 2.17 image -- so its glibc is not a customer-facing floor. The arm64 image stays on bullseye: it is where build:arm64 compiles the shipped linux-arm64 libjavaProfiler.so, so that glibc (2.31) is the runtime floor for arm64 customers and trixie would raise it to 2.41. Both variables now say which of the two they are and why. Also drop the `|| true` from the base image's package install, and with it the dead `apk` branch -- Alpine has its own Dockerfile.musl. As written, a total install failure produced a "successful" image whose tools were simply missing, surfacing later as an unrelated job failure. Verified against the real archives: buster's apt repositories fail as described, bullseye's still resolve, and the full package list installs on trixie with hexdump present (bsdmainutils is still a real package there). Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/base/Dockerfile | 11 +++++++++-- .gitlab/base/centos7/Dockerfile | 11 ++++++++++- .gitlab/build-deploy/images.yml | 13 +++++++++---- .gitlab/config.env | 16 ++++++++++++---- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.gitlab/base/Dockerfile b/.gitlab/base/Dockerfile index 57bca1e809..13b6453e18 100644 --- a/.gitlab/base/Dockerfile +++ b/.gitlab/base/Dockerfile @@ -1,11 +1,18 @@ +# CI always passes BASE_IMAGE explicitly (OPENJDK_BASE_IMAGE / +# OPENJDK_BASE_IMAGE_ARM64), and the two arches deliberately differ -- see the +# comments on those variables. This default is the more conservative of the +# two so a hand-built image matches the arm64 glibc floor. ARG BASE_IMAGE=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b FROM ${BASE_IMAGE} as base ARG CI_JOB_TOKEN WORKDIR /root RUN mkdir -p /usr/share/man/man1 # https://github.com/debuerreotype/docker-debian-artifacts/issues/24 -RUN (apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl git moreutils awscli amazon-ecr-credential-helper gnupg2 npm build-essential wget bsdmainutils clang jq zip unzip) || true -RUN (apk update && apk add curl git moreutils aws-cli docker-credential-ecr-login gnupg alpine-sdk build-base wget npm hexdump linux-headers clang compiler-rt bash jq gradle zip unzip) || true +# Alpine/musl images build from Dockerfile.musl, so this file installs only the +# Debian toolchain. +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl git moreutils awscli amazon-ecr-credential-helper gnupg2 npm build-essential \ + wget bsdmainutils clang jq zip unzip # Install JDK 21 and Maven via SDKMAN (glibc image only; musl uses Dockerfile.musl). RUN curl -s "https://get.sdkman.io" | bash && \ bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install java 21.0.3-tem && sdk install maven" diff --git a/.gitlab/base/centos7/Dockerfile b/.gitlab/base/centos7/Dockerfile index 004a00128b..e6ba977b90 100644 --- a/.gitlab/base/centos7/Dockerfile +++ b/.gitlab/base/centos7/Dockerfile @@ -1,4 +1,13 @@ -ARG BASE_IMAGE=openjdk:11-slim-buster +# CentOS 7 (glibc 2.17) on purpose: this image builds the shipped linux-x64 +# libjavaProfiler.so, and its glibc is the runtime floor for x64 customers. It +# stays on an EOL distro for that reason, which is why the repository URLs +# below point at the vault/archive mirrors rather than mirror.centos.org. +# +# The default has to match what CI passes (BASE_IMAGE_LIBC_2_17 in +# .gitlab/build-deploy/images.yml), because rebuild-images.sh builds without +# --build-arg BASE_IMAGE whenever that variable is unset, and everything below +# is yum-based. +ARG BASE_IMAGE=centos:7 FROM ${BASE_IMAGE} as base ARG CI_JOB_TOKEN WORKDIR /root diff --git a/.gitlab/build-deploy/images.yml b/.gitlab/build-deploy/images.yml index 5bbf7ad760..4e006e8cd5 100644 --- a/.gitlab/build-deploy/images.yml +++ b/.gitlab/build-deploy/images.yml @@ -2,10 +2,15 @@ stages: - images variables: # Base images for the build docker images - # debian:bullseye-slim = Debian 11 (glibc 2.31); pinned by digest for reproducibility. - # Multi-arch manifest covers both amd64 and arm64 — no separate arm64 tag needed. - # JDK 21 is installed at image-build time via SDKMAN (see .gitlab/base/Dockerfile). - OPENJDK_BASE_IMAGE: debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b + # Both pinned by digest for reproducibility. JDK 21 is installed at + # image-build time via SDKMAN (see .gitlab/base/Dockerfile). + # + # amd64: Debian 13 trixie (glibc 2.41), current stable. Compiles no shipped + # native code -- build:x64 uses the glibc 2.17 image -- so it tracks stable. + OPENJDK_BASE_IMAGE: debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 + # arm64: Debian 11 bullseye (glibc 2.31), held back deliberately. build:arm64 + # compiles the shipped linux-arm64 libjavaProfiler.so here, so this glibc is + # the runtime floor for arm64 customers; trixie would raise it to 2.41. OPENJDK_BASE_IMAGE_ARM64: debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_MUSL: eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f diff --git a/.gitlab/config.env b/.gitlab/config.env index 508032d975..20711a4319 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -10,10 +10,18 @@ JAVA_TEST_VERSION=21.0.3-tem JAVA_BENCHMARK_VERSION=21 # Docker Base Images -# debian:bullseye-slim = Debian 11 (glibc 2.31); pinned by digest for reproducibility. -# Multi-arch manifests cover both amd64 and arm64. -# JDK 21 is installed at image-build time via SDKMAN (see .gitlab/base/Dockerfile). -OPENJDK_BASE_IMAGE_AMD64=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b +# Both pinned by digest for reproducibility; multi-arch manifests cover amd64 +# and arm64. JDK 21 is installed at image-build time via SDKMAN (see +# .gitlab/base/Dockerfile). +# +# amd64: debian:trixie-slim = Debian 13 (glibc 2.41), the current stable +# release. No shipped native code is compiled here -- build:x64 uses the +# glibc 2.17 image -- so the release is free to track stable. +OPENJDK_BASE_IMAGE_AMD64=debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 +# arm64: debian:bullseye-slim = Debian 11 (glibc 2.31). Held back +# deliberately: build:arm64 compiles the shipped linux-arm64 +# libjavaProfiler.so in this image, so its glibc version is the runtime floor +# for every arm64 customer. Moving to trixie would raise that floor to 2.41. OPENJDK_BASE_IMAGE_ARM64=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_AMD64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f From 2b2ad37052e1dab648a9dfb3d5ff215719a79433 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Mon, 14 Sep 2026 15:50:00 +0200 Subject: [PATCH 2/9] ci: build the shipped binaries on AlmaLinux 8 with gcc-toolset-14 Release builds link -static-libstdc++ -static-libgcc, so the C++ runtime and unwinder in the shipped libjavaProfiler.so come out of the build image's toolchain. That makes the build image's support status a property of the artifact, not just of CI: on CentOS 7 those objects can never receive another security rebuild, and no gcc newer than devtoolset-10 was ever built for aarch64 there (the dts-11 collection ships binutils, elfutils and its own metapackage for aarch64, but no compiler). AlmaLinux 8 with gcc-toolset-14 is supported until 2029 and publishes the toolset for x86_64 and aarch64 alike, so one image definition now covers both architectures with one compiler version, replacing the hand-rolled CentOS 7 image and its vault/Rocky mirror repositories. almalinux:8 is a multi-arch manifest, so a single BASE_IMAGE_GLIBC serves both. Measured on the real artifact, built in this image for both architectures: max required symbol version GLIBC_2.27 (both x86_64 and aarch64) symbols above 2.17 expf (2.27), getentropy (2.25) dynamic dependencies libc, libdl, libm, libpthread, librt GLIBCXX/CXXABI references none So the runtime floor moves 2.17 -> 2.27, not to the base image's 2.28: symbol versions are per-function. 2.27 keeps Ubuntu 18.04 (glibc 2.27) in support and drops RHEL 7 (2.17), Amazon Linux 2 (2.26) and SLES 15 SP1 (2.26). Both symbols are addressable with a .symver pin if a lower floor is needed later. With the shipped arm64 build moved off the Debian image, that image no longer sets a customer-facing floor, so it moves to trixie alongside amd64 -- which also restores apt on the arm64 sanitizer jobs. Names now describe the purpose rather than a version that has changed twice: BUILD_IMAGE_{X64,ARM64}_GLIBC and BASE_IMAGE_GLIBC, with tag suffixes {x64,arm64}-glibc-base. The variables still hold the current images until rebuild-images.sh publishes the new tags. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/base/centos7/Dockerfile | 52 -------------------------- .gitlab/base/el8/Dockerfile | 37 ++++++++++++++++++ .gitlab/build-deploy/.gitlab-ci.yml | 23 +++++++----- .gitlab/build-deploy/images.yml | 18 +++++---- .gitlab/config.env | 25 +++++++------ .gitlab/scripts/check-image-updates.sh | 3 +- .gitlab/scripts/rebuild-images.sh | 8 ++-- 7 files changed, 83 insertions(+), 83 deletions(-) delete mode 100644 .gitlab/base/centos7/Dockerfile create mode 100644 .gitlab/base/el8/Dockerfile diff --git a/.gitlab/base/centos7/Dockerfile b/.gitlab/base/centos7/Dockerfile deleted file mode 100644 index e6ba977b90..0000000000 --- a/.gitlab/base/centos7/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# CentOS 7 (glibc 2.17) on purpose: this image builds the shipped linux-x64 -# libjavaProfiler.so, and its glibc is the runtime floor for x64 customers. It -# stays on an EOL distro for that reason, which is why the repository URLs -# below point at the vault/archive mirrors rather than mirror.centos.org. -# -# The default has to match what CI passes (BASE_IMAGE_LIBC_2_17 in -# .gitlab/build-deploy/images.yml), because rebuild-images.sh builds without -# --build-arg BASE_IMAGE whenever that variable is unset, and everything below -# is yum-based. -ARG BASE_IMAGE=centos:7 -FROM ${BASE_IMAGE} as base -ARG CI_JOB_TOKEN -WORKDIR /root - -# 1. Replace dead mirrorlist entries with HTTPS vault URLs -RUN set -eux; \ - sed -i -e 's/^mirrorlist/#mirrorlist/' \ - -e 's|^#baseurl=http://mirror.centos.org|baseurl=https://vault.centos.org|' \ - /etc/yum.repos.d/CentOS-*.repo - -# 2. Add a vault mirror that still contains Software Collections -RUN cat > /etc/yum.repos.d/CentOS-SCLo-Vault.repo <<'EOF' -[centos-sclo-rh] -name=CentOS-7 - SCLo rh (Rocky Vault) -baseurl=https://dl.rockylinux.org/vault/centos/7.9.2009/sclo/$basearch/rh/ -gpgcheck=0 -enabled=1 - -[centos-sclo-sclo] -name=CentOS-7 - SCLo sclo (Rocky Vault) -baseurl=https://dl.rockylinux.org/vault/centos/7.9.2009/sclo/$basearch/sclo/ -gpgcheck=0 -enabled=1 -EOF - -# 3. Expose devtoolset-11 binaries & libs by default (they are installed a bit later) -ENV PATH="/opt/rh/devtoolset-11/root/usr/bin:${PATH}" \ - LD_LIBRARY_PATH="/opt/rh/devtoolset-11/root/usr/lib64:${LD_LIBRARY_PATH}" - -RUN yum -y clean all -RUN yum -y update && yum -y install scl-utils devtoolset-11 devtoolset-11-toolchain curl zip unzip git libstdc++-static make which wget cmake binutils -RUN yum -y clean all -RUN (curl -s "https://get.sdkman.io" | bash) -RUN (source ~/.sdkman/bin/sdkman-init.sh && sdk install java 21.0.3-tem) -RUN (curl -sL https://rpm.nodesource.com/setup_16.x | bash -) -# installing JQ requires two steps - adding the repo and then installing the tool -RUN yum install -y epel-release -RUN yum install -y jq -# now install nodejs and datadog CI support -RUN yum -y install nodejs -RUN npm install -g --save-dev @datadog/datadog-ci -RUN rm -rf "/var/lib/apt/lists/*" \ No newline at end of file diff --git a/.gitlab/base/el8/Dockerfile b/.gitlab/base/el8/Dockerfile new file mode 100644 index 0000000000..9bd7b8b2b8 --- /dev/null +++ b/.gitlab/base/el8/Dockerfile @@ -0,0 +1,37 @@ +# Build image for the shipped glibc binaries, both architectures. +# +# AlmaLinux 8 (glibc 2.28) with gcc-toolset-14. Release builds link +# -static-libstdc++ -static-libgcc (see ConfigurationPresets.kt), so the C++ +# runtime and unwinder in the shipped libjavaProfiler.so come out of this +# image: the toolchain has to be one that still receives security rebuilds, +# which rules out the EL7 software collections (frozen at CentOS 7.9, and with +# no gcc newer than devtoolset-10 ever built for aarch64). +# +# almalinux:8 is a multi-arch manifest, so BASE_IMAGE is the same for both +# platforms (BASE_IMAGE_GLIBC_2_28 in .gitlab/build-deploy/images.yml) and +# `docker build --platform` selects the architecture. gcc-toolset-14 is +# published for x86_64 and aarch64 alike, so both arches compile with one +# compiler version. +ARG BASE_IMAGE=almalinux:8 +FROM ${BASE_IMAGE} as base +ARG CI_JOB_TOKEN +WORKDIR /root + +# gcc-toolset-14 carries its own libstdc++.a/libgcc.a, which is what +# -static-libstdc++ resolves against; no separate *-static package is needed. +RUN dnf -y install gcc-toolset-14 \ + git make cmake zip unzip which wget jq \ + && dnf -y clean all + +# Put the toolset ahead of the distro gcc for every shell, including the +# non-login shells GitLab uses to run job scripts (the `scl enable` / +# /opt/rh/gcc-toolset-14/enable wrappers only affect interactive use). +ENV PATH="/opt/rh/gcc-toolset-14/root/usr/bin:${PATH}" \ + LD_LIBRARY_PATH="/opt/rh/gcc-toolset-14/root/usr/lib64:${LD_LIBRARY_PATH}" + +# The build scripts locate a JDK through JAVA_HOME, falling back to SDKMAN's +# current candidate (.gitlab/scripts/build.sh). +RUN curl -s "https://get.sdkman.io" | bash && \ + bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install java 21.0.3-tem && sdk install maven" +ENV JAVA_HOME=/root/.sdkman/candidates/java/current +ENV PATH="/root/.sdkman/candidates/java/current/bin:/root/.sdkman/candidates/maven/current/bin:${PATH}" diff --git a/.gitlab/build-deploy/.gitlab-ci.yml b/.gitlab/build-deploy/.gitlab-ci.yml index ed27bff0cd..a7c550df00 100644 --- a/.gitlab/build-deploy/.gitlab-ci.yml +++ b/.gitlab/build-deploy/.gitlab-ci.yml @@ -5,8 +5,13 @@ variables: BUILD_IMAGE_X64: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-base@sha256:4712b562eb75cf27c5d494081f459fdbd7578ef46a99162e1051ef7d1f79da4f # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 BUILD_IMAGE_ARM64: registry.ddbuild.io/ci/async-profiler-build:v107978918-arm64-base@sha256:15670752fa8e67c81c1b7fd4d93ef34c668649f117584ab29327d8d7733d64d6 - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BUILD_IMAGE_X64_2_17: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-2.17-base@sha256:03fb65c5c7fb2866397ceaf18ccb5a0b91bb377a9f89c94264427b62d48cfffd + # Still the x64-2.17-base image built from the old CentOS 7 definition; + # rebuild-images.sh replaces it with x64-glibc-base off AlmaLinux 8. + BUILD_IMAGE_X64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-2.17-base@sha256:03fb65c5c7fb2866397ceaf18ccb5a0b91bb377a9f89c94264427b62d48cfffd + # Until the arm64-glibc image is built for the first time this points at the + # arm64-base image, which is what build:arm64 used before; rebuild-images.sh + # replaces it with the AlmaLinux 8 one. + BUILD_IMAGE_ARM64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v107978918-arm64-base@sha256:15670752fa8e67c81c1b7fd4d93ef34c668649f117584ab29327d8d7733d64d6 # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 BUILD_IMAGE_X64_MUSL: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-musl-base@sha256:0efe0c5fbd04e5647a53235d4c4942a2dd305b3d76bf5afdc052d64a7c0b9505 # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 @@ -92,8 +97,7 @@ prepare:start: build:x64: extends: .build_job variables: - # default to the libc 2.17 linked version - BUILD_IMAGE: ${BUILD_IMAGE_X64_2_17} + BUILD_IMAGE: ${BUILD_IMAGE_X64_GLIBC} TARGET: linux-x64 build:x64-musl: @@ -106,7 +110,8 @@ build:arm64: extends: .build_job tags: [ "arch:arm64" ] variables: - BUILD_IMAGE: ${BUILD_IMAGE_ARM64} + # shipped-binary image, mirroring build:x64 + BUILD_IMAGE: ${BUILD_IMAGE_ARM64_GLIBC} TARGET: linux-arm64 build:arm64-musl: @@ -125,8 +130,7 @@ stresstest:x64: - job: build:x64 artifacts: true variables: - # default to the libc 2.17 linked version - BUILD_IMAGE: ${BUILD_IMAGE_X64_2_17} + BUILD_IMAGE: ${BUILD_IMAGE_X64_GLIBC} TARGET: linux-x64 stresstest:x64-musl: @@ -149,7 +153,8 @@ stresstest:arm64: artifacts: true tags: [ "arch:arm64" ] variables: - BUILD_IMAGE: ${BUILD_IMAGE_ARM64} + # same image as build:arm64, so the stress run exercises the shipped build + BUILD_IMAGE: ${BUILD_IMAGE_ARM64_GLIBC} TARGET: linux-arm64 stresstest:arm64-musl: @@ -181,7 +186,7 @@ chaos:build: when: always - when: never tags: [ "arch:amd64" ] - image: ${BUILD_IMAGE_X64_2_17} + image: ${BUILD_IMAGE_X64_GLIBC} script: - | if [ -z "${JAVA_HOME}" ] || [ ! -x "${JAVA_HOME}/bin/java" ]; then diff --git a/.gitlab/build-deploy/images.yml b/.gitlab/build-deploy/images.yml index 4e006e8cd5..3774fa71ec 100644 --- a/.gitlab/build-deploy/images.yml +++ b/.gitlab/build-deploy/images.yml @@ -5,16 +5,20 @@ variables: # Both pinned by digest for reproducibility. JDK 21 is installed at # image-build time via SDKMAN (see .gitlab/base/Dockerfile). # - # amd64: Debian 13 trixie (glibc 2.41), current stable. Compiles no shipped - # native code -- build:x64 uses the glibc 2.17 image -- so it tracks stable. + # Debian 13 trixie (glibc 2.41), current stable. These images run the test, + # sanitizer, integration and packaging jobs; the shipped binaries are + # compiled in the AlmaLinux 8 image below, so this glibc is not a customer + # floor and the release can track stable. OPENJDK_BASE_IMAGE: debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 - # arm64: Debian 11 bullseye (glibc 2.31), held back deliberately. build:arm64 - # compiles the shipped linux-arm64 libjavaProfiler.so here, so this glibc is - # the runtime floor for arm64 customers; trixie would raise it to 2.41. - OPENJDK_BASE_IMAGE_ARM64: debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b + OPENJDK_BASE_IMAGE_ARM64: debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_MUSL: eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f OPENJDK_BASE_IMAGE_ARM64_MUSL: eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f - BASE_IMAGE_LIBC_2_17: centos:7 + # AlmaLinux 8 (glibc 2.28) with gcc-toolset-14 compiles the shipped glibc + # binaries on both architectures. Release builds link -static-libstdc++ + # -static-libgcc, so this toolchain's C++ runtime ships inside the library + # and must be one that still gets security rebuilds. The resulting .so + # requires GLIBC_2.27. Multi-arch manifest, pinned by digest. + BASE_IMAGE_GLIBC: almalinux:8@sha256:9f355ae942d6a6c0561f0771dc053a2cfae9580fc45fa4252756db7c7e80c09f DOCKER_IMAGE: 486234852809.dkr.ecr.us-east-1.amazonaws.com/images/docker:24.0.4-gbi-focal diff --git a/.gitlab/config.env b/.gitlab/config.env index 20711a4319..160808a8a9 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -10,19 +10,22 @@ JAVA_TEST_VERSION=21.0.3-tem JAVA_BENCHMARK_VERSION=21 # Docker Base Images -# Both pinned by digest for reproducibility; multi-arch manifests cover amd64 -# and arm64. JDK 21 is installed at image-build time via SDKMAN (see -# .gitlab/base/Dockerfile). +# All pinned by digest for reproducibility; multi-arch manifests cover amd64 +# and arm64. JDK 21 is installed at image-build time via SDKMAN. # -# amd64: debian:trixie-slim = Debian 13 (glibc 2.41), the current stable -# release. No shipped native code is compiled here -- build:x64 uses the -# glibc 2.17 image -- so the release is free to track stable. +# debian:trixie-slim = Debian 13 (glibc 2.41), current stable. These run the +# test, sanitizer, integration and packaging jobs (see .gitlab/base/Dockerfile); +# the shipped binaries come from the AlmaLinux 8 image below, so this glibc +# is not a customer-facing floor. OPENJDK_BASE_IMAGE_AMD64=debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 -# arm64: debian:bullseye-slim = Debian 11 (glibc 2.31). Held back -# deliberately: build:arm64 compiles the shipped linux-arm64 -# libjavaProfiler.so in this image, so its glibc version is the runtime floor -# for every arm64 customer. Moving to trixie would raise that floor to 2.41. -OPENJDK_BASE_IMAGE_ARM64=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b +OPENJDK_BASE_IMAGE_ARM64=debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 +# almalinux:8 = AlmaLinux 8 (glibc 2.28) with gcc-toolset-14, used to compile +# the shipped glibc binaries on both architectures (see +# .gitlab/base/el8/Dockerfile). Release builds link -static-libstdc++ +# -static-libgcc, so this toolchain's C++ runtime ends up inside the shipped +# library and has to be one that still receives security rebuilds. The +# resulting .so requires GLIBC_2.27 (expf), so it runs on glibc 2.27 or newer. +BASE_IMAGE_GLIBC=almalinux:8@sha256:9f355ae942d6a6c0561f0771dc053a2cfae9580fc45fa4252756db7c7e80c09f # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_AMD64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f OPENJDK_BASE_IMAGE_ARM64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f diff --git a/.gitlab/scripts/check-image-updates.sh b/.gitlab/scripts/check-image-updates.sh index 1255d3adc1..7013977421 100755 --- a/.gitlab/scripts/check-image-updates.sh +++ b/.gitlab/scripts/check-image-updates.sh @@ -38,7 +38,8 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # registry_path is relative to REGISTRY/ci/ IMAGE_DEFS=( "BUILD_IMAGE_X64|.gitlab/build-deploy/.gitlab-ci.yml|x64-base|image-base-build-x64|async-profiler-build" - "BUILD_IMAGE_X64_2_17|.gitlab/build-deploy/.gitlab-ci.yml|x64-2.17-base|image-base-build-x64-2.17|async-profiler-build" + "BUILD_IMAGE_X64_GLIBC|.gitlab/build-deploy/.gitlab-ci.yml|x64-glibc-base|image-base-build-x64-glibc|async-profiler-build" + "BUILD_IMAGE_ARM64_GLIBC|.gitlab/build-deploy/.gitlab-ci.yml|arm64-glibc-base|image-base-build-arm64-glibc|async-profiler-build" "BUILD_IMAGE_X64_MUSL|.gitlab/build-deploy/.gitlab-ci.yml|x64-musl-base|image-base-build-x64-musl|async-profiler-build" "BUILD_IMAGE_ARM64|.gitlab/build-deploy/.gitlab-ci.yml|arm64-base|image-base-build-arm64|async-profiler-build" "BUILD_IMAGE_ARM64_MUSL|.gitlab/build-deploy/.gitlab-ci.yml|arm64-musl-base|image-base-build-arm64-musl|async-profiler-build" diff --git a/.gitlab/scripts/rebuild-images.sh b/.gitlab/scripts/rebuild-images.sh index 1ed6bbc2d4..5df3736cd4 100755 --- a/.gitlab/scripts/rebuild-images.sh +++ b/.gitlab/scripts/rebuild-images.sh @@ -16,7 +16,7 @@ # Base image variables come from .gitlab/build-deploy/images.yml which is # included in the root pipeline and available as CI variables: # OPENJDK_BASE_IMAGE, OPENJDK_BASE_IMAGE_ARM64, OPENJDK_BASE_IMAGE_MUSL, -# OPENJDK_BASE_IMAGE_ARM64_MUSL, BASE_IMAGE_LIBC_2_17, BASE_BENCHMARK_IMAGE_NAME, +# OPENJDK_BASE_IMAGE_ARM64_MUSL, BASE_IMAGE_GLIBC, BASE_BENCHMARK_IMAGE_NAME, # DOCKER_IMAGE set -euo pipefail @@ -41,7 +41,8 @@ usage() { echo "" >&2 echo "Valid short names:" >&2 echo " x64 glibc x86_64 build image" >&2 - echo " x64-2.17 glibc 2.17 (centos7) x86_64 build image" >&2 + echo " x64-glibc shipped-binary x86_64 build image (AlmaLinux 8)" >&2 + echo " arm64-glibc shipped-binary aarch64 build image (AlmaLinux 8)" >&2 echo " x64-musl musl x86_64 build image" >&2 echo " arm64 glibc arm64 build image" >&2 echo " arm64-musl musl arm64 build image" >&2 @@ -58,7 +59,8 @@ usage() { # base_image_var is the name of the env var holding the base image (empty if none needed). IMAGE_DEFS=( "x64|BUILD_IMAGE_X64|.gitlab/build-deploy/.gitlab-ci.yml|x64-base|.gitlab/base/Dockerfile|linux/amd64|async-profiler-build|OPENJDK_BASE_IMAGE" - "x64-2.17|BUILD_IMAGE_X64_2_17|.gitlab/build-deploy/.gitlab-ci.yml|x64-2.17-base|.gitlab/base/centos7/Dockerfile|linux/amd64|async-profiler-build|BASE_IMAGE_LIBC_2_17" + "x64-glibc|BUILD_IMAGE_X64_GLIBC|.gitlab/build-deploy/.gitlab-ci.yml|x64-glibc-base|.gitlab/base/el8/Dockerfile|linux/amd64|async-profiler-build|BASE_IMAGE_GLIBC" + "arm64-glibc|BUILD_IMAGE_ARM64_GLIBC|.gitlab/build-deploy/.gitlab-ci.yml|arm64-glibc-base|.gitlab/base/el8/Dockerfile|linux/arm64|async-profiler-build|BASE_IMAGE_GLIBC" "x64-musl|BUILD_IMAGE_X64_MUSL|.gitlab/build-deploy/.gitlab-ci.yml|x64-musl-base|.gitlab/base/Dockerfile.musl|linux/amd64|async-profiler-build|OPENJDK_BASE_IMAGE_MUSL" "arm64|BUILD_IMAGE_ARM64|.gitlab/build-deploy/.gitlab-ci.yml|arm64-base|.gitlab/base/Dockerfile|linux/arm64|async-profiler-build|OPENJDK_BASE_IMAGE_ARM64" "arm64-musl|BUILD_IMAGE_ARM64_MUSL|.gitlab/build-deploy/.gitlab-ci.yml|arm64-musl-base|.gitlab/base/Dockerfile.musl|linux/arm64|async-profiler-build|OPENJDK_BASE_IMAGE_ARM64_MUSL" From 140522b456f57be60eca6a069da4b1687da9743d Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Mon, 14 Sep 2026 16:25:27 +0200 Subject: [PATCH 3/9] ci: move the C++ test toolchain into the build image The sanitizer jobs installed cmake, libgtest-dev, libgmock-dev, binutils, libc6-dbg and llvm in a before_script, which made every run depend on the distro archive still serving them, and which does not survive the move to trixie: sanitizer/asan_interface.h is no longer part of the llvm package there, so compiling linearAllocator.cpp fails with fatal error: 'sanitizer/asan_interface.h' file not found The header comes from libclang-rt--dev. The packages now live in the image instead, using the unversioned libclang-rt-dev metapackage so the right clang runtime is picked up on whatever release the image is built from. Dropping the before_script override also stops these jobs bypassing the pipeline default, so they get the CANCELLED guard and the maven proxy export that every other job already had. Verified on a locally built trixie image (clang 19.1.7, gtest 1.16.0), aarch64: buildGtestAsan and buildGtestTsan both compile, and all 62 asan and 62 tsan binaries pass. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/base/Dockerfile | 9 ++++++++- .gitlab/sanitizer-tests/.gitlab-ci.yml | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitlab/base/Dockerfile b/.gitlab/base/Dockerfile index 13b6453e18..ad86209e47 100644 --- a/.gitlab/base/Dockerfile +++ b/.gitlab/base/Dockerfile @@ -10,9 +10,16 @@ WORKDIR /root RUN mkdir -p /usr/share/man/man1 # https://github.com/debuerreotype/docker-debian-artifacts/issues/24 # Alpine/musl images build from Dockerfile.musl, so this file installs only the # Debian toolchain. +# +# The second group is what the C++ test and sanitizer jobs need. They live in +# the image rather than in a job's before_script so that a job does not depend +# on the distro archive still serving those packages at run time. +# libclang-rt-dev is the unversioned metapackage: it provides +# sanitizer/asan_interface.h from whichever clang runtime matches this release. RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ curl git moreutils awscli amazon-ecr-credential-helper gnupg2 npm build-essential \ - wget bsdmainutils clang jq zip unzip + wget bsdmainutils clang jq zip unzip \ + cmake libgtest-dev libgmock-dev libc6-dbg llvm libclang-rt-dev # Install JDK 21 and Maven via SDKMAN (glibc image only; musl uses Dockerfile.musl). RUN curl -s "https://get.sdkman.io" | bash && \ bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install java 21.0.3-tem && sdk install maven" diff --git a/.gitlab/sanitizer-tests/.gitlab-ci.yml b/.gitlab/sanitizer-tests/.gitlab-ci.yml index 1bf7ca2321..c93cc46ec6 100644 --- a/.gitlab/sanitizer-tests/.gitlab-ci.yml +++ b/.gitlab/sanitizer-tests/.gitlab-ci.yml @@ -20,9 +20,9 @@ when: never - when: on_success interruptible: true - before_script: - - apt-get update -qq - - apt-get install -y -qq cmake libgtest-dev libgmock-dev binutils libc6-dbg llvm + # No before_script override: the toolchain lives in the build image, so the + # pipeline default (CANCELLED guard, maven proxy) applies here like anywhere + # else. script: - ./gradlew :ddprof-lib:buildGtest${SANITIZER_CONFIG} --no-daemon --parallel --build-cache - | From 25e95ca4cb6247a74e078b190c47ebcdf3cf00fd Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Mon, 14 Sep 2026 17:34:52 +0200 Subject: [PATCH 4/9] ci: repin the build images onto the rebuilt bases Pins from the rebuild in pipeline 137281788, which built every image from the definitions on this branch. Verified by pulling each digest: x64-glibc-base AlmaLinux 8.10, glibc 2.28, gcc 14.2.1 arm64-glibc-base AlmaLinux 8.10, glibc 2.28, gcc 14.2.1 x64-base Debian 13 trixie, glibc 2.41, clang 19.1.7 arm64-base Debian 13 trixie The musl, datadog-ci and benchmark images come from the same rebuild; their definitions did not change on this branch. These pins belong here rather than in the image-update PR the rebuild opened against main: main has no BUILD_IMAGE_*_GLIBC variables, so the two shipped-binary pins silently did nothing there, while BUILD_IMAGE_ARM64 -- which main still uses for build:arm64 and stresstest:arm64 -- would have moved the shipped arm64 binary onto a trixie image and its glibc 2.41 floor. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/benchmarks/images.yml | 8 ++++---- .gitlab/build-deploy/.gitlab-ci.yml | 31 +++++++++++++---------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.gitlab/benchmarks/images.yml b/.gitlab/benchmarks/images.yml index e4facfc743..b9a89bff9a 100644 --- a/.gitlab/benchmarks/images.yml +++ b/.gitlab/benchmarks/images.yml @@ -5,7 +5,7 @@ variables: BASE_BENCHMARK_IMAGE_NAME: registry.ddbuild.io/images/benchmarking-platform-tools-ubuntu:newest BASE_CI_IMAGE_NAME: registry.ddbuild.io/ci/async-profiler-build - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BENCHMARK_IMAGE_AMD64: registry.ddbuild.io/ci/async-profiler-build-amd64:v107978918-amd64-benchmarks@sha256:95d4e3719717a6af63ed62437b985e36a95b4552bac04a39cd7b82bafa09271d - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BENCHMARK_IMAGE_ARM64: registry.ddbuild.io/ci/async-profiler-build-arm64:v107978918-arm64-benchmarks@sha256:b534640f415e2c1af44611c1531a0435a387c85f435876fa2e7e5011e4749221 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BENCHMARK_IMAGE_AMD64: registry.ddbuild.io/ci/async-profiler-build-amd64:v137281788-amd64-benchmarks@sha256:28e10be33f32e73ba3850796b3fc7abe747b9e1c71046ea1b91ead84ae7d4ed6 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BENCHMARK_IMAGE_ARM64: registry.ddbuild.io/ci/async-profiler-build-arm64:v137281788-arm64-benchmarks@sha256:c03d7ac565f4b36fc3086a00fe9dcd5d89385b799d78bab7e16b1a8cff713f70 diff --git a/.gitlab/build-deploy/.gitlab-ci.yml b/.gitlab/build-deploy/.gitlab-ci.yml index a7c550df00..fdb5dab756 100644 --- a/.gitlab/build-deploy/.gitlab-ci.yml +++ b/.gitlab/build-deploy/.gitlab-ci.yml @@ -1,23 +1,20 @@ image: alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 variables: - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BUILD_IMAGE_X64: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-base@sha256:4712b562eb75cf27c5d494081f459fdbd7578ef46a99162e1051ef7d1f79da4f - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BUILD_IMAGE_ARM64: registry.ddbuild.io/ci/async-profiler-build:v107978918-arm64-base@sha256:15670752fa8e67c81c1b7fd4d93ef34c668649f117584ab29327d8d7733d64d6 - # Still the x64-2.17-base image built from the old CentOS 7 definition; - # rebuild-images.sh replaces it with x64-glibc-base off AlmaLinux 8. - BUILD_IMAGE_X64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-2.17-base@sha256:03fb65c5c7fb2866397ceaf18ccb5a0b91bb377a9f89c94264427b62d48cfffd - # Until the arm64-glibc image is built for the first time this points at the - # arm64-base image, which is what build:arm64 used before; rebuild-images.sh - # replaces it with the AlmaLinux 8 one. - BUILD_IMAGE_ARM64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v107978918-arm64-base@sha256:15670752fa8e67c81c1b7fd4d93ef34c668649f117584ab29327d8d7733d64d6 - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BUILD_IMAGE_X64_MUSL: registry.ddbuild.io/ci/async-profiler-build:v107978918-x64-musl-base@sha256:0efe0c5fbd04e5647a53235d4c4942a2dd305b3d76bf5afdc052d64a7c0b9505 - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - BUILD_IMAGE_ARM64_MUSL: registry.ddbuild.io/ci/async-profiler-build:v107978918-arm64-musl-base@sha256:1c8a96ded178bfa130077166db049cbea422c94c96a8c5172d66f8c3581d57cf - # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/1600939090 - DATADOG_CI_IMAGE: registry.ddbuild.io/ci/async-profiler-build:v107978918-datadog-ci@sha256:1353737e433ef58b37ae34daf6edc0d6a5fc4706bac76ac97729e981869ea680 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_X64: registry.ddbuild.io/ci/async-profiler-build:v137281788-x64-base@sha256:81eab385c41ac74a4f71140df9972883ca20bb9d97756f443f9001e9d415d832 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_ARM64: registry.ddbuild.io/ci/async-profiler-build:v137281788-arm64-base@sha256:281a24ae99326041711ed4f63e5528f439091ddc8c19f79c490e9021434486be + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_X64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v137281788-x64-glibc-base@sha256:270a9101716a0865071182e0ef0e40e98f874535b90722949b33a4a8998fe08d + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_ARM64_GLIBC: registry.ddbuild.io/ci/async-profiler-build:v137281788-arm64-glibc-base@sha256:511d051d37ed99b2ca9cd3af797aa9de63e4b4b952af1e738b2cea913bbcc057 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_X64_MUSL: registry.ddbuild.io/ci/async-profiler-build:v137281788-x64-musl-base@sha256:fd3803695771de3eabf44e4953c0474273f9dbb97d88c22e99ac507b88572481 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + BUILD_IMAGE_ARM64_MUSL: registry.ddbuild.io/ci/async-profiler-build:v137281788-arm64-musl-base@sha256:edff5b0205db2d1d520debdd35f9ae6b047a6c0f80bd6f3eca02b57cf56c8c98 + # Generated by https://gitlab.ddbuild.io/DataDog/java-profiler/-/jobs/2040944172 + DATADOG_CI_IMAGE: registry.ddbuild.io/ci/async-profiler-build:v137281788-datadog-ci@sha256:84b7902bec3b4cef3e5dbeb0a8a2c2542f388a4c72bdcd736eb9db4fdd107d9f LAST_COMMIT_FILE: .last.commit CACHE_FALLBACK_KEY: async-profiler From 5305beb4b7d1089552f3b902c72d5cd026407da6 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Tue, 15 Sep 2026 16:48:33 +0200 Subject: [PATCH 5/9] ci: point the base image's default at the release CI actually uses The ARG default was still the bullseye digest while both OPENJDK_BASE_IMAGE variables moved to trixie, and the comment justified it as "the more conservative of the two" for an arm64 glibc floor that no longer exists here: the shipped binaries come from .gitlab/base/el8/Dockerfile, so neither arch of this image sets a customer floor. It also stopped being a harmless default. With the package install no longer ending in `|| true`, a direct `docker build` of this file -- or rebuild-images.sh reaching its documented unset-variable fallback -- hit bullseye's pruned security pool and failed on exactly the 404s this PR exists to fix. Verified: building with no BASE_IMAGE argument now produces a trixie image. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/base/Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitlab/base/Dockerfile b/.gitlab/base/Dockerfile index ad86209e47..e3ec03c998 100644 --- a/.gitlab/base/Dockerfile +++ b/.gitlab/base/Dockerfile @@ -1,8 +1,10 @@ # CI always passes BASE_IMAGE explicitly (OPENJDK_BASE_IMAGE / -# OPENJDK_BASE_IMAGE_ARM64), and the two arches deliberately differ -- see the -# comments on those variables. This default is the more conservative of the -# two so a hand-built image matches the arm64 glibc floor. -ARG BASE_IMAGE=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b +# OPENJDK_BASE_IMAGE_ARM64), both of which are this same digest: the shipped +# binaries are built elsewhere (.gitlab/base/el8/Dockerfile), so neither arch +# of this image sets a customer-facing glibc floor. The default matches them, +# so a hand-built image -- or rebuild-images.sh reaching its unset-variable +# fallback -- gets a release whose packages still resolve. +ARG BASE_IMAGE=debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31f2d29e01ccf3182cc4017c709b6259bc132 FROM ${BASE_IMAGE} as base ARG CI_JOB_TOKEN WORKDIR /root From df9252b1f666b381de04617cb7e3a530dd399b84 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 16 Sep 2026 10:46:17 +0200 Subject: [PATCH 6/9] ci: assert the shipped library's ABI floor in the build job The oldest host the shipped library can load on is decided entirely by the build image: the highest versioned glibc symbol it references sets the glibc floor, and the static-link flags decide whether it needs a C++ runtime from the host at all. Nothing checked either, so both could move without a source change and without a failing job. check-abi-floor.sh asserts both against SHIPPED_GLIBC_FLOOR, declared in config.env next to the image pin that determines it, and build.sh runs it on each glibc artifact before the job ends. musl targets carry no glibc symbol versions and are skipped. Failures name the offending symbols or libraries, since the point of the check is to say what needs fixing. Three cases fail loudly rather than reporting a pass: an artifact with no versioned glibc symbols, one with no NEEDED entries, and a sort that cannot order versions. Each would otherwise leave the check quietly inoperative, which is worse than not having it. The checker reads the artifact through $OBJDUMP, so its tests drive it with canned output and need no compiler or real shared object; they run in a new ci-script-tests job alongside shellcheck. Every guard was mutation-checked: disabling it individually turns a named assertion red. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 23 ++ .gitlab/config.env | 6 + .gitlab/scripts/build.sh | 14 + .gitlab/scripts/check-abi-floor.sh | 153 +++++++++++ .gitlab/scripts/tests/check_abi_floor_test.sh | 239 ++++++++++++++++++ 5 files changed, 435 insertions(+) create mode 100755 .gitlab/scripts/check-abi-floor.sh create mode 100755 .gitlab/scripts/tests/check_abi_floor_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20ba736a96..9aa8e4989b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,29 @@ jobs: .github/scripts/tests/test_release_automation.sh .github/scripts/tests/test_release_automation.sh + ci-script-tests: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate pipeline scripts without network access + run: | + bash -n \ + .gitlab/scripts/build.sh \ + .gitlab/scripts/check-abi-floor.sh \ + .gitlab/scripts/tests/check_abi_floor_test.sh \ + .gitlab/scripts/tests/includes_test.sh + shellcheck \ + .gitlab/scripts/check-abi-floor.sh \ + .gitlab/scripts/tests/check_abi_floor_test.sh + .gitlab/scripts/tests/check_abi_floor_test.sh + .gitlab/scripts/tests/includes_test.sh + check-for-pr: runs-on: ubuntu-latest outputs: diff --git a/.gitlab/config.env b/.gitlab/config.env index 160808a8a9..cb2a93a8a9 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -26,6 +26,12 @@ OPENJDK_BASE_IMAGE_ARM64=debian:trixie-slim@sha256:d7e12182ce18b85b93007c1dedf31 # library and has to be one that still receives security rebuilds. The # resulting .so requires GLIBC_2.27 (expf), so it runs on glibc 2.27 or newer. BASE_IMAGE_GLIBC=almalinux:8@sha256:9f355ae942d6a6c0561f0771dc053a2cfae9580fc45fa4252756db7c7e80c09f +# Highest glibc symbol version the shipped binaries are allowed to require, +# asserted by .gitlab/scripts/check-abi-floor.sh at the end of every glibc build +# job. It is the oldest glibc a customer can load the library on, so raising it +# drops hosts and is a deliberate decision rather than a consequence of changing +# the image above. +SHIPPED_GLIBC_FLOOR=2.27 # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_AMD64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f OPENJDK_BASE_IMAGE_ARM64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f diff --git a/.gitlab/scripts/build.sh b/.gitlab/scripts/build.sh index f295e7f4fe..94bd6e8b74 100755 --- a/.gitlab/scripts/build.sh +++ b/.gitlab/scripts/build.sh @@ -60,3 +60,17 @@ set -x mkdir -p "${REPO_ROOT}/libs" cp -r "${REPO_ROOT}/ddprof-lib/build/native/release/META-INF/native-libs/"* "${REPO_ROOT}/libs/" + +# Assert what the artifact requires from the host that will load it. musl +# targets carry no glibc symbol versions, so the check does not apply to them. +case "${TARGET}" in + *-musl) + echo "Skipping ABI floor check for ${TARGET} (musl)" + ;; + *) + source "${REPO_ROOT}/.gitlab/config.env" + "${HERE}/check-abi-floor.sh" \ + "${REPO_ROOT}/libs/${TARGET}/libjavaProfiler.so" \ + "${SHIPPED_GLIBC_FLOOR}" + ;; +esac diff --git a/.gitlab/scripts/check-abi-floor.sh b/.gitlab/scripts/check-abi-floor.sh new file mode 100755 index 0000000000..f2483243a0 --- /dev/null +++ b/.gitlab/scripts/check-abi-floor.sh @@ -0,0 +1,153 @@ +#! /bin/bash +# +# Checks what the shipped library requires from the host that loads it: +# +# 1. the highest versioned glibc symbol it references, which decides the +# oldest glibc it can run on, and +# 2. the shared libraries it needs at run time. +# +# Both are decided by the build image rather than by anything in the source, so +# either can move without a source change: a newer base image raises the symbol +# versions, and a dropped -static-libstdc++ adds a libstdc++ dependency the host +# JVM's environment may not satisfy. Asserting them where the artifact is built +# keeps the floor a stated contract instead of a property of whichever toolchain +# the pipeline happened to pull. +# +# Usage: check-abi-floor.sh +# e.g. check-abi-floor.sh libs/linux-x64/libjavaProfiler.so 2.27 +# +# Set OBJDUMP to point at a different binutils objdump; the unit tests use it to +# feed canned output in. + +set -eo pipefail + +SO="$1" +FLOOR="$2" +OBJDUMP="${OBJDUMP:-objdump}" + +# Run-time dependencies the shipped library may have. libstdc++ and libgcc are +# deliberately absent: release builds link them statically (see +# ConfigurationPresets.kt), so either one appearing here means the library has +# started needing a C++ runtime from the host. Both loader names are listed +# because one list covers both architectures. +ALLOWED_NEEDED=( + libc.so.6 + libm.so.6 + libdl.so.2 + libpthread.so.0 + librt.so.1 + ld-linux-x86-64.so.2 + ld-linux-aarch64.so.1 +) + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +if [ -z "${SO}" ] || [ -z "${FLOOR}" ]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +[ -f "${SO}" ] || die "no such file: ${SO}" +command -v "${OBJDUMP}" >/dev/null 2>&1 || die "${OBJDUMP} not found" + +# Higher of two GLIBC_x.y.z tags. sort -V compares component-wise and +# numerically, so GLIBC_2.2.5 orders below GLIBC_2.17; a lexical sort reverses +# exactly that pair and would let a real regression through. +higher_of() { + printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1 +} + +# Every comparison below is only as good as `sort -V`. A sort without version +# ordering compares these lexically and inverts exactly this pair, which would +# make the check quietly permissive rather than noisy, so establish that it +# works before trusting it. +if [ "$(higher_of GLIBC_2.2.5 GLIBC_2.17)" != "GLIBC_2.17" ]; then + die "sort here does not order versions (sort -V); cannot compare glibc versions" +fi + +FLOOR_TAG="GLIBC_${FLOOR}" +STATUS=0 + +# Read the tables first, so "objdump could not read this file" stays separable +# from "objdump read it and found nothing" -- the two need different messages, +# and under `set -eo pipefail` a failing stage inside the parse pipeline below +# would otherwise abort the script with no output at all. +DYNSYM=$("${OBJDUMP}" -T "${SO}") || die "${OBJDUMP} -T failed on ${SO}" + +# " " for every undefined versioned reference. Matched on the +# trailing fields rather than by column number: objdump adds a weak-binding +# column for some symbols, which shifts every field to its right. `|| true` +# keeps a no-match grep from ending the script before the check below reports +# it properly. +REFS=$(printf '%s\n' "${DYNSYM}" \ + | grep -F '*UND*' \ + | sed -nE 's/.*[[:space:]](GLIBC_[0-9][0-9.]*)[[:space:]]+([^[:space:]]+)[[:space:]]*$/\1 \2/p' \ + || true) + +if [ -z "${REFS}" ]; then + echo "ERROR: ${SO} references no versioned glibc symbols." >&2 + echo " Every glibc build has some, so this is far more likely to mean the" >&2 + echo " wrong file was passed or ${OBJDUMP} could not read it than that the" >&2 + echo " library is clean. Refusing to report a pass." >&2 + exit 1 +fi + +MAX=$(printf '%s\n' "${REFS}" | awk '{ print $1 }' | sort -u -V | tail -1) + +if [ "$(higher_of "${MAX}" "${FLOOR_TAG}")" != "${FLOOR_TAG}" ]; then + echo "FAIL: ${SO} requires ${MAX}, above the declared floor ${FLOOR_TAG}." >&2 + echo " Raising the floor drops hosts, so either pin these symbols to an" >&2 + echo " older version or raise SHIPPED_GLIBC_FLOOR deliberately:" >&2 + { + printf '%s\n' "${REFS}" | while read -r version symbol; do + if [ "$(higher_of "${version}" "${FLOOR_TAG}")" != "${FLOOR_TAG}" ]; then + echo " ${symbol}@${version}" + fi + done | sort -u + } >&2 + STATUS=1 +else + echo "OK: ${SO} requires at most ${MAX} (floor ${FLOOR_TAG})" +fi + +HEADERS=$("${OBJDUMP}" -p "${SO}") || die "${OBJDUMP} -p failed on ${SO}" +NEEDED=$(printf '%s\n' "${HEADERS}" | awk '$1 == "NEEDED" { print $2 }') + +if [ -z "${NEEDED}" ]; then + echo "ERROR: ${SO} declares no NEEDED entries." >&2 + echo " A dynamically linked library always has some; as above, this points" >&2 + echo " at an unreadable or wrong file rather than at a clean result." >&2 + exit 1 +fi + +UNEXPECTED="" +while read -r lib; do + [ -n "${lib}" ] || continue + found=0 + for allowed in "${ALLOWED_NEEDED[@]}"; do + if [ "${lib}" = "${allowed}" ]; then + found=1 + break + fi + done + [ "${found}" -eq 1 ] || UNEXPECTED="${UNEXPECTED} ${lib}" +done <&2 + for lib in ${UNEXPECTED}; do + echo " ${lib}" >&2 + done + echo " libstdc++/libgcc here means the static-link flags were lost, which" >&2 + echo " makes the library depend on a C++ runtime the host may not have." >&2 + STATUS=1 +else + echo "OK: ${SO} needs only allowed shared libraries" +fi + +exit "${STATUS}" diff --git a/.gitlab/scripts/tests/check_abi_floor_test.sh b/.gitlab/scripts/tests/check_abi_floor_test.sh new file mode 100755 index 0000000000..6a6db91d9b --- /dev/null +++ b/.gitlab/scripts/tests/check_abi_floor_test.sh @@ -0,0 +1,239 @@ +#! /bin/bash +# Minimal, dependency-free unit tests for .gitlab/scripts/check-abi-floor.sh. +# Run with: bash .gitlab/scripts/tests/check_abi_floor_test.sh +# +# The checker reads the artifact through $OBJDUMP, so these tests supply a stub +# that prints canned output. That keeps them runnable without a compiler or a +# real shared object, and lets them cover cases a real artifact cannot easily +# produce (a lost -static-libstdc++, an unreadable file). + +# The run_* helpers are invoked indirectly, as arguments to the assert_* +# helpers, which shellcheck cannot see. +# shellcheck disable=SC2329 + +set -eo pipefail + +HERE=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# Overridable so the checker's guards can be mutation-checked against this suite. +CHECKER="${CHECKER:-${HERE}/../check-abi-floor.sh}" + +FAILED=0 +WORK=$(mktemp -d) +trap 'rm -rf "${WORK}"' EXIT + +# A stub objdump: -T prints ${FAKE_DYNSYM}, -p prints ${FAKE_HEADERS}. +cat > "${WORK}/objdump" <<'STUB' +#! /bin/bash +# FAKE_FAIL makes the stub behave like an objdump that cannot read the file. +[ -n "${FAKE_FAIL}" ] && exit 1 +case "$1" in + -T) cat "${FAKE_DYNSYM}" ;; + -p) cat "${FAKE_HEADERS}" ;; + *) exit 1 ;; +esac +STUB +chmod +x "${WORK}/objdump" + +: > "${WORK}/libjavaProfiler.so" + +# The NEEDED set of a correctly linked release artifact, as measured on the +# shipped x86_64 library. +cat > "${WORK}/headers-good" <<'EOF' +Dynamic Section: + NEEDED libdl.so.2 + NEEDED libpthread.so.0 + NEEDED librt.so.1 + NEEDED libm.so.6 + NEEDED libc.so.6 + NEEDED ld-linux-x86-64.so.2 +EOF + +# The same, plus the dependency a lost -static-libstdc++ would introduce. +cat > "${WORK}/headers-libstdcxx" <<'EOF' +Dynamic Section: + NEEDED libstdc++.so.6 + NEEDED libm.so.6 + NEEDED libc.so.6 + NEEDED ld-linux-x86-64.so.2 +EOF + +: > "${WORK}/headers-empty" + +# Undefined references in objdump -T form. The weak-binding column on the +# getentropy line is deliberate: it shifts the fields to its right, which is +# why the checker matches trailing fields instead of fixed columns. +cat > "${WORK}/dynsym-217" <<'EOF' +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 memcpy +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.17 clock_gettime +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.3 dlopen +EOF + +cat > "${WORK}/dynsym-227" <<'EOF' +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 memcpy +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.27 expf +EOF + +cat > "${WORK}/dynsym-225-227" <<'EOF' +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 memcpy +0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.25 getentropy +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.27 expf +EOF + +# A musl artifact, or an objdump that could not read the file: no versioned +# glibc references at all. +: > "${WORK}/dynsym-empty" + +run_checker() { + local dynsym="$1" headers="$2" floor="$3" + FAKE_DYNSYM="${WORK}/${dynsym}" FAKE_HEADERS="${WORK}/${headers}" \ + OBJDUMP="${WORK}/objdump" \ + "${CHECKER}" "${WORK}/libjavaProfiler.so" "${floor}" > "${WORK}/out" 2>&1 +} + +# Invokes the checker with arguments verbatim, for the argument-handling cases. +run_raw() { + env FAKE_DYNSYM="${WORK}/dynsym-217" FAKE_HEADERS="${WORK}/headers-good" \ + OBJDUMP="${WORK}/objdump" "${CHECKER}" "$@" > "${WORK}/out" 2>&1 +} + +# Asserts an exact exit status, to keep a usage error distinguishable from a +# contract violation. +assert_exit_code() { + local desc="$1" expected="$2"; shift 2 + local actual=0 + "$@" || actual=$? + if [ "${actual}" -eq "${expected}" ]; then + echo "PASS: ${desc}" + else + echo "FAIL: ${desc} — expected exit ${expected}, got ${actual}" + sed 's/^/ /' "${WORK}/out" + FAILED=1 + fi +} + +# Runs the checker with a `sort` that ignores -V, as a version-unaware sort +# would. Shadowing it on PATH is enough: the checker calls sort unqualified. +run_with_lexical_sort() { + local floor="$1" + mkdir -p "${WORK}/lexbin" + cat > "${WORK}/lexbin/sort" <<'LEXSORT' +#! /bin/bash +args=() +for a in "$@"; do + [ "$a" = "-V" ] || args+=("$a") +done +exec /usr/bin/sort "${args[@]}" +LEXSORT + chmod +x "${WORK}/lexbin/sort" + FAKE_DYNSYM="${WORK}/dynsym-217" FAKE_HEADERS="${WORK}/headers-good" \ + OBJDUMP="${WORK}/objdump" PATH="${WORK}/lexbin:${PATH}" \ + "${CHECKER}" "${WORK}/libjavaProfiler.so" "${floor}" > "${WORK}/out" 2>&1 +} + +# Simulates an objdump that cannot read the artifact at all. +run_unreadable() { + local floor="$1" + FAKE_FAIL=1 OBJDUMP="${WORK}/objdump" \ + "${CHECKER}" "${WORK}/libjavaProfiler.so" "${floor}" > "${WORK}/out" 2>&1 +} + +assert_pass() { + local desc="$1"; shift + if "$@"; then + echo "PASS: ${desc}" + else + echo "FAIL: ${desc} — expected the check to pass" + sed 's/^/ /' "${WORK}/out" + FAILED=1 + fi +} + +assert_fail() { + local desc="$1"; shift + if "$@"; then + echo "FAIL: ${desc} — expected the check to fail" + sed 's/^/ /' "${WORK}/out" + FAILED=1 + else + echo "PASS: ${desc}" + fi +} + +assert_output_contains() { + local desc="$1" needle="$2" + if grep -qF "${needle}" "${WORK}/out"; then + echo "PASS: ${desc}" + else + echo "FAIL: ${desc} — output does not mention '${needle}'" + sed 's/^/ /' "${WORK}/out" + FAILED=1 + fi +} + +# --- the glibc symbol floor --- + +# An artifact below the floor is fine; the floor is a ceiling on what may be +# required, not an exact match. +assert_pass "requirement below the floor is accepted" \ + run_checker dynsym-217 headers-good 2.28 + +# Requiring exactly the floor is the expected steady state. +assert_pass "requirement equal to the floor is accepted" \ + run_checker dynsym-227 headers-good 2.27 + +assert_fail "requirement above the floor is rejected" \ + run_checker dynsym-225-227 headers-good 2.17 +# The whole point of the guard is naming what to fix, so the message must carry +# the offending symbols rather than only the version. +assert_output_contains "rejection names the offending symbol" "expf@GLIBC_2.27" +assert_output_contains "rejection names every offending symbol" "getentropy@GLIBC_2.25" + +# Version ordering: GLIBC_2.2.5 must compare below GLIBC_2.17, which a lexical +# sort gets backwards. Both directions are checked, because a reversed +# comparison passes one of them by accident. +assert_pass "2.2.5 does not read as higher than a 2.17 floor" \ + run_checker dynsym-217 headers-good 2.17 +assert_fail "2.17 reads as higher than a 2.2.5 floor" \ + run_checker dynsym-217 headers-good 2.2.5 +assert_output_contains "the 2.17 symbol is named, not the 2.2.5 one" "clock_gettime@GLIBC_2.17" + +# An empty symbol list means the file could not be read or is not a glibc +# build. Reporting a pass there would make the guard silently inoperative, +# which is worse than no guard at all. +assert_fail "no versioned glibc symbols is an error, not a pass" \ + run_checker dynsym-empty headers-good 2.28 +assert_output_contains "the empty-symbol-list error explains itself" \ + "references no versioned glibc symbols" + +# An objdump that cannot read the file must say so, rather than being reported +# as an empty-and-therefore-clean artifact. +assert_fail "an unreadable artifact is rejected" \ + run_unreadable 2.28 +assert_output_contains "the unreadable-artifact error names the cause" "failed on" + +# A `sort` without version ordering (busybox, say) must stop the check rather +# than silently inverting every comparison it makes. +assert_fail "a sort without version ordering is refused" \ + run_with_lexical_sort 2.28 +assert_output_contains "the sort error explains the cause" "does not order versions" + +# --- the runtime dependency allowlist --- + +assert_fail "a libstdc++ dependency is rejected" \ + run_checker dynsym-217 headers-libstdcxx 2.28 +assert_output_contains "rejection names the unexpected library" "libstdc++.so.6" + +assert_fail "no NEEDED entries is an error, not a pass" \ + run_checker dynsym-217 headers-empty 2.28 + +# --- argument handling --- + +assert_fail "a missing shared object is rejected" \ + run_raw "${WORK}/does-not-exist.so" 2.28 +assert_output_contains "the missing-file error names the path" "no such file" + +assert_exit_code "a missing floor argument exits with the usage status" 2 \ + run_raw "${WORK}/libjavaProfiler.so" +assert_output_contains "the usage error prints usage" "usage:" + +exit "${FAILED}" From 8adcd7ef222f862fd921e93ae9a43c9bf9d96053 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 16 Sep 2026 12:45:47 +0200 Subject: [PATCH 7/9] ci: pin expf to its GLIBC_2.17 interface and tighten the floor to 2.25 glibc 2.27 added a faster expf under a new symbol version and made it the default, so building on AlmaLinux 8 made the shipped library require expf@GLIBC_2.27 from source that has not changed. glibcCompat.h selects the GLIBC_2.17 interface explicitly, which is still exported and still maintained; .symver picks which of the versions already in the host's libc is called and bundles nothing, so a host on a newer glibc still runs its own implementation. Measured on the AlmaLinux 8 + gcc-toolset-14 build: expf moves from GLIBC_2.27 to GLIBC_2.17 and the artifact's requirement drops from 2.27 to 2.25, so the floor follows to 2.25. The remaining above-2.17 symbol is getentropy, which libstdc++ references from its own std::random_device and which glibc exports in only one version -- no pin can reach it, so 2.17 waits on removing the std::random_device uses. The pin is guarded to glibc: musl has no symbol versioning and a .symver naming a GLIBC_* version fails to link there, which would have broken both musl targets. Verified by linking and running a TU that reaches expf through poissonSampler.h under musl, where it emits no GLIBC_ references at all. The floor check could not read the artifact the pin produces. objdump parenthesises a binding to a non-default version, which is exactly what .symver creates, and the parser matched only the bare form -- so it found no versioned references on a pinned library. The empty-result guard turned that into a loud failure rather than the silent pass it would otherwise have been, which is what it exists for. The parser now accepts both forms, with fixtures for a pinned artifact at and above the floor. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/config.env | 8 +++- .gitlab/scripts/check-abi-floor.sh | 14 ++++-- .gitlab/scripts/tests/check_abi_floor_test.sh | 27 +++++++++++ ddprof-lib/src/main/cpp/glibcCompat.h | 47 +++++++++++++++++++ ddprof-lib/src/main/cpp/poissonSampler.h | 2 + 5 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 ddprof-lib/src/main/cpp/glibcCompat.h diff --git a/.gitlab/config.env b/.gitlab/config.env index cb2a93a8a9..4ccdf6d0ed 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -31,7 +31,13 @@ BASE_IMAGE_GLIBC=almalinux:8@sha256:9f355ae942d6a6c0561f0771dc053a2cfae9580fc45f # job. It is the oldest glibc a customer can load the library on, so raising it # drops hosts and is a deliberate decision rather than a consequence of changing # the image above. -SHIPPED_GLIBC_FLOOR=2.27 +# +# 2.25 is what the artifact requires today, via getentropy. That reference comes +# from libstdc++'s std::random_device rather than from this source, so it cannot +# be pinned the way expf is (glibc exports only one version of getentropy); +# removing the std::random_device uses is what drops it, and the floor goes to +# 2.17 once that lands. +SHIPPED_GLIBC_FLOOR=2.25 # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_AMD64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f OPENJDK_BASE_IMAGE_ARM64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f diff --git a/.gitlab/scripts/check-abi-floor.sh b/.gitlab/scripts/check-abi-floor.sh index f2483243a0..021cbade5c 100755 --- a/.gitlab/scripts/check-abi-floor.sh +++ b/.gitlab/scripts/check-abi-floor.sh @@ -79,12 +79,18 @@ DYNSYM=$("${OBJDUMP}" -T "${SO}") || die "${OBJDUMP} -T failed on ${SO}" # " " for every undefined versioned reference. Matched on the # trailing fields rather than by column number: objdump adds a weak-binding -# column for some symbols, which shifts every field to its right. `|| true` -# keeps a no-match grep from ending the script before the check below reports -# it properly. +# column for some symbols, which shifts every field to its right. +# +# The version is parenthesised when the reference is not to the default version +# -- which is exactly what a .symver pin produces (see glibcCompat.h) -- and +# bare otherwise, so both forms have to parse. Reading only the bare form makes +# a pinned artifact look like it references no versioned symbols at all. +# +# `|| true` keeps a no-match grep from ending the script before the check below +# reports it properly. REFS=$(printf '%s\n' "${DYNSYM}" \ | grep -F '*UND*' \ - | sed -nE 's/.*[[:space:]](GLIBC_[0-9][0-9.]*)[[:space:]]+([^[:space:]]+)[[:space:]]*$/\1 \2/p' \ + | sed -nE 's/.*[[:space:]]\(?(GLIBC_[0-9][0-9.]*)\)?[[:space:]]+([^[:space:]]+)[[:space:]]*$/\1 \2/p' \ || true) if [ -z "${REFS}" ]; then diff --git a/.gitlab/scripts/tests/check_abi_floor_test.sh b/.gitlab/scripts/tests/check_abi_floor_test.sh index 6a6db91d9b..1e4cdfe6c2 100755 --- a/.gitlab/scripts/tests/check_abi_floor_test.sh +++ b/.gitlab/scripts/tests/check_abi_floor_test.sh @@ -79,6 +79,22 @@ cat > "${WORK}/dynsym-225-227" <<'EOF' 0000000000000000 DF *UND* 0000000000000000 GLIBC_2.27 expf EOF +# What a .symver-pinned artifact looks like: objdump parenthesises a binding to +# a non-default version, so both the pinned symbol and anything else bound +# off-default appear in brackets. Reading only the bare form reports this file +# as having no versioned references at all. +cat > "${WORK}/dynsym-pinned" <<'EOF' +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 memcpy +0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.17) expf +0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.25) getentropy +EOF + +# The same artifact once the above-floor symbol is gone. +cat > "${WORK}/dynsym-pinned-clean" <<'EOF' +0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 memcpy +0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.17) expf +EOF + # A musl artifact, or an objdump that could not read the file: no versioned # glibc references at all. : > "${WORK}/dynsym-empty" @@ -197,6 +213,17 @@ assert_fail "2.17 reads as higher than a 2.2.5 floor" \ run_checker dynsym-217 headers-good 2.2.5 assert_output_contains "the 2.17 symbol is named, not the 2.2.5 one" "clock_gettime@GLIBC_2.17" +# Parenthesised versions are what the shipped artifact actually contains once +# expf is pinned, so they must parse like bare ones. Checked in both +# directions: that the pinned symbol is not mistaken for unversioned, and that +# a parenthesised symbol above the floor is still caught. +assert_pass "a pinned artifact at the floor is accepted" \ + run_checker dynsym-pinned-clean headers-good 2.17 +assert_fail "a parenthesised version above the floor is rejected" \ + run_checker dynsym-pinned headers-good 2.17 +assert_output_contains "the parenthesised offender is named" "getentropy@GLIBC_2.25" +assert_output_contains "the parenthesised max is reported" "GLIBC_2.25" + # An empty symbol list means the file could not be read or is not a glibc # build. Reporting a pass there would make the guard silently inoperative, # which is worse than no guard at all. diff --git a/ddprof-lib/src/main/cpp/glibcCompat.h b/ddprof-lib/src/main/cpp/glibcCompat.h new file mode 100644 index 0000000000..6f70b9d537 --- /dev/null +++ b/ddprof-lib/src/main/cpp/glibcCompat.h @@ -0,0 +1,47 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _GLIBCCOMPAT_H +#define _GLIBCCOMPAT_H + +// Pins glibc functions to the oldest symbol version the shipped library +// supports, so the glibc of whatever image builds it does not decide the oldest +// host it can load on. +// +// When glibc changes a function's implementation it keeps the previous one under +// its old symbol version and makes the new one the default, so a build picks up +// the newest version available in its own libc and the artifact then requires +// that version at load time. .symver selects the interface explicitly instead. +// It chooses which of the versions already in the host's libc is called -- it +// does not bundle an implementation, so a host running a newer glibc still +// executes that glibc's code for the pinned symbol. +// +// expf is the only one needed today: glibc 2.27 added a faster implementation +// under GLIBC_2.27 and made it the default, so building on 2.28 requires +// expf@GLIBC_2.27 from source that has not changed. The GLIBC_2.17 interface is +// still exported and still maintained. +// +// Guarded to glibc: musl has no symbol versioning at all, and a .symver naming +// a GLIBC_* version fails to link there. The directive is a no-op when building +// on a glibc whose default already is the pinned version. +// +// This file exists only to hold the floor down. When the oldest supported host +// moves past these versions, delete it, drop its include from poissonSampler.h, +// and raise SHIPPED_GLIBC_FLOOR in .gitlab/config.env -- the ABI floor check in +// the build job reports what the artifact requires once the pins are gone. +#if defined(__linux__) +// __GLIBC__ is defined by features.h, which arrives with the first libc header +// -- so it must be pulled in explicitly here. Testing __GLIBC__ without it +// leaves the directive silently skipped, which looks identical to a working pin +// until the ABI floor check reports the version it was supposed to hold down. +// musl ships features.h too and defines no __GLIBC__, which is the intent. +#include +#endif + +#if defined(__linux__) && defined(__GLIBC__) +__asm__(".symver expf,expf@GLIBC_2.17"); +#endif + +#endif // _GLIBCCOMPAT_H diff --git a/ddprof-lib/src/main/cpp/poissonSampler.h b/ddprof-lib/src/main/cpp/poissonSampler.h index 27fdf5094f..bd9ac0d477 100644 --- a/ddprof-lib/src/main/cpp/poissonSampler.h +++ b/ddprof-lib/src/main/cpp/poissonSampler.h @@ -7,6 +7,8 @@ #define _POISSONSAMPLER_H #include "arch.h" +// Pins the expf call below to its oldest glibc interface; see glibcCompat.h. +#include "glibcCompat.h" #include "xorshift.h" #include From cfd531237d7410a4541b3fd247763529c4d7e80b Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 16 Sep 2026 13:02:49 +0200 Subject: [PATCH 8/9] ci: pin expf per architecture and run both script suites through bash The expf pin named a version that does not exist on x86_64, so every link against it failed there: nativeSocketSampler.o: undefined reference to `expf@GLIBC_2.17' no symbol version section for versioned symbol `expf@GLIBC_2.17' A symbol's oldest version is the baseline of the glibc release that introduced the port, which differs per architecture: x86_64 has expf@GLIBC_2.2.5 and aarch64 expf@GLIBC_2.17, both alongside the GLIBC_2.27 default added in 2.27. Naming a version the architecture does not export is a link error rather than a fallback, so the pin now selects per architecture and an unlisted one gets no pin at all, leaving the ABI floor check to report what it requires. This took out every x86_64 job that links the library: the two sanitizer gtest builds, and the CodeQL and Analyze jobs, which build linkRelease. Verified by linking and running a translation unit that reaches expf through poissonSampler.h under clang on Debian trixie -- the toolchain the sanitizer jobs use -- on both architectures, and by building the release artifact on AlmaLinux 8 for both: expf binds to each architecture's oldest version, getentropy@GLIBC_2.25 is the only remaining reference above 2.17, and the floor check passes at the declared 2.25 on both. Separately, ci-script-tests invoked the test suites directly. includes_test.sh is not executable and documents being run through bash, so the job failed with exit 126 after its own assertions had all passed. Both suites now run through bash, which leaves the existing file's mode alone. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 4 ++-- ddprof-lib/src/main/cpp/glibcCompat.h | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9aa8e4989b..1eb6db52b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,8 +65,8 @@ jobs: shellcheck \ .gitlab/scripts/check-abi-floor.sh \ .gitlab/scripts/tests/check_abi_floor_test.sh - .gitlab/scripts/tests/check_abi_floor_test.sh - .gitlab/scripts/tests/includes_test.sh + bash .gitlab/scripts/tests/check_abi_floor_test.sh + bash .gitlab/scripts/tests/includes_test.sh check-for-pr: runs-on: ubuntu-latest diff --git a/ddprof-lib/src/main/cpp/glibcCompat.h b/ddprof-lib/src/main/cpp/glibcCompat.h index 6f70b9d537..dfde862922 100644 --- a/ddprof-lib/src/main/cpp/glibcCompat.h +++ b/ddprof-lib/src/main/cpp/glibcCompat.h @@ -20,9 +20,16 @@ // // expf is the only one needed today: glibc 2.27 added a faster implementation // under GLIBC_2.27 and made it the default, so building on 2.28 requires -// expf@GLIBC_2.27 from source that has not changed. The GLIBC_2.17 interface is +// expf@GLIBC_2.27 from source that has not changed. The older interface is // still exported and still maintained. // +// The version to pin to is per-architecture, because a symbol's oldest version +// is the baseline of the glibc release that introduced that port: x86_64 has +// expf@GLIBC_2.2.5, while the aarch64 port starts at 2.17 and so has +// expf@GLIBC_2.17. Naming the wrong one is an undefined reference at link time, +// not a silent fallback. An architecture that is not listed gets no pin, and +// the ABI floor check then reports whatever it does require. +// // Guarded to glibc: musl has no symbol versioning at all, and a .symver naming // a GLIBC_* version fails to link there. The directive is a no-op when building // on a glibc whose default already is the pinned version. @@ -41,7 +48,11 @@ #endif #if defined(__linux__) && defined(__GLIBC__) +#if defined(__x86_64__) +__asm__(".symver expf,expf@GLIBC_2.2.5"); +#elif defined(__aarch64__) __asm__(".symver expf,expf@GLIBC_2.17"); #endif +#endif #endif // _GLIBCCOMPAT_H From 68cf4d854df93176f49b070f217c06d5b5957b70 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 16 Sep 2026 17:42:36 +0200 Subject: [PATCH 9/9] ci: lower the shipped floor to 2.17 now that getentropy is gone #794 removed the std::random_device uses, so libstdc++'s getentropy reference no longer enters the link and the last symbol above 2.17 is gone. With expf already pinned per architecture, the artifact requires nothing newer than the glibc that EL7 ships. Measured on release builds in the AlmaLinux 8 image on both architectures: no references above 2.17 at all, and the floor check passes at 2.17 where it previously named getentropy@GLIBC_2.25. Both artifacts also load on Oracle Linux 7.9 (glibc 2.17) with no unresolved symbol versions, which tests the dynamic linker rather than the symbol table. Co-Authored-By: Claude Opus 5 (1M context) --- .gitlab/config.env | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitlab/config.env b/.gitlab/config.env index 4ccdf6d0ed..551b2078fe 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -32,12 +32,13 @@ BASE_IMAGE_GLIBC=almalinux:8@sha256:9f355ae942d6a6c0561f0771dc053a2cfae9580fc45f # drops hosts and is a deliberate decision rather than a consequence of changing # the image above. # -# 2.25 is what the artifact requires today, via getentropy. That reference comes -# from libstdc++'s std::random_device rather than from this source, so it cannot -# be pinned the way expf is (glibc exports only one version of getentropy); -# removing the std::random_device uses is what drops it, and the floor goes to -# 2.17 once that lands. -SHIPPED_GLIBC_FLOOR=2.25 +# 2.17 is the oldest glibc still in support among the distributions this ships +# to (RHEL 7.9 and its derivatives). Holding it takes two things: expf is pinned +# to each architecture's oldest interface (glibcCompat.h), and nothing here uses +# std::random_device, whose libstdc++ implementation would otherwise pull in +# getentropy@GLIBC_2.25 -- a symbol glibc exports in only one version, so no pin +# could reach it. +SHIPPED_GLIBC_FLOOR=2.17 # eclipse-temurin:21-jdk-alpine = Alpine 3.x musl; arm64 manifest confirmed present. OPENJDK_BASE_IMAGE_AMD64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f OPENJDK_BASE_IMAGE_ARM64_MUSL=eclipse-temurin:21-jdk-alpine@sha256:c98f0d2e171c898bf896dc4166815d28a56d428e218190a1f35cdc7d82efd61f