Skip to content

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

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/phpdbg-watch-packed-bucket-overread
Open

Fix phpdbg over-read watching an element of a packed array#22756
iliaal wants to merge 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

Packed arrays store bare zvals, but phpdbg watches every array element as a Bucket and reads Bucket.h/.key in its relocation check, over-reading the neighbouring element. A sibling write then fires a phantom watchpoint, which crashed on 32-bit Windows (WINDOWS_X86_NTS in the nightly) once GH-22480 un-xfailed watch_006. For a packed parent, relocation is now detected by re-resolving the element and only the zval value is compared.

Note: watch_005 still carries a PHP_INT_SIZE == 4 xfail with the same "flaws in the implementation of watchpoints" wording, but on a different path (string/reference indirection, not packed buckets). It likely hides a related but distinct 32-bit flaw masked by that skip; this change does not touch it, so it stays skipped.

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

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