Skip to content

Fix GH-22480: phpdbg use-after-free re-watching a watched variable#22493

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-22480-phpdbg-watch-uaf
Open

Fix GH-22480: phpdbg use-after-free re-watching a watched variable#22493
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/gh-22480-phpdbg-watch-uaf

Conversation

@iliaal

@iliaal iliaal commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #22480

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. The two reported derefs are not the whole bug: with only the proposed two-line capture, w a $a typed twice still crashes, relocated to a later interned-string read. This reports the duplicate back through the parse callback so the wrapper registers nothing, and rejects a recursive watch's hashtable element that collides with an existing watch, at creation and recreation, instead of freeing a still-referenced one.

@Girgias

Girgias commented Jun 28, 2026

Copy link
Copy Markdown
Member

I don't know PHPDBG nor have the time to learn how it works rn.

@iliaal iliaal requested review from TimWolla and ndossche June 28, 2026 21:40
@TimWolla TimWolla removed their request for review June 29, 2026 16:49
@ndossche

Copy link
Copy Markdown
Member

This will have to wait until the weekend, I don't have time right now.
I used to look into some phpdbg issues, but I stopped doing that because some people who were supposed to maintain that don't bother; so why should we bother?

Comment thread sapi/phpdbg/tests/gh22480.phpt Outdated
GH-22480 (Use-after-free when re-watching an already-watched variable)
--SKIPIF--
<?php
if (getenv('SKIP_ASAN')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover from when the test demonstrated the crash. It's ASAN-clean with the fix, so I dropped the skip and let it run under ASAN.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Judging by the last force push, they are re-added now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, the drop didn't make it into the last push. Removed now.

Comment thread sapi/phpdbg/tests/gh22480_2.phpt Outdated
GH-22480 (Use-after-free re-watching a sub-path of a recursive watchpoint)
--SKIPIF--
<?php
if (getenv('SKIP_ASAN')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, a stale crash-marker. Dropped it so this one runs under ASAN too.

Comment thread sapi/phpdbg/phpdbg_watch.c Outdated

phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element) {
phpdbg_watchpoint_t watch;
phpdbg_watch_element *added;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge declaration and assignment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread sapi/phpdbg/phpdbg_watch.c Outdated
phpdbg_watch_parent_ht(element);
return element;
added = phpdbg_add_watch_element(&watch, element);
if (added == element) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this risk ABA-like issues?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, phpdbg_add_watch_element() frees element on a duplicate and returns the pre-existing one, so added == element read a freed pointer. Threaded a bool out-param through the add functions so callers test that instead, and did the same for the sibling checks at the recursion and array-watch sites.

@iliaal iliaal force-pushed the fix/gh-22480-phpdbg-watch-uaf branch 2 times, most recently from cd00f35 to 6d9d07d Compare July 11, 2026 14:10
@iliaal iliaal requested a review from ndossche July 11, 2026 15:09
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.

Fixes phpGH-22480
@iliaal iliaal force-pushed the fix/gh-22480-phpdbg-watch-uaf branch from 6d9d07d to 796b0fd Compare July 13, 2026 21:36

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex / intricate code, but okay.
Please note the following:

  • I put a cleanup remark about using enum returns, this is non-blocking
  • watch_006 is marked as XFAIL, but likely no longer should. Please double check.

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);
#define PHPDBG_WATCH_DUPLICATE 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of mixing SUCCESS / this define; it would be cleaner if we had an enum and used that enum as a return type.

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, still shows a UAF in CI, but only on some targets? I didn't see it locally
EDIT: might be fast-shutdown / non-fast-shutdown related, you should check

@iliaal

iliaal commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Interesting I didn't see UAFs locally or before in CI/CD let me investigate, this needs more checking

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants