From a73fc4607d0cbc49418945ae87d270eb677de5af Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Sun, 11 Jan 2026 06:08:43 +0400 Subject: [PATCH 1/3] Fix GH-10497: Allow direct modification of object properties in constants --- Zend/tests/gh10497.phpt | 39 +++++++++++++++++++++++++++++++++++++++ Zend/tests/gh12102_3.phpt | 14 ++++++++------ Zend/zend_compile.c | 21 +++++++++++++++++---- 3 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 Zend/tests/gh10497.phpt diff --git a/Zend/tests/gh10497.phpt b/Zend/tests/gh10497.phpt new file mode 100644 index 000000000000..3ac7ad027af2 --- /dev/null +++ b/Zend/tests/gh10497.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-10497: Allow direct modification of object stored in a constant +--FILE-- +b = 42; +var_dump(a->b); + +const obj = new stdClass; +obj->foo = 'bar'; +obj->baz = 123; +var_dump(obj->foo, obj->baz); + +const nested = new stdClass; +nested->inner = new stdClass; +nested->inner->value = 999; +var_dump(nested->inner->value); + +const readTest = new stdClass; +readTest->prop = 'test'; +echo readTest->prop . "\n"; + +var_dump(isset(readTest->prop)); +var_dump(empty(readTest->missing)); + +const modTest = new stdClass; +modTest->val = 1; +modTest->val = 2; +var_dump(modTest->val); +?> +--EXPECT-- +int(42) +string(3) "bar" +int(123) +int(999) +test +bool(true) +bool(true) +int(2) diff --git a/Zend/tests/gh12102_3.phpt b/Zend/tests/gh12102_3.phpt index 741bce5ab1ba..c460fe7428a1 100644 --- a/Zend/tests/gh12102_3.phpt +++ b/Zend/tests/gh12102_3.phpt @@ -5,11 +5,8 @@ GH-12102: Incorrect "Cannot use temporary expression in write context" error for function test() { byVal(C[0]); - try { - byRef(C[0]); - } catch (Error $e) { - echo $e->getMessage(), "\n"; - } + byRef(C[0]); + var_dump(C); } /* Intentionally declared after test() to avoid compile-time checking of ref args. */ @@ -21,6 +18,7 @@ function byVal($arg) { } function byRef(&$arg) { + $arg = 'modified'; var_dump($arg); } @@ -29,4 +27,8 @@ test('y'); ?> --EXPECT-- string(3) "foo" -Cannot use temporary expression in write context +string(8) "modified" +array(1) { + [0]=> + string(3) "foo" +} diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6734db09a2e9..a8b23b132ba8 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11309,7 +11309,7 @@ static void zend_compile_array(znode *result, zend_ast *ast) /* {{{ */ } /* }}} */ -static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ +static zend_op *zend_compile_const_inner(znode *result, const zend_ast *ast, bool use_tmp) /* {{{ */ { zend_ast *name_ast = ast->child[0]; @@ -11333,17 +11333,21 @@ static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ result->op_type = IS_CONST; ZVAL_LONG(&result->u.constant, Z_LVAL_P(zend_ast_get_zval(last->child[0]))); zend_string_release_ex(resolved_name, 0); - return; + return NULL; } } if (zend_try_ct_eval_const(&result->u.constant, resolved_name, is_fully_qualified)) { result->op_type = IS_CONST; zend_string_release_ex(resolved_name, 0); - return; + return NULL; } - opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL); + if (use_tmp) { + opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL); + } else { + opline = zend_emit_op(result, ZEND_FETCH_CONSTANT, NULL, NULL); + } opline->op2_type = IS_CONST; if (is_fully_qualified || !FC(current_namespace)) { @@ -11356,6 +11360,13 @@ static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ resolved_name, true); } opline->extended_value = zend_alloc_cache_slot(); + return opline; +} +/* }}} */ + +static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ +{ + zend_compile_const_inner(result, ast, true); } /* }}} */ @@ -12314,6 +12325,8 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty case ZEND_AST_ASSIGN: zend_compile_assign(result, ast, false, type); return NULL; + case ZEND_AST_CONST: + return zend_compile_const_inner(result, ast, false); default: if (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) { zend_error_noreturn(E_COMPILE_ERROR, From 359acf6213d8f04e648b7a2f157ddbc927fdcda2 Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Wed, 14 Jan 2026 03:27:10 +0400 Subject: [PATCH 2/3] feat: Address review feedback for GH-10497 Pass type parameter to zend_compile_const() instead of bool use_tmp Check BP_VAR_R/BP_VAR_IS for read mode Remove unnecessary wrapper function Return void instead of zend_op* --- Zend/zend_compile.c | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index a8b23b132ba8..3530042bacd5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11309,7 +11309,7 @@ static void zend_compile_array(znode *result, zend_ast *ast) /* {{{ */ } /* }}} */ -static zend_op *zend_compile_const_inner(znode *result, const zend_ast *ast, bool use_tmp) /* {{{ */ +static void zend_compile_const(znode *result, const zend_ast *ast, uint32_t type) /* {{{ */ { zend_ast *name_ast = ast->child[0]; @@ -11333,17 +11333,17 @@ static zend_op *zend_compile_const_inner(znode *result, const zend_ast *ast, boo result->op_type = IS_CONST; ZVAL_LONG(&result->u.constant, Z_LVAL_P(zend_ast_get_zval(last->child[0]))); zend_string_release_ex(resolved_name, 0); - return NULL; + return; } } if (zend_try_ct_eval_const(&result->u.constant, resolved_name, is_fully_qualified)) { result->op_type = IS_CONST; zend_string_release_ex(resolved_name, 0); - return NULL; + return; } - if (use_tmp) { + if (type == BP_VAR_R || type == BP_VAR_IS) { opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL); } else { opline = zend_emit_op(result, ZEND_FETCH_CONSTANT, NULL, NULL); @@ -11360,13 +11360,6 @@ static zend_op *zend_compile_const_inner(znode *result, const zend_ast *ast, boo resolved_name, true); } opline->extended_value = zend_alloc_cache_slot(); - return opline; -} -/* }}} */ - -static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ -{ - zend_compile_const_inner(result, ast, true); } /* }}} */ @@ -12229,7 +12222,7 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ zend_compile_array(result, ast); return; case ZEND_AST_CONST: - zend_compile_const(result, ast); + zend_compile_const(result, ast, BP_VAR_R); return; case ZEND_AST_CLASS_CONST: zend_compile_class_const(result, ast); @@ -12326,7 +12319,8 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty zend_compile_assign(result, ast, false, type); return NULL; case ZEND_AST_CONST: - return zend_compile_const_inner(result, ast, false); + zend_compile_const(result, ast, type); + return NULL; default: if (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) { zend_error_noreturn(E_COMPILE_ERROR, From b0a2437dddde5cf19301053120756398b58797cd Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Sat, 4 Apr 2026 01:50:22 +0200 Subject: [PATCH 3/3] Rebase --- UPGRADING | 5 ++ Zend/tests/gh10497.phpt | 82 ++++++++++++++-------- Zend/tests/gh10497_func_arg.phpt | 19 +++++ Zend/tests/gh10497_guardrails.phpt | 9 +++ Zend/tests/gh10497_guardrails_dim_obj.phpt | 9 +++ Zend/tests/gh10497_guardrails_rebind.phpt | 9 +++ Zend/tests/gh12102_3.phpt | 14 ++-- Zend/zend_compile.c | 35 ++++++--- 8 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 Zend/tests/gh10497_func_arg.phpt create mode 100644 Zend/tests/gh10497_guardrails.phpt create mode 100644 Zend/tests/gh10497_guardrails_dim_obj.phpt create mode 100644 Zend/tests/gh10497_guardrails_rebind.phpt diff --git a/UPGRADING b/UPGRADING index 467387a9ea3c..1cdf812b62cf 100644 --- a/UPGRADING +++ b/UPGRADING @@ -55,6 +55,11 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . It is now possible to write to object properties on objects stored in + constants. Previously this resulted in "Cannot use temporary expression + in write context". This change is limited to object property access + (->); dim/array writes on constants (e.g. CONST[0] = value) remain + a compile error. RFC: https://wiki.php.net/rfc/const_object_property_write . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. diff --git a/Zend/tests/gh10497.phpt b/Zend/tests/gh10497.phpt index 3ac7ad027af2..7cbb308cb846 100644 --- a/Zend/tests/gh10497.phpt +++ b/Zend/tests/gh10497.phpt @@ -1,39 +1,61 @@ --TEST-- -GH-10497: Allow direct modification of object stored in a constant +GH-10497: Allow direct modification of object properties on constants --FILE-- b = 42; -var_dump(a->b); - -const obj = new stdClass; -obj->foo = 'bar'; -obj->baz = 123; -var_dump(obj->foo, obj->baz); - -const nested = new stdClass; -nested->inner = new stdClass; -nested->inner->value = 999; -var_dump(nested->inner->value); - -const readTest = new stdClass; -readTest->prop = 'test'; -echo readTest->prop . "\n"; - -var_dump(isset(readTest->prop)); -var_dump(empty(readTest->missing)); - -const modTest = new stdClass; -modTest->val = 1; -modTest->val = 2; -var_dump(modTest->val); + +const OBJ = new stdClass; +OBJ->prop = 123; +var_dump(OBJ->prop); + +OBJ->foo = 'bar'; +OBJ->baz = 456; +var_dump(OBJ->foo, OBJ->baz); + +OBJ->prop = 'overwritten'; +var_dump(OBJ->prop); + +OBJ->inner = new stdClass; +OBJ->inner->value = 999; +var_dump(OBJ->inner->value); + +OBJ->counter = 0; +OBJ->counter++; +OBJ->counter++; +OBJ->counter--; +var_dump(OBJ->counter); + +OBJ->str = 'hello'; +OBJ->str .= ' world'; +var_dump(OBJ->str); + +OBJ->temp = 'remove me'; +var_dump(isset(OBJ->temp)); +unset(OBJ->temp); +var_dump(isset(OBJ->temp)); + +var_dump(isset(OBJ->foo)); +var_dump(empty(OBJ->foo)); +var_dump(isset(OBJ->nonexistent)); +var_dump(empty(OBJ->nonexistent)); + +function incr(&$v) { $v++; } +OBJ->reftest = 10; +incr(OBJ->reftest); +var_dump(OBJ->reftest); + ?> --EXPECT-- -int(42) -string(3) "bar" int(123) +string(3) "bar" +int(456) +string(11) "overwritten" int(999) -test +int(1) +string(11) "hello world" +bool(true) +bool(false) bool(true) +bool(false) +bool(false) bool(true) -int(2) +int(11) diff --git a/Zend/tests/gh10497_func_arg.phpt b/Zend/tests/gh10497_func_arg.phpt new file mode 100644 index 000000000000..047cf4cfe45f --- /dev/null +++ b/Zend/tests/gh10497_func_arg.phpt @@ -0,0 +1,19 @@ +--TEST-- +GH-10497: Passing constant object property by reference via FUNC_ARG +--FILE-- +val = 10; +modify(OBJ->val); +var_dump(OBJ->val); + +function modify(&$v) { + $v = 42; +} + +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/gh10497_guardrails.phpt b/Zend/tests/gh10497_guardrails.phpt new file mode 100644 index 000000000000..a04b563b15e7 --- /dev/null +++ b/Zend/tests/gh10497_guardrails.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - array dim write on constant still fails +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use temporary expression in write context in %s on line %d diff --git a/Zend/tests/gh10497_guardrails_dim_obj.phpt b/Zend/tests/gh10497_guardrails_dim_obj.phpt new file mode 100644 index 000000000000..c6ff2f63d565 --- /dev/null +++ b/Zend/tests/gh10497_guardrails_dim_obj.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - dim write on constant object still fails +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use temporary expression in write context in %s on line %d diff --git a/Zend/tests/gh10497_guardrails_rebind.phpt b/Zend/tests/gh10497_guardrails_rebind.phpt new file mode 100644 index 000000000000..aa548b16d7d2 --- /dev/null +++ b/Zend/tests/gh10497_guardrails_rebind.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - constant rebinding still fails +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected token "=" in %s on line %d diff --git a/Zend/tests/gh12102_3.phpt b/Zend/tests/gh12102_3.phpt index c460fe7428a1..741bce5ab1ba 100644 --- a/Zend/tests/gh12102_3.phpt +++ b/Zend/tests/gh12102_3.phpt @@ -5,8 +5,11 @@ GH-12102: Incorrect "Cannot use temporary expression in write context" error for function test() { byVal(C[0]); - byRef(C[0]); - var_dump(C); + try { + byRef(C[0]); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } } /* Intentionally declared after test() to avoid compile-time checking of ref args. */ @@ -18,7 +21,6 @@ function byVal($arg) { } function byRef(&$arg) { - $arg = 'modified'; var_dump($arg); } @@ -27,8 +29,4 @@ test('y'); ?> --EXPECT-- string(3) "foo" -string(8) "modified" -array(1) { - [0]=> - string(3) "foo" -} +Cannot use temporary expression in write context diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3530042bacd5..b03c73804e2d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3179,6 +3179,28 @@ static zend_op *zend_delayed_compile_prop(znode *result, zend_ast *ast, uint32_t /* We will throw if $this doesn't exist, so there's no need to emit a JMP_NULL * check for a nullsafe access. */ + } else if (obj_ast->kind == ZEND_AST_CONST + && (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET + || type == BP_VAR_FUNC_ARG)) { + zend_ast *name_ast = obj_ast->child[0]; + bool is_fully_qualified; + zend_string *orig_name = zend_ast_get_str(name_ast); + zend_string *resolved_name = zend_resolve_const_name( + orig_name, name_ast->attr, &is_fully_qualified); + + opline = zend_emit_op(&obj_node, ZEND_FETCH_CONSTANT, NULL, NULL); + opline->op2_type = IS_CONST; + + if (is_fully_qualified || !FC(current_namespace)) { + opline->op1.num = 0; + opline->op2.constant = zend_add_const_name_literal( + resolved_name, false); + } else { + opline->op1.num = IS_CONSTANT_UNQUALIFIED_IN_NAMESPACE; + opline->op2.constant = zend_add_const_name_literal( + resolved_name, true); + } + opline->extended_value = zend_alloc_cache_slot(); } else { zend_short_circuiting_mark_inner(obj_ast); opline = zend_delayed_compile_var(&obj_node, obj_ast, type, false); @@ -11309,7 +11331,7 @@ static void zend_compile_array(znode *result, zend_ast *ast) /* {{{ */ } /* }}} */ -static void zend_compile_const(znode *result, const zend_ast *ast, uint32_t type) /* {{{ */ +static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */ { zend_ast *name_ast = ast->child[0]; @@ -11343,11 +11365,7 @@ static void zend_compile_const(znode *result, const zend_ast *ast, uint32_t type return; } - if (type == BP_VAR_R || type == BP_VAR_IS) { - opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL); - } else { - opline = zend_emit_op(result, ZEND_FETCH_CONSTANT, NULL, NULL); - } + opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL); opline->op2_type = IS_CONST; if (is_fully_qualified || !FC(current_namespace)) { @@ -12222,7 +12240,7 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ zend_compile_array(result, ast); return; case ZEND_AST_CONST: - zend_compile_const(result, ast, BP_VAR_R); + zend_compile_const(result, ast); return; case ZEND_AST_CLASS_CONST: zend_compile_class_const(result, ast); @@ -12318,9 +12336,6 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty case ZEND_AST_ASSIGN: zend_compile_assign(result, ast, false, type); return NULL; - case ZEND_AST_CONST: - zend_compile_const(result, ast, type); - return NULL; default: if (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) { zend_error_noreturn(E_COMPILE_ERROR,