Skip to content

Fix phpdbg over-read when watching a packed array element - #22756

Merged
iliaal merged 1 commit into
php:masterfrom
iliaal:fix/phpdbg-watch-packed-bucket-overread
Jul 24, 2026
Merged

Fix phpdbg over-read when watching a packed array element#22756
iliaal merged 1 commit into
php:masterfrom
iliaal:fix/phpdbg-watch-packed-bucket-overread

Conversation

@iliaal

@iliaal iliaal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

phpdbg watched every array element as a Bucket, but packed arrays store bare zvals and de-indirected stack variables aren't buckets, so reading Bucket.h/.key over-read the neighbour and a sibling write tripped a phantom watchpoint. Those elements are now watched as WATCH_ON_ZVAL, comparing the type info first and reading the value union only for initialised slots, so uninitialised CV bytes are never compared.

This also surfaced a pre-existing 32-bit crash: phpdbg_btree_insert_or_update() published each new node before initialising its child pointers, so a write to a node on a watched page faulted into the watchpoint handler and walked the half-built node. It now builds the node fully before linking it in. Both showed up on WINDOWS_X86_NTS once GH-22480 un-xfailed watch_006; watch_005 and watch_006 are un-skipped / re-recorded to match.

@iluuu1994

Copy link
Copy Markdown
Member

This doesn't work for me. The PR still fails with the same error. I'll try to create a PR by tomorrow.

@iliaal

iliaal commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I see now, I thought the linux 32 would validate it fully, but it doesn't. I see the issue on Win32. Will try to take a stab @ it tonight if I get anywhere I'll tag you on this PR, otherwise all yours :(

@iluuu1994

iluuu1994 commented Jul 22, 2026

Copy link
Copy Markdown
Member

You should run with MSAN, it reproduces on Linux. I think there are multiple issues.

  1. We're treating all stack variables as buckets, even though the ones that we de-indirect are not. So phpdbg_check_watch_diff() will access out-of-bounds on &((Bucket *) oldPtr)->h (that's the primary issue here).
  2. The above indicates we might want to just use the normal zval watch point (i.e. WATCH_ON_ZVAL), but we'll still need to unregister the watch point when the stack frame releases.
  3. The stack frame is not zeroed on initialization, and CVs are not fully initialized for IS_UNDEF (just the type), so the memcmp() in phpdbg_check_watch_diff() will read uninitialized data anyway.

iliaal added a commit to iliaal/php-src that referenced this pull request Jul 23, 2026
phpdbg watched every array element as a Bucket, but a packed array
stores bare zvals, so reading Bucket.h/.key over-read the neighbouring
element and a sibling write tripped a phantom break. De-indirected stack
variables are not buckets either. Watch such elements as WATCH_ON_ZVAL,
re-resolve them in the parent on relocation, drop the watch when the
stack frame releases (IS_UNDEF), and compare the type info before the
value so uninitialised CV bytes are never read.

phpdbg_btree_insert_or_update published each freshly allocated node into
the tree before initialising its child pointers. When the node landed on
a watched page the write that initialised it faulted into the watchpoint
handler, which walked the half-built node and dereferenced a wild
pointer. Build the node fully, then link it in with a single store. This
crashed only on 32-bit, where the compact heap places the node on the
same page as the watched zval.

Closes phpGH-22756
@iliaal
iliaal force-pushed the fix/phpdbg-watch-packed-bucket-overread branch from d093c0e to c92ca4b Compare July 23, 2026 02:16
@iliaal

iliaal commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the pointers. WATCH_ON_ZVAL was the right call, covers all three: re-resolve in the parent on relocation, drop the watch once the CV goes IS_UNDEF on frame teardown/reuse, and check type info before the value union so uninitialised CV bytes aren't read.

That fixed the over-read but Win32 still crashed, on a separate pre-existing bug. phpdbg_btree_insert_or_update links a freshly allocated node into the tree before initialising its child pointers; when the node lands on a watched page, the init write faults into the watchpoint handler, which then walks the half-built node. It only reproduced on 32-bit, where the compact heap puts the node on the same page as the watched zval. Building the node before publishing it fixes it.

watch_005/006 pass, full phpdbg suite clean on a local x86 Windows build and under ASAN.

iliaal added a commit to iliaal/php-src that referenced this pull request Jul 23, 2026
phpdbg watched every array element as a Bucket, but a packed array
stores bare zvals, so reading Bucket.h/.key over-read the neighbouring
element and a sibling write tripped a phantom break. De-indirected stack
variables are not buckets either. Watch such elements as WATCH_ON_ZVAL,
re-resolve them in the parent on relocation, drop the watch when the
stack frame releases (IS_UNDEF), and compare the type info before the
value so uninitialised CV bytes are never read.

phpdbg_btree_insert_or_update published each freshly allocated node into
the tree before initialising its child pointers. When the node landed on
a watched page the write that initialised it faulted into the watchpoint
handler, which walked the half-built node and dereferenced a wild
pointer. Build the node fully, then link it in with a single store. This
crashed only on 32-bit, where the compact heap places the node on the
same page as the watched zval.

Closes phpGH-22756
@iliaal
iliaal force-pushed the fix/phpdbg-watch-packed-bucket-overread branch from c92ca4b to 5f279e6 Compare July 23, 2026 02:19
Comment thread sapi/phpdbg/phpdbg_btree.c Outdated
Comment thread sapi/phpdbg/phpdbg_watch.c Outdated
Comment thread sapi/phpdbg/phpdbg_watch.c Outdated
Comment thread sapi/phpdbg/phpdbg_watch.c
@iliaal
iliaal force-pushed the fix/phpdbg-watch-packed-bucket-overread branch from e86f282 to 0c76639 Compare July 23, 2026 20:02
@iliaal

iliaal commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Dropped both blocks and rebuilt the x86 NTS phpdbg; the full phpdbg suite is unchanged from before the removal.

@iluuu1994 iluuu1994 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.

Looks correct now. I'll add Bob for a review, since he wrote most of this code (albeit a very long time ago).

Comment thread sapi/phpdbg/phpdbg_watch.c Outdated
Comment thread sapi/phpdbg/phpdbg_watch.c Outdated
@iluuu1994
iluuu1994 requested a review from bwoebi July 23, 2026 20:13
iliaal added a commit to iliaal/php-src that referenced this pull request Jul 23, 2026
phpdbg watched every array element as a Bucket, but a packed array
stores bare zvals, so reading Bucket.h/.key over-read the neighbouring
element and a sibling write tripped a phantom break. De-indirected stack
variables are not buckets either. Watch such elements as WATCH_ON_ZVAL
and compare the type info before the value so uninitialised CV bytes are
never read.

phpdbg_btree_insert_or_update published each freshly allocated node into
the tree before initialising its child pointers. When the node landed on
a watched page the write that initialised it faulted into the watchpoint
handler, which walked the half-built node and dereferenced a wild
pointer. Build the node fully, then link it in with a single store. This
crashed only on 32-bit, where the compact heap places the node on the
same page as the watched zval.

Closes phpGH-22756
@iliaal
iliaal force-pushed the fix/phpdbg-watch-packed-bucket-overread branch from 0c76639 to 304b67c Compare July 23, 2026 20:26
@iluuu1994 iluuu1994 changed the title Fix phpdbg over-read watching an element of a packed array Fix phpdbg over-read when watching declared stack variables Jul 24, 2026
@iluuu1994

Copy link
Copy Markdown
Member

Feel free to merge this so nightly CI is finally green, we can still adjust if Bob finds something later. Just make sure to fix the description to something more accurate. Thanks!

@ndossche
ndossche removed their request for review July 24, 2026 12:38
@iliaal iliaal changed the title Fix phpdbg over-read when watching declared stack variables Fix phpdbg over-read when watching a packed array element Jul 24, 2026
phpdbg watched every array element as a Bucket, but packed arrays store
bare zvals and de-indirected stack variables aren't buckets, so reading
Bucket.h/.key over-read the neighbour and a sibling write tripped a
phantom break. Watch such elements as WATCH_ON_ZVAL and compare the type
info first, reading the value union only for initialised slots so an
uninitialised CV is never compared by value.

A separate pre-existing bug crashed 32-bit: phpdbg_btree_insert_or_update
linked each new node into the tree before initialising its child
pointers, so a write to a node on a watched page faulted into the
watchpoint handler, which walked the half-built node. Build the node
fully, then publish it with a single store.

Closes phpGH-22756
@iliaal
iliaal force-pushed the fix/phpdbg-watch-packed-bucket-overread branch from 304b67c to 20259f8 Compare July 24, 2026 13:17
@iliaal
iliaal merged commit 250f138 into php:master Jul 24, 2026
17 of 18 checks passed
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.

2 participants