From 3199f8932bac5e09d084e1c6743ae545431a8490 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 14 Jul 2026 23:41:38 +0800 Subject: [PATCH 1/8] [skip ci] UPGRADING: document several added functions and classes --- UPGRADING | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/UPGRADING b/UPGRADING index 74a5e066483f..a27ef2f8f4ac 100644 --- a/UPGRADING +++ b/UPGRADING @@ -393,17 +393,19 @@ PHP 8.6 UPGRADE NOTES 6. New Functions ======================================== +- Intl: + . grapheme_strrev() + RFC: https://wiki.php.net/rfc/grapheme_strrev + . Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() + RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue + - Reflection: . ReflectionConstant::inNamespace() . ReflectionProperty::isReadable() and ReflectionProperty::isWritable() RFC: https://wiki.php.net/rfc/isreadable-iswriteable - . ReflectionParameter::getDocComment(). + . ReflectionParameter::getDocComment() RFC: https://wiki.php.net/rfc/parameter-doccomments -- Intl: - . grapheme_strrev() returns strrev for grapheme cluster unit. - RFC: https://wiki.php.net/rfc/grapheme_strrev - - mysqli: . Added mysqli::quote_string() and mysqli_quote_string(). RFC: https://wiki.php.net/rfc/mysqli_quote_string @@ -412,9 +414,15 @@ PHP 8.6 UPGRADE NOTES . clamp() returns the given value if in range, else return the nearest bound. RFC: https://wiki.php.net/rfc/clamp_v2 - . stream_last_errors() and stream_clear_errors(). + . stream_last_errors() and stream_clear_errors() RFC: https://wiki.php.net/rfc/stream_errors - . stream_socket_get_crypto_status(). + . stream_socket_get_crypto_status() + +- URI: + . Uri\Rfc3986\Uri::getUriType() and Uri\WhatWg\Url::isSpecialScheme() + RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection + . Uri\Rfc3986\Uri::getHostType() and Uri\WhatWg\Url::getHostType() + RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection - Zip: . ZipArchive::openString() @@ -424,6 +432,9 @@ PHP 8.6 UPGRADE NOTES 7. New Classes and Interfaces ======================================== +- Intl: + . IntlNumberRangeFormatter + - OpenSSL: . Openssl\OpensslException . Openssl\Session @@ -457,6 +468,10 @@ PHP 8.6 UPGRADE NOTES . Io\Poll\InvalidHandleException . StreamPollHandle +- URI: + . Uri\Rfc3986\UriBuilder + RFC: https://wiki.php.net/rfc/uri_followup#uri_building + ======================================== 8. Removed Extensions and SAPIs ======================================== From df5e361b66bb3642162225aae8c300cf13b909a2 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 14 Jul 2026 23:44:21 +0800 Subject: [PATCH 2/8] [skip ci] UPGRADING: sort extensions alphabetically --- UPGRADING | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/UPGRADING b/UPGRADING index a27ef2f8f4ac..57a1f6a0ebad 100644 --- a/UPGRADING +++ b/UPGRADING @@ -399,6 +399,10 @@ PHP 8.6 UPGRADE NOTES . Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue +- mysqli: + . Added mysqli::quote_string() and mysqli_quote_string(). + RFC: https://wiki.php.net/rfc/mysqli_quote_string + - Reflection: . ReflectionConstant::inNamespace() . ReflectionProperty::isReadable() and ReflectionProperty::isWritable() @@ -406,10 +410,6 @@ PHP 8.6 UPGRADE NOTES . ReflectionParameter::getDocComment() RFC: https://wiki.php.net/rfc/parameter-doccomments -- mysqli: - . Added mysqli::quote_string() and mysqli_quote_string(). - RFC: https://wiki.php.net/rfc/mysqli_quote_string - - Standard: . clamp() returns the given value if in range, else return the nearest bound. From 103c84ff4c1bb4cdc4b078c306c2c8b8b584f1b1 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 10 Jul 2026 07:57:25 -0400 Subject: [PATCH 3/8] Fix GH-22667: pdo_odbc heap over-read on oversized column value odbc_stmt_describe() binds a colsize+1 buffer for short columns and stores that capacity in datalen, but odbc_stmt_get_col() built the result string from the driver-reported fetched_len. A conforming driver truncates an over-long value into the buffer yet reports the full length, so ZVAL_STRINGL_FAST over-read past the allocation and returned adjacent heap to userland. Clamp fetched_len to the bound capacity, guarded by !is_long so the long-column SQLGetData path (whose fixed buffer is unrelated to datalen) is left untouched. Fixes GH-22667 Closes GH-22672 --- NEWS | 2 ++ ext/pdo_odbc/odbc_stmt.c | 9 ++++++++- ext/pdo_odbc/tests/config.inc | 3 +++ ext/pdo_odbc/tests/gh22667.phpt | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_odbc/tests/config.inc create mode 100644 ext/pdo_odbc/tests/gh22667.phpt diff --git a/NEWS b/NEWS index e71a94ac12ec..9bcb68e52712 100644 --- a/NEWS +++ b/NEWS @@ -45,6 +45,8 @@ PHP NEWS - PDO_ODBC: . Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN carries no credentials). (iliaal) + . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the + driver-reported display size). (iliaal) - Phar: . Fixed inconsistent handling of the magic ".phar" directory. Paths such as diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index c114f721a7a7..5b9104895854 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -744,7 +744,14 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo return 1; } else if (C->fetched_len >= 0) { /* it was stored perfectly */ - ZVAL_STRINGL_FAST(result, C->data, C->fetched_len); + SQLLEN data_len = C->fetched_len; + if (!C->is_long) { + SQLLEN max_len = C->is_unicode ? (SQLLEN)C->datalen + 1 : (SQLLEN)C->datalen; + if (data_len > max_len) { + data_len = max_len; + } + } + ZVAL_STRINGL_FAST(result, C->data, data_len); if (C->is_unicode) { goto unicode_conv; } diff --git a/ext/pdo_odbc/tests/config.inc b/ext/pdo_odbc/tests/config.inc new file mode 100644 index 000000000000..185602bb7ed6 --- /dev/null +++ b/ext/pdo_odbc/tests/config.inc @@ -0,0 +1,3 @@ + +--FILE-- +query("SELECT printf('%.*c', 4096, 'A') AS data"); +$row = $stmt->fetch(PDO::FETCH_ASSOC); +$s = $row['data']; + +echo "clamped to buffer: "; var_dump(strlen($s) < 4096); +echo "only value bytes: "; var_dump(strlen($s) === substr_count($s, 'A')); +?> +--EXPECT-- +clamped to buffer: bool(true) +only value bytes: bool(true) From 0fef3c3895fa80f8fc593164b2e1cb59e664d9a6 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 15 Jul 2026 02:14:51 +0800 Subject: [PATCH 4/8] CI: fix unstable test \ext\sockets\tests\socket_recvfrom_afpacket_no_port.phpt (#22724) Fix flaky CI test: \ext\sockets\tests\socket_recvfrom_afpacket_no_port.phpt failed run example: https://github.com/php/php-src/actions/runs/29266463698/job/86873055452 --- .../socket_recvfrom_afpacket_no_port.phpt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt b/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt index a66398c3a0e1..fd6c4c3a4daf 100644 --- a/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt +++ b/ext/sockets/tests/socket_recvfrom_afpacket_no_port.phpt @@ -29,10 +29,30 @@ $ethertype = pack("n", 0x9000); $payload = "no port test"; $frame = str_pad($dst_mac . $src_mac . $ethertype . $payload, 60, "\x00"); +// ETH_P_ALL sockets on loopback can observe unrelated localhost traffic from +// the parallel test runner. Read until we see our own frame, then validate the +// address returned by recvfrom() without the optional port argument. +function recv_matching(Socket $s, string $header, int $maxlen = 65536, ?string &$addr = null, &$bytes = null): string|false { + socket_set_nonblock($s); + $deadline = microtime(true) + 5.0; + while (microtime(true) < $deadline) { + $recvBytes = @socket_recvfrom($s, $buf, $maxlen, 0, $addr); + if ($recvBytes !== false && is_string($buf) && str_starts_with($buf, $header)) { + $bytes = $recvBytes; + return $buf; + } + if ($recvBytes === false) { + usleep(1000); + } + } + return false; +} + socket_sendto($s_send, $frame, strlen($frame), 0, "lo", 1); // recvfrom without the optional 6th argument (port/ifindex). -$bytes = socket_recvfrom($s_recv, $buf, 65536, 0, $addr); +$bytes = 0; +$buf = recv_matching($s_recv, $dst_mac . $src_mac . $ethertype, 65536, $addr, $bytes); var_dump($bytes >= 60); var_dump($addr === 'lo'); From 207dd1d23f75654f0350d55b246cee40e451645a Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 14 Jul 2026 14:23:34 -0400 Subject: [PATCH 5/8] Fix GH-22480: phpdbg use-after-free re-watching a watched variable (#22493) phpdbg_add_watch_element() frees the passed element and returns the existing one when the variable is already watched, but several callers kept using the freed pointer, so re-watching a variable was a use-after-free in more places than the two reported. Report the duplicate back through the parse callback and register nothing, and reject a recursive watch's hashtable element that collides with an existing watch, at creation and recreation, rather than freeing a still-referenced one. A recursive watch over an already-array-watched variable now watches only the root and skips the shared hashtable. Running the tests under ASAN exposed a second, latent use-after-free at shutdown: the watch tables are module-scoped but hold request-pool memory, so under the tracked allocator freeing a watched variable during executor teardown re-queues an element after the last drain, tracked_free_all frees the pool, and phpdbg_destroy_watchpoints reads it at module shutdown. Tear the watch state down in a post_deactivate handler, which runs after the executor frees values but before the memory manager is torn down. Dropping watch_006's xfail surfaced a separate pre-existing bug on large-page platforms: the fault handler rechecks every watchpoint on the faulting page, so on 16 KB pages a value-only write to an element can fault the page holding a watched hashtable's own struct and run the recursive rescan, which re-reported existing elements as freshly added. Only run that rescan when the element count actually changed. Fixes GH-22480 --- NEWS | 4 + sapi/phpdbg/phpdbg.c | 11 +- sapi/phpdbg/phpdbg_watch.c | 212 +++++++++++++++++++++++++------ sapi/phpdbg/phpdbg_watch.h | 1 + sapi/phpdbg/tests/gh22480.phpt | 34 +++++ sapi/phpdbg/tests/gh22480_2.phpt | 30 +++++ sapi/phpdbg/tests/watch_006.phpt | 7 - 7 files changed, 253 insertions(+), 46 deletions(-) create mode 100644 sapi/phpdbg/tests/gh22480.phpt create mode 100644 sapi/phpdbg/tests/gh22480_2.phpt diff --git a/NEWS b/NEWS index 018f454f57ba..c7bee9b7e958 100644 --- a/NEWS +++ b/NEWS @@ -340,6 +340,10 @@ PHP NEWS . Mark Phar::buildFromIterator() base directory argument as a path. (ndossche) +- phpdbg: + . Fixed GH-22480 (Use-after-free when re-watching an already-watched + variable). (iliaal) + - Posix: . Added validity check to the flags argument for posix_access(). (arshidkv12) diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c index 3c0d5e836dd0..8cdc1aee9d48 100644 --- a/sapi/phpdbg/phpdbg.c +++ b/sapi/phpdbg/phpdbg.c @@ -239,6 +239,13 @@ static PHP_RSHUTDOWN_FUNCTION(phpdbg) /* {{{ */ return SUCCESS; } /* }}} */ +static ZEND_MODULE_POST_ZEND_DEACTIVATE_D(phpdbg) /* {{{ */ +{ + phpdbg_release_watch_elements(); + + return SUCCESS; +} /* }}} */ + /* {{{ Attempt to set the execution context for phpdbg If the execution context was set previously it is returned If the execution context was not set previously boolean true is returned @@ -681,7 +688,9 @@ static zend_module_entry sapi_phpdbg_module_entry = { PHP_RSHUTDOWN(phpdbg), NULL, PHPDBG_VERSION, - STANDARD_MODULE_PROPERTIES + NO_MODULE_GLOBALS, + ZEND_MODULE_POST_ZEND_DEACTIVATE_N(phpdbg), + STANDARD_MODULE_PROPERTIES_EX }; static inline int php_sapi_phpdbg_module_startup(sapi_module_struct *module) /* {{{ */ diff --git a/sapi/phpdbg/phpdbg_watch.c b/sapi/phpdbg/phpdbg_watch.c index b6d5e543a19c..ed6e6dfc70b9 100644 --- a/sapi/phpdbg/phpdbg_watch.c +++ b/sapi/phpdbg/phpdbg_watch.c @@ -501,8 +501,39 @@ void phpdbg_free_watch_element(phpdbg_watch_element *element); void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch); void phpdbg_watch_parent_ht(phpdbg_watch_element *element); -phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element) { +static bool phpdbg_watch_element_collides(void *addr, phpdbg_watchtype type, phpdbg_watch_element *element) { + phpdbg_watch_element *old; + phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) addr); + + if (res) { + phpdbg_watchpoint_t *watch = res->ptr; + if (watch->type == type && (old = zend_hash_find_ptr(&watch->elements, element->str)) && old != element) { + return true; + } + } + + return (old = zend_hash_find_ptr(&PHPDBG_G(watch_recreation), element->str)) && old != element; +} + +static bool phpdbg_ht_watch_element_collides(zval *zv, phpdbg_watch_element *element) { + HashTable *ht = HT_FROM_ZVP(zv); + + if (!ht) { + return false; + } + + return phpdbg_watch_element_collides(HT_WATCH_OFFSET + (char *) ht, WATCH_ON_HASHTABLE, element); +} + +static bool phpdbg_bucket_watch_element_collides(zval *zv, phpdbg_watch_element *element) { + return phpdbg_watch_element_collides((Bucket *) zv, WATCH_ON_BUCKET, element); +} + +phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element, bool *is_new) { phpdbg_btree_result *res; + if (is_new) { + *is_new = true; + } if ((res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr)) == NULL) { phpdbg_watchpoint_t *mem = emalloc(sizeof(*mem)); *mem = *watch; @@ -520,6 +551,9 @@ phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdb if (element != old_element) { phpdbg_free_watch_element(element); } + if (is_new) { + *is_new = false; + } return old_element; } } @@ -534,25 +568,35 @@ phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdb return element; } -phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element) { +phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element, bool *is_new) { phpdbg_watchpoint_t watch; phpdbg_set_bucket_watchpoint(bucket, &watch); - element = phpdbg_add_watch_element(&watch, element); - phpdbg_watch_parent_ht(element); - return element; + bool added_new; + phpdbg_watch_element *added = phpdbg_add_watch_element(&watch, element, &added_new); + if (added_new) { + phpdbg_watch_parent_ht(added); + } + if (is_new) { + *is_new = added_new; + } + return added; } -phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element) { +phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element, bool *is_new) { phpdbg_watchpoint_t watch; HashTable *ht = HT_FROM_ZVP(zv); + if (is_new) { + *is_new = false; + } + if (!ht) { return NULL; } element->flags |= Z_TYPE_P(zv) == IS_ARRAY ? PHPDBG_WATCH_ARRAY : PHPDBG_WATCH_OBJECT; phpdbg_set_ht_watchpoint(ht, &watch); - return phpdbg_add_watch_element(&watch, element); + return phpdbg_add_watch_element(&watch, element, is_new); } bool phpdbg_is_recursively_watched(void *ptr, phpdbg_watch_element *element) { @@ -588,8 +632,15 @@ void phpdbg_add_recursive_watch_from_ht(phpdbg_watch_element *element, zend_long child->parent = element; child->child = NULL; child->parent_container = HT_WATCH_HT(element->watch); - zend_hash_add_ptr(&element->child_container, child->str, child); - phpdbg_add_bucket_watch_element((Bucket *) zv, child); + if (phpdbg_bucket_watch_element_collides(zv, child)) { + phpdbg_free_watch_element(child); + return; + } + bool added_new; + phpdbg_add_bucket_watch_element((Bucket *) zv, child, &added_new); + if (added_new) { + zend_hash_add_ptr(&element->child_container, child->str, child); + } } void phpdbg_recurse_watch_element(phpdbg_watch_element *element) { @@ -627,8 +678,13 @@ void phpdbg_recurse_watch_element(phpdbg_watch_element *element) { child->child = NULL; element->child = child; } + if (phpdbg_ht_watch_element_collides(zv, child)) { + phpdbg_free_watch_element(child); + element->child = NULL; + return; + } zend_hash_init(&child->child_container, 8, NULL, NULL, 0); - phpdbg_add_ht_watch_element(zv, child); + phpdbg_add_ht_watch_element(zv, child, NULL); } else if (zend_hash_num_elements(&element->child_container) == 0) { zend_string *str; zend_long idx; @@ -727,7 +783,10 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem phpdbg_print_watch_diff(WATCH_ON_HASHTABLE, element->str, oldPtr, htPtr); } - phpdbg_add_ht_watch_element(parent, element); + if ((element->flags & PHPDBG_WATCH_RECURSIVE) && phpdbg_ht_watch_element_collides(parent, element)) { + return false; + } + element = phpdbg_add_ht_watch_element(parent, element, NULL); } else if ((zv = zend_symtable_find(ht, element->name_in_parent))) { if (element->flags & PHPDBG_WATCH_IMPLICIT) { zval *next = zv; @@ -746,9 +805,11 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem phpdbg_print_watch_diff(WATCH_ON_ZVAL, element->str, &element->backup.zv, zv); } + if ((element->flags & PHPDBG_WATCH_RECURSIVE) && phpdbg_bucket_watch_element_collides(zv, element)) { + return false; + } element->parent_container = ht; - phpdbg_add_bucket_watch_element((Bucket *) zv, element); - phpdbg_watch_parent_ht(element); + element = phpdbg_add_bucket_watch_element((Bucket *) zv, element, NULL); } else { return false; } @@ -1010,7 +1071,8 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) { zend_string *name = NULL; void *comparePtr; - if (watch->type == WATCH_ON_HASHTABLE) { + if (watch->type == WATCH_ON_HASHTABLE + && phpdbg_check_watch_diff(WATCH_ON_HASHTABLE, (char *) &watch->backup.ht + HT_WATCH_OFFSET, watch->addr.ptr)) { phpdbg_watch_element *element; zend_string *str; zend_long idx; @@ -1248,49 +1310,80 @@ void phpdbg_list_watchpoints(void) { } ZEND_HASH_FOREACH_END(); } -static int phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element *element) { - element->flags = PHPDBG_WATCH_SIMPLE; - phpdbg_add_bucket_watch_element((Bucket *) zv, element); - return SUCCESS; +typedef enum { + PHPDBG_WATCH_ADD_OK, + PHPDBG_WATCH_ADD_FAILED, + PHPDBG_WATCH_ADD_DUPLICATE, +} phpdbg_watch_add_result; + +static phpdbg_watch_add_result phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element **element) { + phpdbg_watch_element *added; + (*element)->flags = PHPDBG_WATCH_SIMPLE; + bool added_new; + added = phpdbg_add_bucket_watch_element((Bucket *) zv, *element, &added_new); + if (!added_new) { + *element = added; + return PHPDBG_WATCH_ADD_DUPLICATE; + } + return PHPDBG_WATCH_ADD_OK; } -static int phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element *element) { - phpdbg_watch_element *new; +static phpdbg_watch_add_result phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element **element_ptr) { + phpdbg_watch_element *element = *element_ptr; + phpdbg_watch_element *new, *added; zend_string *str; zval *orig_zv = zv; ZVAL_DEREF(zv); if (Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT) { - return FAILURE; + return PHPDBG_WATCH_ADD_FAILED; } - new = ecalloc(1, sizeof(phpdbg_watch_element)); - str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str)); zend_string_release(element->str); element->str = str; element->flags = PHPDBG_WATCH_IMPLICIT; - phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element); - element->child = new; + bool added_new; + added = phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element, &added_new); + if (!added_new) { + *element_ptr = added; + return PHPDBG_WATCH_ADD_DUPLICATE; + } + element = added; + new = ecalloc(1, sizeof(phpdbg_watch_element)); new->flags = PHPDBG_WATCH_SIMPLE; new->str = zend_string_copy(str); new->parent = element; - phpdbg_add_ht_watch_element(zv, new); - return SUCCESS; + added = phpdbg_add_ht_watch_element(zv, new, &added_new); + if (added != NULL && !added_new) { + phpdbg_clean_watch_element(element); + phpdbg_free_watch_element(element); + *element_ptr = added; + return PHPDBG_WATCH_ADD_DUPLICATE; + } + element->child = new; + *element_ptr = element; + return PHPDBG_WATCH_ADD_OK; } -static int phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element *element) { - element->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT; - element->child = NULL; - phpdbg_add_bucket_watch_element((Bucket *) zv, element); - return SUCCESS; +static phpdbg_watch_add_result phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element **element) { + phpdbg_watch_element *added; + (*element)->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT; + (*element)->child = NULL; + bool added_new; + added = phpdbg_add_bucket_watch_element((Bucket *) zv, *element, &added_new); + if (!added_new) { + *element = added; + return PHPDBG_WATCH_ADD_DUPLICATE; + } + return PHPDBG_WATCH_ADD_OK; } -typedef struct { int (*callback)(zval *zv, phpdbg_watch_element *); zend_string *str; } phpdbg_watch_parse_struct; +typedef struct { phpdbg_watch_add_result (*callback)(zval *zv, phpdbg_watch_element **); zend_string *str; } phpdbg_watch_parse_struct; static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, phpdbg_watch_parse_struct *info) { - int ret; + int ret = SUCCESS; phpdbg_watch_element *element = ecalloc(1, sizeof(phpdbg_watch_element)); element->str = zend_string_init(name, namelen, 0); element->name_in_parent = zend_string_init(key, keylen, 0); @@ -1298,13 +1391,25 @@ static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key element->parent = PHPDBG_G(watch_tmp); element->child = NULL; - ret = info->callback(zv, element); + phpdbg_watch_add_result result = info->callback(zv, &element); efree(name); efree(key); - if (ret != SUCCESS) { + if (result == PHPDBG_WATCH_ADD_FAILED) { phpdbg_remove_watch_element(element); + ret = FAILURE; + } else if (result == PHPDBG_WATCH_ADD_DUPLICATE) { + phpdbg_notice("%.*s is already being watched", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str)); + while (PHPDBG_G(watch_tmp) && PHPDBG_G(watch_tmp)->child == NULL) { + phpdbg_watch_element *parent = PHPDBG_G(watch_tmp)->parent; + phpdbg_clean_watch_element(PHPDBG_G(watch_tmp)); + phpdbg_free_watch_element(PHPDBG_G(watch_tmp)); + if (parent) { + parent->child = NULL; + } + PHPDBG_G(watch_tmp) = parent; + } } else { if (PHPDBG_G(watch_tmp)) { PHPDBG_G(watch_tmp)->child = element; @@ -1346,7 +1451,7 @@ static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, s element->name_in_parent = zend_string_init(key, keylen, 0); element->parent_container = parent; element->parent = PHPDBG_G(watch_tmp); - element = phpdbg_add_bucket_watch_element((Bucket *) zv, element); + element = phpdbg_add_bucket_watch_element((Bucket *) zv, element, NULL); efree(name); efree(key); @@ -1359,7 +1464,7 @@ static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, s return SUCCESS; } -static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(zval *, phpdbg_watch_element *)) { +static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, phpdbg_watch_add_result (*callback)(zval *, phpdbg_watch_element **)) { zend_class_entry *scope = zend_get_executed_scope(); phpdbg_watch_parse_struct info; int ret; @@ -1529,6 +1634,37 @@ void phpdbg_destroy_watchpoints(void) { free(PHPDBG_G(watchlist_mem_backup)); } +void phpdbg_release_watch_elements(void) { + phpdbg_watch_element *element; + uint32_t guard; + + ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) { + phpdbg_automatic_dequeue_free(element); + } ZEND_HASH_FOREACH_END(); + zend_hash_clean(&PHPDBG_G(watch_recreation)); + + guard = zend_hash_num_elements(&PHPDBG_G(watch_elements)) + 1; + while (zend_hash_num_elements(&PHPDBG_G(watch_elements)) > 0 && guard-- > 0) { + ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_elements), element) { + phpdbg_remove_watch_element(element); + break; + } ZEND_HASH_FOREACH_END(); + } + zend_hash_clean(&PHPDBG_G(watch_recreation)); + + phpdbg_btree_clean(&PHPDBG_G(watchpoint_tree)); + phpdbg_btree_clean(&PHPDBG_G(watch_HashTables)); + + zend_hash_destroy(&PHPDBG_G(watch_elements)); + zend_hash_init(&PHPDBG_G(watch_elements), 8, NULL, NULL, 0); + zend_hash_destroy(&PHPDBG_G(watch_recreation)); + zend_hash_init(&PHPDBG_G(watch_recreation), 8, NULL, NULL, 0); + zend_hash_destroy(&PHPDBG_G(watch_free)); + zend_hash_init(&PHPDBG_G(watch_free), 8, NULL, NULL, 0); + zend_hash_destroy(&PHPDBG_G(watch_collisions)); + zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0); +} + void phpdbg_purge_watchpoint_tree(void) { phpdbg_btree_position pos; phpdbg_btree_result *res; diff --git a/sapi/phpdbg/phpdbg_watch.h b/sapi/phpdbg/phpdbg_watch.h index 380140fd9f1f..e87dd77f0543 100644 --- a/sapi/phpdbg/phpdbg_watch.h +++ b/sapi/phpdbg/phpdbg_watch.h @@ -110,6 +110,7 @@ typedef struct { void phpdbg_setup_watchpoints(void); void phpdbg_destroy_watchpoints(void); +void phpdbg_release_watch_elements(void); void phpdbg_purge_watchpoint_tree(void); #ifndef _WIN32 diff --git a/sapi/phpdbg/tests/gh22480.phpt b/sapi/phpdbg/tests/gh22480.phpt new file mode 100644 index 000000000000..16559a75f27a --- /dev/null +++ b/sapi/phpdbg/tests/gh22480.phpt @@ -0,0 +1,34 @@ +--TEST-- +GH-22480 (Use-after-free when re-watching an already-watched variable) +--PHPDBG-- +b 6 +r +w a $a +w a $a +c + +q +--EXPECTF-- +[Successful compilation of %s] +prompt> [Breakpoint #0 added at %s:6] +prompt> [Breakpoint #0 at %s:6, hits: 1] +>00006: $a[0] = 2; + 00007: + 00008: $a = [0 => 3, 1 => 4]; +prompt> [Added watchpoint #0 for $a[]] +prompt> [$a[] is already being watched] +prompt> [Breaking on watchpoint $a[]] +1 elements were added to the array +>00009: +prompt> [$a[] has been removed, removing watchpoint] +[Script ended normally] +prompt> +--FILE-- + 3, 1 => 4]; diff --git a/sapi/phpdbg/tests/gh22480_2.phpt b/sapi/phpdbg/tests/gh22480_2.phpt new file mode 100644 index 000000000000..aa6e13dbf8d5 --- /dev/null +++ b/sapi/phpdbg/tests/gh22480_2.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-22480 (Use-after-free re-watching a sub-path of a recursive watchpoint) +--INI-- +opcache.optimization_level=0 +--PHPDBG-- +b 5 +r +w r $a +w $a[x] +c +q +--EXPECTF-- +[Successful compilation of %s] +prompt> [Breakpoint #0 added at %s:5] +prompt> [Breakpoint #0 at %s:5, hits: 1] +>00005: $a['x'] = 2; + 00006: +prompt> [Added watchpoint #0 for $a[]] +prompt> [$a[x] is already being watched] +prompt> [Breaking on watchpoint $a] +Old value%s +New value: Array ([x] => 2) +>00006: +prompt> [$a has been removed, removing watchpoint recursively] +--FILE-- + 1]; + +$a['x'] = 2; diff --git a/sapi/phpdbg/tests/watch_006.phpt b/sapi/phpdbg/tests/watch_006.phpt index c5f53a37510c..bf38b8eff1fb 100644 --- a/sapi/phpdbg/tests/watch_006.phpt +++ b/sapi/phpdbg/tests/watch_006.phpt @@ -1,12 +1,5 @@ --TEST-- Test multiple watch elements pointing to the same watchpoint ---SKIPIF-- - --PHPDBG-- b 4 r From df773093effb5f0479634b254753b6d42c645c35 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 12 Jul 2026 23:47:06 +0100 Subject: [PATCH 6/8] GH-22617: avoid null byte truncation of persistent stream keys Abstract unix domain socket addresses begin with a null byte, but the persistent stream list is keyed by a NUL-terminated string. The key was truncated at that null byte, so distinct abstract sockets collapsed onto the same persistent resource in p(f)sockopen() and stream_socket_client(). Escape null bytes (and backslashes, to stay unambiguous) when building the persistent hash key so the full address is preserved. The escaping helper lives in ext/standard rather than the public streams header, since only p(f)sockopen() and stream_socket_client() need it. Fix #22617 close GH-22704 --- NEWS | 4 +++ ext/standard/fsock.c | 6 ++-- ext/standard/streamsfuncs.c | 5 ++- ext/standard/tests/streams/gh22617.phpt | 45 +++++++++++++++++++++++++ main/streams/php_streams_int.h | 4 +++ main/streams/streams.c | 21 ++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/streams/gh22617.phpt diff --git a/NEWS b/NEWS index 9bcb68e52712..d4f7af6f7692 100644 --- a/NEWS +++ b/NEWS @@ -94,6 +94,10 @@ PHP NEWS . Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator mutates the array being sorted). (azchin, iliaal) +- Streams: + . Fixed bug GH-22617 (persistent stream keys truncated at null bytes, causing + distinct abstract unix domain sockets to share a resource). (David Carlier) + - Zip: . Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex() could crash after overwriting an entry and resetting its inherited diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 2b9e00a57554..c8e6ed08e4c0 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -22,6 +22,7 @@ #include #include "php_network.h" #include "file.h" +#include "streams/php_streams_int.h" static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, const char *host, size_t host_len, zend_long port) @@ -84,8 +85,9 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) } if (persistent) { - php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, - host_len, port); + zend_string *escaped = php_stream_escape_persistent_key(host, host_len); + spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, ZSTR_VAL(escaped), port); + zend_string_release_ex(escaped, false); } if (port > 0) { diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 1929075b60b7..391a72b5d9e0 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -23,6 +23,7 @@ #include "streamsfuncs.h" #include "php_network.h" #include "php_string.h" +#include "streams/php_streams_int.h" #ifdef HAVE_UNISTD_H #include #endif @@ -130,7 +131,9 @@ PHP_FUNCTION(stream_socket_client) context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT); if (flags & PHP_STREAM_CLIENT_PERSISTENT) { - spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(host)); + zend_string *escaped = php_stream_escape_persistent_key(ZSTR_VAL(host), ZSTR_LEN(host)); + spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(escaped)); + zend_string_release_ex(escaped, false); } /* prepare the timeout value for use */ diff --git a/ext/standard/tests/streams/gh22617.phpt b/ext/standard/tests/streams/gh22617.phpt new file mode 100644 index 000000000000..83dbd832e652 --- /dev/null +++ b/ext/standard/tests/streams/gh22617.phpt @@ -0,0 +1,45 @@ +--TEST-- +GH-22617: Persistent abstract unix domain sockets resolved to wrong resource +--CREDITS-- +Roysten +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +string(17) "persistent stream" +bool(true) +string(17) "persistent stream" +bool(true) diff --git a/main/streams/php_streams_int.h b/main/streams/php_streams_int.h index 7580088fba31..f9dda9a52ff7 100644 --- a/main/streams/php_streams_int.h +++ b/main/streams/php_streams_int.h @@ -62,3 +62,7 @@ * any other function that expects standard modes and you allow non-standard * ones. result should be a char[5]. */ void php_stream_mode_sanitize_fdopen_fopencookie(php_stream *stream, char *result); + +/* Escapes NUL and backslash bytes so a host containing them cannot truncate or + * collide the persistent stream hash key. */ +zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen); diff --git a/main/streams/streams.c b/main/streams/streams.c index 6feb8b7c01b5..368de1a64774 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -140,6 +140,27 @@ PHPAPI int php_stream_from_persistent_id(const char *persistent_id, php_stream * /* }}} */ +zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen) +{ + zend_string *escaped = zend_string_safe_alloc(hostlen, 2, 0, 0); + char *ptr = ZSTR_VAL(escaped); + for (size_t i = 0; i < hostlen; i++) { + if (host[i] == '\0') { + *ptr++ = '\\'; + *ptr++ = '0'; + } else if (host[i] == '\\') { + *ptr++ = '\\'; + *ptr++ = '\\'; + } else { + *ptr++ = host[i]; + } + } + *ptr = '\0'; + ZSTR_LEN(escaped) = ptr - ZSTR_VAL(escaped); + + return escaped; +} + static zend_llist *php_get_wrapper_errors_list(php_stream_wrapper *wrapper) { if (!FG(wrapper_errors)) { From 8e4daeff7a399847abb78cce89522e90f82cabc0 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 10 Jul 2026 07:34:19 -0400 Subject: [PATCH 7/8] Fix GH-22668: odbc heap over-read on oversized column value ext/odbc bound a displaysize+1 buffer per column but built the result string from the driver-reported vallen. A conforming driver truncates an over-long value into the buffer yet reports the full length, so the fetch over-read past the allocation and returned adjacent heap bytes to userland. Store the bound capacity in odbc_result_value and clamp the string length to it at the three bound-buffer fetch sites (odbc_fetch_array/object, odbc_fetch_into, odbc_result); the SQLGetData long-column paths stay bounded by longreadlen. Fixes GH-22668 Closes GH-22670 --- NEWS | 4 ++++ ext/odbc/php_odbc.c | 21 ++++++++++++++++++--- ext/odbc/php_odbc_includes.h | 1 + ext/odbc/tests/config.inc | 2 ++ ext/odbc/tests/gh22668.phpt | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 ext/odbc/tests/gh22668.phpt diff --git a/NEWS b/NEWS index d4f7af6f7692..a9ad00c2be65 100644 --- a/NEWS +++ b/NEWS @@ -38,6 +38,10 @@ PHP NEWS . Fixed IntlChar methods leaving stale global error state after successful calls. (Xuyang Zhang) +- ODBC: + . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the + driver-reported display size). (iliaal) + - OpenSSL: . Fixed timeout for supplemental read at end of a blocking stream in SSL stream wrapper. (ilutov) diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c index 692841be10e1..16617078d3c1 100644 --- a/ext/odbc/php_odbc.c +++ b/ext/odbc/php_odbc.c @@ -680,6 +680,7 @@ void odbc_bindcols(odbc_result *result) for(i = 0; i < result->numcols; i++) { charextraalloc = 0; + result->values[i].value_max_len = 0; colfieldid = SQL_COLUMN_DISPLAY_SIZE; rc = PHP_ODBC_SQLCOLATTRIBUTE(result->stmt, (SQLUSMALLINT)(i+1), PHP_ODBC_SQL_DESC_NAME, @@ -706,6 +707,7 @@ void odbc_bindcols(odbc_result *result) #ifdef HAVE_ADABAS case SQL_TIMESTAMP: result->values[i].value = (char *)emalloc(27); + result->values[i].value_max_len = 26; SQLBindCol(result->stmt, (SQLUSMALLINT)(i+1), SQL_C_CHAR, result->values[i].value, 27, &result->values[i].vallen); break; @@ -775,6 +777,7 @@ void odbc_bindcols(odbc_result *result) displaysize *= 4; } result->values[i].value = (char *)emalloc(displaysize + 1); + result->values[i].value_max_len = displaysize; rc = SQLBindCol(result->stmt, (SQLUSMALLINT)(i+1), SQL_C_CHAR, result->values[i].value, displaysize + 1, &result->values[i].vallen); break; @@ -1492,7 +1495,11 @@ static void php_odbc_fetch_hash(INTERNAL_FUNCTION_PARAMETERS, int result_type) ZVAL_FALSE(&tmp); break; } - ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen); + SQLLEN str_len = result->values[i].vallen; + if (str_len > result->values[i].value_max_len) { + str_len = result->values[i].value_max_len; + } + ZVAL_STRINGL(&tmp, result->values[i].value, str_len); break; } @@ -1660,7 +1667,11 @@ PHP_FUNCTION(odbc_fetch_into) ZVAL_FALSE(&tmp); break; } - ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen); + SQLLEN str_len = result->values[i].vallen; + if (str_len > result->values[i].value_max_len) { + str_len = result->values[i].value_max_len; + } + ZVAL_STRINGL(&tmp, result->values[i].value, str_len); break; } zend_hash_index_update(Z_ARRVAL_P(pv_res_arr), i, &tmp); @@ -1910,7 +1921,11 @@ PHP_FUNCTION(odbc_result) php_error_docref(NULL, E_WARNING, "Cannot get data of column #%d (driver cannot determine length)", field_ind + 1); RETURN_FALSE; } else { - RETURN_STRINGL(result->values[field_ind].value, result->values[field_ind].vallen); + SQLLEN str_len = result->values[field_ind].vallen; + if (str_len > result->values[field_ind].value_max_len) { + str_len = result->values[field_ind].value_max_len; + } + RETURN_STRINGL(result->values[field_ind].value, str_len); } break; } diff --git a/ext/odbc/php_odbc_includes.h b/ext/odbc/php_odbc_includes.h index 59404ecd266b..1c267c9e6b42 100644 --- a/ext/odbc/php_odbc_includes.h +++ b/ext/odbc/php_odbc_includes.h @@ -205,6 +205,7 @@ typedef struct odbc_result_value { char name[256]; char *value; SQLLEN vallen; + SQLLEN value_max_len; SQLLEN coltype; } odbc_result_value; diff --git a/ext/odbc/tests/config.inc b/ext/odbc/tests/config.inc index 8d74feeb6ffb..a0f24f301558 100644 --- a/ext/odbc/tests/config.inc +++ b/ext/odbc/tests/config.inc @@ -1,5 +1,7 @@ +--FILE-- + +--EXPECT-- +clamped to buffer: bool(true) +only value bytes: bool(true) From a7539ff8344c7290b3e0e275e2b095a572d84b28 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 10 Jul 2026 08:21:24 -0400 Subject: [PATCH 8/8] Fix GH-22666: pdo_odbc heap overflow on oversized output param odbc_stmt_param_hook() sizes an output-param buffer from the declared maxlen but then copies the runtime value into it using the value's own length, and at fetch-back hands ZVAL_STRINGL the driver-reported length. Both trust a length that can exceed the buffer: the input copy overflows the heap (controlled write), and on a truncated OUTPUT value the driver reports the full length via the shared indicator, so the readback over-reads. Record the bound capacity in a new outbuflen field and clamp the copy and the readback to it. LOB output is rejected at bind, so those copy sites stay unreachable. Fixes GH-22666 Closes GH-22674 --- NEWS | 2 ++ ext/pdo_odbc/odbc_stmt.c | 11 +++++++++++ ext/pdo_odbc/php_pdo_odbc_int.h | 1 + ext/pdo_odbc/tests/gh22666.phpt | 30 ++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 ext/pdo_odbc/tests/gh22666.phpt diff --git a/NEWS b/NEWS index a9ad00c2be65..76be38bcaa70 100644 --- a/NEWS +++ b/NEWS @@ -51,6 +51,8 @@ PHP NEWS carries no credentials). (iliaal) . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the driver-reported display size). (iliaal) + . Fixed bug GH-22666 (Heap buffer overflow when an output parameter value is + longer than the declared maxlen). (iliaal) - Phar: . Fixed inconsistent handling of the magic ".phar" directory. Paths such as diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 5b9104895854..8940143da09c 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -360,6 +360,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p param->driver_data = P; P->len = 0; /* is re-populated each EXEC_PRE */ + P->outbuflen = 0; P->outbuf = NULL; P->is_unicode = pdo_odbc_sqltype_is_unicode(S, sqltype); @@ -384,6 +385,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p P->len *= 2; } P->outbuf = emalloc(P->len + (P->is_unicode ? 2:1)); + P->outbuflen = P->len; } } @@ -481,10 +483,16 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p case PDO_ODBC_CONV_FAIL: case PDO_ODBC_CONV_NOT_REQUIRED: P->len = Z_STRLEN_P(parameter); + if (P->len > P->outbuflen) { + P->len = P->outbuflen; + } memcpy(P->outbuf, Z_STRVAL_P(parameter), P->len); break; case PDO_ODBC_CONV_OK: P->len = ulen; + if (P->len > P->outbuflen) { + P->len = P->outbuflen; + } memcpy(P->outbuf, S->convbuf, P->len); break; } @@ -506,6 +514,9 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p zval_ptr_dtor(parameter); if (P->len >= 0) { + if (P->len > P->outbuflen) { + P->len = P->outbuflen; + } ZVAL_STRINGL(parameter, P->outbuf, P->len); switch (pdo_odbc_ucs22utf8(stmt, P->is_unicode, parameter)) { case PDO_ODBC_CONV_FAIL: diff --git a/ext/pdo_odbc/php_pdo_odbc_int.h b/ext/pdo_odbc/php_pdo_odbc_int.h index 473d70ff7076..ea93c58b2b60 100644 --- a/ext/pdo_odbc/php_pdo_odbc_int.h +++ b/ext/pdo_odbc/php_pdo_odbc_int.h @@ -153,6 +153,7 @@ typedef struct { typedef struct { SQLLEN len; + SQLLEN outbuflen; SQLSMALLINT paramtype; char *outbuf; unsigned is_unicode:1; diff --git a/ext/pdo_odbc/tests/gh22666.phpt b/ext/pdo_odbc/tests/gh22666.phpt new file mode 100644 index 000000000000..b6ba2265b9be --- /dev/null +++ b/ext/pdo_odbc/tests/gh22666.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-22666 (Heap buffer overflow when an output param value exceeds its maxlen) +--EXTENSIONS-- +pdo_odbc +--SKIPIF-- + +--FILE-- +prepare('SELECT ?'); +$stmt->bindParam(1, $value, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 4); +$stmt->execute(); + +echo "bounded to maxlen: "; var_dump($value); +?> +--EXPECT-- +bounded to maxlen: string(4) "AAAA"