This is an automated email from the ASF dual-hosted git repository. nddipiazza pushed a commit to branch TIKA-4721-fix-locale-sensitive-tests in repository https://gitbox.apache.org/repos/asf/tika.git
commit c6bb44ad1ddd454d3af4b358cd78d96e3ef903a8 Author: Nicholas DiPiazza <[email protected]> AuthorDate: Mon Apr 27 10:07:01 2026 -0500 TIKA-4721: Fix TOCTOU race in SharedServerManager port assignment Pass TIKA_PIPES_PORT=0 so the server binds to any available ephemeral port. PipesServer now reports the actual bound port in its READY:{port} stdout signal, which SharedServerManager reads and uses directly. This eliminates the classic probe-close-rebind race where a probed free port could be grabbed by another process (especially on slow Windows CI with TIME_WAIT delays) between the probe ServerSocket being closed and the child process binding to that port. Root cause of intermittent testGracefulShutdown failures on the Windows multi-locale CI job (tr_TR.UTF-8 / de_DE.UTF-8 runs). Co-authored-by: Copilot <[email protected]> --- .../tika/pipes/core/SharedServerManager.java | 30 +++++++--------------- .../apache/tika/pipes/core/server/PipesServer.java | 4 +-- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/SharedServerManager.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/SharedServerManager.java index a28148f684..42385d8e75 100644 --- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/SharedServerManager.java +++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/SharedServerManager.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -242,14 +241,6 @@ public class SharedServerManager implements ServerManager { shutdownUnsafe(); } - // Find a free port for the server to listen on - int port; - try (ServerSocket tempSocket = new ServerSocket()) { - tempSocket.setReuseAddress(true); - tempSocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - port = tempSocket.getLocalPort(); - } - // Generate auth token for this server instance byte[] token = new byte[PipesServer.AUTH_TOKEN_LENGTH_BYTES]; new SecureRandom().nextBytes(token); @@ -287,7 +278,10 @@ public class SharedServerManager implements ServerManager { // Pass port and auth token via environment variables so they are not // visible in /proc/<pid>/cmdline. The token is only readable via // /proc/<pid>/environ which requires same-uid access. - pb.environment().put("TIKA_PIPES_PORT", Integer.toString(port)); + // Pass port=0 so the server binds to any available ephemeral port. + // The actual port is read back from the READY:{port} stdout signal, + // eliminating the TOCTOU race between probing a free port and binding it. + pb.environment().put("TIKA_PIPES_PORT", "0"); pb.environment().put("TIKA_PIPES_AUTH_TOKEN", HexFormat.of().formatHex(token)); // Redirect stderr to inherit, capture stdout to read the READY signal pb.redirectErrorStream(false); @@ -306,13 +300,12 @@ public class SharedServerManager implements ServerManager { throw new ServerInitializationException(msg, e); } - // Wait for the server to signal it's ready by printing the port - waitForServerReady(port); - serverPort = port; - LOG.info("Shared server started successfully"); + // Wait for the server to signal it's ready and report the port it actually bound to + serverPort = waitForServerReady(); + LOG.info("Shared server started successfully on port {}", serverPort); } - private void waitForServerReady(int expectedPort) throws IOException, ServerInitializationException { + private int waitForServerReady() throws IOException, ServerInitializationException { long startTime = System.currentTimeMillis(); try (BufferedReader reader = new BufferedReader( @@ -340,13 +333,8 @@ public class SharedServerManager implements ServerManager { if (reader.ready()) { String line = reader.readLine(); if (line != null && line.startsWith("READY:")) { - // Server is ready, parse the port String portStr = line.substring("READY:".length()).trim(); - int actualPort = Integer.parseInt(portStr); - if (actualPort != expectedPort) { - LOG.warn("Server reported different port {} than expected {}", actualPort, expectedPort); - } - return; + return Integer.parseInt(portStr); } } else { // No data available, sleep briefly diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java index dc214514be..fb7a74551f 100644 --- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java +++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java @@ -255,8 +255,8 @@ public class PipesServer implements AutoCloseable { serverSocket.setReuseAddress(true); serverSocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), numConnections); - // Signal readiness to the parent process via stdout - System.out.println("READY:" + port); + // Signal readiness to the parent process via stdout, reporting the actual bound port + System.out.println("READY:" + serverSocket.getLocalPort()); System.out.flush(); LOG.info("Shared server ready, accepting connections");
