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