This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 42892fe8f6afe41799c3a5a896af777dff13fb7f Author: opencode <[email protected]> AuthorDate: Sun Oct 11 20:48:43 2026 +0200 Release the opened channels and selector when a cluster receiver bind fails NioReceiver.bind() opens the server socket channel and the selector before attempting the TCP bind, and opens and binds the datagram channel afterwards. When any of those steps fails, most commonly the TCP or UDP bind after the auto-bind retries are exhausted, the exception propagated out of start() without any cleanup. Since the start failure path does not call stop(), every failed start leaked the file descriptors of the server channel and of a bound but never listened-on datagram channel, plus the epoll descriptor of the selector. The datagram channel leaked even when a stop followed, as it is only registered with the selector in listen() and stopListening() never reaches it. The resource opening is now wrapped so that a failure closes what was opened up to that point, in the order datagram channel, server channel and then selector, clears the field references, and attaches any close failure to the original exception as suppressed, which is rethrown unchanged. Add two regression tests to TestNioReceiver that fail the TCP and the UDP bind against an occupied port and assert that all three references are released. The UDP test disables SO_REUSEADDR on the receiver so that the collision is deterministic on platforms where the option would otherwise permit binding an occupied UDP port. --- .../catalina/tribes/transport/nio/NioReceiver.java | 70 ++++++++++++++------ .../tribes/transport/nio/TestNioReceiver.java | 75 ++++++++++++++++++++++ 2 files changed, 125 insertions(+), 20 deletions(-) diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java index 260748e1b1..00f8b8bd55 100644 --- a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java +++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java @@ -125,26 +125,56 @@ public class NioReceiver extends ReceiverBase implements Runnable, NioReceiverMB * @throws IOException If binding fails */ protected void bind() throws IOException { - // allocate an unbound server socket channel - serverChannel = ServerSocketChannel.open(); - // Get the associated ServerSocket to bind it with - ServerSocket serverSocket = serverChannel.socket(); - // create a new Selector for use below - this.selector.set(Selector.open()); - // set the port the server channel will listen to - // serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort())); - bind(serverSocket, getPort(), getAutoBind()); - // set non-blocking mode for the listening socket - serverChannel.configureBlocking(false); - // register the ServerSocketChannel with the Selector - serverChannel.register(this.selector.get(), SelectionKey.OP_ACCEPT); - - // set up the datagram channel - if (this.getUdpPort() > 0) { - datagramChannel = DatagramChannel.open(); - configureDatagramChannel(); - // bind to the address to avoid security checks - bindUdp(datagramChannel.socket(), getUdpPort(), getAutoBind()); + try { + // allocate an unbound server socket channel + serverChannel = ServerSocketChannel.open(); + // Get the associated ServerSocket to bind it with + ServerSocket serverSocket = serverChannel.socket(); + // create a new Selector for use below + this.selector.set(Selector.open()); + // set the port the server channel will listen to + // serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort())); + bind(serverSocket, getPort(), getAutoBind()); + // set non-blocking mode for the listening socket + serverChannel.configureBlocking(false); + // register the ServerSocketChannel with the Selector + serverChannel.register(this.selector.get(), SelectionKey.OP_ACCEPT); + + // set up the datagram channel + if (this.getUdpPort() > 0) { + datagramChannel = DatagramChannel.open(); + configureDatagramChannel(); + // bind to the address to avoid security checks + bindUdp(datagramChannel.socket(), getUdpPort(), getAutoBind()); + } + } catch (IOException | RuntimeException e) { + // The caller of start() only sees the exception, so release + // everything that was opened up to the failure point. Without + // this, every failed start leaks the server channel descriptor, + // the selector and, when the UDP bind is what failed, a bound + // datagram channel that nothing will ever listen on. + if (datagramChannel != null) { + try { + datagramChannel.close(); + } catch (IOException ioe) { + e.addSuppressed(ioe); + } + datagramChannel = null; + } + if (serverChannel != null) { + try { + serverChannel.close(); + } catch (IOException ioe) { + e.addSuppressed(ioe); + } + serverChannel = null; + } + try { + closeSelector(); + } catch (IOException ioe) { + e.addSuppressed(ioe); + } + throw e; } } diff --git a/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java b/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java index 89e9360819..c85baec767 100644 --- a/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java +++ b/test/org/apache/catalina/tribes/transport/nio/TestNioReceiver.java @@ -16,8 +16,14 @@ */ package org.apache.catalina.tribes.transport.nio; +import java.io.IOException; import java.lang.reflect.Field; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; import java.nio.channels.DatagramChannel; +import java.nio.channels.ServerSocketChannel; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Assert; import org.junit.Test; @@ -121,4 +127,73 @@ public class TestNioReceiver { Assert.assertNull("the datagram channel reference was not cleared", field.get(receiver)); Assert.assertFalse(receiver.isListening()); } + + /* + * A TCP bind that exhausts the auto-bind retries throws out of bind() + * after the server channel and the selector were already opened. Both + * must be released, or every failed start leaks a descriptor and a + * selector that nothing can reach any more. + */ + @Test + public void testFailedTcpBindReleasesTheOpenedResources() throws Exception { + NioReceiver receiver = new NioReceiver(); + receiver.setChannel(new GroupChannel()); + receiver.setAddress("127.0.0.1"); + receiver.setAutoBind(1); + try (ServerSocket blocker = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) { + receiver.setPort(blocker.getLocalPort()); + try { + receiver.bind(); + Assert.fail("the bind to an occupied port did not fail"); + } catch (IOException expected) { + // expected + } + } + assertReleased(receiver); + } + + /* + * When the UDP bind is what fails, the TCP server channel was already + * bound and registered with the selector. The failed start must release + * the server channel and the selector as well, not just the half set up + * datagram channel. + */ + @Test + public void testFailedUdpBindReleasesTheOpenedResources() throws Exception { + NioReceiver receiver = new NioReceiver(); + receiver.setChannel(new GroupChannel()); + receiver.setAddress("127.0.0.1"); + receiver.setAutoBind(1); + receiver.setPort(0); + // Without SO_REUSEADDR on the receiver side as well, the bind to the + // occupied UDP port fails deterministically on every platform + receiver.setSoReuseAddress(false); + try (DatagramSocket blocker = new DatagramSocket(0, InetAddress.getByName("127.0.0.1"))) { + receiver.setUdpPort(blocker.getLocalPort()); + try { + receiver.bind(); + Assert.fail("the UDP bind to an occupied port did not fail"); + } catch (IOException expected) { + // expected + } + } + assertReleased(receiver); + } + + private void assertReleased(NioReceiver receiver) throws Exception { + Field serverChannelField = NioReceiver.class.getDeclaredField("serverChannel"); + serverChannelField.setAccessible(true); + ServerSocketChannel serverChannel = (ServerSocketChannel) serverChannelField.get(receiver); + Assert.assertNull("the server channel reference was not cleared", serverChannel); + + Field datagramChannelField = NioReceiver.class.getDeclaredField("datagramChannel"); + datagramChannelField.setAccessible(true); + DatagramChannel datagramChannel = (DatagramChannel) datagramChannelField.get(receiver); + Assert.assertNull("the datagram channel reference was not cleared", datagramChannel); + + Field selectorField = NioReceiver.class.getDeclaredField("selector"); + selectorField.setAccessible(true); + AtomicReference<?> selector = (AtomicReference<?>) selectorField.get(receiver); + Assert.assertNull("the selector reference was not cleared", selector.get()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
