diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20ba736a96..1eb6db52b1 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 + bash .gitlab/scripts/tests/check_abi_floor_test.sh + bash .gitlab/scripts/tests/includes_test.sh + check-for-pr: runs-on: ubuntu-latest outputs: diff --git a/.gitlab/base/Dockerfile b/.gitlab/base/Dockerfile index 57bca1e809..e3ec03c998 100644 --- a/.gitlab/base/Dockerfile +++ b/.gitlab/base/Dockerfile @@ -1,11 +1,27 @@ -ARG BASE_IMAGE=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b +# CI always passes BASE_IMAGE explicitly (OPENJDK_BASE_IMAGE / +# 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 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. +# +# 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 \ + 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/base/centos7/Dockerfile b/.gitlab/base/centos7/Dockerfile deleted file mode 100644 index 004a00128b..0000000000 --- a/.gitlab/base/centos7/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -ARG BASE_IMAGE=openjdk:11-slim-buster -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/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 ed27bff0cd..fdb5dab756 100644 --- a/.gitlab/build-deploy/.gitlab-ci.yml +++ b/.gitlab/build-deploy/.gitlab-ci.yml @@ -1,18 +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 - # 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 - # 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 @@ -92,8 +94,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 +107,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 +127,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 +150,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 +183,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 5bbf7ad760..3774fa71ec 100644 --- a/.gitlab/build-deploy/images.yml +++ b/.gitlab/build-deploy/images.yml @@ -2,14 +2,23 @@ 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 - OPENJDK_BASE_IMAGE_ARM64: 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). + # + # 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 + 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 508032d975..551b2078fe 100644 --- a/.gitlab/config.env +++ b/.gitlab/config.env @@ -10,11 +10,35 @@ 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 -OPENJDK_BASE_IMAGE_ARM64=debian:bullseye-slim@sha256:0083feb8da4f624e3a0245e7752af2517d4b81d8b8db50c725644672a132a31b +# All pinned by digest for reproducibility; multi-arch manifests cover amd64 +# and arm64. JDK 21 is installed at image-build time via SDKMAN. +# +# 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 +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 +# 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. +# +# 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 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 - | 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..021cbade5c --- /dev/null +++ b/.gitlab/scripts/check-abi-floor.sh @@ -0,0 +1,159 @@ +#! /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. +# +# 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' \ + || 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/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" 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..1e4cdfe6c2 --- /dev/null +++ b/.gitlab/scripts/tests/check_abi_floor_test.sh @@ -0,0 +1,266 @@ +#! /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 + +# 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" + +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" + +# 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. +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}" diff --git a/ddprof-lib/src/main/cpp/glibcCompat.h b/ddprof-lib/src/main/cpp/glibcCompat.h new file mode 100644 index 0000000000..dfde862922 --- /dev/null +++ b/ddprof-lib/src/main/cpp/glibcCompat.h @@ -0,0 +1,58 @@ +/* + * 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 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. +// +// 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__) +#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 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