From 96b9d2ce5cb83e1363810bda0e44c7548d38367e Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 13 May 2025 21:10:19 +0000 Subject: [PATCH 1/5] ext/sockets: FreeBSD SO_SPLICE support. for zero copy socket splicing, so no userland part involved. --- ext/sockets/php_sockets.h | 3 ++ ext/sockets/sockets.c | 52 +++++++++++++++++++++++++++++++++++ ext/sockets/sockets.stub.php | 16 +++++++++++ ext/sockets/sockets_arginfo.h | 36 +++++++++++++++++++++++- 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/ext/sockets/php_sockets.h b/ext/sockets/php_sockets.h index 89c613ba43074..7342e7a2f24f3 100644 --- a/ext/sockets/php_sockets.h +++ b/ext/sockets/php_sockets.h @@ -76,6 +76,9 @@ typedef struct { } php_socket; extern PHP_SOCKETS_API zend_class_entry *socket_ce; +#ifdef SO_SPLICE +extern PHP_SOCKETS_API zend_class_entry *socket_so_splice_ce; +#endif static inline php_socket *socket_from_obj(zend_object *obj) { return (php_socket *)((char *)(obj) - XtOffsetOf(php_socket, std)); diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 2d8b55c6b97ba..b72aa083c635e 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -121,6 +121,10 @@ static PHP_RSHUTDOWN_FUNCTION(sockets); zend_class_entry *socket_ce; static zend_object_handlers socket_object_handlers; +#ifdef SO_SPLICE +zend_class_entry *socket_so_splice_ce; +#endif + static zend_object *socket_create_object(zend_class_entry *class_type) { php_socket *intern = zend_object_alloc(sizeof(php_socket), class_type); @@ -482,6 +486,10 @@ static PHP_MINIT_FUNCTION(sockets) socket_object_handlers.get_gc = socket_get_gc; socket_object_handlers.compare = zend_objects_not_comparable; +#if defined(SO_SPLICE) + socket_so_splice_ce = register_class_SocketSoSplice(); +#endif + address_info_ce = register_class_AddressInfo(); address_info_ce->create_object = address_info_create_object; address_info_ce->default_object_handlers = &address_info_object_handlers; @@ -2301,6 +2309,50 @@ PHP_FUNCTION(socket_set_option) } #endif +#ifdef SO_SPLICE + case SO_SPLICE: { + if (Z_TYPE_P(arg4) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(arg4), socket_so_splice_ce)) { + zend_argument_type_error(4, "must be of type object, %s given", zend_zval_value_name(arg4)); + RETURN_THROWS(); + } + + struct splice s = {0}; + zend_object *so_splice_obj = Z_OBJ_P(arg4); + zval tmp; + + zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), 0, &tmp); + zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), 0, &tmp); + zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), 0, &tmp); + + php_socket *php_sock = Z_SOCKET_P(socket); + zend_long php_max = Z_LVAL_P(max); + zend_array *php_arr = Z_ARRVAL_P(array); + + ENSURE_SOCKET_VALID(php_sock); + + zval *tv_sec; + zval *tv_usec; + + if ((tv_sec = zend_hash_str_find(php_arr, "tv_sec", strlen("tv_sec"))) == NULL) { + zend_argument_value_error(4, "time must have key \"tv_sec\""); + RETURN_THROWS(); + } + if ((tv_usec = zend_hash_str_find(php_arr, "tv_usec", strlen("tv_usec"))) == NULL) { + zend_argument_value_error(4, "time must have key \"tv_sec\""); + RETURN_THROWS(); + } + + s.sp_fd = (int)php_sock->bsd_socket; + s.sp_max = (off_t)php_max; + s.sp_idle.tv_sec = Z_LVAL_P(tv_sec); + s.sp_idle.tv_usec = Z_LVAL_P(tv_usec); + + opt_ptr = &s; + optlen = sizeof(s); + break; + } +#endif + default: default_case: ov = zval_get_long(arg4); diff --git a/ext/sockets/sockets.stub.php b/ext/sockets/sockets.stub.php index d647f46b80dca..f02f4de2188be 100644 --- a/ext/sockets/sockets.stub.php +++ b/ext/sockets/sockets.stub.php @@ -1907,6 +1907,13 @@ * @cvalue SO_NOSIGPIPE */ const SO_NOSIGPIPE = UNKNOWN; +#if defined(SO_SPLICE) +/** + * @var int + * @cvalue SO_SPLICE + */ +const SO_SPLICE = UNKNOWN; +#endif #endif #if defined(TCP_QUICKACK) /** @@ -2038,6 +2045,15 @@ final class AddressInfo { } +#ifdef SO_SPLICE +final class SocketSoSplice +{ + public \Socket $socket; + public int $max; + public array $time; +} +#endif + function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0): int|false {} function socket_create_listen(int $port, int $backlog = SOMAXCONN): Socket|false {} diff --git a/ext/sockets/sockets_arginfo.h b/ext/sockets/sockets_arginfo.h index 45714fb285174..f51481048ca80 100644 --- a/ext/sockets/sockets_arginfo.h +++ b/ext/sockets/sockets_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 42d486d2666d23569e70860e2b1ef203161792b3 */ + * Stub hash: 1350cf0464d5dbcf3bec0ad0f26445da8153aedc */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_socket_select, 0, 4, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(1, read, IS_ARRAY, 1) @@ -1057,6 +1057,9 @@ static void register_sockets_symbols(int module_number) #if defined(SO_NOSIGPIPE) REGISTER_LONG_CONSTANT("SO_NOSIGPIPE", SO_NOSIGPIPE, CONST_PERSISTENT); #endif +#if defined(SO_NOSIGPIPE) && defined(SO_SPLICE) + REGISTER_LONG_CONSTANT("SO_SPLICE", SO_SPLICE, CONST_PERSISTENT); +#endif #if defined(TCP_QUICKACK) REGISTER_LONG_CONSTANT("TCP_QUICKACK", TCP_QUICKACK, CONST_PERSISTENT); #endif @@ -1129,3 +1132,34 @@ static zend_class_entry *register_class_AddressInfo(void) return class_entry; } + +#if defined(SO_SPLICE) +static zend_class_entry *register_class_SocketSoSplice(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "SocketSoSplice", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + + zval property_socket_default_value; + ZVAL_UNDEF(&property_socket_default_value); + zend_string *property_socket_name = zend_string_init("socket", sizeof("socket") - 1, 1); + zend_string *property_socket_class_Socket = zend_string_init("Socket", sizeof("Socket")-1, 1); + zend_declare_typed_property(class_entry, property_socket_name, &property_socket_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_socket_class_Socket, 0, 0)); + zend_string_release(property_socket_name); + + zval property_max_default_value; + ZVAL_UNDEF(&property_max_default_value); + zend_string *property_max_name = zend_string_init("max", sizeof("max") - 1, 1); + zend_declare_typed_property(class_entry, property_max_name, &property_max_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_max_name); + + zval property_time_default_value; + ZVAL_UNDEF(&property_time_default_value); + zend_string *property_time_name = zend_string_init("time", sizeof("time") - 1, 1); + zend_declare_typed_property(class_entry, property_time_name, &property_time_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); + zend_string_release(property_time_name); + + return class_entry; +} +#endif From 56aa6e5f5b4f97ab9e258a59349c74abc8841333 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 13 May 2025 23:23:27 +0000 Subject: [PATCH 2/5] early values checks --- ext/sockets/sockets.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index b72aa083c635e..8fd2283627dc6 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -2330,6 +2330,11 @@ PHP_FUNCTION(socket_set_option) ENSURE_SOCKET_VALID(php_sock); + if (php_max < 0) { + zend_argument_value_error(4, "\"max\" key must be greater than equal to 0"); + RETURN_THROWS(); + } + zval *tv_sec; zval *tv_usec; @@ -2342,10 +2347,16 @@ PHP_FUNCTION(socket_set_option) RETURN_THROWS(); } + if (Z_LVAL_P(tv_sec) > 999999) { + s.sp_idle.tv_sec = Z_LVAL_P(tv_sec) + (Z_LVAL_P(tv_usec) / 1000000); + s.sp_idle.tv_usec = Z_LVAL_P(tv_usec) % 1000000; + } else { + s.sp_idle.tv_sec = Z_LVAL_P(tv_sec); + s.sp_idle.tv_usec = Z_LVAL_P(tv_usec); + } + s.sp_fd = (int)php_sock->bsd_socket; s.sp_max = (off_t)php_max; - s.sp_idle.tv_sec = Z_LVAL_P(tv_sec); - s.sp_idle.tv_usec = Z_LVAL_P(tv_usec); opt_ptr = &s; optlen = sizeof(s); From 087aa3ae471f1168cbb30787dfd17936c78850db Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 13 May 2025 23:49:59 +0000 Subject: [PATCH 3/5] first tests --- ext/sockets/sockets.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 8fd2283627dc6..06471e6145098 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -2312,17 +2312,16 @@ PHP_FUNCTION(socket_set_option) #ifdef SO_SPLICE case SO_SPLICE: { if (Z_TYPE_P(arg4) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(arg4), socket_so_splice_ce)) { - zend_argument_type_error(4, "must be of type object, %s given", zend_zval_value_name(arg4)); + zend_argument_type_error(4, "must be of type SocketSoSplice, %s given", zend_zval_value_name(arg4)); RETURN_THROWS(); } - struct splice s = {0}; zend_object *so_splice_obj = Z_OBJ_P(arg4); - zval tmp; + zval tmpA, tmpB, tmpC; - zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), 0, &tmp); - zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), 0, &tmp); - zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), 0, &tmp); + zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), 0, &tmpA); + zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), 0, &tmpB); + zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), 0, &tmpC); php_socket *php_sock = Z_SOCKET_P(socket); zend_long php_max = Z_LVAL_P(max); @@ -2331,22 +2330,25 @@ PHP_FUNCTION(socket_set_option) ENSURE_SOCKET_VALID(php_sock); if (php_max < 0) { - zend_argument_value_error(4, "\"max\" key must be greater than equal to 0"); + zend_argument_value_error(4, "\"max\" key must be greater than or equal to 0"); RETURN_THROWS(); } zval *tv_sec; zval *tv_usec; - if ((tv_sec = zend_hash_str_find(php_arr, "tv_sec", strlen("tv_sec"))) == NULL) { - zend_argument_value_error(4, "time must have key \"tv_sec\""); + if ((tv_sec = zend_hash_str_find(php_arr, "sec", strlen("sec"))) == NULL) { + zend_argument_value_error(4, "time must have key \"sec\""); RETURN_THROWS(); } - if ((tv_usec = zend_hash_str_find(php_arr, "tv_usec", strlen("tv_usec"))) == NULL) { - zend_argument_value_error(4, "time must have key \"tv_sec\""); + if ((tv_usec = zend_hash_str_find(php_arr, "usec", strlen("usec"))) == NULL) { + zend_argument_value_error(4, "time must have key \"usec\""); RETURN_THROWS(); } + struct splice s; + s.sp_fd = (int)php_sock->bsd_socket; + s.sp_max = (off_t)php_max; if (Z_LVAL_P(tv_sec) > 999999) { s.sp_idle.tv_sec = Z_LVAL_P(tv_sec) + (Z_LVAL_P(tv_usec) / 1000000); s.sp_idle.tv_usec = Z_LVAL_P(tv_usec) % 1000000; @@ -2355,9 +2357,6 @@ PHP_FUNCTION(socket_set_option) s.sp_idle.tv_usec = Z_LVAL_P(tv_usec); } - s.sp_fd = (int)php_sock->bsd_socket; - s.sp_max = (off_t)php_max; - opt_ptr = &s; optlen = sizeof(s); break; From 9ecb367a4b2f0d55c2266334b70eab2039e50905 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 14 May 2025 22:24:06 +0000 Subject: [PATCH 4/5] forgotten test and fix bugs --- ext/sockets/sockets.c | 14 ++++-- .../tests/socket_set_option_so_splice.phpt | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 ext/sockets/tests/socket_set_option_so_splice.phpt diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 06471e6145098..83cd2f124eab4 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -2319,9 +2319,17 @@ PHP_FUNCTION(socket_set_option) zend_object *so_splice_obj = Z_OBJ_P(arg4); zval tmpA, tmpB, tmpC; - zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), 0, &tmpA); - zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), 0, &tmpB); - zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), 0, &tmpC); + zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), false, &tmpA); + if (Z_TYPE_P(socket) == IS_NULL) { + zend_argument_type_error(4, "invalid SocketSoSplice socket member value"); + RETURN_THROWS(); + } + zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), false, &tmpB); + zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), false, &tmpC); + if (Z_TYPE_P(array) == IS_NULL) { + zend_argument_type_error(4, "invalid SocketSoSplice time member value"); + RETURN_THROWS(); + } php_socket *php_sock = Z_SOCKET_P(socket); zend_long php_max = Z_LVAL_P(max); diff --git a/ext/sockets/tests/socket_set_option_so_splice.phpt b/ext/sockets/tests/socket_set_option_so_splice.phpt new file mode 100644 index 0000000000000..b995d176d9fe9 --- /dev/null +++ b/ext/sockets/tests/socket_set_option_so_splice.phpt @@ -0,0 +1,47 @@ +--TEST-- +SO_SPLICE usage with socket_set_option +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- +getMessage(), PHP_EOL; +} + +try { + socket_set_option($socket, SOL_SOCKET, SO_SPLICE, new badClass()); +} catch (\TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + $s = new SocketSoSplice(); + $s->max = -1; + $s->socket = $todrain; + $s->time = ["sec" => 0, "usec" => 0]; + socket_set_option($socket, SOL_SOCKET, SO_SPLICE, $s); +} catch (\ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +socket_close($todrain); +socket_close($socket); +?> +--EXPECT-- +socket_set_option(): Argument #4 ($value) must be of type SocketSoSplice, Socket given +socket_set_option(): Argument #4 ($value) must be of type SocketSoSplice, badClass given +socket_set_option(): Argument #4 ($value) "max" key must be greater than or equal to 0 From 47259e6525a133d18cd7c764f322ee44a3b40ea4 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 15 May 2025 00:13:33 +0000 Subject: [PATCH 5/5] further changes --- ext/sockets/sockets.c | 4 +- .../tests/socket_set_option_so_splice.phpt | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 83cd2f124eab4..2d02335d684d5 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -2321,13 +2321,13 @@ PHP_FUNCTION(socket_set_option) zval *socket = zend_read_property(socket_so_splice_ce, so_splice_obj, "socket", strlen("socket"), false, &tmpA); if (Z_TYPE_P(socket) == IS_NULL) { - zend_argument_type_error(4, "invalid SocketSoSplice socket member value"); + zend_argument_type_error(4, "socket cannot be null"); RETURN_THROWS(); } zval *max = zend_read_property(socket_so_splice_ce, so_splice_obj, "max", strlen("max"), false, &tmpB); zval *array = zend_read_property(socket_so_splice_ce, so_splice_obj, "time", strlen("time"), false, &tmpC); if (Z_TYPE_P(array) == IS_NULL) { - zend_argument_type_error(4, "invalid SocketSoSplice time member value"); + zend_argument_type_error(4, "time cannot be null"); RETURN_THROWS(); } diff --git a/ext/sockets/tests/socket_set_option_so_splice.phpt b/ext/sockets/tests/socket_set_option_so_splice.phpt index b995d176d9fe9..95375219a52ed 100644 --- a/ext/sockets/tests/socket_set_option_so_splice.phpt +++ b/ext/sockets/tests/socket_set_option_so_splice.phpt @@ -38,10 +38,64 @@ try { echo $e->getMessage(), PHP_EOL; } + +try { + $s = new SocketSoSplice(); + socket_set_option($socket, SOL_SOCKET, SO_SPLICE, $s); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + $s = new SocketSoSplice(); + $s->max = 1024; + $s->socket = $todrain; + $s->time = ["invalid" => 0, "usec" => 0]; + socket_set_option($socket, SOL_SOCKET, SO_SPLICE, $s); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + $s = new SocketSoSplice(); + $s->max = 1024; + $s->socket = $todrain; + $s->time = ["sec" => 0, "nosec" => 0]; + socket_set_option($socket, SOL_SOCKET, SO_SPLICE, $s); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +socket_bind($todrain, '127.0.0.1', 0); +socket_listen($todrain, 1); + +socket_getsockname($todrain, $addr, $port); +socket_connect($socket, $addr, $port); +$peer = socket_accept($todrain); + +$s1 = new SocketSoSplice(); +$s1->max = 5; +$s1->socket = $todrain; +$s1->time = ["sec" => 1, "usec" => 1]; + +$s2 = new SocketSoSplice(); +$s2->max = 5; +$s2->socket = $socket; +$s2->time = ["sec" => 1, "usec" => 1]; +var_dump(socket_set_option($socket, SOL_SOCKET, SO_SPLICE, $s1)); +var_dump(socket_set_option($todrain, SOL_SOCKET, SO_SPLICE, $s2)); +socket_write($socket, "HELLO", 5); +var_dump(socket_read($peer, 5, PHP_NORMAL_READ)); + +socket_close($peer); socket_close($todrain); socket_close($socket); + ?> --EXPECT-- socket_set_option(): Argument #4 ($value) must be of type SocketSoSplice, Socket given socket_set_option(): Argument #4 ($value) must be of type SocketSoSplice, badClass given socket_set_option(): Argument #4 ($value) "max" key must be greater than or equal to 0 +Typed property SocketSoSplice::$socket must not be accessed before initialization +socket_set_option(): Argument #4 ($value) time must have key "sec" +socket_set_option(): Argument #4 ($value) time must have key "usec"