diff --git a/NEWS b/NEWS index 708dded9eaa..a7b262c93ea 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ PHP NEWS - Core: . Added optional support for max_execution_time in ZTS/Linux builds (Kévin Dunglas) + . Fixed use-after-free in recursive AST evaluation. (ilutov) - FTP: . Propagate success status of ftp_close(). (nielsdos) diff --git a/Zend/tests/gh10709.phpt b/Zend/tests/gh10709.phpt new file mode 100644 index 00000000000..f394e1a7882 --- /dev/null +++ b/Zend/tests/gh10709.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-10709: Recursive class constant evaluation +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +string(2) "AB" diff --git a/Zend/tests/gh10709_2.phpt b/Zend/tests/gh10709_2.phpt new file mode 100644 index 00000000000..723fa29cc94 --- /dev/null +++ b/Zend/tests/gh10709_2.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-10709: Recursive class constant evaluation +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +object(B)#2 (1) { + ["prop"]=> + string(1) "A" +} +object(B)#2 (1) { + ["prop"]=> + string(1) "A" +} diff --git a/Zend/tests/gh10709_3.phpt b/Zend/tests/gh10709_3.phpt new file mode 100644 index 00000000000..151b84a19e0 --- /dev/null +++ b/Zend/tests/gh10709_3.phpt @@ -0,0 +1,42 @@ +--TEST-- +GH-10709: Recursive class constant evaluation with outer call failing +--FILE-- + +--EXPECTF-- +object(B)#3 (1) { + ["prop"]=> + string(2) "AS" +} + +Fatal error: Uncaught Exception: Thrown from S in %s:%d +Stack trace: +#0 %s(%d): S->__toString() +#1 {main} + thrown in %s on line %d diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index a85a11e7730..2dd05c24fc3 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -685,7 +685,19 @@ ZEND_API zend_result ZEND_FASTCALL zval_update_constant_ex(zval *p, zend_class_e } else { zval tmp; - if (UNEXPECTED(zend_ast_evaluate(&tmp, ast, scope) != SUCCESS)) { + // Increase the refcount during zend_ast_evaluate to avoid releasing the ast too early + // on nested calls to zval_update_constant_ex which can happen when retriggering ast + // evaluation during autoloading. + zend_ast_ref *ast_ref = Z_AST_P(p); + bool ast_is_refcounted = !(GC_FLAGS(ast_ref) & GC_IMMUTABLE); + if (ast_is_refcounted) { + GC_ADDREF(ast_ref); + } + zend_result result = zend_ast_evaluate(&tmp, ast, scope); + if (ast_is_refcounted && !GC_DELREF(ast_ref)) { + rc_dtor_func((zend_refcounted *)ast_ref); + } + if (UNEXPECTED(result != SUCCESS)) { return FAILURE; } zval_ptr_dtor_nogc(p); diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 44ca0179645..447e2269aa3 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -3069,7 +3069,19 @@ static zend_result ZEND_FASTCALL zval_jit_update_constant_ex(zval *p, zend_class } else { zval tmp; - if (UNEXPECTED(zend_ast_evaluate(&tmp, ast, scope) != SUCCESS)) { + // Increase the refcount during zend_ast_evaluate to avoid releasing the ast too early + // on nested calls to zval_update_constant_ex which can happen when retriggering ast + // evaluation during autoloading. + zend_ast_ref *ast_ref = Z_AST_P(p); + bool ast_is_refcounted = !(GC_FLAGS(ast_ref) & GC_IMMUTABLE); + if (ast_is_refcounted) { + GC_ADDREF(ast_ref); + } + zend_result result = zend_ast_evaluate(&tmp, ast, scope); + if (ast_is_refcounted && !GC_DELREF(ast_ref)) { + rc_dtor_func((zend_refcounted *)ast_ref); + } + if (UNEXPECTED(result != SUCCESS)) { return FAILURE; } zval_ptr_dtor_nogc(p); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 6ca8c46892a..49dfea34e33 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -248,6 +248,7 @@ static void zend_persist_zval(zval *z) zend_persist_ast(GC_AST(old_ref)); Z_TYPE_FLAGS_P(z) = 0; GC_SET_REFCOUNT(Z_COUNTED_P(z), 1); + GC_ADD_FLAGS(Z_COUNTED_P(z), GC_IMMUTABLE); efree(old_ref); } break;