Fix UAF with pipe operator + namespaced frameless icall - #23436
Open
Mrmaxmeier wants to merge 1 commit into
Open
Fix UAF with pipe operator + namespaced frameless icall#23436Mrmaxmeier wants to merge 1 commit into
Mrmaxmeier wants to merge 1 commit into
Conversation
… call
zend_compile_pipe() passes its left hand side as a ZEND_AST_ZNODE argument.
Compiling that node is a plain struct copy, so every compilation hands out the
same single reference to the constant it holds -- unlike ZEND_AST_ZVAL, which
does a ZVAL_COPY.
Inside a namespace, zend_compile_ns_call() compiles the argument list twice: once
for the INIT_NS_FCALL_BY_NAME fallback and once for the frameless icall. Both
compilations end in zend_add_literal() -> zval_make_interned_string(), which
releases the string when interning returns a different one, so the second release
frees a string the AST (or the first literal) still points at:
namespace N;
var_dump(' string literal ' |> trim(...));
Take one extra reference per additional compilation, so the frameless
optimization can be kept for pipes.
Assisted-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi,
we ran into a use-after-free with the
php-fuzz-parserfuzzing target:ASAN backtrace for minimal reproducer above
The issue occurs when namespace and constants are combined with a pipe operator and "frameless" handler functions like
trim()andstrtolower(). This reproduces both onmasterand thePHP-8.5branch (the pipe operator was added in 8.5).Root cause and fix
The current logic works like this:
zend_compile_pipe()compiles the left hand side down to a value first and passes it on as aZEND_AST_ZNODEargument.ZEND_AST_ZNODEis a plain struct copy (*result = *zend_ast_get_znode(ast)), so every compilation hands out the same single reference the node holds.ZEND_AST_ZVALdoes aZVAL_COPY()instead.SET_NODE()passes that zval tozend_add_literal(), which takes over the reference:zend_insert_literal()stores it withZVAL_COPY_VALUE()and, for strings, first runszval_make_interned_string().zend_compile_ns_call()compiles the argument list twice: once for theINIT_NS_FCALL_BY_NAMEfallback and a second time for the frameless icall behindZEND_JMP_FRAMELESS.So with a constant on the left hand side one reference is consumed twice. Depending on what
zend_insert_literal()does with it, that surfaces in two ways:destroy_op_array()releases it twice. That is the trace above;namespace N; var_dump(['a', 'b', 'c'] |> implode(...));reaches it via the array case.zval_make_interned_string()releases the string when interning returns a different one, so the second compilation drops the last reference while the AST still points at it andzend_ast_destroy()walks into freed memory.The fix takes one extra reference per additional compilation, right before the argument list is compiled the second time, so the frameless optimization can be kept for pipes. Only
IS_CONSTZEND_AST_ZNODEarguments are touched, and unpacked or named arguments never reach this path becausezend_compile_ns_call()already skips the frameless branch for those.Notably, while the fix in this PR seems reasonable to me, it is entirely LLM-generated.
Thanks!
(The CI failure "No response from 127.0.0.1" seems spurious and doesn't show up in the CI run from my fork.)
Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses.