Skip to content

Commit 8d80db5

Browse files
committed
Stop committing linux-x86-64 .so; guard the GLIBC floor in CI
The committed linux-x86-64/libquestdb.so had regressed to a GLIBC_2.33 load floor (stat@GLIBC_2.33 / fstat@GLIBC_2.33), up from the intended 2.14, making it fail to load on RHEL/Rocky/Alma 8, Ubuntu 20.04, Amazon Linux 2, Debian 10 and RHEL 7. It was also the last committed client binary, contradicting the "no longer committed" CI comment. - Delete the committed linux-x86-64 .so. The release and CI jobs build the native libs from source in low-glibc containers, and Os.java loads them via the prd/bin-local paths, so nothing depends on the committed file. - Add .github/scripts/check-glibc-floor.sh: extracts every versioned GLIBC import via objdump and fails if the highest node exceeds the allowed floor, naming the offending symbols. - ci.yml: new glibc-floor matrix guard that rebuilds the linux libs in the same manylinux_2_28 containers as release and asserts the floor (2.14 on x86-64, 2.17 -- the lowest glibc offers -- on aarch64). The build-jdk8 job builds on ubuntu-latest for functional tests only and cannot catch a floor regression. - maven_central_release.yml: assert the floor in the linux smoke-tests so a regressed library can never be shipped. - rebuild_native_libs.yml: assert the floor before committing so a regressed library can never be committed.
1 parent 3a33cf2 commit 8d80db5

5 files changed

Lines changed: 177 additions & 3 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# Assert the glibc runtime floor of a Linux native library.
3+
#
4+
# Usage: check-glibc-floor.sh <path-to-libquestdb.so> <max-glibc-version>
5+
# e.g. check-glibc-floor.sh core/.../linux-x86-64/libquestdb.so 2.14
6+
# check-glibc-floor.sh core/.../linux-aarch64/libquestdb.so 2.17
7+
#
8+
# The dynamic linker resolves .gnu.version_r at load time, so the HIGHEST
9+
# GLIBC_x.y version node the library imports is its hard load floor: a host
10+
# whose glibc is older than that node fails System.loadLibrary/dlopen with
11+
# `version 'GLIBC_x.y' not found`. This script extracts every versioned import
12+
# and fails if the highest one exceeds the allowed floor.
13+
#
14+
# Why the floors are what they are:
15+
# * linux-x86-64 -> 2.14. The oldest node we intentionally keep is
16+
# memcpy@GLIBC_2.14; clock_gettime is pinned back to GLIBC_2.2.5 by
17+
# src/main/c/share/glibc_compat.h, and stat/fstat resolve to the inline
18+
# __xstat/__fxstat@GLIBC_2.2.5 wrappers when built in a low-glibc container.
19+
# A build on a modern host (glibc >= 2.33) instead emits stat@GLIBC_2.33 /
20+
# fstat@GLIBC_2.33 and trips this guard -- that is exactly the regression it
21+
# exists to catch.
22+
# * linux-aarch64 -> 2.17. glibc gained aarch64 support in 2.17, so 2.17 is
23+
# the lowest floor physically achievable on that architecture.
24+
#
25+
# Portable to bash 3.2 (no mapfile / no negative array indices) so it can be run
26+
# locally on macOS as well as in the glibc build containers.
27+
set -euo pipefail
28+
29+
lib="${1:?usage: check-glibc-floor.sh <lib.so> <max-glibc-version>}"
30+
floor="${2:?usage: check-glibc-floor.sh <lib.so> <max-glibc-version>}"
31+
32+
if [ ! -f "$lib" ]; then
33+
echo "::error::check-glibc-floor: library not found: $lib"
34+
exit 1
35+
fi
36+
37+
# All distinct versioned GLIBC nodes (e.g. 2.14, 2.2.5), sorted ascending.
38+
# objdump prints them as (GLIBC_x.y) or GLIBC_x.y depending on the toolchain;
39+
# the -o regex captures the token regardless of surrounding parentheses.
40+
# GLIBC_PRIVATE has no digit after the underscore, so it is naturally excluded.
41+
versions="$(
42+
objdump -T "$lib" \
43+
| grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' \
44+
| sed 's/^GLIBC_//' \
45+
| sort -Vu
46+
)"
47+
48+
if [ -z "$versions" ]; then
49+
echo "::error::check-glibc-floor: no versioned GLIBC symbols found in $lib (unexpected)."
50+
exit 1
51+
fi
52+
53+
highest="$(printf '%s\n' "$versions" | tail -n1)"
54+
55+
echo "GLIBC version nodes required by $lib:"
56+
printf '%s\n' "$versions" | sed 's/^/ GLIBC_/'
57+
echo "Highest required: GLIBC_${highest} (allowed floor: GLIBC_${floor})"
58+
59+
# leq A B -> succeeds when version A <= version B: sorting {A, B} with -V puts B
60+
# last, or they are equal.
61+
leq() {
62+
[ "$1" = "$2" ] && return 0
63+
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$2" ]
64+
}
65+
66+
if leq "$highest" "$floor"; then
67+
echo "OK: $lib floor is GLIBC_${highest} (<= GLIBC_${floor})."
68+
exit 0
69+
fi
70+
71+
echo "::error::GLIBC floor regression in $lib: requires GLIBC_${highest}, above the GLIBC_${floor} floor."
72+
echo "::error::This library will fail to load on hosts with glibc < ${highest}."
73+
echo "Offending nodes above the floor and the symbols that pull them in:"
74+
printf '%s\n' "$versions" | while IFS= read -r v; do
75+
if ! leq "$v" "$floor"; then
76+
echo " GLIBC_${v}:"
77+
objdump -T "$lib" | grep -E "GLIBC_${v//./\\.}([^0-9]|\$)" | awk '{print " " $NF}' | sort -u
78+
fi
79+
done
80+
exit 1

.github/workflows/ci.yml

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ jobs:
4444

4545
- name: Build native libquestdb.so
4646
# JAVA_HOME points at the JDK 8 above, so the lib is compiled against the
47-
# Java 8 JNI headers -- the artifact's floor. Copy it into src resources
48-
# (not target/) so it survives the `mvn clean` in the next step and gets
49-
# packaged + loaded via the production bin/<platform> path.
47+
# Java 8 JNI headers -- the artifact's Java floor. Copy it into src
48+
# resources (not target/) so it survives the `mvn clean` in the next step
49+
# and gets packaged + loaded via the production bin/<platform> path.
50+
# NOTE: this builds on ubuntu-latest for FUNCTIONAL testing only; the
51+
# library's glibc runtime floor is validated separately by the
52+
# `glibc-floor` job, which rebuilds in the release low-glibc container.
5053
run: |
5154
cd core
5255
cmake -DCMAKE_BUILD_TYPE=Release -B cmake-build-release -S.
@@ -81,3 +84,80 @@ jobs:
8184

8285
- name: Compile (main + test) and build javadoc (no tests run)
8386
run: mvn -B -ntp -P javadoc -DskipTests clean package
87+
88+
# GLIBC floor guard. The native libraries are built at release time in
89+
# low-glibc manylinux containers (see maven_central_release.yml) and are NOT
90+
# committed, so a floor regression is invisible to the functional test job
91+
# above (it builds on ubuntu-latest, whose glibc is new enough to load almost
92+
# anything). This job rebuilds the linux libraries in the SAME low-glibc
93+
# environment as release and asserts the runtime floor with objdump, so a
94+
# change that raises the floor (e.g. a new stat/fstat call pulling in
95+
# stat@GLIBC_2.33 on a modern build host) fails the PR instead of silently
96+
# shipping a library that cannot load on older distros.
97+
#
98+
# * linux-x86-64 -> GLIBC_2.14 (the intended floor: memcpy@GLIBC_2.14).
99+
# * linux-aarch64 -> GLIBC_2.17 (the lowest floor glibc offers on aarch64).
100+
#
101+
# Uses manylinux_2_28 for both arches (stock Node 24, no glibc-2.17 shadow
102+
# hack). The x86-64 floor is identical in manylinux2014 (2.17) and
103+
# manylinux_2_28 (2.28) -- both resolve stat/fstat to the inline
104+
# __xstat/__fxstat@GLIBC_2.2.5 wrappers -- so this validates the real shipped
105+
# floor without the heavier manylinux2014 release toolchain.
106+
glibc-floor:
107+
name: GLIBC floor guard (${{ matrix.platform }})
108+
strategy:
109+
fail-fast: false
110+
matrix:
111+
include:
112+
- platform: linux-x86-64
113+
os: ubuntu-latest
114+
image: quay.io/pypa/manylinux_2_28_x86_64
115+
jdk_arch: x64
116+
floor: "2.14"
117+
cmake_args: ""
118+
build_dir: cmake-build-release
119+
- platform: linux-aarch64
120+
os: ubuntu-22.04-arm
121+
image: quay.io/pypa/manylinux_2_28_aarch64
122+
jdk_arch: aarch64
123+
floor: "2.17"
124+
cmake_args: "-DCMAKE_TOOLCHAIN_FILE=./src/main/c/toolchains/linux-arm64.cmake"
125+
build_dir: cmake-build-release-arm64
126+
runs-on: ${{ matrix.os }}
127+
timeout-minutes: 45
128+
container:
129+
image: ${{ matrix.image }}
130+
steps:
131+
- name: Check out
132+
uses: actions/checkout@v4
133+
with:
134+
# zstd is required to compile the native library.
135+
submodules: recursive
136+
137+
- name: Install tooling
138+
# binutils provides objdump for the floor check; nasm/zstd are build deps.
139+
run: |
140+
yum update -y
141+
yum install -y wget nasm zstd binutils
142+
143+
- name: Install Temurin JDK 8 (for jni.h)
144+
# Build against the Java 8 JNI headers -- JDK 8 is the artifact's floor.
145+
# The JDK version does not affect the glibc floor; it only supplies jni.h.
146+
run: |
147+
wget -v --timeout=180 -O jdk8.tar.gz \
148+
"https://api.adoptium.net/v3/binary/latest/8/ga/linux/${{ matrix.jdk_arch }}/jdk/hotspot/normal/eclipse"
149+
mkdir jdk8
150+
tar xfz jdk8.tar.gz -C jdk8 --strip-components=1
151+
echo "JAVA_HOME=$(pwd)/jdk8" >> "$GITHUB_ENV"
152+
153+
- name: Build native libquestdb.so
154+
run: |
155+
cd core
156+
cmake ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release -B ${{ matrix.build_dir }} -S.
157+
cmake --build ${{ matrix.build_dir }} --config Release
158+
159+
- name: Assert GLIBC floor
160+
run: |
161+
./.github/scripts/check-glibc-floor.sh \
162+
core/target/classes/io/questdb/client/bin-local/libquestdb.so \
163+
"${{ matrix.floor }}"

.github/workflows/maven_central_release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ jobs:
295295
echo "::error::libquestdb.so has unresolved dependencies."
296296
exit 1
297297
fi
298+
# Refuse to ship if a symbol raised the glibc floor above 2.14.
299+
./.github/scripts/check-glibc-floor.sh "$lib" 2.14
298300
cat > LoadCheck.java <<'EOF'
299301
public class LoadCheck {
300302
public static void main(String[] args) {
@@ -360,6 +362,8 @@ jobs:
360362
echo "::error::libquestdb.so has unresolved dependencies."
361363
exit 1
362364
fi
365+
# 2.17 is the lowest floor glibc offers on aarch64.
366+
./.github/scripts/check-glibc-floor.sh "$lib" 2.17
363367
cat > LoadCheck.java <<'EOF'
364368
public class LoadCheck {
365369
public static void main(String[] args) {

.github/workflows/rebuild_native_libs.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ jobs:
108108
mkdir -p src/main/resources/io/questdb/client/bin/linux-x86-64/
109109
mkdir -p src/main/bin/linux-x86-64/
110110
cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-x86-64/
111+
- name: Assert GLIBC floor (2.14)
112+
# Never commit a library whose glibc floor regressed above 2.14.
113+
run: |
114+
bash ./.github/scripts/check-glibc-floor.sh \
115+
core/src/main/resources/io/questdb/client/bin/linux-x86-64/libquestdb.so 2.14
111116
- name: Save linux-x86-64 Libraries to Cache
112117
uses: actions/cache/save@v3
113118
with:
@@ -143,6 +148,11 @@ jobs:
143148
mkdir -p src/main/resources/io/questdb/client/bin/linux-aarch64/
144149
mkdir -p src/main/bin/linux-aarch64/
145150
cp target/classes/io/questdb/client/bin-local/libquestdb.so src/main/resources/io/questdb/client/bin/linux-aarch64/
151+
- name: Assert GLIBC floor (2.17)
152+
# 2.17 is the lowest floor glibc offers on aarch64.
153+
run: |
154+
bash ./.github/scripts/check-glibc-floor.sh \
155+
core/src/main/resources/io/questdb/client/bin/linux-aarch64/libquestdb.so 2.17
146156
- name: Save linux-aarch64 Libraries to Cache
147157
uses: actions/cache/save@v3
148158
with:
Binary file not shown.

0 commit comments

Comments
 (0)