From f22389619851699b50cf3e4171971cd33bc6e35f Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 29 Apr 2025 15:19:43 -0400 Subject: [PATCH] `StackOverflowError` in `isClosedChannelException` --- src/main/java/hudson/remoting/Channel.java | 16 +++++--- .../java/hudson/remoting/ChannelTest.java | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/java/hudson/remoting/Channel.java b/src/main/java/hudson/remoting/Channel.java index 93734396e..06f55d4a1 100644 --- a/src/main/java/hudson/remoting/Channel.java +++ b/src/main/java/hudson/remoting/Channel.java @@ -2050,17 +2050,23 @@ public static void dumpDiagnosticsForAll(@NonNull PrintWriter w) { * anywhere in the exception or suppressed exception chain. */ public static boolean isClosedChannelException(@CheckForNull Throwable t) { - if (t instanceof ClosedChannelException) { + return _isClosedChannelException(t, new HashSet<>()); + } + + private static boolean _isClosedChannelException(@CheckForNull Throwable t, Set seen) { + if (t == null) { + return false; + } else if (!seen.add(t)) { + return false; + } else if (t instanceof ClosedChannelException) { return true; } else if (t instanceof ChannelClosedException) { return true; } else if (t instanceof EOFException) { return true; - } else if (t == null) { - return false; } else { - return isClosedChannelException(t.getCause()) - || Stream.of(t.getSuppressed()).anyMatch(Channel::isClosedChannelException); + return _isClosedChannelException(t.getCause(), seen) + || Stream.of(t.getSuppressed()).anyMatch(x -> _isClosedChannelException(x, seen)); } } diff --git a/src/test/java/hudson/remoting/ChannelTest.java b/src/test/java/hudson/remoting/ChannelTest.java index 406d21709..9fc8acb40 100644 --- a/src/test/java/hudson/remoting/ChannelTest.java +++ b/src/test/java/hudson/remoting/ChannelTest.java @@ -1,5 +1,7 @@ package hudson.remoting; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -11,6 +13,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.remoting.util.GCTask; +import java.io.EOFException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; @@ -18,6 +21,7 @@ import java.io.StringWriter; import java.net.URL; import java.net.URLClassLoader; +import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.ExecutorService; @@ -28,6 +32,7 @@ import org.jenkinsci.remoting.RoleChecker; import org.jenkinsci.remoting.SerializableOnlyOverRemoting; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.jvnet.hudson.test.Issue; @@ -483,4 +488,37 @@ private void assertFailsWithChannelClosedException(Channel channel, TestRunnable } fail("Expected ChannelClosedException, but the call has completed without any exception"); } + + @Test + public void isClosedChannelException() { + assertThat(Channel.isClosedChannelException(null), is(false)); + assertThat(Channel.isClosedChannelException(new IOException()), is(false)); + assertThat(Channel.isClosedChannelException(new ClosedChannelException()), is(true)); + assertThat(Channel.isClosedChannelException(new ChannelClosedException((Channel) null, null)), is(true)); + assertThat(Channel.isClosedChannelException(new EOFException()), is(true)); + assertThat(Channel.isClosedChannelException(new RuntimeException(new ClosedChannelException())), is(true)); + { + var main = new RuntimeException(); + main.addSuppressed(new ClosedChannelException()); + assertThat(Channel.isClosedChannelException(main), is(true)); + } + { + var level2 = new RuntimeException(new ClosedChannelException()); + var level3 = new RuntimeException(); + level3.addSuppressed(level2); + assertThat(Channel.isClosedChannelException(new RuntimeException(level3)), is(true)); + } + { + var cycle1 = new RuntimeException(); + var cycle2 = new RuntimeException(cycle1); + cycle1.addSuppressed(cycle2); + assertThat(Channel.isClosedChannelException(cycle2), is(false)); + } + { + var cycle1 = new ClosedChannelException(); + var cycle2 = new RuntimeException(cycle1); + cycle1.addSuppressed(cycle2); + assertThat(Channel.isClosedChannelException(cycle2), is(true)); + } + } }