From f64cc385dd7eced1f1ca79979c001aa4c768a396 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:57:46 +0200 Subject: [PATCH 1/2] * Fix feof() on sockets: reliable liveness check on Windows WSAPoll(timeout=0) fails to detect FIN packets on Windows, causing feof() to return false on closed sockets. Fix by skipping poll for liveness checks (value==0) and going directly to recv(MSG_PEEK). On Windows, MSG_DONTWAIT is unavailable, so temporarily toggle non-blocking mode via ioctlsocket. Errno is saved immediately after recv because ioctlsocket clears WSAGetLastError(). Extract shared logic into php_socket_check_liveness() in network_async.c to eliminate duplication between xp_socket.c and xp_ssl.c. --- tests/stream/038-feof_live_socket.phpt | 88 +++++++++++++++++++ tests/stream/039-feof_after_connect.phpt | 38 ++++++++ tests/stream/040-feof_after_send.phpt | 40 +++++++++ tests/stream/041-feof_after_recv.phpt | 42 +++++++++ tests/stream/042-feof_after_remote_close.phpt | 43 +++++++++ ...043-feof_after_send_then_remote_close.phpt | 47 ++++++++++ tests/stream/044-feof_with_pending_data.phpt | 47 ++++++++++ 7 files changed, 345 insertions(+) create mode 100644 tests/stream/038-feof_live_socket.phpt create mode 100644 tests/stream/039-feof_after_connect.phpt create mode 100644 tests/stream/040-feof_after_send.phpt create mode 100644 tests/stream/041-feof_after_recv.phpt create mode 100644 tests/stream/042-feof_after_remote_close.phpt create mode 100644 tests/stream/043-feof_after_send_then_remote_close.phpt create mode 100644 tests/stream/044-feof_with_pending_data.phpt diff --git a/tests/stream/038-feof_live_socket.phpt b/tests/stream/038-feof_live_socket.phpt new file mode 100644 index 00000000..e613aac0 --- /dev/null +++ b/tests/stream/038-feof_live_socket.phpt @@ -0,0 +1,88 @@ +--TEST-- +feof() returns false on a live connected socket +--FILE-- +send(stream_socket_get_name($socket, false)); + + $client = stream_socket_accept($socket, 5); + echo "Server: accepted\n"; + + // Signal that connection is established + $ch->send("connected"); + + // Wait for client to finish feof check + $ch->recv(); + + // Close client connection + fclose($client); + echo "Server: closed client\n"; + + // Signal that server closed the connection + $ch->send("closed"); + + fclose($socket); +}); + +$client = spawn(function() use ($ch) { + $address = $ch->recv(); + + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + if (!$sock) { + echo "Client: failed - $errstr\n"; + return; + } + echo "Client: connected\n"; + + // Wait for server to confirm accept + $ch->recv(); + + // Socket is alive — feof() MUST return false + $eof = feof($sock); + echo "feof on live socket: " . ($eof ? "true (BUG!)" : "false") . "\n"; + + // Tell server it can close now + $ch->send("done"); + + // Wait for server to confirm close + $ch->recv(); + + // Give TCP stack time to deliver FIN + \Async\delay(1); + + // Server closed — feof() should return true + $eof = feof($sock); + echo "feof after remote close: " . ($eof ? "true" : "false (BUG!)") . "\n"; + + fclose($sock); +}); + +await_all([$server, $client]); + +echo "End\n"; + +?> +--EXPECT-- +Start +Server: accepted +Client: connected +Server: closed client +feof on live socket: false +feof after remote close: true +End diff --git a/tests/stream/039-feof_after_connect.phpt b/tests/stream/039-feof_after_connect.phpt new file mode 100644 index 00000000..9b650045 --- /dev/null +++ b/tests/stream/039-feof_after_connect.phpt @@ -0,0 +1,38 @@ +--TEST-- +feof() returns false immediately after connect +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + $s2c->send("accepted"); + $c2s->recv(); + fclose($client); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + echo "feof after connect: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $c2s->send("done"); + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +feof after connect: false diff --git a/tests/stream/040-feof_after_send.phpt b/tests/stream/040-feof_after_send.phpt new file mode 100644 index 00000000..6143bb79 --- /dev/null +++ b/tests/stream/040-feof_after_send.phpt @@ -0,0 +1,40 @@ +--TEST-- +feof() returns false after sending data +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + $s2c->send("accepted"); + $c2s->recv(); + fclose($client); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + fwrite($sock, "hello"); + + echo "feof after send: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $c2s->send("done"); + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +feof after send: false diff --git a/tests/stream/041-feof_after_recv.phpt b/tests/stream/041-feof_after_recv.phpt new file mode 100644 index 00000000..49be1551 --- /dev/null +++ b/tests/stream/041-feof_after_recv.phpt @@ -0,0 +1,42 @@ +--TEST-- +feof() returns false after receiving data +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + fwrite($client, "hello from server"); + $s2c->send("sent"); + $c2s->recv(); + fclose($client); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + $data = fread($sock, 1024); + echo "received: $data\n"; + echo "feof after recv: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $c2s->send("done"); + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +received: hello from server +feof after recv: false diff --git a/tests/stream/042-feof_after_remote_close.phpt b/tests/stream/042-feof_after_remote_close.phpt new file mode 100644 index 00000000..6b98165b --- /dev/null +++ b/tests/stream/042-feof_after_remote_close.phpt @@ -0,0 +1,43 @@ +--TEST-- +feof() returns true after remote close +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + $s2c->send("accepted"); + $c2s->recv(); + fclose($client); + $s2c->send("closed"); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + $c2s->send("close now"); + $s2c->recv(); + + \Async\delay(1); + + echo "feof after remote close: " . (feof($sock) ? "true" : "false (BUG!)") . "\n"; + + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +feof after remote close: true diff --git a/tests/stream/043-feof_after_send_then_remote_close.phpt b/tests/stream/043-feof_after_send_then_remote_close.phpt new file mode 100644 index 00000000..c37f312b --- /dev/null +++ b/tests/stream/043-feof_after_send_then_remote_close.phpt @@ -0,0 +1,47 @@ +--TEST-- +feof() after send then remote close +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + $s2c->send("accepted"); + $c2s->recv(); + fclose($client); + $s2c->send("closed"); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + fwrite($sock, "payload"); + echo "feof after send: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $c2s->send("close now"); + $s2c->recv(); + + \Async\delay(1); + + echo "feof after remote close: " . (feof($sock) ? "true" : "false (BUG!)") . "\n"; + + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +feof after send: false +feof after remote close: true diff --git a/tests/stream/044-feof_with_pending_data.phpt b/tests/stream/044-feof_with_pending_data.phpt new file mode 100644 index 00000000..a940d7b0 --- /dev/null +++ b/tests/stream/044-feof_with_pending_data.phpt @@ -0,0 +1,47 @@ +--TEST-- +feof() returns false when unread data is pending +--FILE-- +send(stream_socket_get_name($socket, false)); + $client = stream_socket_accept($socket, 5); + fwrite($client, "data in buffer"); + $s2c->send("sent"); + $c2s->recv(); + fclose($client); + fclose($socket); +}); + +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); + $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); + $s2c->recv(); + + \Async\delay(1); + + echo "feof with pending data: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $data = fread($sock, 1024); + echo "read: $data\n"; + echo "feof after reading all: " . (feof($sock) ? "true (BUG!)" : "false") . "\n"; + + $c2s->send("done"); + fclose($sock); +}); + +await_all([$server, $client]); + +?> +--EXPECT-- +feof with pending data: false +read: data in buffer +feof after reading all: false From 91418646e08c39edb8fc2cc98e33b0b2605b3eff Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sun, 15 Mar 2026 08:04:57 +0000 Subject: [PATCH 2/2] * fix 038-feof_live_socket.phpt --- tests/stream/038-feof_live_socket.phpt | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/stream/038-feof_live_socket.phpt b/tests/stream/038-feof_live_socket.phpt index e613aac0..97eba83e 100644 --- a/tests/stream/038-feof_live_socket.phpt +++ b/tests/stream/038-feof_live_socket.phpt @@ -9,9 +9,10 @@ use function Async\await_all; echo "Start\n"; -$ch = new Channel(1); +$s2c = new Channel(1); // server -> client +$c2s = new Channel(1); // client -> server -$server = spawn(function() use ($ch) { +$server = spawn(function() use ($s2c, $c2s) { $socket = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); if (!$socket) { echo "Server: failed - $errstr\n"; @@ -19,29 +20,29 @@ $server = spawn(function() use ($ch) { } // Signal the address to the client - $ch->send(stream_socket_get_name($socket, false)); + $s2c->send(stream_socket_get_name($socket, false)); $client = stream_socket_accept($socket, 5); echo "Server: accepted\n"; // Signal that connection is established - $ch->send("connected"); + $s2c->send("connected"); // Wait for client to finish feof check - $ch->recv(); + $c2s->recv(); // Close client connection fclose($client); echo "Server: closed client\n"; // Signal that server closed the connection - $ch->send("closed"); + $s2c->send("closed"); fclose($socket); }); -$client = spawn(function() use ($ch) { - $address = $ch->recv(); +$client = spawn(function() use ($s2c, $c2s) { + $address = $s2c->recv(); $sock = stream_socket_client("tcp://$address", $errno, $errstr, 5); if (!$sock) { @@ -51,17 +52,17 @@ $client = spawn(function() use ($ch) { echo "Client: connected\n"; // Wait for server to confirm accept - $ch->recv(); + $s2c->recv(); // Socket is alive — feof() MUST return false $eof = feof($sock); echo "feof on live socket: " . ($eof ? "true (BUG!)" : "false") . "\n"; // Tell server it can close now - $ch->send("done"); + $c2s->send("done"); // Wait for server to confirm close - $ch->recv(); + $s2c->recv(); // Give TCP stack time to deliver FIN \Async\delay(1); @@ -78,11 +79,11 @@ await_all([$server, $client]); echo "End\n"; ?> ---EXPECT-- +--EXPECTF-- Start -Server: accepted -Client: connected +%AServer: accepted +%AClient: connected +%Afeof on live socket: false Server: closed client -feof on live socket: false feof after remote close: true End