Commit c6de4e63c84 for php

commit c6de4e63c84a4a1832f37d92497c98b4c50ab917
Merge: a47a2be280b 055e11e0c7e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Oct 24 18:19:43 2026 -0400

    Merge branch 'PHP-8.4' into PHP-8.5

    * PHP-8.4:
      phpdbg: Backport watchpoint fixes to PHP-8.4

diff --cc NEWS
index 605d0ac8b8c,4a3383c25a4..1ee7d6fd2fb
--- a/NEWS
+++ b/NEWS
@@@ -111,11 -128,11 +111,15 @@@ PH

  - Phpdbg:
    . Fix wrong command names. (ndossche, Weilin Du)
+   . Fixed bug GH-22480 (Use-after-free when re-watching an already-watched
+     variable). (Ilia Alshanetsky)
+   . Fixed over-read when watching a packed array element. (Ilia Alshanetsky)
+   . Fixed double free of watch element chains. (Arnaud)

 +- POSIX:
 +  . Fixed posix_setrlimit() rejecting POSIX_RLIMIT_INFINITY on Solaris.
 +    (Jorg Sowa)
 +
  - Readline:
    . Fixed a crash in readline_info() when completion_append_character
      contains a non-ASCII byte. (Weilin Du)
diff --cc sapi/phpdbg/phpdbg_watch.c
index 5657649efdb,1c63f26ea70..6daf4060222
--- a/sapi/phpdbg/phpdbg_watch.c
+++ b/sapi/phpdbg/phpdbg_watch.c
@@@ -138,13 -148,13 +148,13 @@@ static bool phpdbg_check_zval_watch_dif
  bool phpdbg_check_watch_diff(phpdbg_watchtype type, void *oldPtr, void *newPtr) {
  	switch (type) {
  		case WATCH_ON_BUCKET:
 -			if (memcmp(&((Bucket *) oldPtr)->h, &((Bucket *) newPtr)->h, sizeof(Bucket) - sizeof(zval) /* key/val comparison */) != 0) {
 -				return 2;
 +			if (memcmp(&((Bucket *) oldPtr)->h, &((Bucket *) newPtr)->h, sizeof(Bucket) - sizeof(zval) /* hash+key comparison */) != 0) {
 +				return true;
  			}
 -			/* TODO: Is this intentional? */
 +			/* Fall through to also compare the value from the bucket. */
  			ZEND_FALLTHROUGH;
  		case WATCH_ON_ZVAL:
- 			return memcmp(oldPtr, newPtr, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */) != 0;
+ 			return phpdbg_check_zval_watch_diff((zval *) oldPtr, (zval *) newPtr);
  		case WATCH_ON_HASHTABLE:
  			return zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_PTR_HT(newPtr));
  		case WATCH_ON_REFCOUNTED:
@@@ -748,18 -837,48 +837,48 @@@ bool phpdbg_try_re_adding_watch_element
  			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(zv, element, NULL);
  	} else {
 -		return 0;
 +		return false;
  	}

 -	return 1;
 +	return true;
  }

+ /* watch_recreation is keyed by element->str, so a parent and its child are stored
+  * under distinct keys and the deduplication in phpdbg_queue_element_for_recreation()
+  * cannot notice that they belong to the same chain. As phpdbg_free_watch_element_tree()
+  * frees the whole chain, any other entry referencing a member of it must be dropped
+  * first, or the chain gets freed twice. The buckets are only nulled out as the hash is
+  * being iterated over by the callers; they clean it right after. */
+ static void phpdbg_forget_queued_watch_element(phpdbg_watch_element *element) {
+ 	zval *zv = zend_hash_find(&PHPDBG_G(watch_recreation), element->str);
+ 	if (zv && Z_PTR_P(zv) == element) {
+ 		Z_PTR_P(zv) = NULL;
+ 	}
+ }
+
+ static void phpdbg_dequeue_watch_element_tree(phpdbg_watch_element *element) {
+ 	phpdbg_watch_element *cur;
+
+ 	for (cur = element->parent; cur; cur = cur->parent) {
+ 		phpdbg_forget_queued_watch_element(cur);
+ 	}
+ 	for (cur = element->child; cur; cur = cur->child) {
+ 		phpdbg_forget_queued_watch_element(cur);
+ 	}
+ 	phpdbg_forget_queued_watch_element(element);
+ }
+
  void phpdbg_automatic_dequeue_free(phpdbg_watch_element *element) {
  	phpdbg_watch_element *child = element;
+
+ 	phpdbg_dequeue_watch_element_tree(element);
+
  	while (child->child && !(child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) {
  		child = child->child;
  	}