From c74b2587fefa5e088e8dc152d7fe2caeb010e767 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 29 Jun 2026 10:20:44 -0400 Subject: [PATCH 1/5] Keep the object alive across jsonSerialize() in json_encode() (#22469) php_json_encode_serializable_object() holds a raw pointer to the object across the jsonSerialize() call, then reads its recursion guard and compares the returned value's identity against it. A user error handler triggered from jsonSerialize() can drop the last reference to the object, for example by nulling a reference that aliases the encoded array slot, freeing it before those reads and causing a use-after-free. Hold a reference on the object across the call. The array path already guards against this with a ZVAL_COPY; the JsonSerializable object path did not. Same use-after-free class as GH-21024 in var_dump(). --- ext/json/json_encoder.c | 7 +++++++ ext/json/tests/gh21024.phpt | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ext/json/tests/gh21024.phpt diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index 424315eca7ec..22af1c15a833 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -577,6 +577,11 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje ZEND_GUARD_PROTECT_RECURSION(guard, JSON); + /* jsonSerialize() may drop the last reference to the object, e.g. by + * nulling a reference that aliases the encoded array slot; keep it alive + * so the recursion guard and the identity check below stay valid. */ + GC_ADDREF(obj); + zend_function *json_serialize_method = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("jsonserialize")); ZEND_ASSERT(json_serialize_method != NULL && "This should be guaranteed prior to calling this function"); zend_call_known_function(json_serialize_method, obj, ce, &retval, 0, NULL, NULL); @@ -586,6 +591,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje smart_str_appendl(buf, "null", 4); } ZEND_GUARD_UNPROTECT_RECURSION(guard, JSON); + OBJ_RELEASE(obj); return FAILURE; } @@ -600,6 +606,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje } zval_ptr_dtor(&retval); + OBJ_RELEASE(obj); return return_code; } diff --git a/ext/json/tests/gh21024.phpt b/ext/json/tests/gh21024.phpt new file mode 100644 index 000000000000..612c577b2d88 --- /dev/null +++ b/ext/json/tests/gh21024.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-21024 (UAF in json_encode() when jsonSerialize() frees the object) +--EXTENSIONS-- +json +--FILE-- + 1]; + } +} +$arr = [new Bar]; +$ref = &$arr[0]; +var_dump(json_encode($arr)); +echo "survived\n"; +?> +--EXPECT-- +string(9) "[{"k":1}]" +survived From 8da53b5d31cbf4b50c779440afc34f75e79a0afb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 7 Apr 2026 17:48:43 -0400 Subject: [PATCH 2/5] Fix GH-18173: ext/hash relies on implementation-defined malloc alignment XXH3_state_t requires 64-byte alignment for its acc, customSecret, and buffer members, but php_hash_alloc_context() used ecalloc(), which only guarantees alignof(max_align_t) (typically 16 bytes on x86_64). When heap layout broke that assumption, xxhash's aligned loads segfaulted. Add a context_align field to php_hash_ops; when set, php_hash_alloc_context() over-allocates and manually aligns the returned pointer, storing the offset for php_hash_free_context() to recover the original allocation. Fixes GH-18173 Closes GH-21668 --- NEWS | 4 ++++ ext/hash/hash.c | 20 ++++++++++---------- ext/hash/hash_adler32.c | 1 + ext/hash/hash_crc32.c | 3 +++ ext/hash/hash_fnv.c | 4 ++++ ext/hash/hash_gost.c | 6 ++++-- ext/hash/hash_haval.c | 2 +- ext/hash/hash_joaat.c | 1 + ext/hash/hash_md.c | 9 ++++++--- ext/hash/hash_murmur.c | 3 +++ ext/hash/hash_ripemd.c | 12 ++++++++---- ext/hash/hash_sha.c | 21 ++++++++++++++------- ext/hash/hash_sha3.c | 6 ++++-- ext/hash/hash_snefru.c | 3 ++- ext/hash/hash_tiger.c | 3 ++- ext/hash/hash_whirlpool.c | 3 ++- ext/hash/hash_xxhash.c | 8 ++++++-- ext/hash/php_hash.h | 18 ++++++++++++++++++ 18 files changed, 93 insertions(+), 34 deletions(-) diff --git a/NEWS b/NEWS index 531d6a334c05..86ef47098f77 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.24 +- Hash: + . Fixed bug GH-18173 (ext/hash relies on implementation-defined malloc + alignment). (iliaal) + - Intl: . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the fallback locale when a language tag cannot be canonicalized. (Weilin Du) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 6d03bbcca7ce..1c90f4821f1a 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -392,7 +392,7 @@ static void php_hash_do_hash( } php_stream_close(stream); if (n < 0) { - efree(context); + php_hash_free_context(ops, context); RETURN_FALSE; } } else { @@ -401,7 +401,7 @@ static void php_hash_do_hash( digest = zend_string_alloc(ops->digest_size, 0); ops->hash_final((unsigned char *) ZSTR_VAL(digest), context); - efree(context); + php_hash_free_context(ops, context); if (raw_output) { ZSTR_VAL(digest)[ops->digest_size] = 0; @@ -540,7 +540,7 @@ static void php_hash_do_hash_hmac( } php_stream_close(stream); if (n < 0) { - efree(context); + php_hash_free_context(ops, context); efree(K); zend_string_release(digest); RETURN_FALSE; @@ -558,7 +558,7 @@ static void php_hash_do_hash_hmac( /* Zero the key */ ZEND_SECURE_ZERO(K, ops->block_size); efree(K); - efree(context); + php_hash_free_context(ops, context); if (raw_output) { ZSTR_VAL(digest)[ops->digest_size] = 0; @@ -817,7 +817,7 @@ PHP_FUNCTION(hash_final) ZSTR_VAL(digest)[digest_len] = 0; /* Invalidate the object from further use */ - efree(hash->context); + php_hash_free_context(hash->ops, hash->context); hash->context = NULL; if (raw_output) { @@ -975,7 +975,7 @@ PHP_FUNCTION(hash_hkdf) ZEND_SECURE_ZERO(digest, ops->digest_size); ZEND_SECURE_ZERO(prk, ops->digest_size); efree(K); - efree(context); + php_hash_free_context(ops, context); efree(prk); efree(digest); ZSTR_VAL(returnval)[length] = 0; @@ -1091,7 +1091,7 @@ PHP_FUNCTION(hash_pbkdf2) efree(K1); efree(K2); efree(computed_salt); - efree(context); + php_hash_free_context(ops, context); efree(digest); efree(temp); @@ -1347,7 +1347,7 @@ PHP_FUNCTION(mhash_keygen_s2k) RETVAL_STRINGL(key, bytes); ZEND_SECURE_ZERO(key, bytes); efree(digest); - efree(context); + php_hash_free_context(ops, context); efree(key); } } @@ -1377,7 +1377,7 @@ static void php_hashcontext_dtor(zend_object *obj) { php_hashcontext_object *hash = php_hashcontext_from_object(obj); if (hash->context) { - efree(hash->context); + php_hash_free_context(hash->ops, hash->context); hash->context = NULL; } @@ -1413,7 +1413,7 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) { newobj->ops->hash_init(newobj->context, NULL); if (SUCCESS != newobj->ops->hash_copy(newobj->ops, oldobj->context, newobj->context)) { - efree(newobj->context); + php_hash_free_context(newobj->ops, newobj->context); newobj->context = NULL; return znew; } diff --git a/ext/hash/hash_adler32.c b/ext/hash/hash_adler32.c index 3898ea60e877..e1fdd765b372 100644 --- a/ext/hash/hash_adler32.c +++ b/ext/hash/hash_adler32.c @@ -70,5 +70,6 @@ const php_hash_ops php_hash_adler32_ops = { 4, /* what to say here? */ 4, sizeof(PHP_ADLER32_CTX), + 0, 0 }; diff --git a/ext/hash/hash_crc32.c b/ext/hash/hash_crc32.c index a770d0b55416..2e8de00b518d 100644 --- a/ext/hash/hash_crc32.c +++ b/ext/hash/hash_crc32.c @@ -102,6 +102,7 @@ const php_hash_ops php_hash_crc32_ops = { 4, /* what to say here? */ 4, sizeof(PHP_CRC32_CTX), + 0, 0 }; @@ -117,6 +118,7 @@ const php_hash_ops php_hash_crc32b_ops = { 4, /* what to say here? */ 4, sizeof(PHP_CRC32_CTX), + 0, 0 }; @@ -132,5 +134,6 @@ const php_hash_ops php_hash_crc32c_ops = { 4, /* what to say here? */ 4, sizeof(PHP_CRC32_CTX), + 0, 0 }; diff --git a/ext/hash/hash_fnv.c b/ext/hash/hash_fnv.c index 92d4922bd810..c126de61cfd1 100644 --- a/ext/hash/hash_fnv.c +++ b/ext/hash/hash_fnv.c @@ -32,6 +32,7 @@ const php_hash_ops php_hash_fnv132_ops = { 4, 4, sizeof(PHP_FNV132_CTX), + 0, 0 }; @@ -47,6 +48,7 @@ const php_hash_ops php_hash_fnv1a32_ops = { 4, 4, sizeof(PHP_FNV132_CTX), + 0, 0 }; @@ -62,6 +64,7 @@ const php_hash_ops php_hash_fnv164_ops = { 8, 4, sizeof(PHP_FNV164_CTX), + 0, 0 }; @@ -77,6 +80,7 @@ const php_hash_ops php_hash_fnv1a64_ops = { 8, 4, sizeof(PHP_FNV164_CTX), + 0, 0 }; diff --git a/ext/hash/hash_gost.c b/ext/hash/hash_gost.c index 2ad6948a9a67..ee2f3c89381c 100644 --- a/ext/hash/hash_gost.c +++ b/ext/hash/hash_gost.c @@ -329,7 +329,8 @@ const php_hash_ops php_hash_gost_ops = { 32, 32, sizeof(PHP_GOST_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_gost_crypto_ops = { @@ -344,5 +345,6 @@ const php_hash_ops php_hash_gost_crypto_ops = { 32, 32, sizeof(PHP_GOST_CTX), - 1 + 1, + 0 }; diff --git a/ext/hash/hash_haval.c b/ext/hash/hash_haval.c index 67bc2b2e4780..484b9a582915 100644 --- a/ext/hash/hash_haval.c +++ b/ext/hash/hash_haval.c @@ -252,7 +252,7 @@ const php_hash_ops php_hash_##p##haval##b##_ops = { \ php_hash_serialize, \ php_hash_unserialize, \ PHP_HAVAL_SPEC, \ - ((b) / 8), 128, sizeof(PHP_HAVAL_CTX), 1 }; \ + ((b) / 8), 128, sizeof(PHP_HAVAL_CTX), 1, 0 }; \ PHP_HASH_API void PHP_##p##HAVAL##b##Init(PHP_HAVAL_CTX *context, ZEND_ATTRIBUTE_UNUSED HashTable *args) \ { int i; context->count[0] = context->count[1] = 0; \ for(i = 0; i < 8; i++) context->state[i] = D0[i]; \ diff --git a/ext/hash/hash_joaat.c b/ext/hash/hash_joaat.c index 328f9292c4ca..6a16ceeeda14 100644 --- a/ext/hash/hash_joaat.c +++ b/ext/hash/hash_joaat.c @@ -33,6 +33,7 @@ const php_hash_ops php_hash_joaat_ops = { 4, 4, sizeof(PHP_JOAAT_CTX), + 0, 0 }; diff --git a/ext/hash/hash_md.c b/ext/hash/hash_md.c index 96da7fce82a2..996e71ec9ffb 100644 --- a/ext/hash/hash_md.c +++ b/ext/hash/hash_md.c @@ -29,7 +29,8 @@ const php_hash_ops php_hash_md5_ops = { 16, 64, sizeof(PHP_MD5_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_md4_ops = { @@ -44,7 +45,8 @@ const php_hash_ops php_hash_md4_ops = { 16, 64, sizeof(PHP_MD4_CTX), - 1 + 1, + 0 }; static int php_md2_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv); @@ -61,7 +63,8 @@ const php_hash_ops php_hash_md2_ops = { 16, 16, sizeof(PHP_MD2_CTX), - 1 + 1, + 0 }; /* MD common stuff */ diff --git a/ext/hash/hash_murmur.c b/ext/hash/hash_murmur.c index 0117b2e57d36..d69c5c3bb688 100644 --- a/ext/hash/hash_murmur.c +++ b/ext/hash/hash_murmur.c @@ -33,6 +33,7 @@ const php_hash_ops php_hash_murmur3a_ops = { 4, 4, sizeof(PHP_MURMUR3A_CTX), + 0, 0 }; @@ -95,6 +96,7 @@ const php_hash_ops php_hash_murmur3c_ops = { 16, 4, sizeof(PHP_MURMUR3C_CTX), + 0, 0 }; @@ -174,6 +176,7 @@ const php_hash_ops php_hash_murmur3f_ops = { 16, 8, sizeof(PHP_MURMUR3F_CTX), + 0, 0 }; diff --git a/ext/hash/hash_ripemd.c b/ext/hash/hash_ripemd.c index 4802fdf9a1fc..188d0095cbc3 100644 --- a/ext/hash/hash_ripemd.c +++ b/ext/hash/hash_ripemd.c @@ -33,7 +33,8 @@ const php_hash_ops php_hash_ripemd128_ops = { 16, 64, sizeof(PHP_RIPEMD128_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_ripemd160_ops = { @@ -48,7 +49,8 @@ const php_hash_ops php_hash_ripemd160_ops = { 20, 64, sizeof(PHP_RIPEMD160_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_ripemd256_ops = { @@ -63,7 +65,8 @@ const php_hash_ops php_hash_ripemd256_ops = { 32, 64, sizeof(PHP_RIPEMD256_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_ripemd320_ops = { @@ -78,7 +81,8 @@ const php_hash_ops php_hash_ripemd320_ops = { 40, 64, sizeof(PHP_RIPEMD320_CTX), - 1 + 1, + 0 }; /* {{{ PHP_RIPEMD128Init diff --git a/ext/hash/hash_sha.c b/ext/hash/hash_sha.c index 3129446fcde4..c45947b8584f 100644 --- a/ext/hash/hash_sha.c +++ b/ext/hash/hash_sha.c @@ -75,7 +75,8 @@ const php_hash_ops php_hash_sha1_ops = { 20, 64, sizeof(PHP_SHA1_CTX), - 1 + 1, + 0 }; /* sha224/sha256 */ @@ -92,7 +93,8 @@ const php_hash_ops php_hash_sha256_ops = { 32, 64, sizeof(PHP_SHA256_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_sha224_ops = { @@ -107,7 +109,8 @@ const php_hash_ops php_hash_sha224_ops = { 28, 64, sizeof(PHP_SHA224_CTX), - 1 + 1, + 0 }; #define ROTR32(b,x) ((x >> b) | (x << (32 - b))) @@ -624,7 +627,8 @@ const php_hash_ops php_hash_sha384_ops = { 48, 128, sizeof(PHP_SHA384_CTX), - 1 + 1, + 0 }; /* {{{ PHP_SHA512InitArgs @@ -803,7 +807,8 @@ const php_hash_ops php_hash_sha512_ops = { 64, 128, sizeof(PHP_SHA512_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_sha512_256_ops = { @@ -818,7 +823,8 @@ const php_hash_ops php_hash_sha512_256_ops = { 32, 128, sizeof(PHP_SHA512_CTX), - 1 + 1, + 0 }; const php_hash_ops php_hash_sha512_224_ops = { @@ -833,5 +839,6 @@ const php_hash_ops php_hash_sha512_224_ops = { 28, 128, sizeof(PHP_SHA512_CTX), - 1 + 1, + 0 }; diff --git a/ext/hash/hash_sha3.c b/ext/hash/hash_sha3.c index 07da2cfd2d01..65221593511f 100644 --- a/ext/hash/hash_sha3.c +++ b/ext/hash/hash_sha3.c @@ -251,7 +251,8 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \ bits >> 3, \ (1600 - (2 * bits)) >> 3, \ sizeof(PHP_SHA3_##bits##_CTX), \ - 1 \ + 1, \ + 0 \ } #else @@ -339,7 +340,8 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \ bits >> 3, \ (1600 - (2 * bits)) >> 3, \ sizeof(PHP_SHA3_CTX), \ - 1 \ + 1, \ + 0 \ } #endif diff --git a/ext/hash/hash_snefru.c b/ext/hash/hash_snefru.c index c1dbc3ae57a6..fd2ee28c80e7 100644 --- a/ext/hash/hash_snefru.c +++ b/ext/hash/hash_snefru.c @@ -214,5 +214,6 @@ const php_hash_ops php_hash_snefru_ops = { 32, 32, sizeof(PHP_SNEFRU_CTX), - 1 + 1, + 0 }; diff --git a/ext/hash/hash_tiger.c b/ext/hash/hash_tiger.c index 841693a67dd1..745f69582373 100644 --- a/ext/hash/hash_tiger.c +++ b/ext/hash/hash_tiger.c @@ -265,7 +265,8 @@ static int php_tiger_unserialize(php_hashcontext_object *hash, zend_long magic, b/8, \ 64, \ sizeof(PHP_TIGER_CTX), \ - 1 \ + 1, \ + 0 \ } PHP_HASH_TIGER_OPS(3, 128); diff --git a/ext/hash/hash_whirlpool.c b/ext/hash/hash_whirlpool.c index db5a0da1236a..894e2d9f0afe 100644 --- a/ext/hash/hash_whirlpool.c +++ b/ext/hash/hash_whirlpool.c @@ -457,5 +457,6 @@ const php_hash_ops php_hash_whirlpool_ops = { 64, 64, sizeof(PHP_WHIRLPOOL_CTX), - 1 + 1, + 0 }; diff --git a/ext/hash/hash_xxhash.c b/ext/hash/hash_xxhash.c index 1c1315afd4b2..b7253b0c99c0 100644 --- a/ext/hash/hash_xxhash.c +++ b/ext/hash/hash_xxhash.c @@ -34,6 +34,7 @@ const php_hash_ops php_hash_xxh32_ops = { 4, 4, sizeof(PHP_XXH32_CTX), + 0, 0 }; @@ -101,6 +102,7 @@ const php_hash_ops php_hash_xxh64_ops = { 8, 8, sizeof(PHP_XXH64_CTX), + 0, 0 }; @@ -152,7 +154,8 @@ const php_hash_ops php_hash_xxh3_64_ops = { 8, 8, sizeof(PHP_XXH3_64_CTX), - 0 + 0, + 64 }; typedef XXH_errorcode (*xxh3_reset_with_secret_func_t)(XXH3_state_t*, const void*, size_t); @@ -257,7 +260,8 @@ const php_hash_ops php_hash_xxh3_128_ops = { 16, 8, sizeof(PHP_XXH3_128_CTX), - 0 + 0, + 64 }; PHP_HASH_API void PHP_XXH3_128_Init(PHP_XXH3_128_CTX *ctx, HashTable *args) diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index 3b058ef48bdb..b77557c5bf78 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -52,6 +52,7 @@ typedef struct _php_hash_ops { size_t block_size; size_t context_size; unsigned is_crypto: 1; + size_t context_align; } php_hash_ops; struct _php_hashcontext_object { @@ -155,9 +156,26 @@ PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, const z static inline void *php_hash_alloc_context(const php_hash_ops *ops) { /* Zero out context memory so serialization doesn't expose internals */ + if (ops->context_align > 0) { + size_t align = ops->context_align; + char *base = ecalloc(1, ops->context_size + align); + size_t offset = align - ((uintptr_t)base & (align - 1)); + char *ptr = base + offset; + ptr[-1] = (char)offset; + return ptr; + } return ecalloc(1, ops->context_size); } +static inline void php_hash_free_context(const php_hash_ops *ops, void *ctx) { + if (ops->context_align > 0) { + unsigned char offset = ((unsigned char *)ctx)[-1]; + efree((char *)ctx - offset); + return; + } + efree(ctx); +} + static inline void php_hash_bin2hex(char *out, const unsigned char *in, size_t in_len) { static const char hexits[17] = "0123456789abcdef"; From e44db0cdfd0bb3595dd7e95b48dd5ebf5a25eddd Mon Sep 17 00:00:00 2001 From: Xuyang Zhang <119476662+kn1g78@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:12:46 +0800 Subject: [PATCH 3/5] ext/intl: Reset IntlChar error state on method entry (#22500) IntlChar methods could leave stale global intl error state after a later successful call. Reset the intl error state when entering each IntlChar method so successful calls report U_ZERO_ERROR. Closes #22500 --- NEWS | 2 ++ ext/intl/tests/intlchar_reset_error.phpt | 19 ++++++++++ ext/intl/uchar/uchar.c | 46 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 ext/intl/tests/intlchar_reset_error.phpt diff --git a/NEWS b/NEWS index 86ef47098f77..57d91e345457 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,8 @@ PHP NEWS fallback locale when a language tag cannot be canonicalized. (Weilin Du) . Fixed memory leaks when calling Collator::__construct() or Spoofchecker::__construct() twice. (Weilin Du) + . Fixed IntlChar methods leaving stale global error state after successful + calls. (Xuyang Zhang) - Phar: . Fixed inconsistent handling of the magic ".phar" directory. Paths such as diff --git a/ext/intl/tests/intlchar_reset_error.phpt b/ext/intl/tests/intlchar_reset_error.phpt new file mode 100644 index 000000000000..45f6d1df2961 --- /dev/null +++ b/ext/intl/tests/intlchar_reset_error.phpt @@ -0,0 +1,19 @@ +--TEST-- +IntlChar methods reset intl error on success +--EXTENSIONS-- +intl +--FILE-- + +--EXPECT-- +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/intl/uchar/uchar.c b/ext/intl/uchar/uchar.c index f194e0e0b931..32bb83a09a4b 100644 --- a/ext/intl/uchar/uchar.c +++ b/ext/intl/uchar/uchar.c @@ -54,6 +54,8 @@ IC_METHOD(chr) { char buffer[5]; int buffer_len = 0; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -73,6 +75,8 @@ IC_METHOD(chr) { IC_METHOD(ord) { UChar32 cp; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -88,6 +92,8 @@ IC_METHOD(hasBinaryProperty) { zend_string *string_codepoint; zend_long int_codepoint = 0; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) Z_PARAM_LONG(prop) @@ -108,6 +114,8 @@ IC_METHOD(getIntPropertyValue) { zend_string *string_codepoint; zend_long int_codepoint = 0; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) Z_PARAM_LONG(prop) @@ -125,6 +133,8 @@ IC_METHOD(getIntPropertyValue) { IC_METHOD(getIntPropertyMinValue) { zend_long prop; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_LONG(prop) ZEND_PARSE_PARAMETERS_END(); @@ -137,6 +147,8 @@ IC_METHOD(getIntPropertyMinValue) { IC_METHOD(getIntPropertyMaxValue) { zend_long prop; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_LONG(prop) ZEND_PARSE_PARAMETERS_END(); @@ -149,6 +161,8 @@ IC_METHOD(getIntPropertyMaxValue) { IC_METHOD(getNumericValue) { UChar32 cp; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -191,6 +205,8 @@ static UBool enumCharType_callback(enumCharType_data *context, IC_METHOD(enumCharTypes) { enumCharType_data context; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_FUNC(context.fci, context.fci_cache) ZEND_PARSE_PARAMETERS_END(); @@ -202,6 +218,8 @@ IC_METHOD(enumCharTypes) { IC_METHOD(getBlockCode) { UChar32 cp; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -220,6 +238,8 @@ IC_METHOD(charName) { zend_string *buffer = NULL; int32_t buffer_len; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) Z_PARAM_OPTIONAL @@ -250,6 +270,8 @@ IC_METHOD(charFromName) { UChar32 ret; UErrorCode error = U_ZERO_ERROR; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STRING(name, name_len) Z_PARAM_OPTIONAL @@ -301,6 +323,7 @@ IC_METHOD(enumCharNames) { zend_long nameChoice = U_UNICODE_CHAR_NAME; UErrorCode error = U_ZERO_ERROR; + intl_error_reset(NULL); ZEND_PARSE_PARAMETERS_START(3, 4) Z_PARAM_STR_OR_LONG(string_start, int_start) @@ -326,6 +349,8 @@ IC_METHOD(getPropertyName) { zend_long nameChoice = U_LONG_PROPERTY_NAME; const char *ret; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_LONG(property) Z_PARAM_OPTIONAL @@ -348,6 +373,8 @@ IC_METHOD(getPropertyEnum) { char *alias; size_t alias_len; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(alias, alias_len) ZEND_PARSE_PARAMETERS_END(); @@ -361,6 +388,8 @@ IC_METHOD(getPropertyValueName) { zend_long property, value, nameChoice = U_LONG_PROPERTY_NAME; const char *ret; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_LONG(property) Z_PARAM_LONG(value) @@ -385,6 +414,8 @@ IC_METHOD(getPropertyValueEnum) { char *name; size_t name_len; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(property) Z_PARAM_STRING(name, name_len) @@ -401,6 +432,8 @@ IC_METHOD(foldCase) { zend_string *string_codepoint; zend_long int_codepoint = 0; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) Z_PARAM_OPTIONAL @@ -432,6 +465,8 @@ IC_METHOD(digit) { zend_string *string_codepoint; zend_long int_codepoint = 0; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) Z_PARAM_OPTIONAL @@ -456,6 +491,8 @@ IC_METHOD(digit) { IC_METHOD(forDigit) { zend_long digit, radix = 10; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_LONG(digit) Z_PARAM_OPTIONAL @@ -472,6 +509,8 @@ IC_METHOD(charAge) { UVersionInfo version; int i; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -489,6 +528,8 @@ IC_METHOD(getUnicodeVersion) { UVersionInfo version; int i; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_NONE(); u_getUnicodeVersion(version); @@ -507,6 +548,8 @@ IC_METHOD(getFC_NFKC_Closure) { int32_t closure_len; UErrorCode error = U_ZERO_ERROR; + intl_error_reset(NULL); + if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { RETURN_NULL(); } @@ -535,6 +578,7 @@ IC_METHOD(getFC_NFKC_Closure) { #define IC_BOOL_METHOD_CHAR(name) \ IC_METHOD(name) { \ UChar32 cp; \ + intl_error_reset(NULL); \ if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { \ RETURN_NULL(); \ } \ @@ -575,6 +619,7 @@ IC_BOOL_METHOD_CHAR(isJavaIDPart) #define IC_INT_METHOD_CHAR(name) \ IC_METHOD(name) { \ UChar32 cp; \ + intl_error_reset(NULL); \ if (parse_code_point_param(INTERNAL_FUNCTION_PARAM_PASSTHRU, &cp) == FAILURE) { \ RETURN_NULL(); \ } \ @@ -595,6 +640,7 @@ IC_METHOD(name) { \ UChar32 cp, ret; \ zend_string *string_codepoint; \ zend_long int_codepoint = -1; \ + intl_error_reset(NULL); \ ZEND_PARSE_PARAMETERS_START(1, 1) \ Z_PARAM_STR_OR_LONG(string_codepoint, int_codepoint) \ ZEND_PARSE_PARAMETERS_END(); \ From 9060fd336657b93986e5934d5abb380a6bc19eb3 Mon Sep 17 00:00:00 2001 From: mehmetcan <2010841+mehmetcansahin@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:43:29 +0300 Subject: [PATCH 4/5] Move min/max array fallback into array.c (#22147) --- UPGRADING.INTERNALS | 2 ++ Zend/zend_hash.c | 67 -------------------------------------------- Zend/zend_hash.h | 1 - ext/standard/array.c | 22 ++++++++++++--- 4 files changed, 20 insertions(+), 72 deletions(-) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index a467df3b6be5..a8eb6b805413 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -36,6 +36,8 @@ PHP 8.6 INTERNALS UPGRADE NOTES . The zval_dtor() alias of zval_ptr_dtor_nogc() has been removed. Call zval_ptr_dtor_nogc() directly instead. . The internal zend_copy_parameters_array() function is no longer exposed. + . The internal zend_hash_minmax() function is no longer exposed. Scan the + HashTable directly and use zend_compare() for value comparisons instead. . The zend_make_callable() function has been removed, if a callable zval needs to be obtained use the zend_get_callable_zval_from_fcc() function instead. If this was used to store a callable, then an FCC should be diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 2913af9c2b5f..acc342bc267d 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -3238,73 +3238,6 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_fun } -ZEND_API zval* ZEND_FASTCALL zend_hash_minmax(const HashTable *ht, compare_func_t compar, uint32_t flag) -{ - uint32_t idx; - zval *res; - - IS_CONSISTENT(ht); - - if (ht->nNumOfElements == 0 ) { - return NULL; - } - - if (HT_IS_PACKED(ht)) { - zval *zv; - - idx = 0; - while (1) { - if (idx == ht->nNumUsed) { - return NULL; - } - if (Z_TYPE(ht->arPacked[idx]) != IS_UNDEF) break; - idx++; - } - res = ht->arPacked + idx; - for (; idx < ht->nNumUsed; idx++) { - zv = ht->arPacked + idx; - if (UNEXPECTED(Z_TYPE_P(zv) == IS_UNDEF)) continue; - - if (flag) { - if (compar(res, zv) < 0) { /* max */ - res = zv; - } - } else { - if (compar(res, zv) > 0) { /* min */ - res = zv; - } - } - } - } else { - Bucket *p; - - idx = 0; - while (1) { - if (idx == ht->nNumUsed) { - return NULL; - } - if (Z_TYPE(ht->arData[idx].val) != IS_UNDEF) break; - idx++; - } - res = &ht->arData[idx].val; - for (; idx < ht->nNumUsed; idx++) { - p = ht->arData + idx; - if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) continue; - - if (flag) { - if (compar(res, &p->val) < 0) { /* max */ - res = &p->val; - } - } else { - if (compar(res, &p->val) > 0) { /* min */ - res = &p->val; - } - } - } - } - return res; -} - ZEND_API bool ZEND_FASTCALL _zend_handle_numeric_str_ex(const char *key, size_t length, zend_ulong *idx) { const char *tmp = key; diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 6c0d4a241a1b..1181bee29fae 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -303,7 +303,6 @@ typedef int (*bucket_compare_func_t)(Bucket *a, Bucket *b); ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_func_t compar, bool ordered); ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); -ZEND_API zval* ZEND_FASTCALL zend_hash_minmax(const HashTable *ht, compare_func_t compar, uint32_t flag); static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); diff --git a/ext/standard/array.c b/ext/standard/array.c index 3b17ca3f7942..a233e6c4dcb5 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1089,9 +1089,23 @@ PHP_FUNCTION(key) } /* }}} */ -static int php_data_compare(const void *f, const void *s) /* {{{ */ +static zval *php_array_data_minmax(HashTable *array, bool max) /* {{{ */ { - return zend_compare((zval*)f, (zval*)s); + zval *entry, *result = NULL; + + ZEND_HASH_FOREACH_VAL(array, entry) { + if (!result) { + result = entry; + continue; + } + + int cmp = zend_compare(result, entry); + if (max ? cmp < 0 : cmp > 0) { + result = entry; + } + } ZEND_HASH_FOREACH_END(); + + return result; } /* }}} */ @@ -1114,7 +1128,7 @@ PHP_FUNCTION(min) zend_wrong_parameter_type_error(1, Z_EXPECTED_ARRAY, &args[0]); RETURN_THROWS(); } else { - zval *result = zend_hash_minmax(Z_ARRVAL(args[0]), php_data_compare, 0); + zval *result = php_array_data_minmax(Z_ARRVAL(args[0]), false); if (result) { RETURN_COPY_DEREF(result); } else { @@ -1242,7 +1256,7 @@ PHP_FUNCTION(max) zend_wrong_parameter_type_error(1, Z_EXPECTED_ARRAY, &args[0]); RETURN_THROWS(); } else { - zval *result = zend_hash_minmax(Z_ARRVAL(args[0]), php_data_compare, 1); + zval *result = php_array_data_minmax(Z_ARRVAL(args[0]), true); if (result) { RETURN_COPY_DEREF(result); } else { From c1a6258015eca9e3edb768114d13db954ce77a65 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 4 Apr 2026 07:14:02 +0100 Subject: [PATCH 5/5] 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 --- NEWS | 2 + ext/sockets/sockets.c | 147 +++++++----- .../socket_recvfrom_afpacket_no_port.phpt | 44 ++++ .../socket_sendto_recvfrom_afpacket.phpt | 126 ++++++++++ ...ocket_sendto_recvfrom_afpacket_errors.phpt | 69 ++++++ ...et_sendto_recvfrom_afpacket_malformed.phpt | 225 ++++++++++++++++++ 6 files changed, 557 insertions(+), 56 deletions(-) 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/NEWS b/NEWS index 91e7169f98fd..308ebe4d8f2e 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 7b9b903585b8..dbf7f4e6ad4b 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 000000000000..a66398c3a0e1 --- /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 000000000000..f2857671b94d --- /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-- + +--FILE-- += 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 000000000000..775a1381506c --- /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-- + +--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); + +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 000000000000..68af685cf0bf --- /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-- + +--FILE-- += 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)