From c4c415dbd1ae6868241c345d54ef1d293750cd6a Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 4 Apr 2026 07:14:02 +0100 Subject: [PATCH 1/3] ext/sockets: Enable AF_PACKET raw buffer support in socket_sendto/socket_recvfrom. Take a new approach from PR #17926: instead of parsing ethernet/IP/TCP/UDP headers in C, expose the raw frame as a string to userland, letting users handle protocol decoding safely in PHP. This addresses the security concerns raised during review. Also rename opaque argument variables (arg1..arg6) to meaningful names in both functions and fix a bug in the commented-out sendto code that was using &sin instead of &sll. --- ext/sockets/sockets.c | 104 ++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 4582de4bc8ecb..aebb856060094 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -1497,7 +1497,7 @@ PHP_FUNCTION(socket_send) /* {{{ Receives data from a socket, connected or not */ PHP_FUNCTION(socket_recvfrom) { - zval *arg1, *arg2, *arg5, *arg6 = NULL; + zval *zsocket, *zdata, *zaddr, *zport = NULL; php_socket *php_sock; struct sockaddr_un s_un; struct sockaddr_in sin; @@ -1505,36 +1505,36 @@ PHP_FUNCTION(socket_recvfrom) struct sockaddr_in6 sin6; #endif #ifdef AF_PACKET - //struct sockaddr_ll sll; + struct sockaddr_ll sll; #endif char addrbuf[INET6_ADDRSTRLEN]; socklen_t slen; int retval; - zend_long arg3, arg4; + zend_long length, flags; const char *address; zend_string *recv_buf; ZEND_PARSE_PARAMETERS_START(5, 6) - Z_PARAM_OBJECT_OF_CLASS(arg1, socket_ce) - Z_PARAM_ZVAL(arg2) - Z_PARAM_LONG(arg3) - Z_PARAM_LONG(arg4) - Z_PARAM_ZVAL(arg5) + Z_PARAM_OBJECT_OF_CLASS(zsocket, socket_ce) + Z_PARAM_ZVAL(zdata) + Z_PARAM_LONG(length) + Z_PARAM_LONG(flags) + Z_PARAM_ZVAL(zaddr) Z_PARAM_OPTIONAL - Z_PARAM_ZVAL(arg6) + Z_PARAM_ZVAL(zport) ZEND_PARSE_PARAMETERS_END(); - php_sock = Z_SOCKET_P(arg1); + php_sock = Z_SOCKET_P(zsocket); ENSURE_SOCKET_VALID(php_sock); /* overflow check */ /* Shouldthrow ? */ - if (arg3 <= 0 || arg3 > ZEND_LONG_MAX - 1) { + if (length <= 0 || length > ZEND_LONG_MAX - 1) { RETURN_FALSE; } - recv_buf = zend_string_alloc(arg3 + 1, 0); + recv_buf = zend_string_alloc(length + 1, 0); switch (php_sock->type) { case AF_UNIX: @@ -1542,7 +1542,7 @@ PHP_FUNCTION(socket_recvfrom) memset(&s_un, 0, slen); s_un.sun_family = AF_UNIX; - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), arg3, arg4, (struct sockaddr *)&s_un, (socklen_t *)&slen); + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&s_un, (socklen_t *)&slen); if (retval < 0) { PHP_SOCKET_ERROR(php_sock, "Unable to recvfrom", errno); @@ -1552,8 +1552,8 @@ PHP_FUNCTION(socket_recvfrom) ZSTR_LEN(recv_buf) = retval; ZSTR_VAL(recv_buf)[ZSTR_LEN(recv_buf)] = '\0'; - ZEND_TRY_ASSIGN_REF_NEW_STR(arg2, recv_buf); - ZEND_TRY_ASSIGN_REF_STRING(arg5, s_un.sun_path); + ZEND_TRY_ASSIGN_REF_NEW_STR(zdata, recv_buf); + ZEND_TRY_ASSIGN_REF_STRING(zaddr, s_un.sun_path); break; case AF_INET: @@ -1561,7 +1561,7 @@ PHP_FUNCTION(socket_recvfrom) memset(&sin, 0, slen); sin.sin_family = AF_INET; - if (arg6 == NULL) { + if (zport == NULL) { zend_string_efree(recv_buf); zend_throw_exception( zend_ce_argument_count_error, @@ -1570,7 +1570,7 @@ PHP_FUNCTION(socket_recvfrom) RETURN_THROWS(); } - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), arg3, arg4, (struct sockaddr *)&sin, (socklen_t *)&slen); + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin, (socklen_t *)&slen); if (retval < 0) { PHP_SOCKET_ERROR(php_sock, "Unable to recvfrom", errno); @@ -1582,9 +1582,9 @@ PHP_FUNCTION(socket_recvfrom) address = inet_ntop(AF_INET, &sin.sin_addr, addrbuf, sizeof(addrbuf)); - ZEND_TRY_ASSIGN_REF_NEW_STR(arg2, recv_buf); - ZEND_TRY_ASSIGN_REF_STRING(arg5, address ? address : "0.0.0.0"); - ZEND_TRY_ASSIGN_REF_LONG(arg6, ntohs(sin.sin_port)); + ZEND_TRY_ASSIGN_REF_NEW_STR(zdata, recv_buf); + ZEND_TRY_ASSIGN_REF_STRING(zaddr, address ? address : "0.0.0.0"); + ZEND_TRY_ASSIGN_REF_LONG(zport, ntohs(sin.sin_port)); break; #ifdef HAVE_IPV6 case AF_INET6: @@ -1592,7 +1592,7 @@ PHP_FUNCTION(socket_recvfrom) memset(&sin6, 0, slen); sin6.sin6_family = AF_INET6; - if (arg6 == NULL) { + if (zport == NULL) { zend_string_efree(recv_buf); zend_throw_exception( zend_ce_argument_count_error, @@ -1601,7 +1601,7 @@ PHP_FUNCTION(socket_recvfrom) RETURN_THROWS(); } - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), arg3, arg4, (struct sockaddr *)&sin6, (socklen_t *)&slen); + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin6, (socklen_t *)&slen); if (retval < 0) { PHP_SOCKET_ERROR(php_sock, "unable to recvfrom", errno); @@ -1613,22 +1613,20 @@ PHP_FUNCTION(socket_recvfrom) inet_ntop(AF_INET6, &sin6.sin6_addr, addrbuf, sizeof(addrbuf)); - ZEND_TRY_ASSIGN_REF_NEW_STR(arg2, recv_buf); - ZEND_TRY_ASSIGN_REF_STRING(arg5, addrbuf[0] ? addrbuf : "::"); - ZEND_TRY_ASSIGN_REF_LONG(arg6, ntohs(sin6.sin6_port)); + ZEND_TRY_ASSIGN_REF_NEW_STR(zdata, recv_buf); + ZEND_TRY_ASSIGN_REF_STRING(zaddr, addrbuf[0] ? addrbuf : "::"); + ZEND_TRY_ASSIGN_REF_LONG(zport, ntohs(sin6.sin6_port)); break; #endif #ifdef AF_PACKET - /* - case AF_PACKET: - // TODO expose and use proper ethernet frame type instead i.e. src mac, dst mac and payload to userland - // ditto for socket_sendto + case AF_PACKET: { + char ifrname[IFNAMSIZ]; + slen = sizeof(sll); - memset(&sll, 0, sizeof(sll)); + memset(&sll, 0, slen); sll.sll_family = AF_PACKET; - char ifrname[IFNAMSIZ]; - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), arg3, arg4, (struct sockaddr *)&sll, (socklen_t *)&slen); + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sll, (socklen_t *)&slen); if (retval < 0) { PHP_SOCKET_ERROR(php_sock, "unable to recvfrom", errno); @@ -1644,14 +1642,17 @@ PHP_FUNCTION(socket_recvfrom) RETURN_FALSE; } - ZEND_TRY_ASSIGN_REF_NEW_STR(arg2, recv_buf); - ZEND_TRY_ASSIGN_REF_STRING(arg5, ifrname); - ZEND_TRY_ASSIGN_REF_LONG(arg6, sll.sll_ifindex); + ZEND_TRY_ASSIGN_REF_NEW_STR(zdata, recv_buf); + ZEND_TRY_ASSIGN_REF_STRING(zaddr, ifrname); + + if (zport) { + ZEND_TRY_ASSIGN_REF_LONG(zport, sll.sll_ifindex); + } break; - */ + } #endif default: - zend_argument_value_error(1, "must be one of AF_UNIX, AF_INET, or AF_INET6"); + zend_argument_value_error(1, "must be one of AF_UNIX, AF_PACKET, AF_INET, or AF_INET6"); RETURN_THROWS(); } @@ -1662,7 +1663,7 @@ PHP_FUNCTION(socket_recvfrom) /* {{{ Sends a message to a socket, whether it is connected or not */ PHP_FUNCTION(socket_sendto) { - zval *arg1; + zval *zsocket; php_socket *php_sock; struct sockaddr_un s_un; struct sockaddr_in sin; @@ -1670,7 +1671,7 @@ PHP_FUNCTION(socket_sendto) struct sockaddr_in6 sin6; #endif #ifdef AF_PACKET - //struct sockaddr_ll sll; + struct sockaddr_ll sll; #endif int retval; size_t buf_len; @@ -1680,7 +1681,7 @@ PHP_FUNCTION(socket_sendto) zend_string *addr; ZEND_PARSE_PARAMETERS_START(5, 6) - Z_PARAM_OBJECT_OF_CLASS(arg1, socket_ce) + Z_PARAM_OBJECT_OF_CLASS(zsocket, socket_ce) Z_PARAM_STRING(buf, buf_len) Z_PARAM_LONG(len) Z_PARAM_LONG(flags) @@ -1689,14 +1690,19 @@ PHP_FUNCTION(socket_sendto) Z_PARAM_LONG_OR_NULL(port, port_is_null) ZEND_PARSE_PARAMETERS_END(); - php_sock = Z_SOCKET_P(arg1); + php_sock = Z_SOCKET_P(zsocket); ENSURE_SOCKET_VALID(php_sock); - if (port < 0 || port > USHRT_MAX) { - zend_argument_value_error(6, "must be between 0 and %u", USHRT_MAX); - RETURN_THROWS(); +#ifdef AF_PACKET + if (php_sock->type != AF_PACKET) { +#endif + if (port < 0 || port > USHRT_MAX) { + zend_argument_value_error(6, "must be between 0 and %u", USHRT_MAX); + RETURN_THROWS(); + } +#ifdef AF_PACKET } - +#endif if (len < 0) { zend_argument_value_error(3, "must be greater than or equal to 0"); @@ -1753,7 +1759,6 @@ PHP_FUNCTION(socket_sendto) break; #endif #ifdef AF_PACKET - /* case AF_PACKET: if (port_is_null) { zend_argument_value_error(6, "cannot be null when the socket type is AF_PACKET"); @@ -1762,14 +1767,13 @@ PHP_FUNCTION(socket_sendto) memset(&sll, 0, sizeof(sll)); sll.sll_family = AF_PACKET; - sll.sll_ifindex = port; + sll.sll_ifindex = (int)port; - retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &sin, sizeof(sin)); + retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *)&sll, sizeof(sll)); break; - */ #endif default: - zend_argument_value_error(1, "must be one of AF_UNIX, AF_INET, or AF_INET6"); + zend_argument_value_error(1, "must be one of AF_UNIX, AF_PACKET, AF_INET, or AF_INET6"); RETURN_THROWS(); } From cb0a7a62e8908cf97b89212e28a186126d12b3b8 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 4 Apr 2026 07:14:08 +0100 Subject: [PATCH 2/3] Add AF_PACKET sendto/recvfrom tests. Cover ETH_P_ALL, ETH_P_LOOP, large payloads, bogus and invalid ethertypes, truncated IP/IPv6 headers, undersized frames, small receive buffers, optional port argument and error cases. --- .../socket_recvfrom_afpacket_no_port.phpt | 44 ++++ .../socket_sendto_recvfrom_afpacket.phpt | 104 +++++++++ ...ocket_sendto_recvfrom_afpacket_errors.phpt | 56 +++++ ...et_sendto_recvfrom_afpacket_malformed.phpt | 198 ++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt create mode 100644 ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt create mode 100644 ext/sockets/tests/socket_sendto_recvfrom_afpacket_errors.phpt create mode 100644 ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt diff --git a/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt b/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt new file mode 100644 index 0000000000000..a66398c3a0e13 --- /dev/null +++ b/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt @@ -0,0 +1,44 @@ +--TEST-- +AF_PACKET socket_recvfrom() without optional port argument +--EXTENSIONS-- +sockets +posix +--SKIPIF-- + +--FILE-- += 60); +var_dump($addr === 'lo'); + +socket_close($s_send); +socket_close($s_recv); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt b/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt new file mode 100644 index 0000000000000..b58cb81587de2 --- /dev/null +++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt @@ -0,0 +1,104 @@ +--TEST-- +Test if socket_recvfrom() receives raw data sent by socket_sendto() via AF_PACKET +--EXTENSIONS-- +sockets +posix +--SKIPIF-- + +--FILE-- += 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +var_dump(is_string($buf)); +var_dump($addr === 'lo'); +var_dump(str_contains($buf, "ETH_P_ALL test")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- ETH_P_LOOP send and receive ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +$frame = build_frame($dst_mac, $src_mac, ETH_P_LOOP, "loopback payload"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent >= 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +// Verify ETH_P_LOOP ethertype at offset 12-13. +var_dump(unpack("n", $buf, 12)[1] === ETH_P_LOOP); +var_dump(str_contains($buf, "loopback payload")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Large payload ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +$payload = random_bytes(1024); +$frame = build_frame($dst_mac, $src_mac, 0x9000, $payload); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === strlen($frame)); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr, $port); +var_dump($bytes === strlen($frame)); +var_dump(is_int($port)); +// Verify the payload is intact in the raw buffer. +var_dump(str_contains($buf, $payload)); + +socket_close($s_send); +socket_close($s_recv); +?> +--EXPECT-- +--- ETH_P_ALL send and receive --- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +--- ETH_P_LOOP send and receive --- +bool(true) +bool(true) +bool(true) +bool(true) +--- Large payload --- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/sockets/tests/socket_sendto_recvfrom_afpacket_errors.phpt b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_errors.phpt new file mode 100644 index 0000000000000..bd8581b20c1b1 --- /dev/null +++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_errors.phpt @@ -0,0 +1,56 @@ +--TEST-- +AF_PACKET socket_sendto() and socket_recvfrom() error cases +--EXTENSIONS-- +sockets +posix +--SKIPIF-- + +--FILE-- +getMessage(), PHP_EOL; +} +socket_close($s); + +echo "--- sendto with invalid interface name ---\n"; +$s = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s, 'lo'); + +$ret = @socket_sendto($s, str_repeat("\x00", 60), 60, 0, "lo", 999999); +var_dump($ret === false); +socket_close($s); + +echo "--- recvfrom on non-blocking socket with no data ---\n"; +$s = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s, 'lo'); +socket_set_nonblock($s); + +$ret = @socket_recvfrom($s, $buf, 65536, 0, $addr); +var_dump($ret === false); +socket_close($s); + +?> +--EXPECT-- +--- sendto without port (ifindex) --- +socket_sendto(): Argument #6 ($port) cannot be null when the socket type is AF_PACKET +--- sendto with invalid interface name --- +bool(true) +--- recvfrom on non-blocking socket with no data --- +bool(true) diff --git a/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt new file mode 100644 index 0000000000000..658db23de3748 --- /dev/null +++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt @@ -0,0 +1,198 @@ +--TEST-- +AF_PACKET socket_sendto/socket_recvfrom with malformed and edge-case frames +--EXTENSIONS-- +sockets +posix +--SKIPIF-- + +--FILE-- += 60); +// The raw buffer is just padding after the header. +var_dump(strlen($buf) >= 60); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Bogus ethertype ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +// Use a made-up ethertype (0xBEEF). Kernel delivers it fine on loopback. +$frame = str_pad($dst_mac . $src_mac . pack("n", 0xBEEF) . "bogus", 60, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +// Ethertype bytes should be in the raw buffer at offset 12-13. +var_dump(unpack("n", $buf, 12)[1] === 0xBEEF); +var_dump(str_contains($buf, "bogus")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Truncated IP header (valid ether header, garbage IP) ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +// ETH_P_IP ethertype but only 4 bytes of garbage instead of a real IP header. +$frame = str_pad($dst_mac . $src_mac . pack("n", ETH_P_IP) . "\xDE\xAD\xBE\xEF", 60, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +// Raw buffer is delivered as-is — PHP doesn't parse, so no crash. +var_dump(str_contains($buf, "\xDE\xAD\xBE\xEF")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Truncated IPv6 header ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +// ETH_P_IPV6 ethertype but only a few garbage bytes as payload. +$frame = str_pad($dst_mac . $src_mac . pack("n", ETH_P_IPV6) . "\xCA\xFE", 60, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +// Delivered raw — no parsing, no crash. +var_dump(str_contains($buf, "\xCA\xFE")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Invalid ethertype 0x0000 ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +$frame = str_pad($dst_mac . $src_mac . pack("n", 0x0000) . "zerotype", 60, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +var_dump(unpack("n", $buf, 12)[1] === 0x0000); +var_dump(str_contains($buf, "zerotype")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Invalid ethertype 0xFFFF ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +$frame = str_pad($dst_mac . $src_mac . pack("n", 0xFFFF) . "maxtype", 60, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 60); + +$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +var_dump($bytes >= 60); +var_dump(unpack("n", $buf, 12)[1] === 0xFFFF); +var_dump(str_contains($buf, "maxtype")); + +socket_close($s_send); +socket_close($s_recv); + +echo "--- Small receive buffer (truncation) ---\n"; +$s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +$s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); +socket_bind($s_send, 'lo'); +socket_bind($s_recv, 'lo'); + +$payload = str_repeat("X", 200); +$frame = str_pad($dst_mac . $src_mac . pack("n", 0x9000) . $payload, 214, "\x00"); +$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); +var_dump($sent === 214); + +// Request only 30 bytes — less than the frame. Kernel truncates. +$bytes = socket_recvfrom($s_recv, $buf, 30, 0, $addr); +var_dump($bytes === 30); +var_dump(strlen($buf) === 30); + +socket_close($s_send); +socket_close($s_recv); +?> +--EXPECT-- +--- Undersized frame (below 14-byte ethernet header) --- +bool(true) +--- Zero-length payload (header only, padded to 60) --- +bool(true) +bool(true) +bool(true) +--- Bogus ethertype --- +bool(true) +bool(true) +bool(true) +bool(true) +--- Truncated IP header (valid ether header, garbage IP) --- +bool(true) +bool(true) +bool(true) +--- Truncated IPv6 header --- +bool(true) +bool(true) +bool(true) +--- Invalid ethertype 0x0000 --- +bool(true) +bool(true) +bool(true) +bool(true) +--- Invalid ethertype 0xFFFF --- +bool(true) +bool(true) +bool(true) +bool(true) +--- Small receive buffer (truncation) --- +bool(true) +bool(true) +bool(true) From 6d9be1e3e98ca42db67fb367e3b5e8b016c7f564 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 4 Apr 2026 07:53:17 +0100 Subject: [PATCH 3/3] Attempt to fix flaky AF_PACKET tests on 32-bit. Drain stale packets from receive queue before each send/receive pair, use experimental ethertypes (0x88B5/0x88B6) instead of ETH_P_IP/ETH_P_IPV6 to avoid kernel stack interception, and relax strict size comparisons. --- .../socket_sendto_recvfrom_afpacket.phpt | 14 +++++++-- ...et_sendto_recvfrom_afpacket_malformed.phpt | 31 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt b/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt index b58cb81587de2..cc499704a8f37 100644 --- a/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt +++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt @@ -27,11 +27,19 @@ function build_frame(string $dst, string $src, int $ethertype, string $payload): return str_pad($frame, 60, "\x00"); } +// Drain any pending packets from a socket. +function drain_socket(Socket $s): void { + socket_set_nonblock($s); + while (@socket_recvfrom($s, $buf, 65536, 0, $addr) !== false) {} + socket_set_block($s); +} + echo "--- ETH_P_ALL send and receive ---\n"; $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $frame = build_frame($dst_mac, $src_mac, 0x9000, "ETH_P_ALL test"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); @@ -51,6 +59,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $frame = build_frame($dst_mac, $src_mac, ETH_P_LOOP, "loopback payload"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); @@ -70,14 +79,15 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $payload = random_bytes(1024); $frame = build_frame($dst_mac, $src_mac, 0x9000, $payload); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); -var_dump($sent === strlen($frame)); +var_dump($sent >= strlen($frame)); $bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr, $port); -var_dump($bytes === strlen($frame)); +var_dump($bytes >= strlen($frame)); var_dump(is_int($port)); // Verify the payload is intact in the raw buffer. var_dump(str_contains($buf, $payload)); diff --git a/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt index 658db23de3748..4f87074ed0479 100644 --- a/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt +++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt @@ -21,6 +21,13 @@ if (!function_exists("posix_getuid") || posix_getuid() != 0) { $dst_mac = "\xff\xff\xff\xff\xff\xff"; $src_mac = "\x00\x00\x00\x00\x00\x00"; +// Drain any pending packets from a socket. +function drain_socket(Socket $s): void { + socket_set_nonblock($s); + while (@socket_recvfrom($s, $buf, 65536, 0, $addr) !== false) {} + socket_set_block($s); +} + echo "--- Undersized frame (below 14-byte ethernet header) ---\n"; $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); @@ -35,6 +42,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); // Ethernet header with no payload, padded to minimum 60 bytes. $frame = str_pad($dst_mac . $src_mac . pack("n", 0x9000), 60, "\x00"); @@ -54,6 +62,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); // Use a made-up ethertype (0xBEEF). Kernel delivers it fine on loopback. $frame = str_pad($dst_mac . $src_mac . pack("n", 0xBEEF) . "bogus", 60, "\x00"); @@ -69,14 +78,16 @@ var_dump(str_contains($buf, "bogus")); socket_close($s_send); socket_close($s_recv); -echo "--- Truncated IP header (valid ether header, garbage IP) ---\n"; +echo "--- Garbage payload with custom ethertype ---\n"; $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); -// ETH_P_IP ethertype but only 4 bytes of garbage instead of a real IP header. -$frame = str_pad($dst_mac . $src_mac . pack("n", ETH_P_IP) . "\xDE\xAD\xBE\xEF", 60, "\x00"); +// Use a non-standard ethertype (0x88B5, reserved for local experimental use) +// with garbage payload. Avoids kernel IP/IPv6 stack interception. +$frame = str_pad($dst_mac . $src_mac . pack("n", 0x88B5) . "\xDE\xAD\xBE\xEF", 60, "\x00"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); var_dump($sent === 60); @@ -88,14 +99,15 @@ var_dump(str_contains($buf, "\xDE\xAD\xBE\xEF")); socket_close($s_send); socket_close($s_recv); -echo "--- Truncated IPv6 header ---\n"; +echo "--- Another garbage payload with experimental ethertype ---\n"; $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); -// ETH_P_IPV6 ethertype but only a few garbage bytes as payload. -$frame = str_pad($dst_mac . $src_mac . pack("n", ETH_P_IPV6) . "\xCA\xFE", 60, "\x00"); +// Use 0x88B6, another local experimental ethertype. +$frame = str_pad($dst_mac . $src_mac . pack("n", 0x88B6) . "\xCA\xFE", 60, "\x00"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); var_dump($sent === 60); @@ -112,6 +124,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $frame = str_pad($dst_mac . $src_mac . pack("n", 0x0000) . "zerotype", 60, "\x00"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); @@ -130,6 +143,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $frame = str_pad($dst_mac . $src_mac . pack("n", 0xFFFF) . "maxtype", 60, "\x00"); $sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); @@ -148,6 +162,7 @@ $s_send = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); $s_recv = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL); socket_bind($s_send, 'lo'); socket_bind($s_recv, 'lo'); +drain_socket($s_recv); $payload = str_repeat("X", 200); $frame = str_pad($dst_mac . $src_mac . pack("n", 0x9000) . $payload, 214, "\x00"); @@ -174,11 +189,11 @@ bool(true) bool(true) bool(true) bool(true) ---- Truncated IP header (valid ether header, garbage IP) --- +--- Garbage payload with custom ethertype --- bool(true) bool(true) bool(true) ---- Truncated IPv6 header --- +--- Another garbage payload with experimental ethertype --- bool(true) bool(true) bool(true)