From c6bb44ad1ddd454d3af4b358cd78d96e3ef903a8 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Mon, 27 Apr 2026 10:07:01 -0500 Subject: [PATCH 1/2] 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 <223556219+Copilot@users.noreply.github.com> --- .../tika/pipes/core/SharedServerManager.java | 30 ++++++------------- .../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.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 @@ private void startServer() throws IOException, InterruptedException, TimeoutExce 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 @@ private void startServer() throws IOException, InterruptedException, TimeoutExce // Pass port and auth token via environment variables so they are not // visible in /proc//cmdline. The token is only readable via // /proc//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 @@ private void startServer() throws IOException, InterruptedException, TimeoutExce 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 @@ private void waitForServerReady(int expectedPort) throws IOException, ServerInit 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 @@ private static void runSharedMode(int port, int numConnections, Path tikaConfigP 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"); From 1f207ec2d9a7b78a4aa81e6dccbf2a2c08a53151 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Tue, 28 Apr 2026 11:54:03 -0500 Subject: [PATCH 2/2] 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 <223556219+Copilot@users.noreply.github.com> --- .../org/apache/tika/pipes/core/SharedServerManager.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 42385d8e75..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 @@ -334,7 +334,11 @@ private int waitForServerReady() throws IOException, ServerInitializationExcepti String line = reader.readLine(); if (line != null && line.startsWith("READY:")) { String portStr = line.substring("READY:".length()).trim(); - return Integer.parseInt(portStr); + int port = Integer.parseInt(portStr); + if (port <= 0 || port > 65535) { + throw new IOException("Server reported invalid port: " + port); + } + return port; } } else { // No data available, sleep briefly