From 1973c2ea8b9924ca34c331b5fe59444d7040a024 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Tue, 14 Jul 2026 09:46:10 +0200 Subject: [PATCH 1/2] Fix port-reservation race in libtest::Server::start() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_free_port() released its reservation socket immediately, before even returning the port number to the caller, so the "reservation" only protected a few lines inside get_free_port() itself. The release_port() call in Server::start() (just before fork/exec) was consequently a no-op, since the socket was already closed by then — leaving the port free for any other concurrently-running test process's get_free_port() to grab during the fork/exec/bind window. Keep the reservation socket bound until the caller explicitly releases it, and move that release in Server::start() from right before _app.run() to right after the ping loop confirms the child is listening (or has definitively failed to start). This works because both the reservation socket and gearmand's real listening socket set SO_REUSEADDR, and the reservation socket never calls listen(), so Linux allows the real bind to the exact port to succeed while still blocking other processes from picking the same port via get_free_port(). Fixes #471 Signed-off-by: Alexei Pastuchov --- libtest/port.cc | 6 +++++- libtest/server.cc | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libtest/port.cc b/libtest/port.cc index 54dcffe61..517317adc 100644 --- a/libtest/port.cc +++ b/libtest/port.cc @@ -232,7 +232,11 @@ in_port_t get_free_port() } all_socket_fd.last_port= ret_port; - release_port(ret_port); + + // Deliberately leave the reservation socket bound (not released) so the + // port stays unavailable to other processes' get_free_port() calls until + // the caller explicitly release_port()s it, e.g. once its server is + // confirmed listening. See libtest::Server::start(). return ret_port; } diff --git a/libtest/server.cc b/libtest/server.cc index f644465bf..00fdd1384 100644 --- a/libtest/server.cc +++ b/libtest/server.cc @@ -229,8 +229,6 @@ bool Server::start() hostname(), port(), "Could not build command()"); } - libtest::release_port(_port); - Application::error_t ret; if (Application::SUCCESS != (ret= _app.run())) { @@ -306,6 +304,12 @@ bool Server::start() } } + // Only now that the server has either bound the port (ping succeeded) or + // definitively failed to start do we give up the reservation; releasing it + // any earlier reopens the port to other concurrently-running test + // processes' get_free_port() calls before this server has actually bound it. + libtest::release_port(_port); + if (pinged == false) { #if 0 From ae8b4bcc5290f75f517fd584b15c58fdc3a3006b Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Fri, 17 Jul 2026 20:31:16 +0200 Subject: [PATCH 2/2] Guarantee port reservation release on all Server::start() exit paths release_port(_port) was only called after the ping loop succeeded or timed out, so any earlier throw (Application::run() failure, pidfile wait failure) skipped it entirely and leaked the reservation socket for the rest of the test process. Replace the single explicit call with an RAII guard so the reservation is released on every exit path. Signed-off-by: Alexei Pastuchov --- libtest/server.cc | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/libtest/server.cc b/libtest/server.cc index 00fdd1384..ad243238e 100644 --- a/libtest/server.cc +++ b/libtest/server.cc @@ -229,6 +229,15 @@ bool Server::start() hostname(), port(), "Could not build command()"); } + // Hold the port reservation (acquired via get_free_port()) until start() + // returns by any path -- success, ping timeout, or exception -- so no + // other concurrently-running test process's get_free_port() can grab + // _port while this server is still starting up. + struct PortReservation { + in_port_t port; + ~PortReservation() { libtest::release_port(port); } + } port_reservation{_port}; + Application::error_t ret; if (Application::SUCCESS != (ret= _app.run())) { @@ -304,12 +313,6 @@ bool Server::start() } } - // Only now that the server has either bound the port (ping succeeded) or - // definitively failed to start do we give up the reservation; releasing it - // any earlier reopens the port to other concurrently-running test - // processes' get_free_port() calls before this server has actually bound it. - libtest::release_port(_port); - if (pinged == false) { #if 0