Skip to content

[phpdbg] Multiple Use-After-Free bugs in watchpoint management due to ignored return values #22480

Description

@RigelYoung

Description

Summary

Two Use-After-Free (UAF) bugs were identified in the phpdbg module (sapi/phpdbg/phpdbg_watch.c).

The issue stems from an unsafe API consumption pattern: the function phpdbg_add_bucket_watch_element() can internally free the element pointer passed to it (if a duplicate watch string already exists in the hash table) and return a new, valid pointer. However, in multiple outer functions, this return value is ignored. The caller's local pointer becomes a dangling pointer, leading to subsequent UAF reads and writes.

Bug Detail

Bug 1: UAF Read in phpdbg_try_re_adding_watch_element

When attempting to re-add a watch element, phpdbg_add_bucket_watch_element is called on line 750. If the underlying phpdbg_add_watch_element finds the element string in watch->elements, it frees the passed element via efree(element) and returns the existing old_element.

Because phpdbg_try_re_adding_watch_element ignores the returned pointer, it proceeds to use the freed memory on line 751.

// sapi/phpdbg/phpdbg_watch.c
749:        element->parent_container = ht;
750:        phpdbg_add_bucket_watch_element((Bucket *) zv, element); // [!] Return value ignored. `element` might be freed here.
751:        phpdbg_watch_parent_ht(element);                         // [!] UAF Read: dereferencing the dangling `element`.

Note: The call to phpdbg_watch_parent_ht on line 751 is also redundant, as phpdbg_add_bucket_watch_element already invokes it internally before returning.

Bug 2: UAF Write in phpdbg_create_array_watchpoint

A nearly identical alias disconnect occurs here. The function passes element to phpdbg_add_bucket_watch_element on line 1273 and ignores the updated pointer. Immediately after, on line 1274, it writes to a field within the freed struct, causing a memory corruption/UAF write.

// sapi/phpdbg/phpdbg_watch.c
1272:    element->flags = PHPDBG_WATCH_IMPLICIT;
1273:    phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element); // [!] Return value ignored.
1274:    element->child = new;                                         // [!] UAF Write: memory corruption occurs here.

Proposed Fix

The fix requires capturing the returned pointer to ensure the local element variable remains valid. For Bug 1, we also remove the redundant call to phpdbg_watch_parent_ht.

--- a/sapi/phpdbg/phpdbg_watch.c
+++ b/sapi/phpdbg/phpdbg_watch.c
@@ -747,8 +747,7 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem
         }
 
         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);
     } else {
         return false;
     }
@@ -1270,8 +1269,7 @@ static int phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element *elemen
     zend_string_release(element->str);
     element->str = str;
     element->flags = PHPDBG_WATCH_IMPLICIT;
-    phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element);
+    element = phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element);
     element->child = new;
 
     new->flags = PHPDBG_WATCH_SIMPLE;

PHP Version

PHP 8.6.0-dev (cli) (built: Apr 28 2026 17:35:13) (NTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.6.0-dev, Copyright © Zend by Perforce
    with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce

Operating System

No response

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions