This is an automated email from the ASF dual-hosted git repository.

nddipiazza pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 4e0340b8a2 TIKA-4721: Fix TOCTOU race in SharedServerManager port 
assignment (#2791)
4e0340b8a2 is described below

commit 4e0340b8a2552d23e7be86a39293a5b0ef0b11f7
Author: Nicholas DiPiazza <[email protected]>
AuthorDate: Tue Apr 28 17:42:13 2026 +0000

    TIKA-4721: Fix TOCTOU race in SharedServerManager port assignment (#2791)
    
    * 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-4721: Validate port range from READY signal
    
    Throw IOException if the server reports a port outside [1, 65535]
    to catch any malformed or out-of-range values early.
    
    Co-authored-by: Copilot <[email protected]>
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 .../tika/pipes/core/SharedServerManager.java       | 32 ++++++++--------------
 .../apache/tika/pipes/core/server/PipesServer.java |  4 +--
 2 files changed, 14 insertions(+), 22 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..c5b2126ada 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,12 @@ 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);
+                        int port = Integer.parseInt(portStr);
+                        if (port <= 0 || port > 65535) {
+                            throw new IOException("Server reported invalid 
port: " + port);
                         }
-                        return;
+                        return port;
                     }
                 } 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