Skip to content

Fix GH-21639: Protect frameless args during __toString() reentry#21815

Open
prateekbhujel wants to merge 3 commits intophp:PHP-8.4from
prateekbhujel:prateekbhujel/fix-gh-21639-frameless-volatile-args
Open

Fix GH-21639: Protect frameless args during __toString() reentry#21815
prateekbhujel wants to merge 3 commits intophp:PHP-8.4from
prateekbhujel:prateekbhujel/fix-gh-21639-frameless-volatile-args

Conversation

@prateekbhujel
Copy link
Copy Markdown
Contributor

@prateekbhujel prateekbhujel commented Apr 20, 2026

Fixes GH-21639.

Frameless internal calls can borrow CV operands directly from the caller frame. If one of those arguments is converted through __toString(), userland can mutate the borrowed values while the frameless handler is still reading them.

This keeps the fix on the actual reentry path:

  • when __toString() runs from a frameless call, keep copies of string/array CV operands for that active frameless opcode
  • release those copies after the frameless handler returns, so the borrowed operands stay valid for the whole handler
  • use the same post-handler cleanup point for JIT frameless calls

Tests run:

make -j8
git diff --check HEAD~3 HEAD
sapi/cli/php run-tests.php -q Zend/tests/gh21639.phpt ext/pcre/tests/gh21639.phpt ext/standard/tests/strings/bug22224.phpt ext/standard/tests/strings/implode_variation.phpt ext/standard/tests/array/in_array/in_array_variation1_mixed.phpt ext/standard/tests/strings/strtr_basic.phpt ext/standard/tests/strings/str_replace_basic.phpt ext/pcre/tests/preg_replace.phpt
sapi/cli/php -d zend_extension=$PWD/modules/opcache.so -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.protect_memory=1 -d opcache.jit_buffer_size=64M run-tests.php -q Zend/tests/gh21639.phpt ext/pcre/tests/gh21639.phpt Zend/tests/frameless_undefined_var.phpt

@iluuu1994
Copy link
Copy Markdown
Member

Thanks for the PR. implode() is just one example, this applies to:

  • preg_replace
  • in_array (with strict: false)
  • strtr
  • str_replace

I'm afraid this will need a more general solution. My hope was to avoid overhead in the VM, but it might be unavoidable for a proper fix, even if this is a largely artificial issue.

@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 35148db to 15ec47b Compare April 20, 2026 22:04
@prateekbhujel prateekbhujel requested a review from dstogov as a code owner April 20, 2026 22:04
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Protect frameless implode args Fix GH-21639: Protect frameless call args Apr 20, 2026
@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 15ec47b to c818fa7 Compare April 20, 2026 22:31
Comment thread Zend/zend_vm_def.h Outdated
@iluuu1994
Copy link
Copy Markdown
Member

The Symfony benchmark IC increases by 0.64%, the Wordpress one by 1.15%. This removes much of the benefit of frameless calls, so I'm not to keen on solving this issue in that way. As long as other engine bugs aren't solved (#20001 and #20018) I don't think this needs to be rushed.

@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from c818fa7 to 08cc08f Compare April 21, 2026 08:04
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Protect frameless call args Fix GH-21639: Guard frameless handler reentry Apr 21, 2026
@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 08cc08f to 38e2314 Compare April 21, 2026 09:47
@bwoebi
Copy link
Copy Markdown
Member

bwoebi commented Apr 21, 2026

The other alternative would be checking inside the tostring handler whether the parent frame is currently at a frameless opcode and then safely copy its CV args to a buffer, set EG(vm_interrupt) and free them on the next EG(vm_interrupt), completely moving the overhead off the main paths and be a truly generic solution.
(Not necessarily vm_interrupt, it can rather be like the delayed error handler solution.)

Obviously comes at a small tostring handler cost, but I'd really rather see overhead there...?

@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 38e2314 to 163b9ef Compare April 21, 2026 11:55
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Guard frameless handler reentry Fix GH-21639: Protect frameless args during __toString reentry Apr 21, 2026
@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 163b9ef to 9ae220f Compare April 21, 2026 12:10
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Protect frameless args during __toString reentry Fix GH-21639: Keep frameless CV args alive during __toString() Apr 21, 2026
@prateekbhujel
Copy link
Copy Markdown
Contributor Author

@bwoebi Yeah, agreed. That cost belongs on the __toString() side, not on every frameless call.

I pushed a follow-up in that direction. It checks from __toString() whether the parent frame is currently on a frameless opcode, then keeps copies of the string/array CV operands for that opcode.

I did not keep the cleanup on EG(vm_interrupt): in the failing implode() cases it can run before the parent frameless handler has returned, so the parent frame still looks like it is sitting on the same frameless opline and the cleanup can keep treating the copies as live. I moved the release to the frameless handler return instead, with the same cleanup point for JIT frameless calls.

So the copy/dtor work is on actual __toString() reentry. The ordinary frameless path only has the cold pending-copy check after the handler returns, which seemed like the safer version of this approach.

@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 28d83f6 to 2671fc4 Compare April 24, 2026 14:54
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Keep frameless CV args alive during __toString() Fix GH-21639: Keep frameless call args stable during reentry Apr 24, 2026
@prateekbhujel prateekbhujel changed the base branch from PHP-8.4 to master April 24, 2026 14:55
@prateekbhujel prateekbhujel force-pushed the prateekbhujel/fix-gh-21639-frameless-volatile-args branch from 2671fc4 to 28d83f6 Compare April 24, 2026 17:33
@prateekbhujel prateekbhujel changed the title Fix GH-21639: Keep frameless call args stable during reentry Fix GH-21639: Protect frameless args during __toString() reentry Apr 24, 2026
@prateekbhujel prateekbhujel changed the base branch from master to PHP-8.4 April 24, 2026 17:45
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.

Volatile arguments in frameless calls

4 participants