From ff4b2b6b951ae664a0a252a05f5b1c50d0e39ac4 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 15:46:22 -0700 Subject: [PATCH 1/4] fix(cassandra): refresh vulnerable runtime dependencies Upgrade OpenSSL to at least 3.5.7-1~deb13u2, Jackson to 2.21.4, and Netty to 4.1.136.Final. Keep the downloaded Java artifacts locked to their Maven Central SHA-256 checksums. Signed-off-by: Stephanie Baum --- infra/cassandra/Dockerfile | 32 ++++++++++++++++++++++++++++- infra/cassandra/java-libraries.lock | 21 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 infra/cassandra/java-libraries.lock diff --git a/infra/cassandra/Dockerfile b/infra/cassandra/Dockerfile index f01920f9af..196bddaca4 100644 --- a/infra/cassandra/Dockerfile +++ b/infra/cassandra/Dockerfile @@ -19,7 +19,36 @@ RUN apk add --no-cache curl && \ printf '%s /tmp/yq\n' "${YQ_SHA256}" | sha256sum -c - && \ chmod +x /tmp/yq -FROM cassandra:5.0.9 +# Cassandra 5.0.9 is still the latest supported 5.0 release, but its bundled +# Jackson and Netty versions can lag security-only patch releases. Download a +# checksum-pinned replacement set without adding build tools to the final image. +COPY java-libraries.lock /tmp/java-libraries.lock +RUN mkdir /tmp/java-libraries && \ + while read -r checksum url; do \ + case "${checksum}" in \#*|'') continue ;; esac; \ + destination="/tmp/java-libraries/${url##*/}"; \ + curl -fsSLo "${destination}" "${url}"; \ + printf '%s %s\n' "${checksum}" "${destination}" | sha256sum -c -; \ + done < /tmp/java-libraries.lock + +FROM cassandra:5.0.9@sha256:d35e159439b302146f964919904f84fd3c2cebf347272b8cb8c4368c1cf200e5 + +# Refresh the OpenSSL runtime packages independently of the upstream image +# rebuild cadence. Fail the build if the Debian repository cannot provide the +# first release containing the required fixes. +RUN apt-get update && \ + apt-get install -y --no-install-recommends --only-upgrade \ + libssl3t64 \ + openssl-provider-legacy && \ + dpkg --compare-versions "$(dpkg-query -W -f='${Version}' libssl3t64)" ge 3.5.7-1~deb13u2 && \ + dpkg --compare-versions "$(dpkg-query -W -f='${Version}' openssl-provider-legacy)" ge 3.5.7-1~deb13u2 && \ + rm -rf /var/lib/apt/lists/* && \ + rm -f \ + /opt/cassandra/lib/jackson-annotations-*.jar \ + /opt/cassandra/lib/jackson-core-*.jar \ + /opt/cassandra/lib/jackson-databind-*.jar \ + /opt/cassandra/lib/jackson-datatype-jsr310-*.jar && \ + find /opt/cassandra/lib -maxdepth 1 -type f -name 'netty-*-4.1.130.Final*.jar' -delete # Metrics exporter agent. The OSS snapshot does not redistribute the jar; # only files/.gitkeep ships, so the default here points at that placeholder @@ -40,6 +69,7 @@ ARG EXPORTER_JAVAAGENT="" COPY ${EXPORTER_JAR} /opt/cassandra/lib/cassandra-exporter-agent.jar COPY scripts/cassandra-env.sh /etc/cassandra/cassandra-env.sh COPY --from=yq-downloader /tmp/yq /usr/local/bin/yq +COPY --from=yq-downloader /tmp/java-libraries/*.jar /opt/cassandra/lib/ # load cassandra exporter prom metrics library # The exporter agent reflects into JDK-internal com.sun.jmx classes to diff --git a/infra/cassandra/java-libraries.lock b/infra/cassandra/java-libraries.lock new file mode 100644 index 0000000000..bf58e5d0c7 --- /dev/null +++ b/infra/cassandra/java-libraries.lock @@ -0,0 +1,21 @@ +# SHA-256 and Maven Central URL for Cassandra runtime dependency overlays. +# Jackson annotations follows the upstream family's major.minor versioning. +53ca085f4a150f703f49e1aabd935bd03b43e1ea3d55d135438292af22cef56b https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar +4b40a06396f239f8de2da57419adde6e94e5edc18a2171d471ea05eeed4e5c2d https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-core/2.21.4/jackson-core-2.21.4.jar +3888e9e69ab66fbacaacc9aea0e9ffbf15368288e4aca468b024dba11c09fbf9 https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.21.4/jackson-databind-2.21.4.jar +d1ac4b98b70304e56448423589fde5e775b100889643ad1ead62cc7811633684 https://repo.maven.apache.org/maven2/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.21.4/jackson-datatype-jsr310-2.21.4.jar +75cb7db3492915953aa19c566e477fb371ae2b32aa12bc4fe85bd7a2a6b8f5e8 https://repo.maven.apache.org/maven2/io/netty/netty-all/4.1.136.Final/netty-all-4.1.136.Final.jar +c88f13fc41156fde5df918c7b240038dfbe97ac57576163f208630391789b5d5 https://repo.maven.apache.org/maven2/io/netty/netty-buffer/4.1.136.Final/netty-buffer-4.1.136.Final.jar +2de4fc13005c7740b46427a47ee04265a19779c147d53e583f441889b1148159 https://repo.maven.apache.org/maven2/io/netty/netty-codec/4.1.136.Final/netty-codec-4.1.136.Final.jar +e2f73be7ec359b46583ad875521137000eff520bafd320e730846ec9974f3be6 https://repo.maven.apache.org/maven2/io/netty/netty-common/4.1.136.Final/netty-common-4.1.136.Final.jar +54bb1a59f46a3aefd117942e6acee09e555b756776bad731fc06159d89c2da28 https://repo.maven.apache.org/maven2/io/netty/netty-handler/4.1.136.Final/netty-handler-4.1.136.Final.jar +47400551f0444dea33629ee0c52d4e30856b2ddf00b12adf1881902957e74cf2 https://repo.maven.apache.org/maven2/io/netty/netty-handler-proxy/4.1.136.Final/netty-handler-proxy-4.1.136.Final.jar +d9830d3e64fd5448a67a54deaf8ff1be40d05db1b862f98c90399a947958f249 https://repo.maven.apache.org/maven2/io/netty/netty-handler-ssl-ocsp/4.1.136.Final/netty-handler-ssl-ocsp-4.1.136.Final.jar +e64972fec474f5b9bd086b738154d190a923e82c4923ac550aff4f5ea9e98600 https://repo.maven.apache.org/maven2/io/netty/netty-resolver/4.1.136.Final/netty-resolver-4.1.136.Final.jar +b92881ea925721ed42fee5122a42b8bce4b84da737dbc3ca2d84dfaca52c28b6 https://repo.maven.apache.org/maven2/io/netty/netty-transport/4.1.136.Final/netty-transport-4.1.136.Final.jar +f6a0b631b98667f131daf4ca07a9cae1072d58e259dcbc0f8c6a053d843449c6 https://repo.maven.apache.org/maven2/io/netty/netty-transport-classes-epoll/4.1.136.Final/netty-transport-classes-epoll-4.1.136.Final.jar +4ed7ee5cb8c611e879d0d6621e8a1b4255e7ba5a31e5f7828d635682ef244a20 https://repo.maven.apache.org/maven2/io/netty/netty-transport-classes-kqueue/4.1.136.Final/netty-transport-classes-kqueue-4.1.136.Final.jar +41a30e3e096091695ef7b7cf16c85802bbc05d4be94643ecc5e8a8d9759acb77 https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-epoll/4.1.136.Final/netty-transport-native-epoll-4.1.136.Final.jar +f264cf4bcc1e97ad148cd4f0d8777eacf404cd4ed80830de3f8a2359f60edc38 https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-epoll/4.1.136.Final/netty-transport-native-epoll-4.1.136.Final-linux-aarch_64.jar +c3956f90241582bbbad5612c3159cffeb7c3533324759f3c3bd778ca9b176cae https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-epoll/4.1.136.Final/netty-transport-native-epoll-4.1.136.Final-linux-x86_64.jar +7e014c9b13defd9d254d4e5a5edd8ab6dde17533e1152f99699a6b28b2967a8a https://repo.maven.apache.org/maven2/io/netty/netty-transport-native-unix-common/4.1.136.Final/netty-transport-native-unix-common-4.1.136.Final.jar From bff09b3584b7829e0395c3266106fa14c1cef402 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 15:57:04 -0700 Subject: [PATCH 2/4] fix(cassandra): fail fast on checksum errors Signed-off-by: Stephanie Baum --- infra/cassandra/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/cassandra/Dockerfile b/infra/cassandra/Dockerfile index 196bddaca4..2248ef0478 100644 --- a/infra/cassandra/Dockerfile +++ b/infra/cassandra/Dockerfile @@ -27,8 +27,8 @@ RUN mkdir /tmp/java-libraries && \ while read -r checksum url; do \ case "${checksum}" in \#*|'') continue ;; esac; \ destination="/tmp/java-libraries/${url##*/}"; \ - curl -fsSLo "${destination}" "${url}"; \ - printf '%s %s\n' "${checksum}" "${destination}" | sha256sum -c -; \ + curl -fsSLo "${destination}" "${url}" && \ + printf '%s %s\n' "${checksum}" "${destination}" | sha256sum -c - || exit 1; \ done < /tmp/java-libraries.lock FROM cassandra:5.0.9@sha256:d35e159439b302146f964919904f84fd3c2cebf347272b8cb8c4368c1cf200e5 From a03e602ebf283d3a0cab69847edfe45d2d0a9d25 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sun, 6 Sep 2026 18:15:04 -0700 Subject: [PATCH 3/4] fix(cassandra): repack exporter with patched Netty (#1613) --- infra/cassandra/AGENTS.md | 8 +- infra/cassandra/Dockerfile | 24 ++- infra/cassandra/README.md | 4 +- .../scripts/repack-exporter-netty-test.sh | 89 +++++++++ .../scripts/repack-exporter-netty.sh | 180 ++++++++++++++++++ 5 files changed, 300 insertions(+), 5 deletions(-) create mode 100755 infra/cassandra/scripts/repack-exporter-netty-test.sh create mode 100755 infra/cassandra/scripts/repack-exporter-netty.sh diff --git a/infra/cassandra/AGENTS.md b/infra/cassandra/AGENTS.md index 71269580bb..67e0a9acc5 100644 --- a/infra/cassandra/AGENTS.md +++ b/infra/cassandra/AGENTS.md @@ -17,7 +17,10 @@ Container image that runs Apache Cassandra for NVCF, built on the official metrics agent. To enable metrics on port 9500, supply a jar under `files/` and pass both `--build-arg EXPORTER_JAR=files/` and `--build-arg EXPORTER_JAVAAGENT=-javaagent:/opt/cassandra/lib/cassandra-exporter-agent.jar`. - Both args are required together; either alone runs without metrics. + Both args are required together; the build rejects either mismatched + combination. It replaces the jar's exact shaded Netty module set with + checksum-pinned artifacts through `scripts/repack-exporter-netty.sh` and fails + on an unexpected layout. ## Build @@ -27,6 +30,9 @@ docker build -t nvcf-cassandra:dev infra/cassandra # multi-arch docker buildx build --platform linux/amd64,linux/arm64 -t infra/cassandra + +# exporter dependency unit test (downloads checksum-pinned Netty jars) +infra/cassandra/scripts/repack-exporter-netty-test.sh ``` The `--platform=$BUILDPLATFORM` on the yq stage is intentional: it lets yq diff --git a/infra/cassandra/Dockerfile b/infra/cassandra/Dockerfile index 2248ef0478..36b259489e 100644 --- a/infra/cassandra/Dockerfile +++ b/infra/cassandra/Dockerfile @@ -31,6 +31,26 @@ RUN mkdir /tmp/java-libraries && \ printf '%s %s\n' "${checksum}" "${destination}" | sha256sum -c - || exit 1; \ done < /tmp/java-libraries.lock +FROM --platform=$BUILDPLATFORM alpine:3.21 AS exporter-repacker +RUN apk add --no-cache curl unzip zip + +# The release build supplies a compatible exporter agent. Replace the complete +# shaded Netty module set with checksum-pinned security updates while retaining +# the agent implementation, manifest, and unrelated dependencies. +ARG EXPORTER_JAR=files/.gitkeep +ARG EXPORTER_JAVAAGENT="" +COPY ${EXPORTER_JAR} /tmp/exporter-input.jar +COPY scripts/repack-exporter-netty.sh /usr/local/bin/repack-exporter-netty +RUN if [ -s /tmp/exporter-input.jar ] && [ -z "${EXPORTER_JAVAAGENT}" ]; then \ + echo "EXPORTER_JAR and EXPORTER_JAVAAGENT must be set together" >&2; \ + exit 1; \ + fi && \ + if [ ! -s /tmp/exporter-input.jar ] && [ -n "${EXPORTER_JAVAAGENT}" ]; then \ + echo "EXPORTER_JAR and EXPORTER_JAVAAGENT must be set together" >&2; \ + exit 1; \ + fi && \ + repack-exporter-netty /tmp/exporter-input.jar /tmp/cassandra-exporter-agent.jar + FROM cassandra:5.0.9@sha256:d35e159439b302146f964919904f84fd3c2cebf347272b8cb8c4368c1cf200e5 # Refresh the OpenSSL runtime packages independently of the upstream image @@ -56,8 +76,6 @@ RUN apt-get update && \ # builds override this with --build-arg EXPORTER_JAR=files/ (see # Makefile / .gitlab-ci.yml). To build with the exporter locally, download # the jar into files/ and pass --build-arg EXPORTER_JAR=files/. -ARG EXPORTER_JAR=files/.gitkeep - # The javaagent flag itself is also gated behind a build arg. Without a real # jar, wiring -javaagent unconditionally would point the JVM at the empty # .gitkeep placeholder and abort startup with "Error opening zip file or JAR @@ -66,7 +84,7 @@ ARG EXPORTER_JAR=files/.gitkeep # passes neither, so the image boots with no agent and no metrics. ARG EXPORTER_JAVAAGENT="" -COPY ${EXPORTER_JAR} /opt/cassandra/lib/cassandra-exporter-agent.jar +COPY --from=exporter-repacker /tmp/cassandra-exporter-agent.jar /opt/cassandra/lib/cassandra-exporter-agent.jar COPY scripts/cassandra-env.sh /etc/cassandra/cassandra-env.sh COPY --from=yq-downloader /tmp/yq /usr/local/bin/yq COPY --from=yq-downloader /tmp/java-libraries/*.jar /opt/cassandra/lib/ diff --git a/infra/cassandra/README.md b/infra/cassandra/README.md index b9c851e32c..3c829ccb47 100644 --- a/infra/cassandra/README.md +++ b/infra/cassandra/README.md @@ -32,10 +32,12 @@ docker build \ -t //nvcf-cassandra: . ``` -Supplying only `EXPORTER_JAR` without `EXPORTER_JAVAAGENT` copies the jar into the image but never loads it; the image still runs without metrics. Both build args must be set together to get a working exporter. +Both build args must be set together to get a working exporter. The build rejects either mismatched combination so an exporter jar cannot be silently omitted from the JVM or an empty placeholder loaded as a Java agent. The jar must expose the same metrics interface the image's `JVM_EXTRA_OPTS` javaagent wiring expects (see `Dockerfile`). If you change the jar, update the `EXPORTER_JAR` build-arg accordingly; the source path and filename are entirely up to you. +The build replaces the exporter's complete shaded Netty module set with the checksum-pinned version declared in `scripts/repack-exporter-netty.sh`. It fails if the input jar's Netty module set changes, rather than leaving a partially updated dependency graph in the image. The exporter implementation, Java agent manifest, and unrelated shaded dependencies are preserved. + ## Prerequisites - Docker or another OCI-compatible builder (with `buildx` for multi-arch) diff --git a/infra/cassandra/scripts/repack-exporter-netty-test.sh b/infra/cassandra/scripts/repack-exporter-netty-test.sh new file mode 100755 index 0000000000..b18ed97553 --- /dev/null +++ b/infra/cassandra/scripts/repack-exporter-netty-test.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +work_dir=$(mktemp -d) +trap 'rm -rf "${work_dir}"' EXIT HUP INT TERM + +input_dir=${work_dir}/input +mkdir -p \ + "${input_dir}/META-INF/maven/com.zegelin.cassandra-exporter/agent" \ + "${input_dir}/META-INF/maven/com.zegelin.cassandra-exporter/common" \ + "${input_dir}/com/zegelin/cassandra/exporter" \ + "${input_dir}/io/netty/legacy" + +printf '%s\n' \ + 'Manifest-Version: 1.0' \ + 'Premain-Class: com.zegelin.cassandra.exporter.Agent' \ + > "${input_dir}/META-INF/MANIFEST.MF" +printf '%s\n' 'preserve this exporter payload' \ + > "${input_dir}/com/zegelin/cassandra/exporter/Agent.class" +printf '%s\n' 'remove this old Netty payload' \ + > "${input_dir}/io/netty/legacy/Old.class" + +for project in agent common; do + printf '%s\n' \ + '' \ + ' ' \ + ' io.netty' \ + ' 4.1.135.Final' \ + ' ' \ + '' \ + > "${input_dir}/META-INF/maven/com.zegelin.cassandra-exporter/${project}/pom.xml" +done + +for artifact in \ + netty-buffer \ + netty-codec \ + netty-codec-http \ + netty-common \ + netty-handler \ + netty-resolver \ + netty-transport \ + netty-transport-native-unix-common; do + metadata_dir=${input_dir}/META-INF/maven/io.netty/${artifact} + mkdir -p "${metadata_dir}" + printf '%s\n' "artifactId=${artifact}" 'groupId=io.netty' 'version=4.1.135.Final' \ + > "${metadata_dir}/pom.properties" +done + +input_jar=${work_dir}/input.jar +output_jar=${work_dir}/output.jar +( + cd "${input_dir}" + find . -type f -print | LC_ALL=C sort | zip -Xq "${input_jar}" -@ +) + +"${script_dir}/repack-exporter-netty.sh" "${input_jar}" "${output_jar}" + +original_payload=$(unzip -p "${input_jar}" com/zegelin/cassandra/exporter/Agent.class) +repacked_payload=$(unzip -p "${output_jar}" com/zegelin/cassandra/exporter/Agent.class) +[ "${original_payload}" = "${repacked_payload}" ] + +original_manifest=$(unzip -p "${input_jar}" META-INF/MANIFEST.MF) +repacked_manifest=$(unzip -p "${output_jar}" META-INF/MANIFEST.MF) +[ "${original_manifest}" = "${repacked_manifest}" ] + +if unzip -Z1 "${output_jar}" | grep -q '^io/netty/legacy/Old.class$'; then + echo 'old Netty class remains in the repacked jar' >&2 + exit 1 +fi +unzip -Z1 "${output_jar}" | grep -q '^io/netty/handler/codec/http/HttpRequest.class$' + +module_count=$(unzip -Z1 "${output_jar}" | + sed -n 's#^META-INF/maven/io\.netty/[^/]*/pom\.properties$#module#p' | + wc -l | tr -d ' ') +[ "${module_count}" = 8 ] + +for properties in $(unzip -Z1 "${output_jar}" | grep '^META-INF/maven/io.netty/.*/pom.properties$'); do + [ "$(unzip -p "${output_jar}" "${properties}" | sed -n 's/^version=//p')" = '4.1.137.Final' ] +done + +if unzip -p "${output_jar}" | grep -q '4.1.135.Final'; then + echo 'old Netty version remains in the repacked jar' >&2 + exit 1 +fi +echo 'repack-exporter-netty: PASS' diff --git a/infra/cassandra/scripts/repack-exporter-netty.sh b/infra/cassandra/scripts/repack-exporter-netty.sh new file mode 100755 index 0000000000..2b248c2699 --- /dev/null +++ b/infra/cassandra/scripts/repack-exporter-netty.sh @@ -0,0 +1,180 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +NETTY_VERSION=4.1.137.Final +MAVEN_CENTRAL_URL=https://repo1.maven.org/maven2/io/netty +NETTY_ARTIFACTS='netty-buffer f474b14c7734f15e0540394cb6f39d67777b7581a42919e4ac89d253d4efd929 +netty-codec 9987b6a660b0a6b1f0d791485dae33180b3d1c63687c006fe6d3fd025e9e3798 +netty-codec-http 0535bb5a736472bef5c948d15eb273c4ab9f796656fc7c5d6b982ad92bddbd49 +netty-common d31926b01adcc07af86f5e27b81b6d6c115df17d366e835d1fc3f5a1924e7e52 +netty-handler d0e4c6ee4779f59f6ab2fb5d388e4f57147c82270164b37945764bb9bda96a44 +netty-resolver b4cf2aeedd9fc7c8c439bbfe574f63cfe5b83392e88bbc07ca0e8424b7cff955 +netty-transport 6251adc2a2921572382732a2db188d4f4f2251fd6ebb49c5d44bbf33d6bfb1a7 +netty-transport-native-unix-common 8056e7637f9948f953314894cf995ab664711b66c73c4cd5b593ae84f0b1048c' + +usage() { + echo "usage: $0 " >&2 + exit 2 +} + +[ "$#" -eq 2 ] || usage +input=$1 +output=$2 + +for tool in awk basename cp curl dirname find grep mkdir mktemp mv rm sed sha256sum sort touch unzip zip; do + command -v "${tool}" >/dev/null 2>&1 || { + echo "missing required command: ${tool}" >&2 + exit 1 + } +done + +[ -f "${input}" ] || { + echo "exporter input does not exist: ${input}" >&2 + exit 1 +} + +# The public build uses an empty placeholder when no exporter is supplied. +if [ ! -s "${input}" ]; then + cp "${input}" "${output}" + exit 0 +fi + +unzip -tq "${input}" >/dev/null + +expected_modules=$(printf '%s\n' "${NETTY_ARTIFACTS}" | awk 'NF { print $1 }' | sort) +input_modules=$( + unzip -Z1 "${input}" | + sed -n 's#^META-INF/maven/io\.netty/\([^/]*\)/pom\.properties$#\1#p' | + sort -u +) +[ "${input_modules}" = "${expected_modules}" ] || { + echo "exporter Netty module set changed; refusing a partial replacement" >&2 + echo "expected modules:" >&2 + printf '%s\n' "${expected_modules}" >&2 + echo "input modules:" >&2 + printf '%s\n' "${input_modules}" >&2 + exit 1 +} + +work_dir=$(mktemp -d) +trap 'rm -rf "${work_dir}"' EXIT HUP INT TERM +payload_dir=${work_dir}/payload +downloads_dir=${work_dir}/downloads +mkdir -p "${payload_dir}" "${downloads_dir}" +unzip -q "${input}" -d "${payload_dir}" + +# Remove the complete shaded Netty payload and its dependency metadata. The +# module-set check above makes this fail closed if the exporter gains another +# Netty module that is not pinned here. +rm -rf \ + "${payload_dir}/io/netty" \ + "${payload_dir}/META-INF/maven/io.netty" \ + "${payload_dir}/META-INF/native-image/io.netty" +rm -f \ + "${payload_dir}/META-INF/io.netty.versions.properties" \ + "${payload_dir}/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration" + +printf '%s\n' "${NETTY_ARTIFACTS}" | +while read -r artifact checksum; do + [ -n "${artifact}" ] || continue + jar=${downloads_dir}/${artifact}-${NETTY_VERSION}.jar + curl -fsSLo "${jar}" \ + "${MAVEN_CENTRAL_URL}/${artifact}/${NETTY_VERSION}/${artifact}-${NETTY_VERSION}.jar" + printf '%s %s\n' "${checksum}" "${jar}" | sha256sum -c - + + # Only copy Netty-owned entries. This keeps the exporter implementation, + # manifest, and all unrelated shaded dependencies byte-for-byte intact. + unzip -oq "${jar}" \ + 'io/netty/*' \ + 'META-INF/maven/io.netty/*' \ + -d "${payload_dir}" + if unzip -Z1 "${jar}" | grep -q '^META-INF/native-image/io.netty/'; then + unzip -oq "${jar}" 'META-INF/native-image/io.netty/*' -d "${payload_dir}" + fi + if unzip -Z1 "${jar}" | + grep -q '^META-INF/services/reactor.blockhound.integration.BlockHoundIntegration$'; then + unzip -oq "${jar}" \ + 'META-INF/services/reactor.blockhound.integration.BlockHoundIntegration' \ + -d "${payload_dir}" + fi +done + +# The exporter POMs are scanner-visible provenance. Update only their Netty +# version declarations so they describe the dependency payload just inserted. +find "${payload_dir}/META-INF/maven/com.zegelin.cassandra-exporter" -name pom.xml -type f | +while read -r pom; do + if ! awk -v version="${NETTY_VERSION}" ' + // { in_dependency = 1; dependency = "" } + in_dependency { + dependency = dependency $0 ORS + if (/<\/dependency>/) { + if (dependency ~ /io[.]netty<\/groupId>/) { + if (dependency ~ /4[.]1[.][0-9][0-9]*[.]Final<\/version>/) { + sub(/4[.]1[.][0-9][0-9]*[.]Final<\/version>/, + "" version "", dependency) + changed = 1 + } + } + printf "%s", dependency + in_dependency = 0 + next + } + next + } + { print } + END { + if (in_dependency || !changed) { + exit 2 + } + } + ' "${pom}" > "${pom}.updated"; then + rm -f "${pom}.updated" + echo "unexpected exporter POM dependency layout: ${pom}" >&2 + exit 1 + fi + mv "${pom}.updated" "${pom}" +done + +output_modules=$( + find "${payload_dir}/META-INF/maven/io.netty" -name pom.properties -type f | + sed 's#.*/META-INF/maven/io\.netty/\([^/]*\)/pom\.properties$#\1#' | + sort -u +) +[ "${output_modules}" = "${expected_modules}" ] || { + echo "replacement Netty module set is incomplete" >&2 + exit 1 +} + +for properties in "${payload_dir}"/META-INF/maven/io.netty/*/pom.properties; do + grep_version=$(sed -n 's/^version=//p' "${properties}") + [ "${grep_version}" = "${NETTY_VERSION}" ] || { + echo "unexpected Netty version in ${properties}: ${grep_version}" >&2 + exit 1 + } +done + +# Netty's per-module files share this name. Combine them so Version.identify() +# can report every module from the repacked fat jar. +versions_file=${payload_dir}/META-INF/io.netty.versions.properties +: > "${versions_file}" +printf '%s\n' "${NETTY_ARTIFACTS}" | +while read -r artifact checksum; do + [ -n "${artifact}" ] || continue + unzip -p "${downloads_dir}/${artifact}-${NETTY_VERSION}.jar" \ + META-INF/io.netty.versions.properties >> "${versions_file}" +done + +mkdir -p "$(dirname "${output}")" +output_abs=$(cd "$(dirname "${output}")" && pwd)/$(basename "${output}") +rm -f "${output_abs}" +# ZIP records DOS timestamps. Normalize them so identical inputs and pinned +# dependencies produce an identical repacked jar on every build. +find "${payload_dir}" -type f -exec touch -t 198001010000 {} + +( + cd "${payload_dir}" + find . -type f -print | LC_ALL=C sort | zip -Xq "${output_abs}" -@ +) +unzip -tq "${output}" >/dev/null From 521c5048ec1871095f821a70bc20ecd0677ed565 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Mon, 7 Sep 2026 23:12:58 -0700 Subject: [PATCH 4/4] fix(cassandra): harden exporter jar repacking Signed-off-by: Stephanie Baum --- .../scripts/repack-exporter-netty-test.sh | 44 +++++++++++++++++++ .../scripts/repack-exporter-netty.sh | 20 ++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/infra/cassandra/scripts/repack-exporter-netty-test.sh b/infra/cassandra/scripts/repack-exporter-netty-test.sh index b18ed97553..347ee9ff1e 100755 --- a/infra/cassandra/scripts/repack-exporter-netty-test.sh +++ b/infra/cassandra/scripts/repack-exporter-netty-test.sh @@ -19,6 +19,9 @@ printf '%s\n' \ 'Manifest-Version: 1.0' \ 'Premain-Class: com.zegelin.cassandra.exporter.Agent' \ > "${input_dir}/META-INF/MANIFEST.MF" +printf '%s\n' 'stale signature metadata' > "${input_dir}/META-INF/TEST.SF" +printf '%s\n' 'stale signature block' > "${input_dir}/META-INF/TEST.RSA" +printf '%s\n' 'stale alternate signature' > "${input_dir}/META-INF/SIG-TEST" printf '%s\n' 'preserve this exporter payload' \ > "${input_dir}/com/zegelin/cassandra/exporter/Agent.class" printf '%s\n' 'remove this old Netty payload' \ @@ -57,6 +60,41 @@ output_jar=${work_dir}/output.jar find . -type f -print | LC_ALL=C sort | zip -Xq "${input_jar}" -@ ) +missing_metadata_dir=${work_dir}/missing-metadata +cp -R "${input_dir}" "${missing_metadata_dir}" +rm -rf "${missing_metadata_dir}/META-INF/maven/com.zegelin.cassandra-exporter" +missing_metadata_jar=${work_dir}/missing-metadata.jar +( + cd "${missing_metadata_dir}" + find . -type f -print | LC_ALL=C sort | zip -Xq "${missing_metadata_jar}" -@ +) +if "${script_dir}/repack-exporter-netty.sh" \ + "${missing_metadata_jar}" "${work_dir}/unexpected-output.jar" \ + > "${work_dir}/missing-metadata.stdout" 2> "${work_dir}/missing-metadata.stderr"; then + echo 'repack unexpectedly accepted exporter input without Maven metadata' >&2 + exit 1 +fi +grep -q 'exporter Maven metadata is missing' "${work_dir}/missing-metadata.stderr" + +missing_pom_dir=${work_dir}/missing-pom +cp -R "${input_dir}" "${missing_pom_dir}" +rm -rf "${missing_pom_dir}/META-INF/maven/com.zegelin.cassandra-exporter" +mkdir -p "${missing_pom_dir}/META-INF/maven/com.zegelin.cassandra-exporter" +printf '%s\n' 'metadata directory without a POM' \ + > "${missing_pom_dir}/META-INF/maven/com.zegelin.cassandra-exporter/README" +missing_pom_jar=${work_dir}/missing-pom.jar +( + cd "${missing_pom_dir}" + find . -type f -print | LC_ALL=C sort | zip -Xq "${missing_pom_jar}" -@ +) +if "${script_dir}/repack-exporter-netty.sh" \ + "${missing_pom_jar}" "${work_dir}/unexpected-pom-output.jar" \ + > "${work_dir}/missing-pom.stdout" 2> "${work_dir}/missing-pom.stderr"; then + echo 'repack unexpectedly accepted exporter metadata without a POM' >&2 + exit 1 +fi +grep -q 'exporter Maven metadata contains no pom.xml files' "${work_dir}/missing-pom.stderr" + "${script_dir}/repack-exporter-netty.sh" "${input_jar}" "${output_jar}" original_payload=$(unzip -p "${input_jar}" com/zegelin/cassandra/exporter/Agent.class) @@ -67,6 +105,12 @@ original_manifest=$(unzip -p "${input_jar}" META-INF/MANIFEST.MF) repacked_manifest=$(unzip -p "${output_jar}" META-INF/MANIFEST.MF) [ "${original_manifest}" = "${repacked_manifest}" ] +if unzip -Z1 "${output_jar}" | + grep -Eq '^META-INF/[^/]+\.(SF|RSA|DSA|EC)$|^META-INF/SIG-'; then + echo 'invalidated JAR signature metadata remains in the repacked jar' >&2 + exit 1 +fi + if unzip -Z1 "${output_jar}" | grep -q '^io/netty/legacy/Old.class$'; then echo 'old Netty class remains in the repacked jar' >&2 exit 1 diff --git a/infra/cassandra/scripts/repack-exporter-netty.sh b/infra/cassandra/scripts/repack-exporter-netty.sh index 2b248c2699..aa174c70b4 100755 --- a/infra/cassandra/scripts/repack-exporter-netty.sh +++ b/infra/cassandra/scripts/repack-exporter-netty.sh @@ -66,6 +66,24 @@ downloads_dir=${work_dir}/downloads mkdir -p "${payload_dir}" "${downloads_dir}" unzip -q "${input}" -d "${payload_dir}" +exporter_metadata_dir=${payload_dir}/META-INF/maven/com.zegelin.cassandra-exporter +[ -d "${exporter_metadata_dir}" ] || { + echo "exporter Maven metadata is missing: META-INF/maven/com.zegelin.cassandra-exporter" >&2 + exit 1 +} +exporter_poms=$(find "${exporter_metadata_dir}" -name pom.xml -type f -print) +[ -n "${exporter_poms}" ] || { + echo "exporter Maven metadata contains no pom.xml files" >&2 + exit 1 +} + +# Replacing shaded classes invalidates any signatures covering the input JAR. +# Remove top-level signature metadata so the JVM does not reject the repacked +# agent with a SecurityException when it reads modified entries. +find "${payload_dir}/META-INF" -maxdepth 1 -type f \ + \( -name '*.SF' -o -name '*.RSA' -o -name '*.DSA' -o -name '*.EC' -o -name 'SIG-*' \) \ + -exec rm -f {} + + # Remove the complete shaded Netty payload and its dependency metadata. The # module-set check above makes this fail closed if the exporter gains another # Netty module that is not pinned here. @@ -104,7 +122,7 @@ done # The exporter POMs are scanner-visible provenance. Update only their Netty # version declarations so they describe the dependency payload just inserted. -find "${payload_dir}/META-INF/maven/com.zegelin.cassandra-exporter" -name pom.xml -type f | +printf '%s\n' "${exporter_poms}" | while read -r pom; do if ! awk -v version="${NETTY_VERSION}" ' // { in_dependency = 1; dependency = "" }