Commit c1a6258015e for php.net
commit c1a6258015eca9e3edb768114d13db954ce77a65
Author: David Carlier <devnexen@gmail.com>
Date: Sat Apr 4 07:14:02 2026 +0100
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.
close GH-21631
diff --git a/NEWS b/NEWS
index 91e7169f98f..308ebe4d8f2 100644
--- a/NEWS
+++ b/NEWS
@@ -233,6 +233,8 @@ PHP NEWS
AF_INET* family only. (David Carlier)
. Fixed GH-20532 (socket_addrinfo_lookup gives the error code with a new
optional parameter). (David Carlier)
+ . Added AF_PACKET support completion for socket_sendto()/socket_recvfrom().
+ (David Carlier)
- Sodium:
. Added support for libsodium 1.0.21 IPcrypt and XOF APIs. (jedisct1)
diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c
index 7b9b903585b..dbf7f4e6ad4 100644
--- a/ext/sockets/sockets.c
+++ b/ext/sockets/sockets.c
@@ -1492,7 +1492,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;
@@ -1500,36 +1500,69 @@ 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);
+#ifdef AF_PACKET
+ /* On packet sockets, restrict flags to a finite safe subset. In
+ * particular MSG_TRUNC must be excluded: it makes recvfrom() report the
+ * untruncated frame length, which can exceed the buffer. */
+ if (php_sock->type == AF_PACKET) {
+ const zend_long allowed_flags = 0
+#ifdef MSG_OOB
+ | MSG_OOB
+#endif
+#ifdef MSG_PEEK
+ | MSG_PEEK
+#endif
+#ifdef MSG_WAITALL
+ | MSG_WAITALL
+#endif
+#ifdef MSG_DONTWAIT
+ | MSG_DONTWAIT
+#endif
+#ifdef MSG_ERRQUEUE
+ | MSG_ERRQUEUE
+#endif
+#ifdef MSG_CMSG_CLOEXEC
+ | MSG_CMSG_CLOEXEC
+#endif
+ ;
+
+ if (flags & ~allowed_flags) {
+ zend_argument_value_error(4, "must be a combination of MSG_OOB, MSG_PEEK, MSG_WAITALL, MSG_DONTWAIT, MSG_ERRQUEUE, and MSG_CMSG_CLOEXEC for AF_PACKET sockets");
+ RETURN_THROWS();
+ }
+ }
+#endif
+
/* 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:
@@ -1537,18 +1570,18 @@ 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);
zend_string_efree(recv_buf);
RETURN_FALSE;
}
- ZSTR_LEN(recv_buf) = retval;
+ ZSTR_LEN(recv_buf) = MIN((size_t)retval, (size_t)length);
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:
@@ -1556,7 +1589,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,
@@ -1565,21 +1598,21 @@ 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);
zend_string_efree(recv_buf);
RETURN_FALSE;
}
- ZSTR_LEN(recv_buf) = retval;
+ ZSTR_LEN(recv_buf) = MIN((size_t)retval, (size_t)length);
ZSTR_VAL(recv_buf)[ZSTR_LEN(recv_buf)] = '\0';
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:
@@ -1587,7 +1620,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,
@@ -1596,41 +1629,38 @@ 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);
zend_string_efree(recv_buf);
RETURN_FALSE;
}
- ZSTR_LEN(recv_buf) = retval;
+ ZSTR_LEN(recv_buf) = MIN((size_t)retval, (size_t)length);
ZSTR_VAL(recv_buf)[ZSTR_LEN(recv_buf)] = '\0';
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
- slen = sizeof(sll);
- memset(&sll, 0, sizeof(sll));
- sll.sll_family = AF_PACKET;
+ case AF_PACKET: {
char ifrname[IFNAMSIZ];
- retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), arg3, arg4, (struct sockaddr *)&sll, (socklen_t *)&slen);
+ slen = sizeof(sll);
+ memset(&sll, 0, 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);
zend_string_efree(recv_buf);
RETURN_FALSE;
}
- ZSTR_LEN(recv_buf) = retval;
+ ZSTR_LEN(recv_buf) = MIN((size_t)retval, (size_t)length);
ZSTR_VAL(recv_buf)[ZSTR_LEN(recv_buf)] = '\0';
if (UNEXPECTED(!if_indextoname(sll.sll_ifindex, ifrname))) {
@@ -1639,15 +1669,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_string_efree(recv_buf);
- 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();
}
@@ -1658,7 +1690,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;
@@ -1666,7 +1698,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;
@@ -1676,7 +1708,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)
@@ -1685,14 +1717,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");
@@ -1749,7 +1786,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");
@@ -1758,14 +1794,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();
}
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 00000000000..a66398c3a0e
--- /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--
+<?php
+if (!defined("AF_PACKET")) {
+ die('SKIP AF_PACKET not supported on this platform.');
+}
+if (!defined("ETH_P_ALL")) {
+ die('SKIP ETH_P_ALL not available on this platform.');
+}
+if (!function_exists("posix_getuid") || posix_getuid() != 0) {
+ die('SKIP AF_PACKET requires root permissions.');
+}
+?>
+--FILE--
+<?php
+$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');
+
+$dst_mac = "\xff\xff\xff\xff\xff\xff";
+$src_mac = "\x00\x00\x00\x00\x00\x00";
+$ethertype = pack("n", 0x9000);
+$payload = "no port test";
+$frame = str_pad($dst_mac . $src_mac . $ethertype . $payload, 60, "\x00");
+
+socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+
+// recvfrom without the optional 6th argument (port/ifindex).
+$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr);
+var_dump($bytes >= 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 00000000000..f2857671b94
--- /dev/null
+++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket.phpt
@@ -0,0 +1,126 @@
+--TEST--
+Test if socket_recvfrom() receives raw data sent by socket_sendto() via AF_PACKET
+--EXTENSIONS--
+sockets
+posix
+--SKIPIF--
+<?php
+if (!defined("AF_PACKET")) {
+ die('SKIP AF_PACKET not supported on this platform.');
+}
+if (!defined("ETH_P_ALL")) {
+ die('SKIP ETH_P_ALL not available on this platform.');
+}
+if (!function_exists("posix_getuid") || posix_getuid() != 0) {
+ die('SKIP AF_PACKET requires root permissions.');
+}
+?>
+--FILE--
+<?php
+
+$dst_mac = "\xff\xff\xff\xff\xff\xff";
+$src_mac = "\x00\x00\x00\x00\x00\x00";
+
+// Helper to build a padded ethernet frame.
+function build_frame(string $dst, string $src, int $ethertype, string $payload): string {
+ $frame = $dst . $src . pack("n", $ethertype) . $payload;
+ return str_pad($frame, 60, "\x00");
+}
+
+// An ETH_P_ALL socket bound to 'lo' captures every frame on loopback, including
+// unrelated localhost traffic from the parallel test runner. Read frames until
+// one starts with our own ethernet header, discarding foreign packets, so the
+// assertions never run against someone else's frame. Bounded by a deadline.
+function recv_matching(Socket $s, string $header, int $maxlen = 65536, ?string &$addr = null, &$port = null): string|false {
+ socket_set_nonblock($s);
+ $deadline = microtime(true) + 5.0;
+ while (microtime(true) < $deadline) {
+ $bytes = @socket_recvfrom($s, $buf, $maxlen, 0, $addr, $port);
+ if ($bytes !== false && is_string($buf) && str_starts_with($buf, $header)) {
+ return $buf;
+ }
+ if ($bytes === false) {
+ usleep(1000);
+ }
+ }
+ return false;
+}
+
+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');
+
+$header = $dst_mac . $src_mac . pack("n", 0x9000);
+$frame = build_frame($dst_mac, $src_mac, 0x9000, "ETH_P_ALL test");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent >= 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+var_dump(is_string($buf));
+var_dump($addr === 'lo');
+var_dump(is_string($buf) && 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');
+
+$header = $dst_mac . $src_mac . pack("n", ETH_P_LOOP);
+$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);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+// Verify ETH_P_LOOP ethertype at offset 12-13.
+var_dump(is_string($buf) && unpack("n", $buf, 12)[1] === ETH_P_LOOP);
+var_dump(is_string($buf) && 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');
+
+$header = $dst_mac . $src_mac . pack("n", 0x9000);
+$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));
+
+$buf = recv_matching($s_recv, $header, 65536, $addr, $port);
+var_dump($buf !== false && strlen($buf) >= strlen($frame));
+var_dump(is_int($port));
+// Verify the payload is intact in the raw buffer.
+var_dump(is_string($buf) && 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 00000000000..775a1381506
--- /dev/null
+++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_errors.phpt
@@ -0,0 +1,69 @@
+--TEST--
+AF_PACKET socket_sendto() and socket_recvfrom() error cases
+--EXTENSIONS--
+sockets
+posix
+--SKIPIF--
+<?php
+if (!defined("AF_PACKET")) {
+ die('SKIP AF_PACKET not supported on this platform.');
+}
+if (!defined("ETH_P_ALL")) {
+ die('SKIP ETH_P_ALL not available on this platform.');
+}
+if (!function_exists("posix_getuid") || posix_getuid() != 0) {
+ die('SKIP AF_PACKET requires root permissions.');
+}
+?>
+--FILE--
+<?php
+
+echo "--- sendto without port (ifindex) ---\n";
+$s = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL);
+socket_bind($s, 'lo');
+
+try {
+ socket_sendto($s, str_repeat("\x00", 60), 60, 0, "lo");
+} catch (ValueError $e) {
+ echo $e->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);
+
+echo "--- recvfrom with MSG_TRUNC is rejected ---\n";
+$s = socket_create(AF_PACKET, SOCK_RAW, ETH_P_ALL);
+socket_bind($s, 'lo');
+
+try {
+ socket_recvfrom($s, $buf, 65536, MSG_TRUNC, $addr);
+} catch (ValueError $e) {
+ echo $e->getMessage(), PHP_EOL;
+}
+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)
+--- recvfrom with MSG_TRUNC is rejected ---
+socket_recvfrom(): Argument #4 ($flags) must be a combination of MSG_OOB, MSG_PEEK, MSG_WAITALL, MSG_DONTWAIT, MSG_ERRQUEUE, and MSG_CMSG_CLOEXEC for AF_PACKET sockets
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 00000000000..68af685cf0b
--- /dev/null
+++ b/ext/sockets/tests/socket_sendto_recvfrom_afpacket_malformed.phpt
@@ -0,0 +1,225 @@
+--TEST--
+AF_PACKET socket_sendto/socket_recvfrom with malformed and edge-case frames
+--EXTENSIONS--
+sockets
+posix
+--SKIPIF--
+<?php
+if (!defined("AF_PACKET")) {
+ die('SKIP AF_PACKET not supported on this platform.');
+}
+if (!defined("ETH_P_ALL")) {
+ die('SKIP ETH_P_ALL not available on this platform.');
+}
+if (!function_exists("posix_getuid") || posix_getuid() != 0) {
+ die('SKIP AF_PACKET requires root permissions.');
+}
+?>
+--FILE--
+<?php
+
+$dst_mac = "\xff\xff\xff\xff\xff\xff";
+$src_mac = "\x00\x00\x00\x00\x00\x00";
+
+// An ETH_P_ALL socket bound to 'lo' captures every frame on loopback, including
+// unrelated localhost traffic from the parallel test runner. Read frames until
+// one starts with our own ethernet header, discarding foreign packets, so the
+// assertions never run against someone else's frame. Bounded by a deadline.
+function recv_matching(Socket $s, string $header, int $maxlen = 65536, ?string &$addr = null, &$port = null): string|false {
+ socket_set_nonblock($s);
+ $deadline = microtime(true) + 5.0;
+ while (microtime(true) < $deadline) {
+ $bytes = @socket_recvfrom($s, $buf, $maxlen, 0, $addr, $port);
+ if ($bytes !== false && is_string($buf) && str_starts_with($buf, $header)) {
+ return $buf;
+ }
+ if ($bytes === false) {
+ usleep(1000);
+ }
+ }
+ return false;
+}
+
+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');
+// Kernel requires at least an ethernet header, sendto should fail.
+$tiny = "\xff\xff\xff\xff";
+$ret = @socket_sendto($s_send, $tiny, strlen($tiny), 0, "lo", 1);
+var_dump($ret === false);
+socket_close($s_send);
+
+echo "--- Zero-length payload (header only, padded to 60) ---\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');
+
+// Ethernet header with no payload, padded to minimum 60 bytes.
+$header = $dst_mac . $src_mac . pack("n", 0x9000);
+$frame = str_pad($header, 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+// The raw buffer is just padding after the header.
+var_dump(is_string($buf) && 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.
+$header = $dst_mac . $src_mac . pack("n", 0xBEEF);
+$frame = str_pad($header . "bogus", 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+// Ethertype bytes should be in the raw buffer at offset 12-13.
+var_dump(is_string($buf) && unpack("n", $buf, 12)[1] === 0xBEEF);
+var_dump(is_string($buf) && str_contains($buf, "bogus"));
+
+socket_close($s_send);
+socket_close($s_recv);
+
+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');
+
+// Use a non-standard ethertype (0x88B5, reserved for local experimental use)
+// with garbage payload. Avoids kernel IP/IPv6 stack interception.
+$header = $dst_mac . $src_mac . pack("n", 0x88B5);
+$frame = str_pad($header . "\xDE\xAD\xBE\xEF", 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+// Raw buffer is delivered as-is — PHP doesn't parse, so no crash.
+var_dump(is_string($buf) && str_contains($buf, "\xDE\xAD\xBE\xEF"));
+
+socket_close($s_send);
+socket_close($s_recv);
+
+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');
+
+// Use 0x88B6, another local experimental ethertype.
+$header = $dst_mac . $src_mac . pack("n", 0x88B6);
+$frame = str_pad($header . "\xCA\xFE", 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+// Delivered raw — no parsing, no crash.
+var_dump(is_string($buf) && 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');
+
+$header = $dst_mac . $src_mac . pack("n", 0x0000);
+$frame = str_pad($header . "zerotype", 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+var_dump(is_string($buf) && unpack("n", $buf, 12)[1] === 0x0000);
+var_dump(is_string($buf) && 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');
+
+$header = $dst_mac . $src_mac . pack("n", 0xFFFF);
+$frame = str_pad($header . "maxtype", 60, "\x00");
+$sent = socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1);
+var_dump($sent === 60);
+
+$buf = recv_matching($s_recv, $header, 65536, $addr);
+var_dump($buf !== false && strlen($buf) >= 60);
+var_dump(is_string($buf) && unpack("n", $buf, 12)[1] === 0xFFFF);
+var_dump(is_string($buf) && 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');
+
+$header = $dst_mac . $src_mac . pack("n", 0x9000);
+$payload = str_repeat("X", 200);
+$frame = str_pad($header . $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.
+$buf = recv_matching($s_recv, $header, 30, $addr);
+var_dump($buf !== false && strlen($buf) === 30);
+var_dump(is_string($buf) && 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)
+--- Garbage payload with custom ethertype ---
+bool(true)
+bool(true)
+bool(true)
+--- Another garbage payload with experimental ethertype ---
+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)