stream-service: Avoid race condition when accepting sockets

Even if `poll()` indicates that the socket is ready it might block if
it's in blocking mode. This change avoids blocking in such cases (accept
will fail with EAGAIN/EWOULDBLOCK and `watch()` will return TRUE).

As the non-blocking mode is inherited on Windows (on Linux, the man page
documents the non-inheritance as a Linux specialty), we set the mode for
the accepted socket explicitly to blocking to match the expectations of
`stream_t`.

Fixes: daf1880b39 ("stream: add a stream service class abstracting services using BSD sockets")
This commit is contained in:
Tobias Brunner
2026-07-24 08:47:36 +02:00
parent c3b99c6982
commit b52fc6c284
2 changed files with 36 additions and 3 deletions
@@ -25,6 +25,7 @@
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
typedef struct private_stream_service_t private_stream_service_t;
@@ -149,6 +150,22 @@ static void destroy_async_data(async_data_t *data)
free(data);
}
/**
* Enable/disable non-blocking mode on a socket.
*/
static bool set_blocking(int fd, bool block)
{
#ifdef WIN32
u_long on = block ? 0 : 1;
return ioctlsocket(fd, FIONBIO, &on) == 0;
#else
int flags = fcntl(fd, F_GETFL);
return flags != -1 &&
fcntl(fd, F_SETFL, block ? (flags & ~O_NONBLOCK)
: (flags | O_NONBLOCK)) != -1;
#endif
}
/**
* Reduce running counter
*/
@@ -180,6 +197,14 @@ static job_requeue_t accept_async(async_data_t *data)
this->running++;
this->mutex->unlock(this->mutex);
/* accepted socket may inherit non-blocking mode from listening socket
* depending on the implementation, so we normalize this to blocking mode */
if (!set_blocking(data->fd, TRUE))
{
reduce_running(data);
return JOB_REQUEUE_NONE;
}
stream = stream_create_from_fd(data->fd);
if (stream)
{
@@ -296,6 +321,12 @@ stream_service_t *stream_service_create_from_fd(int fd)
{
private_stream_service_t *this;
if (!set_blocking(fd, FALSE))
{
close(fd);
return NULL;
}
INIT(this,
.public = {
.on_accept = _on_accept,
@@ -76,10 +76,12 @@ struct stream_service_t {
/**
* Create a service from a file descriptor.
*
* The file descriptor MUST be a socket.
* The file descriptor MUST be a socket. It is set to non-blocking mode to
* safely accept() connections from the watcher thread. For consistency, the
* accepted connection sockets are set to blocking mode on all platforms.
*
* @param fd file descriptor to wrap into a stream_service_t
* @return stream_service instance
* @param fd file descriptor to wrap (adopted, closed on failure)
* @return service, NULL if fd can't be switched to non-blocking mode
*/
stream_service_t *stream_service_create_from_fd(int fd);