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");

Reply via email to