Skip to content

Add support for JDK25 - #5011

Open
JeremiahDJordan wants to merge 26 commits into
apache:trunkfrom
JeremiahDJordan:CASSANDRA-21171-support-jdk25-trunk
Open

Add support for JDK25#5011
JeremiahDJordan wants to merge 26 commits into
apache:trunkfrom
JeremiahDJordan:CASSANDRA-21171-support-jdk25-trunk

Conversation

@JeremiahDJordan

@JeremiahDJordan JeremiahDJordan commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

CASSANDRA-21171: Add JDK 25 support

Adds JDK 25 as a supported build and runtime target alongside JDK 11/17/21.

Build & dependencies

  • New jvm25-server.options / jvm25-clients.options; java.supported includes 25
  • Upgrade ASM, ByteBuddy, Mockito (drop mockito-inline), JaCoCo, allocation-instrumenter, and the Amazon Corretto Crypto Provider (all platforms) to versions that read/run JDK 25 bytecode.
  • Build jamm from a JDK-25-patched submodule (released 0.4.0 misreads a HotSpot flag removed in JDK 25 and corrupts tool output); build Accord with Gradle 9 on JDK 17+ so it can read JDK 25 class files.

SecurityManager removal (JEP 411, finalized in JDK 24)

  • Sandbox UDFs without a SecurityManager, and deny java.lang.ClassLoader wholesale in the UDF byte-code verifier.
  • Trap System.exit in tests via Byteman; resolve JMX authorization subjects via Subject.current() / callAs; overwrite static final fields via Unsafe instead of reflection.

JDK 25 runtime correctness

  • AES-GCM TLS: JDK 25 rewrote GaloisCounterMode.overlapDetection to walk a direct buffer's attachment chain and cast each link to java.nio.Buffer. BufferPool stashed a Chunk there, so every encrypted connection threw ClassCastException. Fixed by attaching a per-chunk zero-capacity Buffer marker and recovering the owning Chunk from a weak-keyed side-map (gated on JDK ≥ 25).
  • Leak detector: Ref's field walk now skips record components it can't read via Unsafe (which throws on JDK 25) instead of aborting the scan.

Tests & CI

  • The common ForkJoinPool on JDK 25 doesn't spawn a compensating worker under low parallelism; blocking test actions (DisableBinaryTest and several dtests) now run on dedicated executors for determinism.
  • Modernize incidental TLS in tests to protocols/ciphers enabled on all supported JDKs; use a SAN-bearing keystore for the JMX-over-TLS test; keep FileTest's empty-path case stable across the JDK 25 pathname change; support and seed-pin the simulator on JDK 25.
  • Add JDK 25 to the packaging and CI docker images; allow the docker test runner to use a local ccm working copy.

Companion branches (same ticket)

Add 25 to java.supported and provide conf/jvm25-server.options and
conf/jvm25-clients.options (derived from the jvm21 baseline). The launch
scripts (bin/cassandra.in.sh, tools/bin/cassandra.in.sh, redhat/cassandra.in.sh)
select these files when running on JDK 25.

The JDK 25 argument sets drop flags that no longer exist on that release
(-XX:+ZGenerational, removed once ZGC became generational-only; and
-Djdk.reflect.useDirectMethodHandle=false, removed in JDK 22), add
--sun-misc-unsafe-memory-access=allow to silence the sun.misc.Unsafe deprecation
warning, and add --enable-native-access=ALL-UNNAMED to grant native access to
class-path code (e.g. netty's System::loadLibrary) so the JDK does not print
"restricted method called" warnings to stderr (JEP 472), which future JDKs will
otherwise block. -Djava.security.manager=allow is intentionally not set because
installing a SecurityManager is no longer supported on JDK 25.

JDK 23+ no longer runs annotation processors discovered on the class path unless
annotation processing is explicitly requested, so build with -proc:full on
JDK 21 and 25 to keep generating META-INF/hotspot_compiler.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…agents

Update the dependencies that gate JDK 25 support:
- ASM 9.5 -> 9.8 (reads the class file version emitted by JDK 25).
- Byte Buddy 1.17.8 -> 1.18.10, declared as a direct dependency so the managed
  version (which natively supports JDK 25) is used instead of an older one pulled
  in transitively; required by Mockito's inline mock maker.
- Mockito 5.12.0 -> 5.23.0 and drop the separate mockito-inline artifact (the
  inline maker is the default in Mockito 5).
- JaCoCo 0.8.8 -> 0.8.15 and java-allocation-instrumenter 3.1.0 -> 3.3.5.

The allocation-instrumenter upgrade no longer re-exports a shaded Guava, which
surfaced a handful of source files that had accidentally imported Guava through
com.google.monitoring.runtime.instrumentation.common.*; point them at the real
com.google.common.* classes. Refresh the IDE project classpath jar names.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The AmazonCorrettoCryptoProvider native artifact is platform-specific, so the
Maven classifier is chosen by the build/test host's OS and CPU. Replace the two
Linux-only profiles with four mutually exclusive (os, arch) profiles that also
cover macOS (osx-x86_64 and osx-aarch_64), using the "mac"/"!mac" os family to
separate macOS from Linux since both report aarch64/x86_64. Bump the provider
from 2.2.0 to 2.5.0, whose macOS classifiers let the crypto tests run on macOS
developer machines (Cassandra itself ships on Linux).

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
User-defined functions run untrusted Java in the server process and were
confined with a thread-based SecurityManager. A SecurityManager can no longer
be installed on recent JDKs, so add a SecurityManager-free sandbox and select
between the two mechanisms at runtime.

The new sandbox rejects dangerous code at CREATE FUNCTION time with a bytecode
verifier deny-list -- blocking System.exit/Runtime.halt, process execution,
file and network I/O, reflection and method-handle escapes, JVM-internal and
Unsafe APIs, and system-property reads including aliases such as
Integer.getInteger -- backed by a filtering UDF class loader and the existing
asynchronous execution watchdog, so unsafe code can never be linked or loaded.
cassandra.udf.security_mechanism (auto|securitymanager|sandbox) chooses the
mechanism; auto installs a SecurityManager where that is still possible and
uses the sandbox otherwise. allow_extra_insecure_udfs continues to relax the
restrictions where an operator has opted in.

See cql3/functions/UDFSecurity.md for the sandbox design and
security/SecurityManagerReplacement.md for the broader overview of how the
SecurityManager was replaced.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Tests that assert a tool or node calls System.exit installed a SecurityManager
(PreventSystemExit) that turned the exit into a catchable exception. That
mechanism cannot be installed on recent JDKs, so replace it with a Byteman rule
that rewrites Runtime.exit/Runtime.halt to throw instead.

SystemExitManager installs the rule against an already-running Byteman agent and
reference-counts a process-global block; BytemanAgentSupport centralizes the
agent lifecycle (fixed loopback host and a single dynamically chosen free port
reused for the life of the JVM) and is shared with the Injections framework.
ToolRunner, ClusterUtils, Instance, AbstractCluster, SSTableIdGenerationTest and
the isolated LoaderOptionsTest are converted to the new interceptor, and
PreventSystemExit is removed. Tool output capture is cleaned up so the Byteman
rule installation and jamm's startup warning no longer leak into captured
stdout/stderr.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…wer JDKs

JMX authorization and audit looked up the authenticated principal with
Subject.getSubject(AccessControlContext) and ran work under Subject.doAs, both
tied to the access-control machinery that is removed along with the
SecurityManager. Add JMXSubjects, which uses Subject.current()/Subject.callAs on
JDKs where the legacy methods no longer function and falls back to
getSubject/doAs where they do, and route AuthorizationProxy and AuditLogManager
through it. The choice is a runtime capability check so the code still compiles
against JDK 11.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Clearing a field's modifiers bit to write a final field through reflection
stopped working in JDK 22. Replace those writes with sun.misc.Unsafe-based
assignment in ReflectionUtils and FieldUtil, and update the call sites that
reset final fields (Verb, Message, and the simulator's ClusterSimulation) to use
the new helpers.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
JDK 25 disables legacy TLS protocols (TLSv1, TLSv1.1) and a number of older
cipher suites by default. The internode, native-transport and sstableloader
encryption tests hard-coded protocol and cipher expectations that no longer hold
there. Teach AbstractEncryptionOptionsImpl to compute which protocols and
ciphers are enabled by default on the running JDK and assert against that, so the
tests pass on every supported JDK.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The simulator rewrites bootstrap classes to make execution deterministic, which
is sensitive to JDK internals. Update the transformer for JDK 25: pin a non-zero
ThreadLocalRandom probe so the rewritten ForkJoinPool does not livelock, and hash
only simulated enums by ordinal while keeping the real identity hash for JDK
enums (with the helper added to InterceptorOfSystemMethods). Remap nestmate
attributes in ShadowingTransformer so shadowed JDK classes such as
ConcurrentHashMap's TreeBin retain a consistent nest host, and let RunStartDefiner
initialize cleanly when no run id is present.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Add a cassandra.simulator.seed property and route the simulator test entry
points (SimulationTestBase, HarrySimulatorTest, SemaphoreTest,
SingleNodeSingleTableASTTest and SimulationRunner) through it, so an entire run
can be reproduced from a single base seed. Each call site falls back to its
previous default when the property is unset, leaving existing behaviour
unchanged.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Allow building and packaging Cassandra on JDK 25: add it to the Debian and RPM
build dependencies and install it in the build/test Docker images. Where the
distribution's package repositories do not yet provide OpenJDK 25 it is installed
from a checksum-verified GA tarball download (both x86_64 and aarch64).

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
new java.io.File("") reports as non-existent on older JDKs but resolves to the
current working directory on JDK 25 (exists(), isDirectory() and lastModified()
all reflect user.dir). Cassandra's File(String) maps the empty string to an
always-non-existent path, so the java.io.File equivalence harness no longer
matches for the empty path. Assert Cassandra File("")'s documented contract
directly instead, keeping the case covered and stable on every supported JDK.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The SHA256 check read field two of sha256sum's output, but the digest and the
filename are separated by two spaces, so field two is empty and the following
grep matched any input -- the check could never fail. Cut field one so the
actual digest is compared.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Accord's build needs Gradle 9 to read JDK 25 bytecode (class file v69) when
compiling its precompiled convention plugins, but Gradle 9 requires JDK 17+, while
building on JDK 11 still needs Gradle 8. Select the Gradle distribution by the
building JDK: keep the committed Gradle 8 wrapper for JDK 11 and repoint it at
Gradle 9.6 for JDK 17+ for the duration of the build, restoring the committed
wrapper afterwards. accord-maelstrom references the accord-core source sets
directly so it builds under Gradle 9 (accord submodule bumped to a061c88).

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The docker dtest runner installs ccm from the dtest requirements.txt, so local
ccm changes cannot be exercised without first publishing them. Add an optional
cassandra_ccm_dir, mirroring cassandra_dtest_dir: when set it is mounted into the
container and pip-installed over the requirements.txt ccm, so a local ccm working
copy can be validated end-to-end by the python dtests.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The Accord submodule is pinned to a commit that adds Gradle 9 compatibility
(referencing the accord-core source sets directly) which is not yet in
apache/cassandra-accord, so a fresh "git submodule update --init" cannot fetch it
from the upstream URL. Point the submodule at the JDK 25 Accord fork/branch so CI
can check it out. Revert to apache/cassandra-accord once the Accord change is
merged upstream.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
JDK 25 disables the legacy TLSv1/TLSv1.1 protocols and the
TLS_RSA_WITH_AES_128_CBC_SHA cipher by default (jdk.tls.disabledAlgorithms). Tests
that pinned these only incidentally -- to exercise config/cache-key equality, SSL
context construction, or internode framing rather than the specific legacy
version/cipher -- are updated to a protocol and cipher that stay enabled on JDK
11/17/21/25: TLSv1.2 and TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (the modern suite
already used elsewhere as the JDK-25 fallback).

Tests that specifically assert the legacy protocols/ciphers are rejected, or that
gate them behind isProtocolEnabledByDefault()/isCipherEnabledByDefault(), are left
unchanged so they keep verifying that behaviour.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Document JDK 25 as a supported build and runtime target under 7.0, mirroring the
JDK 21 entry: generational ZGC is the recommended collector (the non-generational
ZGC mode was removed from the JDK), and installing a SecurityManager is no longer
supported on JDK 25 (JEP 411), with user-defined function sandboxing moved to a
SecurityManager-free mechanism.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
jamm 0.4.0 reads the UseEmptySlotsInSupers HotSpot VM option to choose its memory
sizing strategy. JDK 25 removed that option, which jamm mis-detects as the option
being disabled: it selects the DoesNotUseEmptySlotInSuper strategy (incorrect
sizing) and prints a warning to stdout that corrupts tool output parsed by tests
and users (e.g. sstabledump JSON, nodetool). The published 0.4.0 is the latest
release and no property or JVM flag disables this.

Build jamm from the modules/jamm submodule (a fork that treats the absent option as
enabled on Java 15+, so Java 25 uses the correct strategy and prints no warning) and
install it to the local Maven repository as org.apache.cassandra:cassandra-jamm,
mirroring how Accord is built. The parent pom depends on cassandra-jamm so the
corrected build is resolved from the local build rather than the upstream
com.github.jbellis:jamm release; the resolved jar is not overwritten.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…ks on JDK 25

JDK 25 rewrote GaloisCounterMode.overlapDetection to walk a direct buffer's attachment chain casting each link to java.nio.Buffer (NIO_ACCESS.getBufferAddress((Buffer) att)); JDK <= 24 cast to sun.nio.ch.DirectBuffer and called address(). BufferPool stashes the owning Chunk (or a Ref.DirectBufferRef when -Dcassandra.debugrefcount is enabled) in the pooled buffer's DirectByteBuffer.att field, and neither is a java.nio.Buffer. Because AES-GCM is the default TLS 1.2/1.3 cipher, every encrypted internode and native connection failed on JDK 25 with a ClassCastException during the handshake's GCM decrypt.

On JDK 25+ stash a per-chunk java.nio.Buffer marker (a hollow direct buffer with a null attachment and an address of the chunk base) in each sliced buffer's att instead, and recover the owning Chunk from CHUNK_BY_MARKER keyed by the marker's object identity. The null attachment stops overlapDetection's walk at the chunk boundary rather than chaining up to the 8MB macro root, keeping the window where it defensively heap-copies dst down to a single 128KB chunk. Identity keying is required because nested chunks share base addresses (a macro chunk and its first normal child both start at offset 0), so an address key would recover the wrong chunk.

CHUNK_BY_MARKER is a weak-keyed identity map (Guava MapMaker().weakKeys()); entries are dropped in Chunk.dropAttachmentMarker() from both unsafeFree() and when a tiny chunk is recycled back to its parent. Tiny chunks - and their markers - are created and retired millions of times per second under load, and a strong-reference map (e.g. NonBlockingIdentityHashMap) retains removed keys in its internal table until a resize, leaking the marker buffers and OOMing the heap; weak keys reclaim a marker's entry once it is unreferenced even if an explicit drop is missed.

JDK <= 24 keeps the original zero-overhead direct Chunk attachment (no map, no per-free lookup, DirectBufferRef leak tracking preserved); the map is gated on Runtime.version() and never populated there, since the bug does not exist on those JDKs (verified the cast lands in JDK 25, not 22/23/24).

Adds a BufferPool + AES/GCM/NoPadding direct-ByteBuffer round-trip regression test, with both buffers from the same macro slab, that fails with the ClassCastException on the old attachment under JDK 25; the marker-map leak is covered by the LongBufferPoolTest burn test.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…wn on a dedicated executor

testDisallowsNewRequests submitted the post-shutdown query via CompletableFuture.supplyAsync, which runs on the common ForkJoinPool. Common-pool parallelism is 1 (the in-JVM dtest JVM sees two processors) and its single worker is already blocked in the disablebinary call, which drains the in-flight queries for about a second. On JDK 17 the pool promptly starts a compensating worker, so the query runs immediately and the stopped native transport rejects it with OverloadedException. The JDK 25 ForkJoinPool rework does not start a compensating worker, so the task is starved until disablebinary completes about a second later, after the server has reset the connection and the driver marked the host down, and the query fails client-side with NoHostAvailableException instead. The behaviour is deterministic and opposite per JDK (JDK 17 OverloadedException; JDK 25 NoHostAvailableException), not a flake.

Run the query on a dedicated single-thread executor instead of the common pool so it executes immediately, inside the window where the stopped transport returns OverloadedException, restoring the strict assertion on all JDKs. Validated on JDK 17 and JDK 25.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The UDF sandbox allows the java/lang/ class-load prefix, so a UDF can name java.lang.ClassLoader, and configureBaseDisallowed denied only an enumerated set of its methods. It omitted getPlatformClassLoader(), getParent() and resources(String), each of which hands back a live ClassLoader or resource stream. No present-day break-out results (the capabilities reachable from an obtained loader are independently blocked), but a name-based denylist that must enumerate every dangerous method is fragile: a future JDK adding a loader/resource-yielding method to ClassLoader would silently widen the sandbox with no failing test.

Deny java/lang/ClassLoader as a class instead. The verifier already rejects any call whose owner is a disallowed class, so this covers the three missing methods and any added later, without enumerating them. The generated UDF wrapper never references ClassLoader, so no legitimate UDF is affected; the existing per-method entries are kept as documentation of the surface. The denial lives in configureBaseDisallowed, so it applies under both the SecurityManager and SecurityManager-free mechanisms.

Add two regression tests to UFSecurityTest: per-method rejection of getPlatformClassLoader/getParent (verifier) and resources (rejected at compilation, since its Stream<URL> return type is not resolvable in the sandbox), plus a reflective guard that asserts every public loader/resource-yielding ClassLoader method is un-creatable, so a future JDK addition or a downgrade of the denial fails CI. Validated on JDK 25 (6/0/0) and JDK 17 (6 run, 1 expected skip).

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…Settings on JDK 25

testSystemSettings connects with the JDK's stock SslRMIClientSocketFactory, which on JDK 25 performs TLS endpoint identity verification. The default test keystore (cassandra_ssl_test.keystore, CN=Apache Cassandra) carries no SubjectAltName, so the handshake fails with SSLHandshakeException: (certificate_unknown) No subject alternative names present. The other tests in this class connect through a custom client socket factory that does not verify endpoint identity, so they are unaffected.

Point testSystemSettings at cassandra_ssl_test_endpoint_verify.keystore (CN=127.0.0.1 with an IPAddress:127.0.0.1 SAN), which the shared test truststore already trusts (alias mykey). Validated on JDK 25 and JDK 17 (1/0/0 each).

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
…ForkJoinPool

A codebase audit for JDK 25's reworked ForkJoinPool (which no longer eagerly starts a compensating worker when a common-pool worker blocks at low parallelism) found four tests that submit blocking work - CQL queries and nodetool calls - to the common pool via IntStream.parallel() or CompletableFuture.supplyAsync/runAsync with no explicit executor. Under JDK 25 these can serialize or starve, the same failure class as DisableBinaryTest.

Route each through a dedicated executor: VectorSiftSmallTest (4 parallel insert/recall loops -> a fixed thread pool helper), and MixedModeRepairTest, RepairCoordinatorNeighbourDown and DecommissionAvoidTimeouts (supplyAsync/runAsync -> a single-thread executor, shut down after the task is awaited). Production code was confirmed unaffected (it uses Cassandra's own ExecutorPlus framework, not the common pool). VectorSiftSmallTest validated on JDK 17 and JDK 25 (2/0/0 each); the three dtests compile clean.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
… JDK 16+

Ref.getFieldValue falls back to Unsafe.objectFieldOffset when a field cannot be
made reflectively accessible (module-protected). On JDK 16+ that call throws
UnsupportedOperationException for record components, and JDK 25 enforces this
hard. The resulting UnaccessibleFieldException (a RuntimeException) was not
caught by the field walk in InProgressVisit.nextChild, so it propagated to the
outermost handler in Visitor.run and aborted the entire leak-detection pass:
the first JDK record reachable from any tracked Tidy graph (e.g.
java.security.SecureClassLoader$CodeSourceKey) silently disabled leak checking
for every remaining GlobalState. Records are pervasive in the JDK since 16, so
on JDK 25 the strong-reference leak detector was largely defeated.

Catch UnaccessibleFieldException in nextChild, log it throttled via
NoSpamLogger, and skip the single unreadable field (record, hidden class, or
module-protected) instead of aborting the whole scan; the field index is
already advanced, so the walk safely continues with the rest of the graph.
Also drop a redundant second getFieldValue call on the same field.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
The [ \$USING_ZGC -eq 0] test was missing the space before the
closing bracket. Follow-on fix to be squashed into the JDK 25 branch.

patch by Jeremiah Jordan; reviewed by <Reviewer Here> for CASSANDRA-21171
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant