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

[email protected] (RigelYoung) Sat, 27 Jun 2026 02:11:09 +0000
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/22480
Author: RigelYoung

### Description

### Summary

Two Use-After-Free (UAF) vulnerabilities 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.

```C
// 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.

```C
// 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`.

```Diff
--- 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

```plain
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_