From 05c7b00d796be26aac15994803444519992dd78e Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 11 Jun 2026 09:11:35 -0600 Subject: [PATCH 1/4] Fix another vm_interrupt bug for tailcall VM (#22265) --- NEWS | 3 ++ Zend/zend_execute.c | 7 +++++ ...observer_vm_interrupt_tailcall_return.phpt | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt diff --git a/NEWS b/NEWS index f09487c5da06..08d381209f8e 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,9 @@ PHP NEWS LXB_API as __declspec(dllimport) when linked statically into PHP. (Luther Monson) +- Opcache: + . Fixed bug GH-22265 (Another tailcall vm_interrupt bug). (Levi Morrison) + - Phar: . Fixed a bypass of the magic ".phar" directory protection in Phar::addEmptyDir() for paths starting with "/.phar", while allowing diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 99e4cb131439..6405780f5051 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4302,8 +4302,15 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *ca } \ } while (0) +#if ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL +# define ZEND_VM_KIND_TAILCALL_SAVE_OPLINE() SAVE_OPLINE() +#else +# define ZEND_VM_KIND_TAILCALL_SAVE_OPLINE() +#endif + #define ZEND_VM_LOOP_INTERRUPT_CHECK() do { \ if (UNEXPECTED(zend_atomic_bool_load_ex(&EG(vm_interrupt)))) { \ + ZEND_VM_KIND_TAILCALL_SAVE_OPLINE(); \ ZEND_VM_LOOP_INTERRUPT(); \ } \ } while (0) diff --git a/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt b/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt new file mode 100644 index 000000000000..95af2681c253 --- /dev/null +++ b/ext/zend_test/tests/observer_vm_interrupt_tailcall_return.phpt @@ -0,0 +1,29 @@ +--TEST-- +Observer: VM interrupt during tailcall return to caller +--DESCRIPTION-- +This exercises a VM interrupt raised immediately before a user function returns +to a caller that invoked it through DO_FCALL. On the tailcall VM, the caller's +saved opline must point to the opcode after DO_FCALL before a pending interrupt +is handled. +--EXTENSIONS-- +zend_test +--INI-- +opcache.jit=0 +zend_test.observer.set_vm_interrupt_on_begin=1 +--FILE-- + +--EXPECT-- +ok From c9cb1f9bec6f7dd40c49ca0e65eecfe2dfeaf7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Thu, 11 Jun 2026 21:24:00 +0200 Subject: [PATCH 2/4] Clean Lexbor logs before each Uri\WhatWg\Url wither call (#22276) Clean Lexbor error logs before each Uri\WhatWg\Url wither call so that errors from previous wither calls are not returned the next time a UrlValidationError is thrown. --- NEWS | 3 +++ .../multiple_error_with_warnings.phpt | 23 +++++++++++++++++++ ext/uri/uri_parser_whatwg.c | 16 +++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 ext/uri/tests/whatwg/modification/multiple_error_with_warnings.phpt diff --git a/NEWS b/NEWS index 08d381209f8e..28337a592f0f 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,9 @@ PHP NEWS . Add LEXBOR_STATIC to CFLAGS_URI on Windows so ext/uri does not see LXB_API as __declspec(dllimport) when linked statically into PHP. (Luther Monson) + . Clean error logs before each Uri\WhatWg\Url wither call so that errors from + previous wither calls are not returned the next time a UrlValidationError + is thrown. (kocsismate) - Opcache: . Fixed bug GH-22265 (Another tailcall vm_interrupt bug). (Levi Morrison) diff --git a/ext/uri/tests/whatwg/modification/multiple_error_with_warnings.phpt b/ext/uri/tests/whatwg/modification/multiple_error_with_warnings.phpt new file mode 100644 index 000000000000..b5dd89446601 --- /dev/null +++ b/ext/uri/tests/whatwg/modification/multiple_error_with_warnings.phpt @@ -0,0 +1,23 @@ +--TEST-- +Test Uri\WhatWg\Url component modification - error - modifying multiple components with warnings before throwing an exception +--FILE-- +withScheme("\tscheme") + ->withHost("\tex.com") + ->withQuery("\refoo=bar") + ->withFragment("\nfoo"); + +try { + $url->withScheme("0"); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; + var_dump($e->errors); +} + +?> +--EXPECT-- +Uri\WhatWg\InvalidUrlException: The specified scheme is malformed +array(0) { +} diff --git a/ext/uri/uri_parser_whatwg.c b/ext/uri/uri_parser_whatwg.c index 055f130af7c6..9364fd5d3d80 100644 --- a/ext/uri/uri_parser_whatwg.c +++ b/ext/uri/uri_parser_whatwg.c @@ -265,6 +265,8 @@ static zend_result php_uri_parser_whatwg_scheme_write(void *uri, zval *value, zv zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_protocol_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "scheme"); @@ -300,6 +302,8 @@ static zend_result php_uri_parser_whatwg_username_write(void *uri, zval *value, zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_username_set(lexbor_uri, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "username"); @@ -329,6 +333,8 @@ static zend_result php_uri_parser_whatwg_password_write(void *uri, zval *value, zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_password_set(lexbor_uri, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "password"); @@ -389,6 +395,8 @@ static zend_result php_uri_parser_whatwg_host_write(void *uri, zval *value, zval zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_hostname_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "host"); @@ -418,6 +426,8 @@ static zend_result php_uri_parser_whatwg_port_write(void *uri, zval *value, zval zval_long_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_port_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "port"); @@ -447,6 +457,8 @@ static zend_result php_uri_parser_whatwg_path_write(void *uri, zval *value, zval zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_pathname_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "path"); @@ -476,6 +488,8 @@ static zend_result php_uri_parser_whatwg_query_write(void *uri, zval *value, zva zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_search_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "query string"); @@ -505,6 +519,8 @@ static zend_result php_uri_parser_whatwg_fragment_write(void *uri, zval *value, zval_string_or_null_to_lexbor_str(value, &str); + lxb_url_parser_clean(&lexbor_parser); + if (lxb_url_api_hash_set(lexbor_uri, &lexbor_parser, str.data, str.length) != LXB_STATUS_OK) { throw_invalid_url_exception_during_write(errors, "fragment"); From f11c1facb446f19ff41d9850bc986c0c290f0ef8 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:06:20 +0200 Subject: [PATCH 3/4] date: Fix typo in condition in date_period_init_iso8601_string() Now the address is being checked instead of the value, but I believe the value should be checked against 0, not the pointer against NULL. Closes GH-22253. --- NEWS | 4 ++++ ext/date/php_date.c | 2 +- ext/date/tests/date_period_bad_iso_format.phpt | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 2698420c7a5a..7aa9ec337441 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed issues with oversized allocations and signed overflow in bcround() and BcMath\Number::round(). (edorian) +- Date: + . Fix incorrect recurrence check of DatePeriod::createFromISO8601String(). + (ndossche) + - GD: . Fixed bug GH-22121 (Double free in gdImageSetStyle() after overflow-triggered early return). (iliaal) diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 2fbfe6e14f7e..08a91a6318e9 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -5125,7 +5125,7 @@ static bool date_period_init_iso8601_string(php_period_obj *dpobj, zend_class_en zend_string_release(func); return false; } - if (dpobj->end == NULL && recurrences == 0) { + if (dpobj->end == NULL && *recurrences == 0) { zend_string *func = get_active_function_or_method_name(); zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain an end date or a recurrence count, \"%s\" given", ZSTR_VAL(func), isostr); zend_string_release(func); diff --git a/ext/date/tests/date_period_bad_iso_format.phpt b/ext/date/tests/date_period_bad_iso_format.phpt index cf2025bef0e3..1ab8197d76c9 100644 --- a/ext/date/tests/date_period_bad_iso_format.phpt +++ b/ext/date/tests/date_period_bad_iso_format.phpt @@ -50,5 +50,5 @@ DateMalformedPeriodStringException: DatePeriod::__construct(): ISO interval must DateMalformedPeriodStringException: DatePeriod::createFromISO8601String(): ISO interval must contain an interval, "R4/2012-07-01T00:00:00Z" given Deprecated: Calling DatePeriod::__construct(string $isostr, int $options = 0) is deprecated, use DatePeriod::createFromISO8601String() instead in %s on line %d -DateMalformedPeriodStringException: DatePeriod::__construct(): Recurrence count must be greater or equal to 1 and lower than %d -DateMalformedPeriodStringException: DatePeriod::createFromISO8601String(): Recurrence count must be greater or equal to 1 and lower than %d +DateMalformedPeriodStringException: DatePeriod::__construct(): ISO interval must contain an end date or a recurrence count, "2012-07-01T00:00:00Z/P7D" given +DateMalformedPeriodStringException: DatePeriod::createFromISO8601String(): ISO interval must contain an end date or a recurrence count, "2012-07-01T00:00:00Z/P7D" given From 407c45debce31f5544cbd3c62983646d39523436 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Fri, 15 May 2026 12:55:39 +0200 Subject: [PATCH 4/4] sqlite: fix error checks for column retrieval These can return NULL on OOM. And for blobs, it can return NULL for empty blobs (so *no* failure, just an edge case). Passing NULL to memcpy is UB, so we have to check for a NULL pointer there to avoid UB. Closes GH-22045. --- NEWS | 3 +++ ext/sqlite3/sqlite3.c | 44 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 7aa9ec337441..faeb27fdf6df 100644 --- a/NEWS +++ b/NEWS @@ -36,6 +36,9 @@ PHP NEWS Phar::addEmptyDir() for paths starting with "/.phar", while allowing non-magic directory names that merely share the ".phar" prefix. (Weilin Du) +- Sqlite: + . Fix error checks for column retrieval. (ndossche) + - Zlib: . Fixed memory leak if deflate initialization fails and there is a dict. (ndossche) diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 100cf32680e9..1bb93e0e2675 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -627,7 +627,7 @@ PHP_METHOD(SQLite3, query) } /* }}} */ -static void sqlite_value_to_zval(sqlite3_stmt *stmt, int column, zval *data) /* {{{ */ +static void sqlite_value_to_zval(php_sqlite3_db_object *db_obj, sqlite3_stmt *stmt, int column, zval *data) /* {{{ */ { sqlite3_int64 val; @@ -636,7 +636,13 @@ static void sqlite_value_to_zval(sqlite3_stmt *stmt, int column, zval *data) /* val = sqlite3_column_int64(stmt, column); #if LONG_MAX <= 2147483647 if (val > ZEND_LONG_MAX || val < ZEND_LONG_MIN) { - ZVAL_STRINGL(data, (char *)sqlite3_column_text(stmt, column), sqlite3_column_bytes(stmt, column)); + const char *text = (const char *) sqlite3_column_text(stmt, column); + if (UNEXPECTED(text == NULL)) { + php_sqlite3_error(db_obj, SQLITE_NOMEM, "Failed to retrieve column value due to out of memory"); + ZVAL_NULL(data); + } else { + ZVAL_STRINGL(data, text, sqlite3_column_bytes(stmt, column)); + } } else { #endif ZVAL_LONG(data, (zend_long) val); @@ -653,13 +659,33 @@ static void sqlite_value_to_zval(sqlite3_stmt *stmt, int column, zval *data) /* ZVAL_NULL(data); break; - case SQLITE3_TEXT: - ZVAL_STRING(data, (char*)sqlite3_column_text(stmt, column)); + case SQLITE3_TEXT: { + const char *text = (const char *) sqlite3_column_text(stmt, column); + if (UNEXPECTED(text == NULL)) { + php_sqlite3_error(db_obj, SQLITE_NOMEM, "Failed to retrieve column value due to out of memory"); + ZVAL_NULL(data); + } else { + ZVAL_STRING(data, text); + } break; + } case SQLITE_BLOB: - default: - ZVAL_STRINGL(data, (char*)sqlite3_column_blob(stmt, column), sqlite3_column_bytes(stmt, column)); + default: { + const char *blob = (const char *) sqlite3_column_blob(stmt, column); + if (UNEXPECTED(blob == NULL)) { + if (sqlite3_errcode(sqlite3_db_handle(stmt)) == SQLITE_NOMEM) { + php_sqlite3_error(db_obj, SQLITE_NOMEM, "Failed to retrieve column value due to out of memory"); + ZVAL_NULL(data); + } else { + /* Zero-length BLOB */ + ZVAL_EMPTY_STRING(data); + } + } else { + ZVAL_STRINGL(data, blob, sqlite3_column_bytes(stmt, column)); + } + break; + } } } /* }}} */ @@ -709,13 +735,13 @@ PHP_METHOD(SQLite3, querySingle) case SQLITE_ROW: /* Valid Row */ { if (!entire_row) { - sqlite_value_to_zval(stmt, 0, return_value); + sqlite_value_to_zval(db_obj, stmt, 0, return_value); } else { int i = 0; array_init(return_value); for (i = 0; i < sqlite3_data_count(stmt); i++) { zval data; - sqlite_value_to_zval(stmt, i, &data); + sqlite_value_to_zval(db_obj, stmt, i, &data); add_assoc_zval(return_value, (char*)sqlite3_column_name(stmt, i), &data); } } @@ -1973,7 +1999,7 @@ PHP_METHOD(SQLite3Result, fetchArray) for (i = 0; i < n_cols; i++) { zval data; - sqlite_value_to_zval(result_obj->stmt_obj->stmt, i, &data); + sqlite_value_to_zval(result_obj->db_obj, result_obj->stmt_obj->stmt, i, &data); if (mode & PHP_SQLITE3_NUM) { add_index_zval(return_value, i, &data);