diff --git a/NEWS b/NEWS index 1212b4fae181..6558ecd450d5 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,11 @@ PHP NEWS . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when __serialize() removes an element). (David Carlier) +- Streams: + . Fixed bug GH-22981 (Connecting to a unix socket whose listen backlog is + full fails with "Resource temporarily unavailable" instead of waiting). + (Zhiqi Zhang) + 10 Sep 2026, PHP 8.6.0beta3 diff --git a/ext/standard/tests/streams/gh22981.phpt b/ext/standard/tests/streams/gh22981.phpt new file mode 100644 index 000000000000..c5eac6b74315 --- /dev/null +++ b/ext/standard/tests/streams/gh22981.phpt @@ -0,0 +1,125 @@ +--TEST-- +gh22981: connecting to a unix socket with a full listen backlog +--SKIPIF-- + ['backlog' => 1]]); + $server = stream_socket_server('unix://' . $path, $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); + if (!$server) { + fwrite(STDERR, "server: $errstr\n"); + exit(1); + } + fwrite(STDOUT, "ready\n"); + if ($argv[1] === 'stuck') { + /* never accept anything */ + sleep(3); + exit(0); + } + /* let the clients pile up in the listen backlog before accepting */ + usleep(200000); + $end = microtime(true) + 5; + for ($accepted = 0; $accepted < 20 && microtime(true) < $end;) { + $conn = @stream_socket_accept($server, 0.1); + if ($conn) { + fwrite($conn, "pong\n"); + $accepted++; + } + } + exit(0); +} + +/* the server starts accepting only after a delay: everyone has to wait for + it, but nobody fails */ +$proc = proc_open([PHP_BINARY, __FILE__, 'server', $socketPath], + [1 => ['pipe', 'w']], $pipes); +if (!is_resource($proc)) { + echo "cannot start server\n"; + exit(1); +} +$procs[] = [$proc, $pipes[1]]; +fgets($pipes[1]); /* wait until the server is listening */ + +/* listen(1) only leaves room for a couple of pending connections, so most + of these have to wait for the server to accept */ +$conns = []; +for ($i = 0; $i < 20; $i++) { + $conn = stream_socket_client('unix://' . $socketPath, $errno, $errstr, 5); + if (!$conn) { + printf("connect #%d failed: %s\n", $i + 1, $errstr); + exit(1); + } + $conns[] = $conn; +} + +if (trim((string) fgets($conns[count($conns) - 1])) !== 'pong') { + echo "no pong\n"; + exit(1); +} + +echo "ok\n"; + +/* the server never accepts: the connect timeout has to be honoured */ +$proc = proc_open([PHP_BINARY, __FILE__, 'stuck', $stuckPath], + [1 => ['pipe', 'w']], $pipes); +if (!is_resource($proc)) { + echo "cannot start stuck server\n"; + exit(1); +} +$procs[] = [$proc, $pipes[1]]; +fgets($pipes[1]); /* wait until the server is listening */ + +for ($i = 0; $i < 2; $i++) { + $conn = stream_socket_client('unix://' . $stuckPath, $errno, $errstr, 3); + if (!$conn) { + printf("fill #%d failed: %s\n", $i + 1, $errstr); + exit(1); + } + $conns[] = $conn; +} + +$t = microtime(true); +$conn = @stream_socket_client('unix://' . $stuckPath, $errno, $errstr, 0.5); +$elapsed = microtime(true) - $t; + +if ($conn) { + echo "connect unexpectedly succeeded\n"; +} elseif ($elapsed < 0.45 || $elapsed > 2.5) { + printf("connect timeout not honoured: %.2fs (%s)\n", $elapsed, $errstr); +} elseif (strpos($errstr, 'timed out') === false) { + printf("unexpected error: %s\n", $errstr); +} else { + echo "timeout ok\n"; +} +--EXPECT-- +ok +timeout ok diff --git a/main/network.c b/main/network.c index 01e4f2e778d4..ae6f0aa7d047 100644 --- a/main/network.c +++ b/main/network.c @@ -322,6 +322,13 @@ static inline void php_network_set_limit_time(struct timeval *limit_time, } #endif +/* whether a connect() error means the attempt has to be made again */ +#if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN +# define CONNECT_WOULD_BLOCK(e) ((e) == EAGAIN || (e) == EWOULDBLOCK) +#else +# define CONNECT_WOULD_BLOCK(e) ((e) == EAGAIN) +#endif + /* Connect to a socket using an interruptible connect with optional timeout. * Optionally, the connect can be made asynchronously, which will implicitly * enable non-blocking mode on the socket. @@ -350,6 +357,63 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, *error_code = error; } +#ifdef AF_UNIX + /* connect() to a unix domain socket whose listen backlog is full + * fails with EAGAIN while the socket is in non-blocking mode, + * whereas a blocking connect would wait for a slot to free up. + * Wait and retry until the timeout (if any) expires instead of + * surfacing the error to the caller. */ + if (!asynchronous + && CONNECT_WOULD_BLOCK(error) + && addr->sa_family == AF_UNIX) { +#ifdef HAVE_GETTIMEOFDAY + struct timeval limit_time, time_now, remaining; + + if (timeout) { + php_network_set_limit_time(&limit_time, timeout); + } +#endif + + while (true) { + struct timeval slice = {0, 10000}; + +#ifdef HAVE_GETTIMEOFDAY + if (timeout) { + gettimeofday(&time_now, NULL); + + if (!timercmp(&time_now, &limit_time, <)) { + error = PHP_TIMEOUT_ERROR_VALUE; + break; + } + sub_times(limit_time, time_now, &remaining); + if (timercmp(&remaining, &slice, <)) { + slice = remaining; + } + } +#endif + /* nothing to poll for here, the connection never started */ + php_pollfd_for(sockfd, 0, &slice); + if ((n = connect(sockfd, addr, addrlen)) == 0) { + error = 0; + goto ok; + } + error = php_socket_errno(); + if (!CONNECT_WOULD_BLOCK(error)) { + break; + } + } + + if (error_code) { + *error_code = error; + } + if (error_string) { + *error_string = php_socket_error_str(error); + } + + return -1; + } +#endif + if (error != EINPROGRESS) { if (error_string) { *error_string = php_socket_error_str(error);