Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
125 changes: 125 additions & 0 deletions ext/standard/tests/streams/gh22981.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
--TEST--
gh22981: connecting to a unix socket with a full listen backlog
--SKIPIF--
<?php
if (!is_callable('proc_open')) die('skip proc_open() is not available');
/* only Linux reports EAGAIN for a connect() to a unix socket with a full
listen backlog; other systems fail with a hard error instead */
if (PHP_OS !== 'Linux') die('skip requires EAGAIN on a full unix socket backlog');
$path = sys_get_temp_dir() . '/gh22981_skip.sock';
@unlink($path);
$server = @stream_socket_server('unix://' . $path, $errno, $errstr);
if (!$server) die('skip unix domain sockets are not supported');
fclose($server);
@unlink($path);
--FILE--
<?php
$socketPath = sys_get_temp_dir() . '/gh22981_' . getmypid() . '.sock';
$stuckPath = sys_get_temp_dir() . '/gh22981_' . getmypid() . 'b.sock';
$procs = [];

register_shutdown_function(function () use ($socketPath, $stuckPath, &$procs) {
foreach ($procs as $proc) {
if (is_resource($proc[0])) {
fclose($proc[1]);
proc_terminate($proc[0]);
proc_close($proc[0]);
}
}
@unlink($socketPath);
@unlink($stuckPath);
});

if (($argv[1] ?? '') === 'server' || ($argv[1] ?? '') === 'stuck') {
$path = $argv[2];
$ctx = stream_context_create(['socket' => ['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
64 changes: 64 additions & 0 deletions main/network.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
Loading