[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_