Skip to content

Commit 6897971

Browse files
committed
Fix GH-13220: stream_socket_accept() timeout sometimes does not work
1 parent 73b1ebf commit 6897971

File tree

1 file changed

+128
-17
lines changed

1 file changed

+128
-17
lines changed

main/network.c

Lines changed: 128 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,30 @@ PHPAPI int php_network_get_sock_name(php_socket_t sock,
753753
* version of the address will be emalloc'd and returned.
754754
* */
755755

756+
/* Helper functions for timeout calculation */
757+
static struct timeval php_network_subtract_timeval(struct timeval a, struct timeval b)
758+
{
759+
struct timeval difference;
760+
difference.tv_sec = a.tv_sec - b.tv_sec;
761+
difference.tv_usec = a.tv_usec - b.tv_usec;
762+
if (a.tv_usec < b.tv_usec) {
763+
difference.tv_sec -= 1L;
764+
difference.tv_usec += 1000000L;
765+
}
766+
return difference;
767+
}
768+
769+
static int php_network_compare_timeval(struct timeval a, struct timeval b)
770+
{
771+
if (a.tv_sec > b.tv_sec || (a.tv_sec == b.tv_sec && a.tv_usec > b.tv_usec)) {
772+
return 1;
773+
} else if (a.tv_sec == b.tv_sec && a.tv_usec == b.tv_usec) {
774+
return 0;
775+
} else {
776+
return -1;
777+
}
778+
}
779+
756780
/* {{{ php_network_accept_incoming */
757781
PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
758782
zend_string **textaddr,
@@ -768,33 +792,120 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
768792
int error = 0, n;
769793
php_sockaddr_storage sa;
770794
socklen_t sl;
795+
int original_flags = 0;
796+
int made_nonblocking = 0;
797+
struct timeval start_time, *current_timeout = timeout;
798+
struct timeval remaining_timeout;
799+
int has_timeout = 0;
800+
801+
/* Initialize timeout tracking if we have a timeout */
802+
if (timeout && (timeout->tv_sec > 0 || timeout->tv_usec > 0)) {
803+
has_timeout = 1;
804+
/* gettimeofday is not monotonic; using it here is not strictly correct */
805+
gettimeofday(&start_time, NULL);
806+
remaining_timeout = *timeout;
807+
current_timeout = &remaining_timeout;
808+
}
771809

772-
n = php_pollfd_for(srvsock, PHP_POLLREADABLE, timeout);
810+
while (1) {
811+
n = php_pollfd_for(srvsock, PHP_POLLREADABLE, current_timeout);
773812

774-
if (n == 0) {
775-
error = PHP_TIMEOUT_ERROR_VALUE;
776-
} else if (n == -1) {
777-
error = php_socket_errno();
778-
} else {
779-
sl = sizeof(sa);
813+
if (n == 0) {
814+
error = PHP_TIMEOUT_ERROR_VALUE;
815+
break;
816+
} else if (n == -1) {
817+
error = php_socket_errno();
818+
break;
819+
} else {
820+
sl = sizeof(sa);
780821

781-
clisock = accept(srvsock, (struct sockaddr*)&sa, &sl);
822+
/* Get original socket flags */
823+
original_flags = fcntl(srvsock, F_GETFL, 0);
824+
if (original_flags == -1) {
825+
error = php_socket_errno();
826+
break;
827+
}
828+
829+
/* Make socket non-blocking if it wasn't already */
830+
if (!(original_flags & O_NONBLOCK)) {
831+
if (fcntl(srvsock, F_SETFL, original_flags | O_NONBLOCK) == -1) {
832+
error = php_socket_errno();
833+
break;
834+
}
835+
made_nonblocking = 1;
836+
}
837+
838+
clisock = accept(srvsock, (struct sockaddr*)&sa, &sl);
782839

783-
if (clisock != SOCK_ERR) {
784-
php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl,
785-
textaddr,
786-
addr, addrlen
787-
);
788-
if (tcp_nodelay) {
840+
/* Restore original blocking mode if we changed it */
841+
if (made_nonblocking) {
842+
fcntl(srvsock, F_SETFL, original_flags);
843+
made_nonblocking = 0;
844+
}
845+
846+
if (clisock != SOCK_ERR) {
847+
/* Successfully accepted connection */
848+
php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl,
849+
textaddr,
850+
addr, addrlen
851+
);
852+
if (tcp_nodelay) {
789853
#ifdef TCP_NODELAY
790-
setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay));
854+
setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay));
855+
#endif
856+
}
857+
break; /* Success - exit the loop */
858+
} else {
859+
error = php_socket_errno();
860+
861+
/* If we got EAGAIN/EWOULDBLOCK, the connection was accepted by another process
862+
* Go back to polling with remaining timeout */
863+
#if EAGAIN == EWOULDBLOCK
864+
if (error == EAGAIN) {
865+
#else
866+
if (error == EAGAIN || error == EWOULDBLOCK) {
791867
#endif
868+
if (has_timeout) {
869+
struct timeval cur_time, elapsed_time;
870+
871+
/* Calculate how much time has elapsed */
872+
gettimeofday(&cur_time, NULL);
873+
elapsed_time = php_network_subtract_timeval(cur_time, start_time);
874+
875+
/* Check if we've already exceeded the timeout */
876+
if (php_network_compare_timeval(elapsed_time, *timeout) >= 0) {
877+
error = PHP_TIMEOUT_ERROR_VALUE;
878+
break;
879+
}
880+
881+
/* Calculate remaining timeout */
882+
remaining_timeout = php_network_subtract_timeval(*timeout, elapsed_time);
883+
884+
/* Ensure we don't have negative values */
885+
if (remaining_timeout.tv_sec < 0 ||
886+
(remaining_timeout.tv_sec == 0 && remaining_timeout.tv_usec <= 0)) {
887+
error = PHP_TIMEOUT_ERROR_VALUE;
888+
break;
889+
}
890+
891+
current_timeout = &remaining_timeout;
892+
}
893+
894+
error = 0; /* Reset error - this is expected behavior */
895+
continue; /* Go back to poll with reduced timeout */
896+
} else {
897+
/* Real error occurred */
898+
break;
899+
}
792900
}
793-
} else {
794-
error = php_socket_errno();
795901
}
796902
}
797903

904+
/* Clean up if we still have non-blocking mode set (error case) */
905+
if (made_nonblocking) {
906+
fcntl(srvsock, F_SETFL, original_flags);
907+
}
908+
798909
if (error_code) {
799910
*error_code = error;
800911
}

0 commit comments

Comments
 (0)