diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a722be720..dfd47df93 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -40,6 +40,16 @@ jobs: - setup: al2023-x86_64-aws_lc docker-compose-run: "-f docker/docker-compose.al2023.yaml run build" docker-bake-args: "-f docker-compose.al2023.yaml" + # boringssl-static on a current toolchain, and the only CI leg that builds the FIPS + # profile (it needs clang; see docker/Dockerfile.debian13). Neither is a release + # artifact. Before these legs the FIPS profile had no CI at all, so a change to the + # release profiles that was not ported to it went unnoticed until a downstream build. + - setup: debian13-x86_64 + docker-compose-run: "-f docker/docker-compose.debian-13.yaml run build" + docker-bake-args: "-f docker-compose.debian-13.yaml" + - setup: debian13-x86_64-fips + docker-compose-run: "-f docker/docker-compose.debian-13.yaml run build-fips" + docker-bake-args: "-f docker-compose.debian-13.yaml" name: ${{ matrix.setup }} permissions: @@ -219,6 +229,28 @@ jobs: drop-elftools: "0" build-service: runtime-setup run-service: verify + # The Debian 13-built jars: the FIPS artifact is the one that matters (its power-on + # self-test and integrity check run inside an ELF constructor at dlopen, so only a + # real load proves the patchelf'd library is still intact), and the default-profile + # one shows what a modern-gcc build looks like on musl. bare x86_64 only: aarch64 + # would need an arm64 FIPS build leg, and the gcompat/nolibgcc variants add nothing + # the CentOS 6 legs do not already establish. + - setup: alpine-x86_64-fips + os: ubuntu-24.04 + jars: build-debian13-x86_64-fips-jars + variant: bare + extra-pkgs: "" + drop-elftools: "1" + build-service: runtime-setup + run-service: verify + - setup: alpine-x86_64-debian13 + os: ubuntu-24.04 + jars: build-debian13-x86_64-jars + variant: bare + extra-pkgs: "" + drop-elftools: "1" + build-service: runtime-setup + run-service: verify - setup: glibc-control-x86_64 os: ubuntu-24.04 jars: build-centos6-x86_64-jars diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index e34756631..088e44025 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -38,6 +38,16 @@ jobs: - setup: al2023-x86_64-aws_lc docker-compose-run: "-f docker/docker-compose.al2023.yaml run build" docker-bake-args: "-f docker-compose.al2023.yaml" + # boringssl-static on a current toolchain, and the only CI leg that builds the FIPS + # profile (it needs clang; see docker/Dockerfile.debian13). Neither is a release + # artifact. Before these legs the FIPS profile had no CI at all, so a change to the + # release profiles that was not ported to it went unnoticed until a downstream build. + - setup: debian13-x86_64 + docker-compose-run: "-f docker/docker-compose.debian-13.yaml run build" + docker-bake-args: "-f docker-compose.debian-13.yaml" + - setup: debian13-x86_64-fips + docker-compose-run: "-f docker/docker-compose.debian-13.yaml run build-fips" + docker-bake-args: "-f docker-compose.debian-13.yaml" name: ${{ matrix.setup }} permissions: @@ -280,6 +290,28 @@ jobs: drop-elftools: "0" build-service: runtime-setup run-service: verify + # The Debian 13-built jars: the FIPS artifact is the one that matters (its power-on + # self-test and integrity check run inside an ELF constructor at dlopen, so only a + # real load proves the patchelf'd library is still intact), and the default-profile + # one shows what a modern-gcc build looks like on musl. bare x86_64 only: aarch64 + # would need an arm64 FIPS build leg, and the gcompat/nolibgcc variants add nothing + # the CentOS 6 legs do not already establish. + - setup: alpine-x86_64-fips + os: ubuntu-24.04 + jars: build-pr-debian13-x86_64-fips-jars + variant: bare + extra-pkgs: "" + drop-elftools: "1" + build-service: runtime-setup + run-service: verify + - setup: alpine-x86_64-debian13 + os: ubuntu-24.04 + jars: build-pr-debian13-x86_64-jars + variant: bare + extra-pkgs: "" + drop-elftools: "1" + build-service: runtime-setup + run-service: verify # Control: anything failing on Alpine must pass here, or the check is at fault rather # than the artifact. - setup: glibc-control-x86_64 diff --git a/boringssl-static/pom.xml b/boringssl-static/pom.xml index 59c59fde2..d4d9304ab 100644 --- a/boringssl-static/pom.xml +++ b/boringssl-static/pom.xml @@ -116,40 +116,51 @@ fips-boringssl-static - ${project.build.directory}/boringssl-${boringsslBranch}/boringssl + ${project.build.directory}/boringssl-${boringsslFipsBranch}/boringssl ${boringsslCheckoutDir}/build - - 6d503ae1cf8b2e25162435225610b8c1f063d6f4 + + fips-20260721 + b2f6124823a1b1611e66c94dc38a5eb21db0f5c7 true ${boringsslCheckoutDir}/include ${boringsslBuildDir}/ssl;${boringsslBuildDir}/crypto;${boringsslBuildDir}/decrepit ssl.lib;crypto.lib;decrepit.lib ${os.detected.arch} netty-tcnative-boringssl-static-fips + + clang + clang++ - + - com.googlecode.maven-download-plugin - download-maven-plugin - 1.6.8 + maven-scm-plugin - install-fips-boringssl - process-sources + get-fips-boringssl + generate-sources - wget + checkout + + ${boringsslCheckoutDir} + developerConnection + scm:git:${boringsslRepository} + ${boringsslFipsBranch} + branch + true + - - https://commondatastorage.googleapis.com/chromium-boringssl-fips/boringssl-${boringsslBranch}.tar.xz - true - ${project.build.directory}/boringssl-${boringsslBranch} - @@ -177,8 +188,8 @@ ${aprVersion} - ${boringsslBuildNumber} - ${boringsslBranch} + ${boringsslFipsCommitSha} + ${boringsslFipsBranch} true @@ -207,6 +218,12 @@ + + + + + + @@ -234,11 +251,18 @@ + + - - + + @@ -257,8 +281,14 @@ - + + + + + @@ -413,7 +443,10 @@ --libdir=${project.build.directory}/native-build/target/lib CFLAGS=-O3 -Werror -fno-omit-frame-pointer -fvisibility=hidden -Wunused -Wno-unused-value CPPFLAGS=-DHAVE_OPENSSL -I${boringsslCheckoutDir}/include - LDFLAGS=-L${boringsslBuildDir} -lssl -lcrypto -ldecrepit -l:libstdc++.a -l:libgcc.a -l:libgcc_eh.a + + LDFLAGS=-L${boringsslBuildDir} -Wl,--gc-sections -lssl -lcrypto -ldecrepit -l:libstdc++.a -l:libgcc.a -l:libgcc_eh.a diff --git a/docker/Dockerfile.arch b/docker/Dockerfile.arch index a4a574fc0..0ab3369c9 100644 --- a/docker/Dockerfile.arch +++ b/docker/Dockerfile.arch @@ -23,6 +23,7 @@ RUN pacman -Sy --noconfirm --needed \ lsb-release \ make \ ninja \ + patchelf \ perl \ tar \ unzip \ diff --git a/docker/Dockerfile.debian13 b/docker/Dockerfile.debian13 new file mode 100644 index 000000000..a46a45e2a --- /dev/null +++ b/docker/Dockerfile.debian13 @@ -0,0 +1,82 @@ +# Everything in this image is pinned: the base by digest, the Debian archive by a +# snapshot.debian.org timestamp, clang by version, Go by version and checksum. To refresh it, +# bump the four values below together and rebuild. +ARG debian_image=debian:13.7@sha256:9cc080028c43b27d2074d63a5f9caf7166d731494965616c1a6d2827a004585c +FROM $debian_image +ARG debian_snapshot=20260915T000000Z +ARG clang_version=19 +ARG go_version=1.27.1 +ARG go_sha256_amd64=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 +ARG go_sha256_arm64=3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec +ENV DEBIAN_FRONTEND noninteractive + +# A modern glibc builder for boringssl-static, and the only in-tree image that can build the +# fips-boringssl-static profile, which compiles BoringSSL with clang. Google's FIPS.md asks for +# recent stable Clang, Go, Ninja and CMake: clang, ninja and cmake are Debian 13's own. Go is +# not: BoringSSL's go.mod floor (1.25.8 on the pinned fips-20260721) is ahead of trixie's 1.24, +# so it comes from go.dev, checksum-verified. patchelf 0.18 has --remove-needed; APR's +# buildconf wants the `libtool` script, which is in libtool-bin. +# +# No JDK 8 in trixie: the build runs on JDK 21. The pom compiles with --release 8, so the +# class files are still Java 8. +# +# Not pinned to linux/amd64 like the older images, so it can also be built natively on an +# arm64 host for a quick local run. CI builds it on amd64 runners. +# +# Unlike the CentOS 6 image this is NOT a release builder: its artifact needs a newer glibc than +# the release ones. It exists to give the boringssl-static and FIPS builds CI coverage on a +# current toolchain. + +# Freeze the archive. snapshot.debian.org serves the archive as it was at that instant, so the +# same package versions install no matter when the image is built; Valid-Until has long passed +# by then, hence check-valid-until=no. +RUN rm -f /etc/apt/sources.list.d/debian.sources \ + && echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/$debian_snapshot trixie main" > /etc/apt/sources.list \ + && echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/$debian_snapshot trixie-updates main" >> /etc/apt/sources.list \ + && echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/$debian_snapshot trixie-security main" >> /etc/apt/sources.list + +RUN apt-get update && apt-get install -y --no-install-recommends \ + autoconf \ + automake \ + bzip2 \ + ca-certificates \ + clang-$clang_version \ + cmake \ + curl \ + g++ \ + gcc \ + git \ + gnupg \ + libapr1-dev \ + libtool \ + libtool-bin \ + make \ + ninja-build \ + openjdk-21-jdk-headless \ + patch \ + patchelf \ + perl \ + pkg-config \ + tar \ + unzip \ + wget \ + xz-utils \ + zip \ + && rm -rf /var/lib/apt/lists/* \ + && ln -s clang-$clang_version /usr/bin/clang && ln -s clang++-$clang_version /usr/bin/clang++ + +# dpkg's amd64/arm64 spelling matches go.dev's and the JVM directory name. +RUN ARCH=$(dpkg --print-architecture) \ + && case $ARCH in amd64) GO_SHA256=$go_sha256_amd64;; arm64) GO_SHA256=$go_sha256_arm64;; esac \ + && wget -q https://go.dev/dl/go$go_version.linux-$ARCH.tar.gz \ + && echo "$GO_SHA256 go$go_version.linux-$ARCH.tar.gz" | sha256sum -c - \ + && tar -C /opt -xzf go$go_version.linux-$ARCH.tar.gz && rm go$go_version.linux-$ARCH.tar.gz \ + && ln -s /opt/go/bin/go /usr/local/bin/go && ln -s /opt/go/bin/gofmt /usr/local/bin/gofmt \ + && ln -s /usr/lib/jvm/java-21-openjdk-$ARCH /usr/lib/jvm/java-21 \ + && go version && clang --version && ninja --version && cmake --version +ENV JAVA_HOME /usr/lib/jvm/java-21 +# Use exactly the pinned Go; never let it fetch another toolchain. +ENV GOTOOLCHAIN local + +# /code is a bind mount owned by the host user; newer git refuses to touch it otherwise. +RUN git config --global --add safe.directory '*' diff --git a/docker/Dockerfile.opensuse b/docker/Dockerfile.opensuse index e0e6e2033..15456de7d 100644 --- a/docker/Dockerfile.opensuse +++ b/docker/Dockerfile.opensuse @@ -28,6 +28,7 @@ RUN zypper install --force-resolution --no-recommends --no-confirm \ make \ ninja \ patch \ + patchelf \ perl \ tar \ unzip \ diff --git a/docker/README.md b/docker/README.md index 771d87ab6..09321d1cd 100644 --- a/docker/README.md +++ b/docker/README.md @@ -39,6 +39,20 @@ docker compose -f docker/docker-compose.opensuse.yaml -f docker/docker-compose.o docker compose -f docker/docker-compose.centos-7.yaml run cross-compile-aarch64-build ``` +## Debian 13 with java 21: boringssl-static on a current toolchain, and the FIPS profile + +Not a release builder: its artifacts need a newer glibc than the release ones. It is the one +image that can build the `fips-boringssl-static` profile, which compiles BoringSSL with clang. +Everything is pinned: the base image by digest, the Debian archive by a snapshot.debian.org +timestamp, clang by version, Go by version and checksum. Both services stop at `package`, +which is where the musl compatibility check runs. Not pinned to amd64, so on an arm64 host it +builds natively. + +``` +docker compose -f docker/docker-compose.debian-13.yaml run build +docker compose -f docker/docker-compose.debian-13.yaml run build-fips +``` + etc, etc diff --git a/docker/docker-compose.debian-13.yaml b/docker/docker-compose.debian-13.yaml new file mode 100644 index 000000000..5b8ce56bd --- /dev/null +++ b/docker/docker-compose.debian-13.yaml @@ -0,0 +1,46 @@ +version: "3" + +# Debian 13 builder. Two things run here that no other image covers, see Dockerfile.debian13: +# build boringssl-static (default profile) on a current gcc, so the Linux native +# build is exercised somewhere other than the CentOS 6 release image +# build-fips the fips-boringssl-static profile, which needs clang +# Both stop at `package`, which is where the native-jar musl check runs. + +services: + + runtime-setup: + image: netty-tcnative-debian:13 + build: + context: ../ + dockerfile: docker/Dockerfile.debian13 + cache_from: + - type=registry,ref=ghcr.io/netty/netty-tcnative-build-cache:debian13 + cache_to: + - type=registry,ref=ghcr.io/netty/netty-tcnative-build-cache:debian13,mode=max,ignore-error=true + + common: &common + image: netty-tcnative-debian:13 + depends_on: [runtime-setup] + environment: + - MAVEN_OPTS + volumes: + - ~/.m2/repository:/root/.m2/repository + - ..:/code:delegated + working_dir: /code + + build: + <<: *common + command: /bin/bash -cl "./mvnw -am -pl boringssl-static clean package" + + build-fips: + <<: *common + command: /bin/bash -cl "./mvnw -Pfips-boringssl-static -am -pl boringssl-static clean package" + + shell: + <<: *common + volumes: + - ~/.m2/repository:/root/.m2/repository + - ~/.gitconfig:/root/.gitconfig:delegated + - ~/.gitignore:/root/.gitignore:delegated + - ..:/code:delegated + entrypoint: /bin/bash diff --git a/docs/musl-compatibility.md b/docs/musl-compatibility.md index 0d93ccc82..226caf414 100644 --- a/docs/musl-compatibility.md +++ b/docs/musl-compatibility.md @@ -102,6 +102,9 @@ Error relocating .so: __getauxval: symbol not found | `__isinf`, `__isnan` | APR-era glibc math aliases | no | | `__strdup` | APR | no | | `__pthread_key_create` | APR | no — but it is imported **WEAK**, so it may stay unresolved harmlessly | +| `__libc_single_threaded` | libstdc++ 11+ headers on glibc 2.32+ read this byte to skip atomic refcounting; C++ in BoringSSL imports it as a **data** symbol | no. Defined as `0`, the conservative value | +| `__isoc23_strtol`, `__isoc23_strtoul`, `__isoc23_strtoull` | glibc 2.38+ redirects `strtol` and friends there under `_GNU_SOURCE`, and no `-D` switches it off; from APR, the static `libstdc++.a` and BoringSSL's libcrypto respectively | no. Forwarded to the plain names via asm labels (a literal `strtol()` in the fallback would be redirected too and recurse on musl) | +| `_dl_find_object` | `libgcc_eh.a` from gcc 12 on, when built against glibc 2.35+, uses it to find `.eh_frame` while unwinding | no. Stub returns -1 ("not found"); nothing in the artifact throws | ### Class C — Class B inside an ELF init constructor → **JVM crash, not an exception** @@ -220,6 +223,14 @@ The check must go further than loading. Minimum bar, in order of strength: Only (3) would catch a library that loads but whose crypto is broken. +In CI this is the `musl-verify` job. It runs against the CentOS 6 and CentOS 7 release jars on +several Alpine variants, and, bare x86_64 only, against the two Debian 13 jars: the +default-profile one and the **FIPS** one (`debian13-x86_64-fips`, see `docker/Dockerfile.debian13`). +The FIPS leg is the one with no substitute: its power-on self-test and integrity check run in an +ELF constructor during `dlopen`, so only a real load shows that the post-link `patchelf` left the +module intact. Before that leg existed the FIPS profile was built by nobody but downstream users, +and a change made to the release profiles and not ported to it surfaced only there. + Two TLS 1.3 behaviours will make a naive handshake test report false failures: - the client reaches `NOT_HANDSHAKING` while the server still sits in `NEED_UNWRAP` waiting for optional post-handshake traffic — treat an idle `NEED_UNWRAP` as settled; @@ -241,6 +252,7 @@ protocol/cipher against a released version. It must be identical. | `boringssl-static/pom.xml`, antrun `native-jar` target | post-link `patchelf --remove-needed ld-linux-*` (Class A). Present in the FIPS profile and both release profiles: `fips-boringssl-static`, `boringssl-static-default` (x86_64), and `linux-aarch64`. | | `docker/Dockerfile.centos6` | installs `patchelf` from the upstream prebuilt **static** binary — CentOS 6 is EOL with no EPEL, and `objcopy` cannot remove a `DT_NEEDED`. Needs `--no-check-certificate`, same as the OpenSSL download: the CA bundle cannot verify modern GitHub TLS. | | `docker/Dockerfile.cross_compile_aarch64` | installs `patchelf` from EPEL 7 (available there, unlike CentOS 6) | +| `docker/Dockerfile.arch`, `docker/Dockerfile.opensuse` | also install `patchelf`, from the distro. Their `build` compose service runs a module-unfiltered `./mvnw clean package`, which builds `boringssl-static` and so hits the `patchelf` exec (`failonerror="true"`). CI does not run them, so a missing binary there is invisible to CI. The Debian 7 image has none: wheezy packages no `patchelf`, and its GCC 4.9 cannot build BoringSSL anyway, so only its dynamic-only service is usable. | Note the FIPS profile and two release profiles duplicate the whole native build, so **a change to one does not apply to the others**. `linux-aarch64` cross-compiles from an x86_64 host; patchelf @@ -437,6 +449,9 @@ Other notes: *before* hawtjni. Use the `native-jar` target (phase `package`) or `process-classes`. - Ant's `` does not echo silent commands, so absence of `strip`/`patchelf` output in the log does **not** mean they did not run. Verify on the artifact instead. +- The FIPS profile links with `-Wl,--gc-sections`. Its newer BoringSSL drags in libstdc++'s + `std::random_device`, which nothing calls and which imports `arc4random` (glibc 2.36+, absent + from musl). Dropping the dead code removes the import, so no fallback is needed for it. - Link flags are set per profile and are duplicated: the x86_64 default profile sets `hawtjniLdflags` in the `ldflags-setup` antrun execution, while the FIPS and `linux-aarch64` profiles hardcode `LDFLAGS` in their hawtjni `configureArgs`. Changing one does not change the diff --git a/openssl-dynamic/src/main/c/musl_compat.c b/openssl-dynamic/src/main/c/musl_compat.c index 08c04328b..40df4f907 100644 --- a/openssl-dynamic/src/main/c/musl_compat.c +++ b/openssl-dynamic/src/main/c/musl_compat.c @@ -147,4 +147,55 @@ TCN_MUSL_COMPAT char *__strdup(const char *str) { return strdup(str); } +/* + * glibc 2.38 made strtol and friends C23-conformant under a new symbol version, and redirects + * every call to an __isoc23_* name whenever _GNU_SOURCE is defined, whatever -std says; no -D + * switches it off. Built on such a glibc, APR imports __isoc23_strtol, the static libstdc++ + * __isoc23_strtoul and BoringSSL's libcrypto __isoc23_strtoull. musl exports only the plain + * names. + * + * The bodies must call the PLAIN symbols. A literal strtol() here is subject to the same + * redirect, so on musl it would resolve to this very function and recurse. The asm labels + * bind each reference to the unversioned name, which both libcs export. + * + * __restrict, not restrict: the Debian 7 image compiles with GCC 4.9, whose default is gnu90, + * where `restrict` is not a keyword. + */ +extern long tcn_plain_strtol(const char *, char **, int) __asm__("strtol"); +extern unsigned long tcn_plain_strtoul(const char *, char **, int) __asm__("strtoul"); +extern unsigned long long tcn_plain_strtoull(const char *, char **, int) __asm__("strtoull"); + +TCN_MUSL_COMPAT long __isoc23_strtol(const char *__restrict nptr, char **__restrict endptr, int base) { + return tcn_plain_strtol(nptr, endptr, base); +} + +TCN_MUSL_COMPAT unsigned long __isoc23_strtoul(const char *__restrict nptr, char **__restrict endptr, int base) { + return tcn_plain_strtoul(nptr, endptr, base); +} + +TCN_MUSL_COMPAT unsigned long long __isoc23_strtoull(const char *__restrict nptr, char **__restrict endptr, int base) { + return tcn_plain_strtoull(nptr, endptr, base); +} + +/* + * glibc 2.32+ exports this byte, and libstdc++ reads it (ext/atomicity.h) to skip atomic + * reference counting while a process is still single-threaded. The static libstdc++ imports + * it as a plain data symbol; musl has no such thing. Zero is the conservative value: "not + * single-threaded", so the atomic path is always taken, which is also the truth inside a JVM. + */ +TCN_MUSL_COMPAT char __libc_single_threaded = 0; + +/* + * glibc 2.35 added _dl_find_object, and libgcc_eh.a from gcc 12 on calls it to locate a + * frame's .eh_frame when unwinding. musl has no equivalent. Returning -1 means "no object + * found": a C++ exception would then terminate instead of propagating, and nothing in this + * library lets one escape. Declared with void * on purpose: the real struct only exists in + * glibc >= 2.35 headers and the release image is glibc 2.12. + */ +TCN_MUSL_COMPAT int _dl_find_object(void *address, void *result) { + (void) address; + (void) result; + return -1; +} + #endif /* __linux__ */ diff --git a/scripts/check_musl_compat.sh b/scripts/check_musl_compat.sh index 0e63aa010..2995064d1 100755 --- a/scripts/check_musl_compat.sh +++ b/scripts/check_musl_compat.sh @@ -97,11 +97,13 @@ __ctype_b_loc __ctype_get_mb_cur_max __ctype_tolower_loc __ctype_toupper_loc __errno_location __fpclassify __fpclassifyf __fpclassifyl __h_errno_location __libc_start_main __signbit __signbitf __signbitl __stack_chk_fail __stack_chk_guard __tls_get_addr +__isoc99_fscanf __isoc99_fwscanf __isoc99_scanf __isoc99_sscanf __isoc99_swscanf __isoc99_vfscanf +__isoc99_vfwscanf __isoc99_vscanf __isoc99_vsscanf __isoc99_vswscanf __isoc99_vwscanf __isoc99_wscanf ' # The fallbacks openssl-dynamic/src/main/c/musl_compat.c defines. Asserting these are *defined* # rather than undefined catches the compatibility file being dropped or excluded from the link. -MUSL_COMPAT_SYMS='__getauxval fopen64 __isinf __isnan __strdup' +MUSL_COMPAT_SYMS='__getauxval fopen64 __isinf __isnan __strdup __libc_single_threaded __isoc23_strtol __isoc23_strtoul __isoc23_strtoull _dl_find_object' rc=0 MACHINE=$("$READELF" -h "$SO" 2>/dev/null | sed -n 's/.*Machine:[[:space:]]*//p')