From 33e3966f8b28564700dfe96509d47f3d54688bc7 Mon Sep 17 00:00:00 2001 From: eskyuu Date: Mon, 24 Aug 2026 21:08:31 +0800 Subject: [PATCH 1/9] snmp: Additional SNMP library output formatting options (#21502) RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls --- ext/snmp/php_snmp.h | 7 + ext/snmp/snmp.c | 385 ++++++++++++++++-- ext/snmp/snmp.stub.php | 82 +++- ext/snmp/snmp_arginfo.h | 175 +++++++- ext/snmp/snmp_decl.h | 39 ++ ext/snmp/tests/snmp-object-properties.phpt | 82 +++- .../tests/snmp_set_oid_output_format.phpt | 2 +- 7 files changed, 688 insertions(+), 84 deletions(-) create mode 100644 ext/snmp/snmp_decl.h diff --git a/ext/snmp/php_snmp.h b/ext/snmp/php_snmp.h index c5376ee2e1b2..52100b09b77a 100644 --- a/ext/snmp/php_snmp.h +++ b/ext/snmp/php_snmp.h @@ -48,6 +48,13 @@ typedef struct _php_snmp_object { int valueretrieval; bool quick_print; bool enum_print; + bool numeric_index; + bool numeric_timeticks; + bool extended_index; + bool dont_print_units; + bool escape_quotes; + bool print_hex_text; + int string_output_format; int oid_output_format; int snmp_errno; bool oid_increasing_check; diff --git a/ext/snmp/snmp.c b/ext/snmp/snmp.c index 8fac85a236ab..94b7d803a030 100644 --- a/ext/snmp/snmp.c +++ b/ext/snmp/snmp.c @@ -22,6 +22,7 @@ #endif #include "php.h" +#include "Zend/zend_enum.h" #include "main/php_network.h" #include "ext/standard/info.h" @@ -64,7 +65,12 @@ #include #include +#include "snmp_decl.h" #include "snmp_arginfo.h" +static zend_class_entry *SnmpMib_ce; +static zend_class_entry *SnmpOidOutput_ce; +static zend_class_entry *SnmpOutput_ce; +static zend_class_entry *SnmpStringOutput_ce; /* For net-snmp prior to 5.4 */ #ifndef HAVE_SHUTDOWN_SNMP_LOGGING @@ -90,6 +96,10 @@ typedef struct snmp_session php_snmp_session; } static bool mib_needs_reset; +static php_snmp_object saved_snmp_settings; +static int saved_mib_allow_underscores; +static int saved_mib_comment_term; +static int saved_mib_replace; ZEND_DECLARE_MODULE_GLOBALS(snmp) static PHP_GINIT_FUNCTION(snmp); @@ -1246,6 +1256,42 @@ static ZEND_ATTRIBUTE_NONNULL_ARGS(2) bool snmp_session_set_security(struct snmp } /* }}} */ +/* {{{ Save the snmplib state into the given php_snmp_object */ +static void save_snmplib_output_options(php_snmp_object *snmp_object) +{ + // Booleans + snmp_object->quick_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT); + snmp_object->enum_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM); + snmp_object->numeric_index = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_DONT_BREAKDOWN_OIDS); + snmp_object->numeric_timeticks = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_NUMERIC_TIMETICKS); + snmp_object->extended_index = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_EXTENDED_INDEX); + snmp_object->dont_print_units = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_DONT_PRINT_UNITS); + snmp_object->escape_quotes = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_ESCAPE_QUOTES); + snmp_object->print_hex_text = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_HEX_TEXT); + // Integers + snmp_object->string_output_format = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_STRING_OUTPUT_FORMAT); + snmp_object->oid_output_format = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT); +} +/* }}} */ + +/* {{{ Set the snmplib output options using the given php_snmp_object */ +static void set_snmplib_output_options(php_snmp_object *snmp_object) +{ + // Booleans + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT, snmp_object->quick_print); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM, snmp_object->enum_print); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_DONT_BREAKDOWN_OIDS, snmp_object->numeric_index); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_NUMERIC_TIMETICKS, snmp_object->numeric_timeticks); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_EXTENDED_INDEX, snmp_object->extended_index); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_DONT_PRINT_UNITS, snmp_object->dont_print_units); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_ESCAPE_QUOTES, snmp_object->escape_quotes); + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_HEX_TEXT, snmp_object->print_hex_text); + // Integers + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_STRING_OUTPUT_FORMAT, snmp_object->string_output_format); + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT, snmp_object->oid_output_format); +} +/* }}} */ + /* {{{ php_snmp * * Generic SNMP handler for all versions. @@ -1429,12 +1475,10 @@ static void php_snmp(INTERNAL_FUNCTION_PARAMETERS, int st, int version) } objid_query.oid_increasing_check = snmp_object->oid_increasing_check; objid_query.valueretrieval = snmp_object->valueretrieval; - glob_snmp_object.enum_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM); - netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM, snmp_object->enum_print); - glob_snmp_object.quick_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT); - netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT, snmp_object->quick_print); - glob_snmp_object.oid_output_format = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT); - netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT, snmp_object->oid_output_format); + + // Save the global snmplib output options and set the options to those defined by the object instance + save_snmplib_output_options(&glob_snmp_object); + set_snmplib_output_options(snmp_object); } if (objid_query.max_repetitions < 0) { @@ -1448,9 +1492,8 @@ static void php_snmp(INTERNAL_FUNCTION_PARAMETERS, int st, int version) if (session_less_mode) { snmp_session_free(&session); } else { - netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM, glob_snmp_object.enum_print); - netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT, glob_snmp_object.quick_print); - netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT, glob_snmp_object.oid_output_format); + // Restore the snmplib output options back to the global state + set_snmplib_output_options(&glob_snmp_object); } } /* }}} */ @@ -1527,28 +1570,147 @@ PHP_FUNCTION(snmp_set_enum_print) } /* }}} */ +/* {{{ Set walk option. */ +PHP_FUNCTION(snmp_set_mib_option) +{ + zend_enum_Snmp_Mib opt; + int snmp_opt; + bool val; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ENUM(opt, SnmpMib_ce) + Z_PARAM_BOOL(val) + ZEND_PARSE_PARAMETERS_END(); + + switch (opt) { + case ZEND_ENUM_Snmp_Mib_AllowUnderscores: + snmp_opt = NETSNMP_DS_LIB_MIB_PARSE_LABEL; + break; + case ZEND_ENUM_Snmp_Mib_CommentTerm: + snmp_opt = NETSNMP_DS_LIB_MIB_COMMENT_TERM; + break; + case ZEND_ENUM_Snmp_Mib_Replace: + snmp_opt = NETSNMP_DS_LIB_MIB_REPLACE; + break; + default: + ZEND_UNREACHABLE(); + } + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, snmp_opt, (int) val); +} +/* }}} */ + +/* {{{ Set the string output format. */ +PHP_FUNCTION(snmp_set_string_output_format) +{ + zend_enum_Snmp_StringOutput format; + int snmp_format; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ENUM(format, SnmpStringOutput_ce) + ZEND_PARSE_PARAMETERS_END(); + + switch (format) { + case ZEND_ENUM_Snmp_StringOutput_Guess: + snmp_format = NETSNMP_STRING_OUTPUT_GUESS; + break; + case ZEND_ENUM_Snmp_StringOutput_Ascii: + snmp_format = NETSNMP_STRING_OUTPUT_ASCII; + break; + case ZEND_ENUM_Snmp_StringOutput_Hex: + snmp_format = NETSNMP_STRING_OUTPUT_HEX; + break; + default: + ZEND_UNREACHABLE(); + } + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_STRING_OUTPUT_FORMAT, snmp_format); +} +/* }}} */ + +/* {{{ Set output format option. */ +PHP_FUNCTION(snmp_set_output_option) +{ + zend_enum_Snmp_Output opt; + int snmp_opt; + bool val; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ENUM(opt, SnmpOutput_ce) + Z_PARAM_BOOL(val) + ZEND_PARSE_PARAMETERS_END(); + + switch (opt) { + case ZEND_ENUM_Snmp_Output_NumericIndex: + snmp_opt = NETSNMP_DS_LIB_DONT_BREAKDOWN_OIDS; + break; + case ZEND_ENUM_Snmp_Output_EnumPrint: + snmp_opt = NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM; + break; + case ZEND_ENUM_Snmp_Output_EscapeQuotes: + snmp_opt = NETSNMP_DS_LIB_ESCAPE_QUOTES; + break; + case ZEND_ENUM_Snmp_Output_QuickPrint: + snmp_opt = NETSNMP_DS_LIB_QUICK_PRINT; + break; + case ZEND_ENUM_Snmp_Output_NumericTimeticks: + snmp_opt = NETSNMP_DS_LIB_NUMERIC_TIMETICKS; + break; + case ZEND_ENUM_Snmp_Output_HexText: + snmp_opt = NETSNMP_DS_LIB_PRINT_HEX_TEXT; + break; + case ZEND_ENUM_Snmp_Output_DontPrintUnits: + snmp_opt = NETSNMP_DS_LIB_DONT_PRINT_UNITS; + break; + case ZEND_ENUM_Snmp_Output_ExtendedIndex: + snmp_opt = NETSNMP_DS_LIB_EXTENDED_INDEX; + break; + default: + ZEND_UNREACHABLE(); + } + netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, snmp_opt, (int) val); +} +/* }}} */ + /* {{{ Set the OID output format. */ PHP_FUNCTION(snmp_set_oid_output_format) { zend_long format; + int snmp_format; + zend_object *format_object = NULL; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &format) == FAILURE) { - RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJ_OF_CLASS_OR_LONG(format_object, SnmpOidOutput_ce, format) + ZEND_PARSE_PARAMETERS_END(); + + if (format_object != NULL) { + format = zend_enum_fetch_case_id(format_object); } switch (format) { - case NETSNMP_OID_OUTPUT_SUFFIX: - case NETSNMP_OID_OUTPUT_MODULE: - case NETSNMP_OID_OUTPUT_FULL: - case NETSNMP_OID_OUTPUT_NUMERIC: - case NETSNMP_OID_OUTPUT_UCD: - case NETSNMP_OID_OUTPUT_NONE: - netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT, format); - RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_Suffix: + snmp_format = NETSNMP_OID_OUTPUT_SUFFIX; + break; + case ZEND_ENUM_Snmp_OidOutput_Module: + snmp_format = NETSNMP_OID_OUTPUT_MODULE; + break; + case ZEND_ENUM_Snmp_OidOutput_Full: + snmp_format = NETSNMP_OID_OUTPUT_FULL; + break; + case ZEND_ENUM_Snmp_OidOutput_Numeric: + snmp_format = NETSNMP_OID_OUTPUT_NUMERIC; + break; + case ZEND_ENUM_Snmp_OidOutput_Ucd: + snmp_format = NETSNMP_OID_OUTPUT_UCD; + break; + case ZEND_ENUM_Snmp_OidOutput_None: + snmp_format = NETSNMP_OID_OUTPUT_NONE; + break; default: - zend_argument_value_error(1, "must be an SNMP_OID_OUTPUT_* constant"); + zend_argument_value_error(1, "must be a Snmp\\OidOutput constant"); RETURN_THROWS(); } + + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT, snmp_format); + RETURN_TRUE; } /* }}} */ @@ -1731,9 +1893,7 @@ PHP_METHOD(SNMP, __construct) } snmp_object->max_oids = 0; snmp_object->valueretrieval = SNMP_G(valueretrieval); - snmp_object->enum_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_PRINT_NUMERIC_ENUM); - snmp_object->oid_output_format = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_OID_OUTPUT_FORMAT); - snmp_object->quick_print = netsnmp_ds_get_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_QUICK_PRINT); + save_snmplib_output_options(snmp_object); snmp_object->oid_increasing_check = true; snmp_object->exceptions_enabled = 0; } @@ -1812,6 +1972,74 @@ PHP_METHOD(SNMP, setSecurity) } /* }}} */ +/* {{{ Set OID output format */ +PHP_METHOD(SNMP, setOidOutputFormat) +{ + php_snmp_object *snmp_object; + zval *object = ZEND_THIS; + zend_enum_Snmp_OidOutput format; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ENUM(format, SnmpOidOutput_ce) + ZEND_PARSE_PARAMETERS_END(); + + snmp_object = Z_SNMP_P(object); + + switch (format) { + case ZEND_ENUM_Snmp_OidOutput_Suffix: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_SUFFIX; + RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_Module: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_MODULE; + RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_Full: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_FULL; + RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_Numeric: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_NUMERIC; + RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_Ucd: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_UCD; + RETURN_TRUE; + case ZEND_ENUM_Snmp_OidOutput_None: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_NONE; + RETURN_TRUE; + default: + ZEND_UNREACHABLE(); + } + +} +/* }}} */ + +/* {{{ Set string output format */ +PHP_METHOD(SNMP, setStringOutputFormat) +{ + php_snmp_object *snmp_object; + zval *object = ZEND_THIS; + zend_enum_Snmp_StringOutput format; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ENUM(format, SnmpStringOutput_ce) + ZEND_PARSE_PARAMETERS_END(); + + snmp_object = Z_SNMP_P(object); + + switch (format) { + case ZEND_ENUM_Snmp_StringOutput_Guess: + snmp_object->string_output_format = NETSNMP_STRING_OUTPUT_GUESS; + RETURN_TRUE; + case ZEND_ENUM_Snmp_StringOutput_Ascii: + snmp_object->string_output_format = NETSNMP_STRING_OUTPUT_ASCII; + RETURN_TRUE; + case ZEND_ENUM_Snmp_StringOutput_Hex: + snmp_object->string_output_format = NETSNMP_STRING_OUTPUT_HEX; + RETURN_TRUE; + default: + ZEND_UNREACHABLE(); + } +} +/* }}} */ + /* {{{ Get last error code number */ PHP_METHOD(SNMP, getErrno) { @@ -2033,6 +2261,35 @@ static zend_result php_snmp_read_max_oids(php_snmp_object *snmp_object, zval *re } /* }}} */ +/* {{{ */ +static zend_result php_snmp_read_oid_output_format(php_snmp_object *snmp_object, zval *retval) +{ + switch(snmp_object->oid_output_format) { + case NETSNMP_OID_OUTPUT_SUFFIX: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_Suffix); + return SUCCESS; + case NETSNMP_OID_OUTPUT_MODULE: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_Module); + return SUCCESS; + case NETSNMP_OID_OUTPUT_FULL: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_Full); + return SUCCESS; + case NETSNMP_OID_OUTPUT_NUMERIC: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_Numeric); + return SUCCESS; + case NETSNMP_OID_OUTPUT_UCD: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_Ucd); + return SUCCESS; + case NETSNMP_OID_OUTPUT_NONE: + ZVAL_LONG(retval, ZEND_ENUM_Snmp_OidOutput_None); + return SUCCESS; + default: + ZVAL_NULL(retval); + return SUCCESS; + } +} +/* }}} */ + #define PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(name) \ static zend_result php_snmp_read_##name(php_snmp_object *snmp_object, zval *retval) \ { \ @@ -2043,6 +2300,12 @@ static zend_result php_snmp_read_max_oids(php_snmp_object *snmp_object, zval *re PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(oid_increasing_check) PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(quick_print) PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(enum_print) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(numeric_index) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(numeric_timeticks) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(extended_index) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(dont_print_units) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(escape_quotes) +PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(print_hex_text) #define PHP_SNMP_LONG_PROPERTY_READER_FUNCTION(name) \ static zend_result php_snmp_read_##name(php_snmp_object *snmp_object, zval *retval) \ @@ -2052,7 +2315,6 @@ PHP_SNMP_BOOL_PROPERTY_READER_FUNCTION(enum_print) } PHP_SNMP_LONG_PROPERTY_READER_FUNCTION(valueretrieval) -PHP_SNMP_LONG_PROPERTY_READER_FUNCTION(oid_output_format) PHP_SNMP_LONG_PROPERTY_READER_FUNCTION(exceptions_enabled) /* {{{ */ @@ -2106,9 +2368,15 @@ static zend_result php_snmp_write_##name(php_snmp_object *snmp_object, zval *new return SUCCESS; \ } +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(oid_increasing_check) PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(quick_print) PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(enum_print) -PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(oid_increasing_check) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(numeric_index) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(numeric_timeticks) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(extended_index) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(dont_print_units) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(escape_quotes) +PHP_SNMP_BOOL_PROPERTY_WRITER_FUNCTION(print_hex_text) /* {{{ */ static zend_result php_snmp_write_oid_output_format(php_snmp_object *snmp_object, zval *newval) @@ -2116,16 +2384,26 @@ static zend_result php_snmp_write_oid_output_format(php_snmp_object *snmp_object zend_long lval = zval_get_long(newval); switch(lval) { - case NETSNMP_OID_OUTPUT_SUFFIX: - case NETSNMP_OID_OUTPUT_MODULE: - case NETSNMP_OID_OUTPUT_FULL: - case NETSNMP_OID_OUTPUT_NUMERIC: - case NETSNMP_OID_OUTPUT_UCD: - case NETSNMP_OID_OUTPUT_NONE: - snmp_object->oid_output_format = lval; + case ZEND_ENUM_Snmp_OidOutput_Suffix: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_SUFFIX; + return SUCCESS; + case ZEND_ENUM_Snmp_OidOutput_Module: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_MODULE; + return SUCCESS; + case ZEND_ENUM_Snmp_OidOutput_Full: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_FULL; + return SUCCESS; + case ZEND_ENUM_Snmp_OidOutput_Numeric: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_NUMERIC; + return SUCCESS; + case ZEND_ENUM_Snmp_OidOutput_Ucd: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_UCD; + return SUCCESS; + case ZEND_ENUM_Snmp_OidOutput_None: + snmp_object->oid_output_format = NETSNMP_OID_OUTPUT_NONE; return SUCCESS; default: - zend_value_error("SNMP output print format must be an SNMP_OID_OUTPUT_* constant"); + zend_value_error("SNMP output print format must be a SNMP_OID_OUTPUT_* constant"); return FAILURE; } } @@ -2155,11 +2433,17 @@ static void free_php_snmp_properties(zval *el) /* {{{ */ const php_snmp_prop_handler php_snmp_property_entries[] = { PHP_SNMP_READONLY_PROPERTY_ENTRY_RECORD(info), PHP_SNMP_PROPERTY_ENTRY_RECORD(max_oids), - PHP_SNMP_PROPERTY_ENTRY_RECORD(valueretrieval), + PHP_SNMP_PROPERTY_ENTRY_RECORD(oid_increasing_check), PHP_SNMP_PROPERTY_ENTRY_RECORD(quick_print), PHP_SNMP_PROPERTY_ENTRY_RECORD(enum_print), + PHP_SNMP_PROPERTY_ENTRY_RECORD(numeric_index), + PHP_SNMP_PROPERTY_ENTRY_RECORD(numeric_timeticks), + PHP_SNMP_PROPERTY_ENTRY_RECORD(extended_index), + PHP_SNMP_PROPERTY_ENTRY_RECORD(dont_print_units), + PHP_SNMP_PROPERTY_ENTRY_RECORD(escape_quotes), + PHP_SNMP_PROPERTY_ENTRY_RECORD(print_hex_text), + PHP_SNMP_PROPERTY_ENTRY_RECORD(valueretrieval), PHP_SNMP_PROPERTY_ENTRY_RECORD(oid_output_format), - PHP_SNMP_PROPERTY_ENTRY_RECORD(oid_increasing_check), PHP_SNMP_PROPERTY_ENTRY_RECORD(exceptions_enabled), { NULL, 0, NULL, NULL} }; @@ -2209,6 +2493,12 @@ PHP_MINIT_FUNCTION(snmp) /* Register SNMPException class */ php_snmp_exception_ce = register_class_SNMPException(spl_ce_RuntimeException); + /* Register enums */ + SnmpMib_ce = register_class_Snmp_Mib(); + SnmpOidOutput_ce = register_class_Snmp_OidOutput(); + SnmpOutput_ce = register_class_Snmp_Output(); + SnmpStringOutput_ce = register_class_Snmp_StringOutput(); + register_snmp_symbols(module_number); return SUCCESS; @@ -2226,6 +2516,21 @@ PHP_MSHUTDOWN_FUNCTION(snmp) } /* }}} */ +/* {{{ PHP_INIT_FUNCTION */ +static PHP_RINIT_FUNCTION(snmp) +{ + // Save the output options + save_snmplib_output_options(&saved_snmp_settings); + + // Save the MIB options + saved_mib_allow_underscores = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_PARSE_LABEL); + saved_mib_comment_term = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_COMMENT_TERM); + saved_mib_replace = netsnmp_ds_get_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_REPLACE); + + return SUCCESS; +} +/* }}} */ + /* {{{ PHP_RSHUTDOWN_FUNCTION */ static PHP_RSHUTDOWN_FUNCTION(snmp) { @@ -2239,6 +2544,14 @@ static PHP_RSHUTDOWN_FUNCTION(snmp) #endif } + // Restore the output options + set_snmplib_output_options(&saved_snmp_settings); + + // Restore MIB options + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_PARSE_LABEL, saved_mib_allow_underscores); + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_COMMENT_TERM, saved_mib_comment_term); + netsnmp_ds_set_int(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_MIB_REPLACE, saved_mib_replace); + return SUCCESS; } /* }}} */ @@ -2269,7 +2582,7 @@ zend_module_entry snmp_module_entry = { ext_functions, PHP_MINIT(snmp), PHP_MSHUTDOWN(snmp), - NULL, + PHP_RINIT(snmp), PHP_RSHUTDOWN(snmp), PHP_MINFO(snmp), PHP_SNMP_VERSION, diff --git a/ext/snmp/snmp.stub.php b/ext/snmp/snmp.stub.php index b40a2fcd9f20..f68da7e75f1c 100644 --- a/ext/snmp/snmp.stub.php +++ b/ext/snmp/snmp.stub.php @@ -1,36 +1,40 @@ NULL - ["valueretrieval"]=> - int(1) + ["oid_increasing_check"]=> + bool(true) ["quick_print"]=> bool(false) ["enum_print"]=> bool(false) + ["numeric_index"]=> + bool(false) + ["numeric_timeticks"]=> + bool(false) + ["extended_index"]=> + bool(false) + ["dont_print_units"]=> + bool(false) + ["escape_quotes"]=> + bool(false) + ["print_hex_text"]=> + bool(false) + ["valueretrieval"]=> + int(1) ["oid_output_format"]=> int(3) - ["oid_increasing_check"]=> - bool(true) ["exceptions_enabled"]=> int(0) } @@ -117,16 +129,28 @@ object(SNMP)#%d (%d) { } ["max_oids"]=> int(40) - ["valueretrieval"]=> - int(0) + ["oid_increasing_check"]=> + bool(false) ["quick_print"]=> bool(true) ["enum_print"]=> bool(true) + ["numeric_index"]=> + bool(false) + ["numeric_timeticks"]=> + bool(false) + ["extended_index"]=> + bool(false) + ["dont_print_units"]=> + bool(false) + ["escape_quotes"]=> + bool(false) + ["print_hex_text"]=> + bool(false) + ["valueretrieval"]=> + int(0) ["oid_output_format"]=> int(4) - ["oid_increasing_check"]=> - bool(false) ["exceptions_enabled"]=> int(0) } @@ -142,16 +166,28 @@ object(SNMP)#%d (%d) { } ["max_oids"]=> int(40) - ["valueretrieval"]=> - int(1) + ["oid_increasing_check"]=> + bool(true) ["quick_print"]=> bool(true) ["enum_print"]=> bool(true) + ["numeric_index"]=> + bool(false) + ["numeric_timeticks"]=> + bool(false) + ["extended_index"]=> + bool(false) + ["dont_print_units"]=> + bool(false) + ["escape_quotes"]=> + bool(false) + ["print_hex_text"]=> + bool(false) + ["valueretrieval"]=> + int(1) ["oid_output_format"]=> int(3) - ["oid_increasing_check"]=> - bool(true) ["exceptions_enabled"]=> int(0) } @@ -172,16 +208,28 @@ object(SNMP)#%d (%d) { } ["max_oids"]=> int(40) - ["valueretrieval"]=> - int(1) + ["oid_increasing_check"]=> + bool(true) ["quick_print"]=> bool(true) ["enum_print"]=> bool(true) + ["numeric_index"]=> + bool(false) + ["numeric_timeticks"]=> + bool(false) + ["extended_index"]=> + bool(false) + ["dont_print_units"]=> + bool(false) + ["escape_quotes"]=> + bool(false) + ["print_hex_text"]=> + bool(false) + ["valueretrieval"]=> + int(1) ["oid_output_format"]=> int(3) - ["oid_increasing_check"]=> - bool(true) ["exceptions_enabled"]=> int(0) ["123"]=> @@ -195,6 +243,6 @@ Warning: Undefined property: SNMP::$there is no such parameter in %s on line %d NULL bool(false) SNMP retrieval method must be a bitmask of SNMP_VALUE_LIBRARY, SNMP_VALUE_PLAIN, and SNMP_VALUE_OBJECT -SNMP output print format must be an SNMP_OID_OUTPUT_* constant +SNMP output print format must be a SNMP_OID_OUTPUT_* constant Cannot write read-only property SNMP::$info NULL diff --git a/ext/snmp/tests/snmp_set_oid_output_format.phpt b/ext/snmp/tests/snmp_set_oid_output_format.phpt index bb8c63bdda1a..582ba79e7c29 100644 --- a/ext/snmp/tests/snmp_set_oid_output_format.phpt +++ b/ext/snmp/tests/snmp_set_oid_output_format.phpt @@ -26,7 +26,7 @@ var_dump(snmp_set_oid_output_format(SNMP_OID_OUTPUT_NUMERIC)); ?> --EXPECT-- Checking error handling -snmp_set_oid_output_format(): Argument #1 ($format) must be an SNMP_OID_OUTPUT_* constant +snmp_set_oid_output_format(): Argument #1 ($format) must be a Snmp\OidOutput constant Checking working bool(true) bool(true) From 399e6bf5b96210ce356ce4081490ff87592576d6 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 24 Aug 2026 14:25:36 +0100 Subject: [PATCH 2/9] Update NEWS/UPGRADING for snmp and session changes --- NEWS | 19 +++++++++++++++++++ UPGRADING | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/NEWS b/NEWS index 2e7adae0d951..70a83948d7b8 100644 --- a/NEWS +++ b/NEWS @@ -79,6 +79,25 @@ PHP NEWS (lazerg) . SessionHandler::validateId() is now implemented, so session.use_strict_mode applies to the built-in handler. (Girgias) + . It is now deprecated to pass an object that does not implement the + create_sid() and validateId() methods. (Girgias) + . A deprecation is now emitted when implementing SessionHandlerInterface on a + class which doesn't define the create_sid() or validateId() methods, as + those will be moved from SessionUpdateTimestampHandlerInterface and + SessionIdInterface to SessionHandlerInterface. (Girgias) + +- SNMP: + . It is now possible to use the AES192, AES192C, AES256, and AES256C as + SNMPv3 security protocols if the underlying library supports them. + (eskyuu) + . It is now possible to reset the MIB tree using the new snmp_read_mib() + function. (eskyuu) + . Additional MIB parsing and output control functionality has been exposed + via the snmp_set_mib_option(), snmp_set_output_option(), + snmp_set_string_output_format() functions, the $numeric_index, + $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, + $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), + SNMP::setStringOutputFormat() methods. (eskyuu) - Sodium: . Fixed incorrect parameter name in sodium_add(), sodium_memcmp(), and diff --git a/UPGRADING b/UPGRADING index b55cb53a2bad..14cea21664c0 100644 --- a/UPGRADING +++ b/UPGRADING @@ -415,6 +415,21 @@ PHP 8.6 UPGRADE NOTES influences the result of the phar buildFrom family of functions. This makes it possible to override the timestamp and names of files. +- SNMP: + . It is now possible to use the AES192, AES192C, AES256, and AES256C as + SNMPv3 security protocols if the underlying library supports them. + RFC: https://wiki.php.net/rfc/snmp_improvements_2026#increase_the_number_of_snmpv3_security_protocols_supported + . It is now possible to reset the MIB tree using the new snmp_read_mib() + function. + RFC: https://wiki.php.net/rfc/snmp_improvements_2026#allow_the_snmp_mib_to_be_reset + . Additional MIB parsing and output control functionality has been exposed + via the snmp_set_mib_option(), snmp_set_output_option(), + snmp_set_string_output_format() functions, the $numeric_index, + $numeric_timeticks, $extended_index, $dont_print_units, $escape_quotes, + $print_hex_text SNMP properties, and the SNMP::setOidOutputFormat(), + SNMP::setStringOutputFormat() methods. (eskyuu) + RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls + - Streams: . Added new stream errors API including new classes, enums, functions and internal API. It is controlled using error_mode, error_store and @@ -514,6 +529,16 @@ PHP 8.6 UPGRADE NOTES an object and a static method is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_reflectionmethodinvoke_and_reflectionmethodinvokeargs_with_objects_for_static_methods +- Session: + . It is now deprecated to pass an object that does not implement the + create_sid() and validateId() methods. + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid + . A deprecation is now emitted when implementing SessionHandlerInterface on a + class which doesn't define the create_sid() or validateId() methods, as + those will be moved from SessionUpdateTimestampHandlerInterface and + SessionIdInterface to SessionHandlerInterface. + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_passing_a_sessionhandler_object_to_session_set_save_handler_which_does_not_contain_the_create_sid_and_validateid + - SPL: . The spl_classes() function is now deprecated, use ReflectionExtension::getClassNames() instead. @@ -660,6 +685,12 @@ PHP 8.6 UPGRADE NOTES . ReflectionAttribute::getNamespaceName() . ReflectionAttribute::getShortName() +- SNMP: + . snmp_init_mib() + . snmp_set_mib_option() + . snmp_set_output_option() + . snmp_set_string_output_format() + - Standard: . clamp() returns the given value if in range, else return the nearest bound. @@ -697,6 +728,12 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Openssl\Psk +- SNMP: + . enum: Snmp\Mib + . enum: Snmp\OidOutput + . enum: Snmp\Output + . enum: Snmp\StringOutput + - Standard: . enum SortDirection RFC: https://wiki.php.net/rfc/sort_direction_enum From 7873640e08e707c2cfbdf72daa10c592244a1cda Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 22 Aug 2026 13:36:34 -0400 Subject: [PATCH 3/9] JIT: persist the SHM op_array in trace exit_info exit_info.op_array was taken from the current frame. Methods of linked classes that miss the inheritance cache use a heap copy of the op_array header, so that pointer is invalid in other processes and later requests. Store the original from the JIT extension, as root traces already do. Closes GH-21710 --- NEWS | 3 ++ ext/opcache/jit/zend_jit_trace.c | 5 +++ ext/opcache/tests/jit/gh21710.inc | 15 +++++++++ ext/opcache/tests/jit/gh21710.phpt | 50 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 ext/opcache/tests/jit/gh21710.inc create mode 100644 ext/opcache/tests/jit/gh21710.phpt diff --git a/NEWS b/NEWS index d25d441ca8b5..a2c65685b4ce 100644 --- a/NEWS +++ b/NEWS @@ -33,6 +33,9 @@ PHP NEWS . Fixed opcache.protect_memory race under ZTS. (realFlowControl) . Fixed bug GH-23288 (Crash on restart when opcache.interned_strings_buffer is overridden in an individual FPM pool). (David Carlier) + . Fixed a tracing JIT crash when compiling a side trace for a method of a + class that could not be stored in the inheritance cache. (GH-21710) + (Arnaud, iliaal) - PDO: . Fixed a leak when a persistent connection failed a liveness check diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 225257ecd6a4..6a3e8c3a4711 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -145,6 +145,11 @@ static uint32_t zend_jit_trace_get_exit_point(const zend_op *to_opline, uint32_t } if (JIT_G(current_frame)) { op_array = &JIT_G(current_frame)->func->op_array; + if (!(op_array->fn_flags & ZEND_ACC_IMMUTABLE)) { + zend_jit_op_array_trace_extension *jit_extension = + (zend_jit_op_array_trace_extension*)ZEND_FUNC_INFO(op_array); + op_array = jit_extension->op_array; + } stack_size = op_array->last_var + op_array->T; if (stack_size) { stack = JIT_G(current_frame)->stack; diff --git a/ext/opcache/tests/jit/gh21710.inc b/ext/opcache/tests/jit/gh21710.inc new file mode 100644 index 000000000000..a727c8f12560 --- /dev/null +++ b/ext/opcache/tests/jit/gh21710.inc @@ -0,0 +1,15 @@ + +--FILE-- + +--EXPECT-- +bool(true) From b83afc6caf05e12a96745a102e43a92dd264a24a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 24 Aug 2026 14:38:18 +0100 Subject: [PATCH 4/9] Zend: refactor zend_parse_arg_impl() to return zend_expected_type (#23052) This effectively mimics part of what Fast ZPP does and allows us to re-use the fast ZPP error APIs --- Zend/zend_API.c | 128 +++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index e36ca8d1cfe2..43e21cafd564 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -844,7 +844,7 @@ ZEND_API bool ZEND_FASTCALL zend_parse_arg_str_or_long_slow(zval *arg, zend_stri } /* }}} */ -static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */ +static zend_expected_type zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */ { const char *spec_walk = *spec; char c = *spec_walk++; @@ -878,7 +878,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec } if (!zend_parse_arg_long(arg, p, is_null, check_null, arg_num)) { - return check_null ? "?int" : "int"; + return check_null ? Z_EXPECTED_LONG_OR_NULL : Z_EXPECTED_LONG; } } break; @@ -893,7 +893,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec } if (!zend_parse_arg_double(arg, p, is_null, check_null, arg_num)) { - return check_null ? "?float" : "float"; + return check_null ? Z_EXPECTED_DOUBLE_OR_NULL : Z_EXPECTED_DOUBLE; } } break; @@ -903,7 +903,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zval **p = va_arg(*va, zval **); if (!zend_parse_arg_number(arg, p, check_null, arg_num)) { - return check_null ? "int|float|null" : "int|float"; + return check_null ? Z_EXPECTED_NUMBER_OR_NULL : Z_EXPECTED_NUMBER; } } break; @@ -913,7 +913,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec char **p = va_arg(*va, char **); size_t *pl = va_arg(*va, size_t *); if (!zend_parse_arg_string(arg, p, pl, check_null, arg_num)) { - return check_null ? "?string" : "string"; + return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING; } } break; @@ -923,12 +923,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec char **p = va_arg(*va, char **); size_t *pl = va_arg(*va, size_t *); if (!zend_parse_arg_path(arg, p, pl, check_null, arg_num)) { - if (Z_TYPE_P(arg) == IS_STRING) { - zend_spprintf(error, 0, "must not contain any null bytes"); - return ""; - } else { - return check_null ? "?string" : "string"; - } + return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH; } } break; @@ -937,12 +932,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec { zend_string **str = va_arg(*va, zend_string **); if (!zend_parse_arg_path_str(arg, str, check_null, arg_num)) { - if (Z_TYPE_P(arg) == IS_STRING) { - zend_spprintf(error, 0, "must not contain any null bytes"); - return ""; - } else { - return check_null ? "?string" : "string"; - } + return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH; } } break; @@ -951,7 +941,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec { zend_string **str = va_arg(*va, zend_string **); if (!zend_parse_arg_str(arg, str, check_null, arg_num)) { - return check_null ? "?string" : "string"; + return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING; } } break; @@ -966,7 +956,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec } if (!zend_parse_arg_bool(arg, p, is_null, check_null, arg_num)) { - return check_null ? "?bool" : "bool"; + return check_null ? Z_EXPECTED_BOOL_OR_NULL : Z_EXPECTED_BOOL; } } break; @@ -976,7 +966,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zval **p = va_arg(*va, zval **); if (!zend_parse_arg_resource(arg, p, check_null)) { - return check_null ? "resource or null" : "resource"; + return check_null ? Z_EXPECTED_RESOURCE_OR_NULL : Z_EXPECTED_RESOURCE; } } break; @@ -987,7 +977,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zval **p = va_arg(*va, zval **); if (!zend_parse_arg_array(arg, p, check_null, c == 'A')) { - return check_null ? "?array" : "array"; + return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY; } } break; @@ -998,7 +988,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec HashTable **p = va_arg(*va, HashTable **); if (!zend_parse_arg_array_ht(arg, p, check_null, c == 'H', separate)) { - return check_null ? "?array" : "array"; + return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY; } } break; @@ -1008,7 +998,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zval **p = va_arg(*va, zval **); if (!zend_parse_arg_object(arg, p, NULL, check_null)) { - return check_null ? "?object" : "object"; + return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT; } } break; @@ -1020,15 +1010,9 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec if (!zend_parse_arg_object(arg, p, ce, check_null)) { if (ce) { - if (check_null) { - zend_spprintf(error, 0, "must be of type ?%s, %s given", ZSTR_VAL(ce->name), zend_zval_value_name(arg)); - return ""; - } else { - return ZSTR_VAL(ce->name); - } - } else { - return check_null ? "?object" : "object"; + *error = ZSTR_VAL(ce->name); } + return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT; } } break; @@ -1046,14 +1030,13 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec /* Only accept string and Stringable(?) as int/foat/bool are not valid class names */ if (UNEXPECTED(Z_TYPE_P(arg) != IS_STRING)) { if (Z_TYPE_P(arg) != IS_OBJECT || !zend_parse_arg_str_slow(arg, arg_num)) { - *pce = NULL; /* __toString may throw */ if (!EG(exception)) { zend_spprintf(error, 0, "must be a valid class name%s, %s given", check_null ? " or null" : "", zend_zval_value_name(arg)); } *pce = NULL; - return ""; + return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME; } /* Object was converted to string */ ZEND_ASSERT(Z_TYPE_P(arg) == IS_STRING); @@ -1066,13 +1049,11 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zend_spprintf(error, 0, "must be a class name derived from %s%s, \"%s\" given", ZSTR_VAL(ce_base->name), check_null ? " or null" : "", Z_STRVAL_P(arg)); *pce = NULL; - return ""; + return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME; } } if (!*pce) { - zend_spprintf(error, 0, "must be a valid class name%s, \"%s\" given", - check_null ? " or null" : "", Z_STRVAL_P(arg)); - return ""; + return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME; } break; @@ -1084,19 +1065,11 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec { zend_fcall_info *fci = va_arg(*va, zend_fcall_info *); zend_fcall_info_cache *fcc = va_arg(*va, zend_fcall_info_cache *); - char *is_callable_error = NULL; - if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, &is_callable_error, c == 'f'))) { - ZEND_ASSERT(!is_callable_error); + if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, error, c == 'f'))) { + ZEND_ASSERT(!*error); break; } - - if (is_callable_error) { - zend_spprintf(error, 0, "must be a valid callback%s, %s", check_null ? " or null" : "", is_callable_error); - efree(is_callable_error); - return ""; - } else { - return check_null ? "a valid callback or null" : "a valid callback"; - } + return check_null ? Z_EXPECTED_FUNC_OR_NULL : Z_EXPECTED_FUNC; } case 'z': @@ -1112,37 +1085,68 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec ZEND_ASSERT(0 && "ZPP modifier no longer supported"); ZEND_FALLTHROUGH; default: - return "unknown"; + ZEND_ASSERT(false && "Unknown ZPP modifier"); } *spec = spec_walk; - return NULL; + return Z_EXPECTED_LAST; } /* }}} */ static zend_result zend_parse_arg(uint32_t arg_num, zval *arg, va_list *va, const char **spec, int flags) /* {{{ */ { - const char *expected_type = NULL; char *error = NULL; - expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num); - if (expected_type) { + zend_expected_type expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num); + if (expected_type != Z_EXPECTED_LAST) { if (EG(exception)) { return FAILURE; } - if (!(flags & ZEND_PARSE_PARAMS_QUIET) && (*expected_type || error)) { + + if (!(flags & ZEND_PARSE_PARAMS_QUIET)) { + /* More complex error, can only happen for: + * Objects of a specific class + * Z_EXPECTED_OBJECT + * Z_EXPECTED_OBJECT_OR_NULL + * Class names + * Z_EXPECTED_CLASS_NAME + * Z_EXPECTED_CLASS_NAME_OR_NULL + * Functions + * Z_EXPECTED_FUNC + * Z_EXPECTED_FUNC_OR_NULL + */ if (error) { - if (strcmp(error, "must not contain any null bytes") == 0) { - zend_argument_value_error(arg_num, "%s", error); - } else { - zend_argument_type_error(arg_num, "%s", error); + switch (expected_type) { + case Z_EXPECTED_OBJECT: + /* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */ + zend_wrong_parameter_class_error(arg_num, error, arg); + break; + case Z_EXPECTED_OBJECT_OR_NULL: + /* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */ + zend_wrong_parameter_class_or_null_error(arg_num, error, arg); + break; + case Z_EXPECTED_FUNC: + /* error is freed by zend_wrong_callback_error() */ + zend_wrong_callback_error(arg_num, error); + break; + case Z_EXPECTED_FUNC_OR_NULL: + /* error is freed by zend_wrong_callback_or_null_error() */ + zend_wrong_callback_or_null_error(arg_num, error); + break; + case Z_EXPECTED_CLASS_NAME: + case Z_EXPECTED_CLASS_NAME_OR_NULL: + zend_argument_type_error(arg_num, "%s", error); + efree(error); + break; + default: + ZEND_UNREACHABLE(); } - efree(error); - } else { - zend_argument_type_error(arg_num, "must be of type %s, %s given", expected_type, zend_zval_value_name(arg)); } - } else if (error) { + zend_wrong_parameter_type_error(arg_num, expected_type, arg); + } else if (error + /* DO NOT FREE error when it's a pointer to ZSTR_VAL(ce->name) */ + && expected_type != Z_EXPECTED_OBJECT && expected_type != Z_EXPECTED_OBJECT_OR_NULL) { efree(error); } From 9983490424abee7d1add7a4b785077970ab94cd9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 24 Aug 2026 16:05:45 +0200 Subject: [PATCH 5/9] [skip ci] Document the closure $this internals change in UPGRADING.INTERNALS (#23434) zend_create_closure(), zend_create_fake_closure() and zend_create_partial_closure() take a zend_object* since fbb2e1f23d6, and zend_get_closure_this_ptr() returns one since 7a5e452f14c. Neither change was listed, so extensions built against the new headers get only a -Wincompatible-pointer-types warning and read garbage at runtime. --- UPGRADING.INTERNALS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index dc2a35cc9c2c..bd671018e038 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -177,6 +177,11 @@ PHP 8.6 INTERNALS UPGRADE NOTES hand if there is one, E_WARNING for the severity, and ZEND_ENUM_StreamErrorCode_Generic for the code. terminating should be true only if the error aborts the operation. + . zend_create_closure(), zend_create_fake_closure() and + zend_create_partial_closure() now take the bound $this as a zend_object* + instead of a zval*. Accordingly, zend_get_closure_this_ptr() now returns + that zend_object*, or NULL when the closure is unbound, instead of a + zval* that is IS_UNDEF when the closure is unbound. - Added: . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were From 02229e2337aa6188d2eaa01d586b33a69fd39218 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:21:03 +0200 Subject: [PATCH 6/9] standard: Add support for endianness modifier in pack()/unpack() format codes (#21074) RFC (integer format codes): https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support RFC (float format codes): https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier --- NEWS | 2 + UPGRADING | 8 + ext/standard/pack.c | 390 ++++++++++++++---- .../tests/strings/pack_endian_modifiers.phpt | 182 ++++++++ .../strings/pack_endian_modifiers_32.phpt | 37 ++ .../strings/pack_endian_modifiers_64.phpt | 63 +++ .../strings/pack_endian_modifiers_error.phpt | 105 +++++ 7 files changed, 706 insertions(+), 81 deletions(-) create mode 100644 ext/standard/tests/strings/pack_endian_modifiers.phpt create mode 100644 ext/standard/tests/strings/pack_endian_modifiers_32.phpt create mode 100644 ext/standard/tests/strings/pack_endian_modifiers_64.phpt create mode 100644 ext/standard/tests/strings/pack_endian_modifiers_error.phpt diff --git a/NEWS b/NEWS index ccc3291c018a..022d48973602 100644 --- a/NEWS +++ b/NEWS @@ -110,6 +110,8 @@ PHP NEWS - Standard: . Fixed incorrect parameter name in convert_uudecode() warning. (lacatoire) + . Added support for the "<" and ">" endianness modifiers in pack() and + unpack() format codes. (alexandre-daubois) - Zip: . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive diff --git a/UPGRADING b/UPGRADING index 14cea21664c0..ad76c2c2e294 100644 --- a/UPGRADING +++ b/UPGRADING @@ -430,6 +430,14 @@ PHP 8.6 UPGRADE NOTES SNMP::setStringOutputFormat() methods. (eskyuu) RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls +- Standard: + . pack() and unpack() now accept the "<" and ">" endianness modifiers on + the signed and unsigned integer format codes. + RFC: https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support + . pack() and unpack() now accept the "<" and ">" endianness modifiers on + the float and double format codes. + RFC: https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier + - Streams: . Added new stream errors API including new classes, enums, functions and internal API. It is controlled using error_mode, error_store and diff --git a/ext/standard/pack.c b/ext/standard/pack.c index 1ab5d4858cbe..1da93228f71f 100644 --- a/ext/standard/pack.c +++ b/ext/standard/pack.c @@ -22,6 +22,7 @@ if ((a) < 0 || ((INT_MAX - outputpos)/((int)b)) < (a)) { \ efree(formatcodes); \ efree(formatargs); \ + efree(formatendian); \ zend_value_error("Type %c: integer overflow in format string", code); \ RETURN_THROWS(); \ } \ @@ -30,6 +31,7 @@ typedef enum { PHP_LITTLE_ENDIAN, PHP_BIG_ENDIAN, + PHP_NO_ENDIAN_MODIFIER, } php_pack_endianness; #ifdef WORDS_BIGENDIAN @@ -218,12 +220,27 @@ PHP_FUNCTION(pack) /* We have a maximum of format codes to deal with */ formatcodes = safe_emalloc(formatlen, sizeof(*formatcodes), 0); formatargs = safe_emalloc(formatlen, sizeof(*formatargs), 0); + php_pack_endianness *formatendian = safe_emalloc(formatlen, sizeof(*formatendian), 0); currentarg = 0; /* Preprocess format into formatcodes and formatargs */ for (i = 0; i < formatlen; formatcount++) { char code = format[i++]; int arg = 1; + php_pack_endianness endian = PHP_NO_ENDIAN_MODIFIER; + + /* Handle endianness modifier if any */ + if (i < formatlen) { + char c = format[i]; + + if (c == '<') { + endian = PHP_LITTLE_ENDIAN; + i++; + } else if (c == '>') { + endian = PHP_BIG_ENDIAN; + i++; + } + } /* Handle format arguments if any */ if (i < formatlen) { @@ -248,6 +265,13 @@ PHP_FUNCTION(pack) case 'x': case 'X': case '@': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("Endianness modifier is not supported for format code '%c'", code); + RETURN_THROWS(); + } if (arg < 0) { php_error_docref(NULL, E_WARNING, "Type %c: '*' ignored", code); arg = 1; @@ -260,9 +284,17 @@ PHP_FUNCTION(pack) case 'Z': case 'h': case 'H': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("Endianness modifier is not supported for format code '%c'", code); + RETURN_THROWS(); + } if (currentarg >= num_args) { efree(formatcodes); efree(formatargs); + efree(formatendian); zend_value_error("Type %c: not enough arguments", code); RETURN_THROWS(); } @@ -271,6 +303,7 @@ PHP_FUNCTION(pack) if (!try_convert_to_string(&argv[currentarg])) { efree(formatcodes); efree(formatargs); + efree(formatendian); RETURN_THROWS(); } @@ -286,35 +319,115 @@ PHP_FUNCTION(pack) currentarg++; break; - /* Use as many args as specified */ - case 'q': - case 'Q': + /* 64-bit codes with explicit endianness, endianness modifiers not allowed */ case 'J': case 'P': -#if SIZEOF_ZEND_LONG < 8 + if (endian != PHP_NO_ENDIAN_MODIFIER) { efree(formatcodes); efree(formatargs); - zend_value_error("64-bit format codes are not available for 32-bit versions of PHP"); + efree(formatendian); + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code); RETURN_THROWS(); + } + ZEND_FALLTHROUGH; + + /* 64-bit codes that support endianness modifiers */ + case 'q': + case 'Q': +#if SIZEOF_ZEND_LONG < 8 + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("64-bit format codes are not available for 32-bit versions of PHP"); + RETURN_THROWS(); +#else + if (arg < 0) { + arg = num_args - currentarg; + } + if (currentarg > INT_MAX - arg) { + goto too_few_args; + } + currentarg += arg; + + if (currentarg > num_args) { + goto too_few_args; + } + break; #endif - case 'c': - case 'C': - case 's': - case 'S': - case 'i': - case 'I': - case 'l': - case 'L': + + /* Codes with explicit endianness, endianness modifiers not allowed */ case 'n': case 'N': case 'v': case 'V': - case 'f': /* float */ + if (endian != PHP_NO_ENDIAN_MODIFIER) { + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code); + RETURN_THROWS(); + } + ZEND_FALLTHROUGH; + + /* Codes that support endianness modifiers */ + case 's': + case 'S': + case 'l': + case 'L': + if (arg < 0) { + arg = num_args - currentarg; + } + if (currentarg > INT_MAX - arg) { + goto too_few_args; + } + currentarg += arg; + + if (currentarg > num_args) { + goto too_few_args; + } + break; + + case 'c': + case 'C': + case 'i': + case 'I': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("Endianness modifier is not supported for format code '%c'", code); + RETURN_THROWS(); + } + if (arg < 0) { + arg = num_args - currentarg; + } + if (currentarg > INT_MAX - arg) { + goto too_few_args; + } + currentarg += arg; + + if (currentarg > num_args) { + goto too_few_args; + } + break; + + /* Codes with explicit endianness, endianness modifiers not allowed */ case 'g': /* little endian float */ case 'G': /* big endian float */ - case 'd': /* double */ case 'e': /* little endian double */ case 'E': /* big endian double */ + if (endian != PHP_NO_ENDIAN_MODIFIER) { + efree(formatcodes); + efree(formatargs); + efree(formatendian); + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code); + RETURN_THROWS(); + } + ZEND_FALLTHROUGH; + + /* Codes that support endianness modifiers */ + case 'f': /* float */ + case 'd': /* double */ if (arg < 0) { arg = num_args - currentarg; } @@ -327,6 +440,7 @@ PHP_FUNCTION(pack) too_few_args: efree(formatcodes); efree(formatargs); + efree(formatendian); zend_value_error("Type %c: too few arguments", code); RETURN_THROWS(); } @@ -335,12 +449,14 @@ PHP_FUNCTION(pack) default: efree(formatcodes); efree(formatargs); + efree(formatendian); zend_value_error("Type %c: unknown format code", code); RETURN_THROWS(); } formatcodes[formatcount] = code; formatargs[formatcount] = arg; + formatendian[formatcount] = endian; } if (currentarg < num_args) { @@ -507,12 +623,16 @@ PHP_FUNCTION(pack) case 'S': case 'n': case 'v': { - php_pack_endianness endianness = PHP_MACHINE_ENDIAN; + php_pack_endianness endianness; if (code == 'n') { endianness = PHP_BIG_ENDIAN; } else if (code == 'v') { endianness = PHP_LITTLE_ENDIAN; + } else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) { + endianness = formatendian[i]; + } else { + endianness = PHP_MACHINE_ENDIAN; } while (arg-- > 0) { @@ -534,12 +654,16 @@ PHP_FUNCTION(pack) case 'L': case 'N': case 'V': { - php_pack_endianness endianness = PHP_MACHINE_ENDIAN; + php_pack_endianness endianness; if (code == 'N') { endianness = PHP_BIG_ENDIAN; } else if (code == 'V') { endianness = PHP_LITTLE_ENDIAN; + } else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) { + endianness = formatendian[i]; + } else { + endianness = PHP_MACHINE_ENDIAN; } while (arg-- > 0) { @@ -554,12 +678,16 @@ PHP_FUNCTION(pack) case 'Q': case 'J': case 'P': { - php_pack_endianness endianness = PHP_MACHINE_ENDIAN; + php_pack_endianness endianness; if (code == 'J') { endianness = PHP_BIG_ENDIAN; } else if (code == 'P') { endianness = PHP_LITTLE_ENDIAN; + } else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) { + endianness = formatendian[i]; + } else { + endianness = PHP_MACHINE_ENDIAN; } while (arg-- > 0) { @@ -570,59 +698,35 @@ PHP_FUNCTION(pack) } #endif - case 'f': { - while (arg-- > 0) { - float v = (float) zval_get_double(&argv[currentarg++]); - memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v)); - outputpos += sizeof(v); - } - break; - } - - case 'g': { - /* pack little endian float */ - while (arg-- > 0) { - float v = (float) zval_get_double(&argv[currentarg++]); - php_pack_copy_float(1, &ZSTR_VAL(output)[outputpos], v); - outputpos += sizeof(v); - } - - break; - } + case 'f': + case 'g': case 'G': { - /* pack big endian float */ while (arg-- > 0) { float v = (float) zval_get_double(&argv[currentarg++]); - php_pack_copy_float(0, &ZSTR_VAL(output)[outputpos], v); - outputpos += sizeof(v); - } - break; - } - - case 'd': { - while (arg-- > 0) { - double v = zval_get_double(&argv[currentarg++]); - memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v)); - outputpos += sizeof(v); - } - break; - } - - case 'e': { - /* pack little endian double */ - while (arg-- > 0) { - double v = zval_get_double(&argv[currentarg++]); - php_pack_copy_double(1, &ZSTR_VAL(output)[outputpos], v); + if (code == 'g' || formatendian[i] == PHP_LITTLE_ENDIAN) { + php_pack_copy_float(1, &ZSTR_VAL(output)[outputpos], v); + } else if (code == 'G' || formatendian[i] == PHP_BIG_ENDIAN) { + php_pack_copy_float(0, &ZSTR_VAL(output)[outputpos], v); + } else { + memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v)); + } outputpos += sizeof(v); } break; } + case 'd': + case 'e': case 'E': { - /* pack big endian double */ while (arg-- > 0) { double v = zval_get_double(&argv[currentarg++]); - php_pack_copy_double(0, &ZSTR_VAL(output)[outputpos], v); + if (code == 'e' || formatendian[i] == PHP_LITTLE_ENDIAN) { + php_pack_copy_double(1, &ZSTR_VAL(output)[outputpos], v); + } else if (code == 'E' || formatendian[i] == PHP_BIG_ENDIAN) { + php_pack_copy_double(0, &ZSTR_VAL(output)[outputpos], v); + } else { + memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v)); + } outputpos += sizeof(v); } break; @@ -652,6 +756,7 @@ PHP_FUNCTION(pack) efree(formatcodes); efree(formatargs); + efree(formatendian); ZSTR_VAL(output)[outputpos] = '\0'; ZSTR_LEN(output) = outputpos; RETURN_NEW_STR(output); @@ -710,6 +815,21 @@ PHP_FUNCTION(unpack) char *name; int namelen; int size = 0; + php_pack_endianness endian = PHP_NO_ENDIAN_MODIFIER; + + if (formatlen > 0) { + char c = *format; + + if (c == '<') { + endian = PHP_LITTLE_ENDIAN; + format++; + formatlen--; + } else if (c == '>') { + endian = PHP_BIG_ENDIAN; + format++; + formatlen--; + } + } /* Handle format arguments if any */ if (formatlen > 0) { @@ -755,6 +875,10 @@ PHP_FUNCTION(unpack) switch (type) { /* Never use any input */ case 'X': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = -1; if (repetitions < 0) { php_error_docref(NULL, E_WARNING, "Type %c: '*' ignored", type); @@ -763,18 +887,30 @@ PHP_FUNCTION(unpack) break; case '@': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = 0; break; case 'a': case 'A': case 'Z': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = repetitions; repetitions = 1; break; case 'h': case 'H': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = (repetitions > 0) ? ((unsigned int) repetitions + 1) / 2 : repetitions; repetitions = 1; break; @@ -783,36 +919,73 @@ PHP_FUNCTION(unpack) case 'c': case 'C': case 'x': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = 1; break; - /* Use 2 bytes of input */ + /* Use 2 bytes of input, endianness modifiers allowed */ case 's': case 'S': + size = 2; + break; + + /* Use 2 bytes of input with inherent endianness */ case 'n': case 'v': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type); + RETURN_THROWS(); + } size = 2; break; /* Use sizeof(int) bytes of input */ case 'i': case 'I': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier is not supported for format code '%c'", type); + RETURN_THROWS(); + } size = sizeof(int); break; - /* Use 4 bytes of input */ + /* Use 4 bytes of input, endianness modifiers allowed */ case 'l': case 'L': + size = 4; + break; + + /* Use 4 bytes of input with inherent endianness */ case 'N': case 'V': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type); + RETURN_THROWS(); + } size = 4; break; - /* Use 8 bytes of input */ + /* Use 8 bytes of input, endianness modifiers allowed */ case 'q': case 'Q': +#if SIZEOF_ZEND_LONG > 4 + size = 8; + break; +#else + zend_value_error("64-bit format codes are not available for 32-bit versions of PHP"); + RETURN_THROWS(); +#endif + + /* Use 8 bytes of input with inherent endianness */ case 'J': case 'P': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type); + RETURN_THROWS(); + } #if SIZEOF_ZEND_LONG > 4 size = 8; break; @@ -821,17 +994,33 @@ PHP_FUNCTION(unpack) RETURN_THROWS(); #endif - /* Use sizeof(float) bytes of input */ + /* Use sizeof(float) bytes of input, endianness modifiers allowed */ case 'f': + size = sizeof(float); + break; + + /* Use sizeof(float) bytes of input with inherent endianness */ case 'g': case 'G': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type); + RETURN_THROWS(); + } size = sizeof(float); break; - /* Use sizeof(double) bytes of input */ + /* Use sizeof(double) bytes of input, endianness modifiers allowed */ case 'd': + size = sizeof(double); + break; + + /* Use sizeof(double) bytes of input with inherent endianness */ case 'e': case 'E': + if (endian != PHP_NO_ENDIAN_MODIFIER) { + zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type); + RETURN_THROWS(); + } size = sizeof(double); break; @@ -996,17 +1185,30 @@ PHP_FUNCTION(unpack) break; } - case 's': /* signed machine endian */ - case 'S': /* unsigned machine endian */ + case 's': /* signed, machine endian or explicit */ + case 'S': /* unsigned, machine endian or explicit */ case 'n': /* unsigned big endian */ case 'v': { /* unsigned little endian */ zend_long v = 0; uint16_t x = *((unaligned_uint16_t*) &input[inputpos]); + bool need_swap = false; + if (type == 'n') { + need_swap = MACHINE_LITTLE_ENDIAN; + } else if (type == 'v') { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_LITTLE_ENDIAN) { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_BIG_ENDIAN) { + need_swap = MACHINE_LITTLE_ENDIAN; + } + + if (need_swap) { + x = php_pack_reverse_int16(x); + } + if (type == 's') { v = (int16_t) x; - } else if ((type == 'n' && MACHINE_LITTLE_ENDIAN) || (type == 'v' && !MACHINE_LITTLE_ENDIAN)) { - v = php_pack_reverse_int16(x); } else { v = x; } @@ -1030,17 +1232,30 @@ PHP_FUNCTION(unpack) break; } - case 'l': /* signed machine endian */ - case 'L': /* unsigned machine endian */ + case 'l': /* signed, machine endian or explicit */ + case 'L': /* unsigned, machine endian or explicit */ case 'N': /* unsigned big endian */ case 'V': { /* unsigned little endian */ zend_long v = 0; uint32_t x = *((unaligned_uint32_t*) &input[inputpos]); + bool need_swap = false; + if (type == 'N') { + need_swap = MACHINE_LITTLE_ENDIAN; + } else if (type == 'V') { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_LITTLE_ENDIAN) { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_BIG_ENDIAN) { + need_swap = MACHINE_LITTLE_ENDIAN; + } + + if (need_swap) { + x = php_pack_reverse_int32(x); + } + if (type == 'l') { v = (int32_t) x; - } else if ((type == 'N' && MACHINE_LITTLE_ENDIAN) || (type == 'V' && !MACHINE_LITTLE_ENDIAN)) { - v = php_pack_reverse_int32(x); } else { v = x; } @@ -1050,17 +1265,30 @@ PHP_FUNCTION(unpack) } #if SIZEOF_ZEND_LONG > 4 - case 'q': /* signed machine endian */ - case 'Q': /* unsigned machine endian */ + case 'q': /* signed, machine endian or explicit */ + case 'Q': /* unsigned, machine endian or explicit */ case 'J': /* unsigned big endian */ case 'P': { /* unsigned little endian */ zend_long v = 0; uint64_t x = *((unaligned_uint64_t*) &input[inputpos]); + bool need_swap = false; + if (type == 'J') { + need_swap = MACHINE_LITTLE_ENDIAN; + } else if (type == 'P') { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_LITTLE_ENDIAN) { + need_swap = !MACHINE_LITTLE_ENDIAN; + } else if (endian == PHP_BIG_ENDIAN) { + need_swap = MACHINE_LITTLE_ENDIAN; + } + + if (need_swap) { + x = php_pack_reverse_int64(x); + } + if (type == 'q') { v = (int64_t) x; - } else if ((type == 'J' && MACHINE_LITTLE_ENDIAN) || (type == 'P' && !MACHINE_LITTLE_ENDIAN)) { - v = php_pack_reverse_int64(x); } else { v = x; } @@ -1076,9 +1304,9 @@ PHP_FUNCTION(unpack) { float v; - if (type == 'g') { + if (type == 'g' || endian == PHP_LITTLE_ENDIAN) { v = php_pack_parse_float(1, &input[inputpos]); - } else if (type == 'G') { + } else if (type == 'G' || endian == PHP_BIG_ENDIAN) { v = php_pack_parse_float(0, &input[inputpos]); } else { memcpy(&v, &input[inputpos], sizeof(float)); @@ -1094,9 +1322,9 @@ PHP_FUNCTION(unpack) case 'E': /* big endian float */ { double v; - if (type == 'e') { + if (type == 'e' || endian == PHP_LITTLE_ENDIAN) { v = php_pack_parse_double(1, &input[inputpos]); - } else if (type == 'E') { + } else if (type == 'E' || endian == PHP_BIG_ENDIAN) { v = php_pack_parse_double(0, &input[inputpos]); } else { memcpy(&v, &input[inputpos], sizeof(double)); diff --git a/ext/standard/tests/strings/pack_endian_modifiers.phpt b/ext/standard/tests/strings/pack_endian_modifiers.phpt new file mode 100644 index 000000000000..0ab0a4b2126c --- /dev/null +++ b/ext/standard/tests/strings/pack_endian_modifiers.phpt @@ -0,0 +1,182 @@ +--TEST-- +pack()/unpack() endianness modifiers +--FILE-- +", 0x0102))); +var_dump(bin2hex(pack("S>", 0x0102))); + +var_dump(pack("s<", 0x0102) === pack("v", 0x0102)); +var_dump(pack("S<", 0x0102) === pack("v", 0x0102)); + +var_dump(pack("s>", 0x0102) === pack("n", 0x0102)); +var_dump(pack("S>", 0x0102) === pack("n", 0x0102)); + +var_dump(bin2hex(pack("l<", 0x01020304))); +var_dump(bin2hex(pack("L<", 0x01020304))); + +var_dump(bin2hex(pack("l>", 0x01020304))); +var_dump(bin2hex(pack("L>", 0x01020304))); + +var_dump(pack("l<", 0x01020304) === pack("V", 0x01020304)); +var_dump(pack("L<", 0x01020304) === pack("V", 0x01020304)); + +var_dump(pack("l>", 0x01020304) === pack("N", 0x01020304)); +var_dump(pack("L>", 0x01020304) === pack("N", 0x01020304)); + +// === Integer unpack with endianness modifiers === + +var_dump(unpack("s<", "\x02\x01")); +var_dump(unpack("S<", "\x02\x01")); + +var_dump(unpack("s>", "\x01\x02")); +var_dump(unpack("S>", "\x01\x02")); + +var_dump(unpack("s<", "\xfe\xff")); // -2 in little-endian +var_dump(unpack("s>", "\xff\xfe")); // -2 in big-endian + +var_dump(unpack("l<", "\x04\x03\x02\x01")); +var_dump(unpack("L<", "\x04\x03\x02\x01")); + +var_dump(unpack("l>", "\x01\x02\x03\x04")); +var_dump(unpack("L>", "\x01\x02\x03\x04")); + +var_dump(unpack("l<", "\xfe\xff\xff\xff")); // -2 in little-endian +var_dump(unpack("l>", "\xff\xff\xff\xfe")); // -2 in big-endian + +var_dump(bin2hex(pack("s<2", 0x0102, 0x0304))); +var_dump(bin2hex(pack("s>2", 0x0102, 0x0304))); + +var_dump(unpack("s<2", "\x02\x01\x04\x03")); +var_dump(unpack("s>2", "\x01\x02\x03\x04")); + +var_dump(unpack("scount", "\x02\x01\x00\x00\x00\x05")); + +var_dump(pack("f<", 3.14) === pack("g", 3.14)); +var_dump(pack("f>", 3.14) === pack("G", 3.14)); + +var_dump(pack("d<", 3.14) === pack("e", 3.14)); +var_dump(pack("d>", 3.14) === pack("E", 3.14)); + +$packed_le = pack("g", 3.14); +$packed_be = pack("G", 3.14); +$unpacked_le = unpack("f<", $packed_le); +$unpacked_be = unpack("f>", $packed_be); +$unpacked_g = unpack("g", $packed_le); +$unpacked_G = unpack("G", $packed_be); +var_dump($unpacked_le[1] === $unpacked_g[1]); +var_dump($unpacked_be[1] === $unpacked_G[1]); + +$packed_le = pack("e", 3.14); +$packed_be = pack("E", 3.14); +$unpacked_le = unpack("d<", $packed_le); +$unpacked_be = unpack("d>", $packed_be); +$unpacked_e = unpack("e", $packed_le); +$unpacked_E = unpack("E", $packed_be); +var_dump($unpacked_le[1] === $unpacked_e[1]); +var_dump($unpacked_be[1] === $unpacked_E[1]); + +$machine_float = pack("f", 1.5); +var_dump(unpack("f", $machine_float)[1] === 1.5); + +$machine_double = pack("d", 1.5); +var_dump(unpack("d", $machine_double)[1] === 1.5); +?> +--EXPECT-- +string(4) "0201" +string(4) "0201" +string(4) "0102" +string(4) "0102" +bool(true) +bool(true) +bool(true) +bool(true) +string(8) "04030201" +string(8) "04030201" +string(8) "01020304" +string(8) "01020304" +bool(true) +bool(true) +bool(true) +bool(true) +array(1) { + [1]=> + int(258) +} +array(1) { + [1]=> + int(258) +} +array(1) { + [1]=> + int(258) +} +array(1) { + [1]=> + int(258) +} +array(1) { + [1]=> + int(-2) +} +array(1) { + [1]=> + int(-2) +} +array(1) { + [1]=> + int(16909060) +} +array(1) { + [1]=> + int(16909060) +} +array(1) { + [1]=> + int(16909060) +} +array(1) { + [1]=> + int(16909060) +} +array(1) { + [1]=> + int(-2) +} +array(1) { + [1]=> + int(-2) +} +string(8) "02010403" +string(8) "01020304" +array(2) { + [1]=> + int(258) + [2]=> + int(772) +} +array(2) { + [1]=> + int(258) + [2]=> + int(772) +} +array(2) { + ["value"]=> + int(258) + ["count"]=> + int(5) +} +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/strings/pack_endian_modifiers_32.phpt b/ext/standard/tests/strings/pack_endian_modifiers_32.phpt new file mode 100644 index 000000000000..13c2a3a73025 --- /dev/null +++ b/ext/standard/tests/strings/pack_endian_modifiers_32.phpt @@ -0,0 +1,37 @@ +--TEST-- +pack()/unpack() endianness modifiers on 64-bit format codes, 32-bit systems +--SKIPIF-- + 4) die("skip 32bit test only"); +?> +--FILE-- +', 'Q<', 'Q>']; +foreach ($formats as $fmt) { + try { + pack($fmt, 0); + echo "FAIL: Expected ValueError for pack('$fmt', 0)\n"; + } catch (ValueError $e) { + echo "pack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +foreach ($formats as $fmt) { + try { + unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00"); + echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n"; + } catch (ValueError $e) { + echo "unpack('$fmt'): " . $e->getMessage() . "\n"; + } +} +?> +--EXPECT-- +pack('q<'): 64-bit format codes are not available for 32-bit versions of PHP +pack('q>'): 64-bit format codes are not available for 32-bit versions of PHP +pack('Q<'): 64-bit format codes are not available for 32-bit versions of PHP +pack('Q>'): 64-bit format codes are not available for 32-bit versions of PHP +unpack('q<'): 64-bit format codes are not available for 32-bit versions of PHP +unpack('q>'): 64-bit format codes are not available for 32-bit versions of PHP +unpack('Q<'): 64-bit format codes are not available for 32-bit versions of PHP +unpack('Q>'): 64-bit format codes are not available for 32-bit versions of PHP diff --git a/ext/standard/tests/strings/pack_endian_modifiers_64.phpt b/ext/standard/tests/strings/pack_endian_modifiers_64.phpt new file mode 100644 index 000000000000..61a8f3f95f1c --- /dev/null +++ b/ext/standard/tests/strings/pack_endian_modifiers_64.phpt @@ -0,0 +1,63 @@ +--TEST-- +pack()/unpack() endianness modifiers on 64-bit format codes +--SKIPIF-- + +--FILE-- +", 0x0102030405060708))); +var_dump(bin2hex(pack("Q>", 0x0102030405060708))); + +var_dump(pack("q<", 0x0102030405060708) === pack("P", 0x0102030405060708)); +var_dump(pack("Q<", 0x0102030405060708) === pack("P", 0x0102030405060708)); + +var_dump(pack("q>", 0x0102030405060708) === pack("J", 0x0102030405060708)); +var_dump(pack("Q>", 0x0102030405060708) === pack("J", 0x0102030405060708)); + +var_dump(unpack("q<", "\x08\x07\x06\x05\x04\x03\x02\x01")); +var_dump(unpack("Q<", "\x08\x07\x06\x05\x04\x03\x02\x01")); + +var_dump(unpack("q>", "\x01\x02\x03\x04\x05\x06\x07\x08")); +var_dump(unpack("Q>", "\x01\x02\x03\x04\x05\x06\x07\x08")); + +var_dump(unpack("q<", "\xfe\xff\xff\xff\xff\xff\xff\xff")); // -2 in little-endian +var_dump(unpack("q>", "\xff\xff\xff\xff\xff\xff\xff\xfe")); // -2 in big-endian +?> +--EXPECT-- +string(16) "0807060504030201" +string(16) "0807060504030201" +string(16) "0102030405060708" +string(16) "0102030405060708" +bool(true) +bool(true) +bool(true) +bool(true) +array(1) { + [1]=> + int(72623859790382856) +} +array(1) { + [1]=> + int(72623859790382856) +} +array(1) { + [1]=> + int(72623859790382856) +} +array(1) { + [1]=> + int(72623859790382856) +} +array(1) { + [1]=> + int(-2) +} +array(1) { + [1]=> + int(-2) +} diff --git a/ext/standard/tests/strings/pack_endian_modifiers_error.phpt b/ext/standard/tests/strings/pack_endian_modifiers_error.phpt new file mode 100644 index 000000000000..685d21c7c00e --- /dev/null +++ b/ext/standard/tests/strings/pack_endian_modifiers_error.phpt @@ -0,0 +1,105 @@ +--TEST-- +pack()/unpack() endianness modifiers, invalid combinations +--FILE-- +', 'N<', 'V>', 'J<', 'P>']; +foreach ($inherent_formats as $fmt) { + try { + pack($fmt, 1); + echo "FAIL: Expected ValueError for pack('$fmt', 1)\n"; + } catch (ValueError $e) { + echo "pack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +$inherent_float_formats = ['g<', 'G>', 'e<', 'E>']; +foreach ($inherent_float_formats as $fmt) { + try { + pack($fmt, 1.0); + echo "FAIL: Expected ValueError for pack('$fmt', 1.0)\n"; + } catch (ValueError $e) { + echo "pack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +$unsupported_formats = ['c<', 'C>', 'a<', 'A>', 'h<', 'H>', 'i<', 'I>', 'x<', 'X>', '@<']; +foreach ($unsupported_formats as $fmt) { + try { + pack($fmt, 1); + echo "FAIL: Expected ValueError for pack('$fmt', 1)\n"; + } catch (ValueError $e) { + echo "pack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +foreach (['n<', 'v>', 'N<', 'V>', 'J<', 'P>'] as $fmt) { + try { + unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00"); + echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n"; + } catch (ValueError $e) { + echo "unpack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +foreach (['g<', 'G>', 'e<', 'E>'] as $fmt) { + try { + unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00"); + echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n"; + } catch (ValueError $e) { + echo "unpack('$fmt'): " . $e->getMessage() . "\n"; + } +} + +foreach (['c<', 'C>', 'a<', 'A>', 'h<', 'H>', 'i<', 'I>', 'x<', 'X>', '@<'] as $fmt) { + try { + unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00"); + echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n"; + } catch (ValueError $e) { + echo "unpack('$fmt'): " . $e->getMessage() . "\n"; + } +} +?> +--EXPECT-- +pack('n<'): Endianness modifier '<' cannot be applied to format code 'n' which already has inherent endianness +pack('v>'): Endianness modifier '>' cannot be applied to format code 'v' which already has inherent endianness +pack('N<'): Endianness modifier '<' cannot be applied to format code 'N' which already has inherent endianness +pack('V>'): Endianness modifier '>' cannot be applied to format code 'V' which already has inherent endianness +pack('J<'): Endianness modifier '<' cannot be applied to format code 'J' which already has inherent endianness +pack('P>'): Endianness modifier '>' cannot be applied to format code 'P' which already has inherent endianness +pack('g<'): Endianness modifier '<' cannot be applied to format code 'g' which already has inherent endianness +pack('G>'): Endianness modifier '>' cannot be applied to format code 'G' which already has inherent endianness +pack('e<'): Endianness modifier '<' cannot be applied to format code 'e' which already has inherent endianness +pack('E>'): Endianness modifier '>' cannot be applied to format code 'E' which already has inherent endianness +pack('c<'): Endianness modifier is not supported for format code 'c' +pack('C>'): Endianness modifier is not supported for format code 'C' +pack('a<'): Endianness modifier is not supported for format code 'a' +pack('A>'): Endianness modifier is not supported for format code 'A' +pack('h<'): Endianness modifier is not supported for format code 'h' +pack('H>'): Endianness modifier is not supported for format code 'H' +pack('i<'): Endianness modifier is not supported for format code 'i' +pack('I>'): Endianness modifier is not supported for format code 'I' +pack('x<'): Endianness modifier is not supported for format code 'x' +pack('X>'): Endianness modifier is not supported for format code 'X' +pack('@<'): Endianness modifier is not supported for format code '@' +unpack('n<'): Endianness modifier '<' cannot be applied to format code 'n' which already has inherent endianness +unpack('v>'): Endianness modifier '>' cannot be applied to format code 'v' which already has inherent endianness +unpack('N<'): Endianness modifier '<' cannot be applied to format code 'N' which already has inherent endianness +unpack('V>'): Endianness modifier '>' cannot be applied to format code 'V' which already has inherent endianness +unpack('J<'): Endianness modifier '<' cannot be applied to format code 'J' which already has inherent endianness +unpack('P>'): Endianness modifier '>' cannot be applied to format code 'P' which already has inherent endianness +unpack('g<'): Endianness modifier '<' cannot be applied to format code 'g' which already has inherent endianness +unpack('G>'): Endianness modifier '>' cannot be applied to format code 'G' which already has inherent endianness +unpack('e<'): Endianness modifier '<' cannot be applied to format code 'e' which already has inherent endianness +unpack('E>'): Endianness modifier '>' cannot be applied to format code 'E' which already has inherent endianness +unpack('c<'): Endianness modifier is not supported for format code 'c' +unpack('C>'): Endianness modifier is not supported for format code 'C' +unpack('a<'): Endianness modifier is not supported for format code 'a' +unpack('A>'): Endianness modifier is not supported for format code 'A' +unpack('h<'): Endianness modifier is not supported for format code 'h' +unpack('H>'): Endianness modifier is not supported for format code 'H' +unpack('i<'): Endianness modifier is not supported for format code 'i' +unpack('I>'): Endianness modifier is not supported for format code 'I' +unpack('x<'): Endianness modifier is not supported for format code 'x' +unpack('X>'): Endianness modifier is not supported for format code 'X' +unpack('@<'): Endianness modifier is not supported for format code '@' From f081c1854be9dd3baec7df8e0b183504da32405f Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:56:04 +0200 Subject: [PATCH 7/9] PFA: Fix magic method resolution (#23251) We conveniently use the function's scope for the scope of the generated closure as this allows const exprs referencing self:: or parent:: to behave normally. However this affects method resolution for magic methods. Fix by using the actual scope for PFAs of magic methods. --- .../default_arg_scope.phpt | 47 ++++++++++++++++++ Zend/tests/partial_application/magic_001.phpt | 2 +- Zend/tests/partial_application/magic_002.phpt | 6 +-- .../partial_application/magic_scope.phpt | 49 +++++++++++++++++++ Zend/zend_ast.c | 2 +- Zend/zend_partial.c | 12 ++++- Zend/zend_partial.h | 2 +- Zend/zend_vm_def.h | 2 +- Zend/zend_vm_execute.h | 8 +-- 9 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 Zend/tests/partial_application/default_arg_scope.phpt create mode 100644 Zend/tests/partial_application/magic_scope.phpt diff --git a/Zend/tests/partial_application/default_arg_scope.phpt b/Zend/tests/partial_application/default_arg_scope.phpt new file mode 100644 index 000000000000..5cab50c5af26 --- /dev/null +++ b/Zend/tests/partial_application/default_arg_scope.phpt @@ -0,0 +1,47 @@ +--TEST-- +PFA default argument value scope +--ENV-- +A=1 +--FILE-- + +--EXPECT-- +string(1) "C" +string(1) "C" +string(1) "C" +string(1) "C" +string(1) "C" +string(1) "C" diff --git a/Zend/tests/partial_application/magic_001.phpt b/Zend/tests/partial_application/magic_001.phpt index bdcc10675785..a3a7a9673b8f 100644 --- a/Zend/tests/partial_application/magic_001.phpt +++ b/Zend/tests/partial_application/magic_001.phpt @@ -46,7 +46,7 @@ Closure [ public method {closure:%s:%d} ] { Parameter #0 [ mixed $arguments0 ] } } -ArgumentCountError: Too few arguments to function Foo::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected +ArgumentCountError: Too few arguments to function Closure::{closure:%s:%d}(), 0 passed in %s on line %d and exactly 1 expected Foo::method int(1) Foo::method diff --git a/Zend/tests/partial_application/magic_002.phpt b/Zend/tests/partial_application/magic_002.phpt index 1d5efaea7c63..2771c823e4d4 100644 --- a/Zend/tests/partial_application/magic_002.phpt +++ b/Zend/tests/partial_application/magic_002.phpt @@ -31,7 +31,7 @@ echo (string) new ReflectionFunction($bar); $bar(100); ?> --EXPECTF-- -Closure [ static public method {closure:%s:%d} ] { +Closure [ static function {closure:%s:%d} ] { @@ %s 10 - 10 - Parameters [1] { @@ -42,7 +42,7 @@ Foo::method int(1) Foo::method int(1) -Closure [ static public method {closure:%s:%d} ] { +Closure [ static function {closure:%s:%d} ] { @@ %s 17 - 17 - Parameters [2] { @@ -55,7 +55,7 @@ int(10) Foo::method int(10) int(20) -Closure [ static public method {closure:%s:%d} ] { +Closure [ static function {closure:%s:%d} ] { @@ %s 24 - 24 - Bound Variables [1] { diff --git a/Zend/tests/partial_application/magic_scope.phpt b/Zend/tests/partial_application/magic_scope.phpt new file mode 100644 index 000000000000..82a69f955f96 --- /dev/null +++ b/Zend/tests/partial_application/magic_scope.phpt @@ -0,0 +1,49 @@ +--TEST-- +Magic method scope +--CREDITS-- +Ryan @ Calif.io +--FILE-- +secret('direct'); +StaticTarget::secret('direct'); + +$instancePartial = $instance->secret(?); +$staticPartial = StaticTarget::secret(?); +$instancePartial('controlled'); +$staticPartial('controlled'); + +?> +--EXPECT-- +MAGIC-INSTANCE:secret:direct +MAGIC-STATIC:secret:direct +MAGIC-INSTANCE:secret:controlled +MAGIC-STATIC:secret:controlled diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 4b070d9d5d58..6a71fc5aeca3 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1349,7 +1349,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( if (uses_variadic_placeholder) { flags |= ZEND_PARTIAL_USES_VARIADIC_PLACEHOLDER; } - zend_partial_create(result, &frame->This, fptr, + zend_partial_create(result, scope, &frame->This, fptr, ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1), extra_named_params, named_positions, fcc_ast->filename, &ast->lineno, diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c index 643cc634e7e8..de243bd7e65f 100644 --- a/Zend/zend_partial.c +++ b/Zend/zend_partial.c @@ -1126,7 +1126,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval * } } -void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, +void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function, uint32_t argc, zval *argv, zend_array *extra_named_params, const zend_array *named_positions, zend_string *declaring_filename, @@ -1162,8 +1162,16 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, object = NULL; } + + /* We conveniently use the function's scope for the scope of the generated closure as this allows const exprs + * referencing self:: or parent:: to behave normally without rewriting them. + * This affects method resolution for magic methods, so use the actual scope for them. */ + if (!(function->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) { + scope = function->common.scope; + } + zend_create_partial_closure(result, (zend_function*)op_array, - function->common.scope, called_scope, object, + scope, called_scope, object, (function->common.fn_flags & ZEND_ACC_CLOSURE) != 0); zp_bind(result, function, argc, argv, extra_named_params, const_args); diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h index d3fcdae6afc8..285db1161e78 100644 --- a/Zend/zend_partial.h +++ b/Zend/zend_partial.h @@ -31,7 +31,7 @@ BEGIN_EXTERN_C() * 'declaring_lineno_ptr' should be a pointer the zend_op.lineno or * zend_ast.lineno that declares the PFA. The address is used to build a cache * key. */ -void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, +void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function, uint32_t argc, zval *argv, zend_array *extra_named_params, const zend_array *named_positions, zend_string *declaring_filename, diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 01131b5d3ae0..0e35b5bb95fa 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9897,7 +9897,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM) } zend_partial_create(EX_VAR(opline->result.var), - &call->This, call->func, + EX(func)->common.scope, &call->This, call->func, ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1), (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ? call->extra_named_params : NULL, diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 5061d772ee82..c6158bd507d9 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -8654,7 +8654,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV } zend_partial_create(EX_VAR(opline->result.var), - &call->This, call->func, + EX(func)->common.scope, &call->This, call->func, ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1), (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ? call->extra_named_params : NULL, @@ -12027,7 +12027,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV } zend_partial_create(EX_VAR(opline->result.var), - &call->This, call->func, + EX(func)->common.scope, &call->This, call->func, ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1), (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ? call->extra_named_params : NULL, @@ -61499,7 +61499,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P } zend_partial_create(EX_VAR(opline->result.var), - &call->This, call->func, + EX(func)->common.scope, &call->This, call->func, ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1), (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ? call->extra_named_params : NULL, @@ -64770,7 +64770,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P } zend_partial_create(EX_VAR(opline->result.var), - &call->This, call->func, + EX(func)->common.scope, &call->This, call->func, ZEND_CALL_NUM_ARGS(call), ZEND_CALL_ARG(call, 1), (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ? call->extra_named_params : NULL, From 6748db3ff57208ae37a9cd9736fd14b2b5040b34 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 24 Aug 2026 16:57:24 +0100 Subject: [PATCH 8/9] sapi/cli: check php_cli_server_client_send_through() return value sapi_cli_server_send_headers() sent the header buffer through php_cli_server_client_send_through() but dropped the return value, so it always reported SAPI_HEADER_SENT_SUCCESSFULLY, even when the send failed and php_handle_aborted_connection() did not bail out (ignore_user_abort=1). It now compares the send result against the buffer length and returns SAPI_HEADER_SEND_FAILED when it comes up short. php_cli_server_client_send_through() itself returned the number of bytes left on failure, which equals str_len when nothing was sent: the same value it returns on success. It now returns the number of bytes actually sent on both paths. Since sapi_send_headers() resets headers_sent on SAPI_HEADER_SEND_FAILED, a headers_written flag on the client guards against rebuilding and resending the whole header block on every subsequent output write. Close GH-23428 --- NEWS | 4 ++++ sapi/cli/php_cli_server.c | 14 ++++++++----- sapi/cli/tests/gh23425.phpt | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 sapi/cli/tests/gh23425.phpt diff --git a/NEWS b/NEWS index a2c65685b4ce..a4339ddf600f 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,10 @@ PHP NEWS . Fixed bug GH-23301 (Nested "yield from" yields a value twice when the middle generator delegates again). (Lazizbek Ergashev) +- CLI: + . Fixed bug GH-23425 (sapi_cli_server_send_headers() does not check the + return value of php_cli_server_client_send_through()). (Lazizbek Ergashev) + - DOM: . Fixed a use-after-free when cloning a DOMNameSpaceNode after DOMDocument::xinclude(). (iliaal) diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index f4d29ce56855..36187aaeb035 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -177,6 +177,7 @@ typedef struct php_cli_server_client { zend_string *addr_str; php_http_parser parser; bool request_read; + bool headers_written; zend_string *current_header_name; zend_string *current_header_value; enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element; @@ -555,7 +556,7 @@ static int sapi_cli_server_send_headers(sapi_headers_struct *sapi_headers) /* {{ sapi_header_struct *h; zend_llist_position pos; - if (client == NULL || SG(request_info).no_headers) { + if (client == NULL || SG(request_info).no_headers || client->headers_written) { return SAPI_HEADER_SENT_SUCCESSFULLY; } @@ -578,10 +579,12 @@ static int sapi_cli_server_send_headers(sapi_headers_struct *sapi_headers) /* {{ } smart_str_appendl(&buffer, "\r\n", 2); - php_cli_server_client_send_through(client, ZSTR_VAL(buffer.s), ZSTR_LEN(buffer.s)); + size_t buffer_len = ZSTR_LEN(buffer.s); + bool sent = php_cli_server_client_send_through(client, ZSTR_VAL(buffer.s), buffer_len) == buffer_len; + client->headers_written = true; smart_str_free(&buffer); - return SAPI_HEADER_SENT_SUCCESSFULLY; + return sent ? SAPI_HEADER_SENT_SUCCESSFULLY : SAPI_HEADER_SEND_FAILED; } /* }}} */ @@ -1920,11 +1923,11 @@ static size_t php_cli_server_client_send_through(php_cli_server_client *client, } else { /* error or timeout */ php_handle_aborted_connection(); - return nbytes_left; + return str_len - nbytes_left; } } else { php_handle_aborted_connection(); - return nbytes_left; + return str_len - nbytes_left; } } nbytes_left -= nbytes_sent; @@ -1973,6 +1976,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se php_http_parser_init(&client->parser, PHP_HTTP_REQUEST); client->request_read = false; + client->headers_written = false; client->last_header_element = HEADER_NONE; client->current_header_name = NULL; diff --git a/sapi/cli/tests/gh23425.phpt b/sapi/cli/tests/gh23425.phpt new file mode 100644 index 000000000000..98ed07859174 --- /dev/null +++ b/sapi/cli/tests/gh23425.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-23425 (sapi_cli_server_send_headers() does not check the return value of php_cli_server_client_send_through()) +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + 1, 'l_linger' => 0]); +socket_close($sock); + +$result_file = $info->docRoot . '/result.txt'; +for ($i = 0; $i < 40 && !file_exists($result_file); $i++) { + usleep(50000); +} + +echo file_get_contents($result_file), "\n"; +?> +--EXPECT-- +not-sent From c66ac29dca879680b8d6dc0f0db697173b9d104e Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 24 Aug 2026 10:21:29 -0600 Subject: [PATCH 9/9] Fix JUnit timing aggregation for long-running tests (#23438) --- run-tests.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index ce6bd94db1e0..8728c0d7524b 100755 --- a/run-tests.php +++ b/run-tests.php @@ -3681,6 +3681,7 @@ public function markTestAs( $time = $time ?? $this->getTimer($file_name); $this->record($suite, 'execution_time', $time); + $formatted_time = number_format($time, 4, '.', ''); $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) { @@ -3689,7 +3690,7 @@ public function markTestAs( $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); - $this->rootSuite['files'][$file_name]['xml'] = "\n"; + $this->rootSuite['files'][$file_name]['xml'] = "\n"; if (is_array($type)) { $output_type = $type[0] . 'ED'; @@ -3734,7 +3735,7 @@ private function getTimer(string $file_name) } if (isset($this->rootSuite['files'][$file_name]['total'])) { - return number_format($this->rootSuite['files'][$file_name]['total'], 4); + return $this->rootSuite['files'][$file_name]['total']; } return 0;