From 318719851cba19f66fac60223e9a323f7f3ebc56 Mon Sep 17 00:00:00 2001 From: Idan Sheinberg Date: Wed, 21 Oct 2026 16:29:53 +0300 Subject: [PATCH 1/3] [FLINK-36902][core] Remove SecurityManager lookup from ExecutorThreadFactory SecurityManager#getThreadGroup returns the current thread's group by default and Flink's own security manager never overrides it, so the lookup was a no-op that only kept a dependency on the deprecated-for- removal Security Manager API (JEP 486). Also fixes stale "FlinkUserSecurityManager" names in the javadoc of FlinkSecurityManager. Generated-by: Claude Code (Claude Fable 5.1) --- .../flink/core/security/FlinkSecurityManager.java | 11 +++++------ .../flink/util/concurrent/ExecutorThreadFactory.java | 6 +----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java b/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java index 05faf6f75b97e..7e39799abebe8 100644 --- a/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java +++ b/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java @@ -73,14 +73,13 @@ public class FlinkSecurityManager extends SecurityManager { } /** - * Instantiate FlinkUserSecurityManager from configuration. Return null if no security manager - * check is needed, so that a caller can skip setting security manager avoiding runtime check - * cost, if there is no security check set up already. Use {@link #setFromConfiguration} helper, - * which handles disabled case. + * Instantiate FlinkSecurityManager from configuration. Return null if no security manager check + * is needed, so that a caller can skip setting security manager avoiding runtime check cost, if + * there is no security check set up already. Use {@link #setFromConfiguration} helper, which + * handles disabled case. * * @param configuration to instantiate the security manager from - * @return FlinkUserSecurityManager instantiated based on configuration. Return null if - * disabled. + * @return FlinkSecurityManager instantiated based on configuration. Return null if disabled. */ @VisibleForTesting static FlinkSecurityManager fromConfiguration(Configuration configuration) { diff --git a/flink-core/src/main/java/org/apache/flink/util/concurrent/ExecutorThreadFactory.java b/flink-core/src/main/java/org/apache/flink/util/concurrent/ExecutorThreadFactory.java index fe3c496ef6a2d..bc7789776f77c 100644 --- a/flink-core/src/main/java/org/apache/flink/util/concurrent/ExecutorThreadFactory.java +++ b/flink-core/src/main/java/org/apache/flink/util/concurrent/ExecutorThreadFactory.java @@ -97,11 +97,7 @@ public ExecutorThreadFactory(String poolName, UncaughtExceptionHandler exception this.threadPriority = threadPriority; this.exceptionHandler = exceptionHandler; - SecurityManager securityManager = System.getSecurityManager(); - this.group = - (securityManager != null) - ? securityManager.getThreadGroup() - : Thread.currentThread().getThreadGroup(); + this.group = Thread.currentThread().getThreadGroup(); } // ------------------------------------------------------------------------ From c2e857b7f3f8d6651435602d3d9c795ff0ac9ecd Mon Sep 17 00:00:00 2001 From: Idan Sheinberg Date: Wed, 21 Oct 2026 16:29:53 +0300 Subject: [PATCH 2/3] [FLINK-36902][core] Keep process exit working on JDK 24+ where the Security Manager is disabled Since JDK 24 (JEP 486) System#setSecurityManager always throws an UnsupportedOperationException, even when called with null. As a consequence FlinkSecurityManager#forceProcessExit threw instead of terminating the JVM, so fatal errors handled by FatalExitExceptionHandler, ClusterEntrypoint#onFatalError or TaskManagerRunner left the process running. Only unset the security manager if one is actually installed, which can never be the case on JDK 24+. Additionally, enabling cluster.intercept-user-system-exit or cluster.processes.halt-on-fatal-error on a JVM without Security Manager support (JDK 24+, or JDK 18-23 without -Djava.security.manager=allow) now fails with a message explaining the cause instead of a misleading "no permission" error. ClusterUncaughtExceptionHandlerITCase no longer hangs on JDK 25 and is untagged from FailsOnJava25. FlinkSecurityManagerITCase gets two tests that run on every JDK: a forced exit without a security manager and the descriptive error for an unavailable security manager (child JVM started with -Djava.security.manager=disallow via a new TestJvmProcess hook). Generated-by: Claude Code (Claude Fable 5.1) --- .../core/security/FlinkSecurityManager.java | 37 ++++- ...ClusterUncaughtExceptionHandlerITCase.java | 2 - .../runtime/testutils/TestJvmProcess.java | 9 ++ .../util/FlinkSecurityManagerITCase.java | 126 +++++++++++++----- 4 files changed, 133 insertions(+), 41 deletions(-) diff --git a/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java b/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java index 7e39799abebe8..29cd5e2dfbb72 100644 --- a/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java +++ b/flink-core/src/main/java/org/apache/flink/core/security/FlinkSecurityManager.java @@ -37,6 +37,10 @@ * malicious users who try to tweak security manager on their own, but more for being dependable * against user mistakes by gracefully handling them informing users rather than causing silent * unavailability. + * + *

The Java Security Manager was permanently disabled in JDK 24 (JEP 486). On such JVMs the security manager features + * cannot be enabled, but the static process exit helpers keep working. */ public class FlinkSecurityManager extends SecurityManager { @@ -109,12 +113,22 @@ public static void setFromConfiguration(Configuration configuration) { try { System.setSecurityManager(flinkSecurityManager); } catch (Exception e) { + // UnsupportedOperationException: JDK 18-23 without -Djava.security.manager=allow, + // or JDK 24+ where the Security Manager is permanently disabled (JEP 486). + // Anything else: an existing security manager refuses to be replaced. + final String reason = + e instanceof UnsupportedOperationException + ? "the Java Security Manager is not available in this JVM: it was " + + "permanently disabled in JDK 24 (JEP 486) and must be " + + "explicitly enabled with '-Djava.security.manager=allow' " + + "on JDK 18 to 23." + : "there is no permission to set a SecurityManager; update your " + + "existing SecurityManager to allow it."; throw new IllegalConfigurationException( String.format( - "Could not register security manager due to no permission to " - + "set a SecurityManager. Either update your existing " - + "SecurityManager to allow the permission or do not use " - + "security manager features (e.g., '%s: %s', '%s: %s')", + "Could not register security manager because %s Alternatively, " + + "disable the security manager features ('%s: %s', '%s: %s').", + reason, ClusterOptions.INTERCEPT_USER_SYSTEM_EXIT.key(), ClusterOptions.INTERCEPT_USER_SYSTEM_EXIT.defaultValue(), ClusterOptions.HALT_ON_FATAL_ERROR.key(), @@ -181,7 +195,7 @@ public void checkExit(int status) { // System.exit if (haltOnSystemExit) { // null the security manager to prevent infinite recursion - System.setSecurityManager(null); + clearSecurityManager(); Runtime.getRuntime().halt(status); } } @@ -208,11 +222,22 @@ boolean userSystemExitMonitored() { */ public static void forceProcessExit(int exitCode) { // Unset ourselves to allow exiting in any case. - System.setSecurityManager(null); + clearSecurityManager(); if (flinkSecurityManager != null && flinkSecurityManager.haltOnSystemExit) { Runtime.getRuntime().halt(exitCode); } else { System.exit(exitCode); } } + + /** + * Unsets the system security manager, if any. On JDK 24+ {@code System.setSecurityManager} + * always throws (JEP 486), even when passed {@code null}, but there a security manager can + * never be installed in the first place, so there is nothing to unset either. + */ + private static void clearSecurityManager() { + if (System.getSecurityManager() != null) { + System.setSecurityManager(null); + } + } } diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/entrypoint/ClusterUncaughtExceptionHandlerITCase.java b/flink-runtime/src/test/java/org/apache/flink/runtime/entrypoint/ClusterUncaughtExceptionHandlerITCase.java index fddfc9bb57676..fb5457a99868c 100644 --- a/flink-runtime/src/test/java/org/apache/flink/runtime/entrypoint/ClusterUncaughtExceptionHandlerITCase.java +++ b/flink-runtime/src/test/java/org/apache/flink/runtime/entrypoint/ClusterUncaughtExceptionHandlerITCase.java @@ -32,7 +32,6 @@ import org.apache.flink.util.concurrent.ScheduledExecutor; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,7 +41,6 @@ import static org.assertj.core.api.Assumptions.assumeThat; /** Integration test to check exit behaviour for the {@link ClusterUncaughtExceptionHandler}. */ -@Tag("org.apache.flink.testutils.junit.FailsOnJava25") @ExtendWith(TestLoggerExtension.class) class ClusterUncaughtExceptionHandlerITCase { diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/testutils/TestJvmProcess.java b/flink-runtime/src/test/java/org/apache/flink/runtime/testutils/TestJvmProcess.java index e9ca964350e8b..925a138670522 100644 --- a/flink-runtime/src/test/java/org/apache/flink/runtime/testutils/TestJvmProcess.java +++ b/flink-runtime/src/test/java/org/apache/flink/runtime/testutils/TestJvmProcess.java @@ -110,6 +110,14 @@ public void run() { */ public abstract String getEntryPointClassName(); + /** + * Returns additional arguments for the JVM. They are placed after the arguments derived from + * the surefire configuration, so they take precedence over those. + */ + protected String[] getJvmArgs() { + return new String[0]; + } + // --------------------------------------------------------------------------------------------- /** @@ -149,6 +157,7 @@ public void startProcess() throws IOException { cmd = ArrayUtils.addAll(cmd, moduleConfig.trim().split("\\s+")); } + cmd = ArrayUtils.addAll(cmd, getJvmArgs()); cmd = ArrayUtils.add(cmd, getEntryPointClassName()); String[] mainMethodArgs = getMainMethodArgs(); diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/util/FlinkSecurityManagerITCase.java b/flink-runtime/src/test/java/org/apache/flink/runtime/util/FlinkSecurityManagerITCase.java index 94b2835b4b342..8826309f1d216 100644 --- a/flink-runtime/src/test/java/org/apache/flink/runtime/util/FlinkSecurityManagerITCase.java +++ b/flink-runtime/src/test/java/org/apache/flink/runtime/util/FlinkSecurityManagerITCase.java @@ -20,6 +20,7 @@ import org.apache.flink.configuration.ClusterOptions; import org.apache.flink.configuration.Configuration; +import org.apache.flink.configuration.IllegalConfigurationException; import org.apache.flink.core.security.FlinkSecurityManager; import org.apache.flink.runtime.testutils.TestJvmProcess; import org.apache.flink.util.OperatingSystem; @@ -32,9 +33,10 @@ import static org.assertj.core.api.Assumptions.assumeThat; /** Integration tests for the {@link FlinkSecurityManager}. */ -@Tag("org.apache.flink.testutils.junit.FailsOnJava25") class FlinkSecurityManagerITCase { + private static final int FORCED_EXIT_CODE = 222; + @BeforeEach void ensureSupportedOS() { // based on the assumption in JvmExitOnFatalErrorTest, and manual testing on Mac, we do not @@ -42,40 +44,69 @@ void ensureSupportedOS() { assumeThat(OperatingSystem.isLinux() || OperatingSystem.isMac()).isTrue(); } + @Tag("org.apache.flink.testutils.junit.FailsOnJava25") @Test void testForcedJVMExit() throws Exception { - final ForcedJVMExitProcess testProcess = - new ForcedJVMExitProcess(ForcedExitEntryPoint.class); - - testProcess.startProcess(); - try { - testProcess.waitFor(); - assertThat(testProcess.exitCode()).isEqualTo(222); - } finally { - testProcess.destroy(); - } + runAndAssertExitCode( + new ForcedJVMExitProcess(ForcedExitEntryPoint.class), FORCED_EXIT_CODE); } + @Tag("org.apache.flink.testutils.junit.FailsOnJava25") @Test void testIgnoredJVMExit() throws Exception { - final ForcedJVMExitProcess testProcess = - new ForcedJVMExitProcess(IgnoredExitEntryPoint.class); + runAndAssertExitCode(new ForcedJVMExitProcess(IgnoredExitEntryPoint.class), 0); + } + @Test + void testForcedJVMExitWithoutSecurityManager() throws Exception { + runAndAssertExitCode( + new ForcedJVMExitProcess(ForcedExitWithoutSecurityManagerEntryPoint.class), + FORCED_EXIT_CODE); + } + + @Test + void testUnavailableSecurityManagerIsReported() throws Exception { + // 'disallow' is only understood by JDK 12+; JDK 24+ accepts it although the Security + // Manager is disabled there anyway + assumeThat(Runtime.version().feature()).isGreaterThanOrEqualTo(12); + runAndAssertExitCode( + new ForcedJVMExitProcess( + UnavailableSecurityManagerEntryPoint.class, + "-Djava.security.manager=disallow"), + FORCED_EXIT_CODE); + } + + private static void runAndAssertExitCode(ForcedJVMExitProcess testProcess, int expectedExitCode) + throws Exception { testProcess.startProcess(); try { testProcess.waitFor(); - assertThat(testProcess.exitCode()).isZero(); + assertThat(testProcess.exitCode()) + .withFailMessage( + "Unexpected exit code %d. Process output:%n%s", + testProcess.exitCode(), testProcess.getProcessOutput()) + .isEqualTo(expectedExitCode); } finally { testProcess.destroy(); } } + /** Configures the {@link FlinkSecurityManager} to intercept calls to System.exit(). */ + private static void interceptUserSystemExit() { + Configuration configuration = new Configuration(); + configuration.set( + ClusterOptions.INTERCEPT_USER_SYSTEM_EXIT, ClusterOptions.UserSystemExitMode.THROW); + FlinkSecurityManager.setFromConfiguration(configuration); + } + private static final class ForcedJVMExitProcess extends TestJvmProcess { - private final Class entryPointName; + private final Class entryPoint; + private final String[] jvmArgs; - private ForcedJVMExitProcess(Class entryPointName) throws Exception { - this.entryPointName = entryPointName; + private ForcedJVMExitProcess(Class entryPoint, String... jvmArgs) throws Exception { + this.entryPoint = entryPoint; + this.jvmArgs = jvmArgs; } @Override @@ -90,22 +121,22 @@ public String[] getMainMethodArgs() { @Override public String getEntryPointClassName() { - return entryPointName.getName(); + return entryPoint.getName(); + } + + @Override + protected String[] getJvmArgs() { + return jvmArgs; } } /** An entry point that attempts to force exit the process. */ public static final class ForcedExitEntryPoint { - public static void main(String[] args) throws Exception { - Configuration configuration = new Configuration(); - // configure FlinkSecurityManager to intercept calls to System.exit(). - configuration.set( - ClusterOptions.INTERCEPT_USER_SYSTEM_EXIT, - ClusterOptions.UserSystemExitMode.THROW); - FlinkSecurityManager.setFromConfiguration(configuration); + public static void main(String[] args) { + interceptUserSystemExit(); - FlinkSecurityManager.forceProcessExit(222); + FlinkSecurityManager.forceProcessExit(FORCED_EXIT_CODE); System.exit(0); } @@ -114,13 +145,8 @@ public static void main(String[] args) throws Exception { /** An entry point that attempts to exit the process but expects that call to be ignored. */ public static final class IgnoredExitEntryPoint { - public static void main(String[] args) throws Exception { - Configuration configuration = new Configuration(); - // configure FlinkSecurityManager to intercept calls to System.exit(). - configuration.set( - ClusterOptions.INTERCEPT_USER_SYSTEM_EXIT, - ClusterOptions.UserSystemExitMode.THROW); - FlinkSecurityManager.setFromConfiguration(configuration); + public static void main(String[] args) { + interceptUserSystemExit(); FlinkSecurityManager.monitorUserSystemExitForCurrentThread(); // expect this call to be ignored @@ -132,4 +158,38 @@ public static void main(String[] args) throws Exception { } } } + + /** + * An entry point that force exits the process without any security manager having been + * installed, as is always the case on JDK 24+. + */ + public static final class ForcedExitWithoutSecurityManagerEntryPoint { + + public static void main(String[] args) { + FlinkSecurityManager.forceProcessExit(FORCED_EXIT_CODE); + + System.exit(0); + } + } + + /** + * An entry point that expects enabling the security manager to fail with a descriptive error + * because the JVM does not allow installing one. + */ + public static final class UnavailableSecurityManagerEntryPoint { + + public static void main(String[] args) { + try { + interceptUserSystemExit(); + } catch (IllegalConfigurationException e) { + System.err.println("Caught exception: " + e.getMessage()); + if (e.getMessage().contains("JEP 486") + && e.getCause() instanceof UnsupportedOperationException) { + FlinkSecurityManager.forceProcessExit(FORCED_EXIT_CODE); + } + } + + System.exit(1); + } + } } From 7d23d541da1fc0083a0a75b769f1b1635095197b Mon Sep 17 00:00:00 2001 From: Idan Sheinberg Date: Wed, 21 Oct 2026 16:32:33 +0300 Subject: [PATCH 3/3] [FLINK-36902][docs] Document that Security Manager based options are unavailable on JDK 24+ cluster.intercept-user-system-exit and cluster.processes.halt-on-fatal-error can only work where a Java Security Manager can be installed, which is no longer possible since JDK 24 (JEP 486). Say so in the option descriptions and regenerate the configuration docs. Generated-by: Claude Code (Claude Fable 5.1) --- .../shortcodes/generated/cluster_configuration.html | 4 ++-- .../shortcodes/generated/expert_cluster_section.html | 4 ++-- .../org/apache/flink/configuration/ClusterOptions.java | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/layouts/shortcodes/generated/cluster_configuration.html b/docs/layouts/shortcodes/generated/cluster_configuration.html index 04e605d53e351..4c88b77f62423 100644 --- a/docs/layouts/shortcodes/generated/cluster_configuration.html +++ b/docs/layouts/shortcodes/generated/cluster_configuration.html @@ -18,7 +18,7 @@

cluster.intercept-user-system-exit
DISABLED

Enum

- Flag to check user code exiting system by terminating JVM (e.g., System.exit()). Note that this configuration option can interfere with cluster.processes.halt-on-fatal-error: In intercepted user-code, a call to System.exit() will not cause the JVM to halt, when THROW is configured.

Possible values: + Flag to check user code exiting system by terminating JVM (e.g., System.exit()). Note that this configuration option can interfere with cluster.processes.halt-on-fatal-error: In intercepted user-code, a call to System.exit() will not cause the JVM to halt, when THROW is configured.
This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (JEP 486). On JDK 24 and newer it must be left at its default.

Possible values:
cluster.io-pool.size
@@ -30,7 +30,7 @@
cluster.processes.halt-on-fatal-error
false Boolean - Whether processes should halt on fatal errors instead of performing a graceful shutdown. In some environments (e.g. Java 8 with the G1 garbage collector), a regular graceful shutdown can lead to a JVM deadlock. See FLINK-16510 for details. + Whether processes should halt on fatal errors instead of performing a graceful shutdown. In some environments (e.g. Java 8 with the G1 garbage collector), a regular graceful shutdown can lead to a JVM deadlock. See FLINK-16510 for details.
This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (JEP 486). On JDK 24 and newer it must be left at its default.
cluster.registration.error-delay
diff --git a/docs/layouts/shortcodes/generated/expert_cluster_section.html b/docs/layouts/shortcodes/generated/expert_cluster_section.html index 879d46790035a..8872f65974698 100644 --- a/docs/layouts/shortcodes/generated/expert_cluster_section.html +++ b/docs/layouts/shortcodes/generated/expert_cluster_section.html @@ -18,13 +18,13 @@
cluster.intercept-user-system-exit
DISABLED

Enum

- Flag to check user code exiting system by terminating JVM (e.g., System.exit()). Note that this configuration option can interfere with cluster.processes.halt-on-fatal-error: In intercepted user-code, a call to System.exit() will not cause the JVM to halt, when THROW is configured.

Possible values: + Flag to check user code exiting system by terminating JVM (e.g., System.exit()). Note that this configuration option can interfere with cluster.processes.halt-on-fatal-error: In intercepted user-code, a call to System.exit() will not cause the JVM to halt, when THROW is configured.
This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (JEP 486). On JDK 24 and newer it must be left at its default.

Possible values:
cluster.processes.halt-on-fatal-error
false Boolean - Whether processes should halt on fatal errors instead of performing a graceful shutdown. In some environments (e.g. Java 8 with the G1 garbage collector), a regular graceful shutdown can lead to a JVM deadlock. See FLINK-16510 for details. + Whether processes should halt on fatal errors instead of performing a graceful shutdown. In some environments (e.g. Java 8 with the G1 garbage collector), a regular graceful shutdown can lead to a JVM deadlock. See FLINK-16510 for details.
This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (JEP 486). On JDK 24 and newer it must be left at its default.
cluster.thread-dump.default-mode
diff --git a/flink-core/src/main/java/org/apache/flink/configuration/ClusterOptions.java b/flink-core/src/main/java/org/apache/flink/configuration/ClusterOptions.java index 6d03db19229fb..abd190c2f4e84 100644 --- a/flink-core/src/main/java/org/apache/flink/configuration/ClusterOptions.java +++ b/flink-core/src/main/java/org/apache/flink/configuration/ClusterOptions.java @@ -115,6 +115,11 @@ public class ClusterOptions { link( "https://issues.apache.org/jira/browse/FLINK-16510", "FLINK-16510")) + .linebreak() + .text( + "This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (%s). " + + "On JDK 24 and newer it must be left at its default.", + link("https://openjdk.org/jeps/486", "JEP 486")) .build()); @Documentation.Section(Documentation.Sections.EXPERT_CLUSTER) @@ -130,6 +135,11 @@ public class ClusterOptions { "Note that this configuration option can interfere with %s: " + "In intercepted user-code, a call to System.exit() will not cause the JVM to halt, when %s is configured.", code(HALT_ON_FATAL_ERROR.key()), code(THROW.name())) + .linebreak() + .text( + "This option relies on the Java Security Manager, which was permanently disabled in JDK 24 (%s). " + + "On JDK 24 and newer it must be left at its default.", + link("https://openjdk.org/jeps/486", "JEP 486")) .build()); @Documentation.Section(Documentation.Sections.EXPERT_CLUSTER)