Commit 622f5802fb9 for php
commit 622f5802fb9e021d1b61682cb961c8298b5ca1fb
Author: lazerg <lazerg2@gmail.com>
Date: Sun Oct 25 07:50:57 2026 +0000
Fix GH-24668: zend_set_local_var() leaks the previous CV value (#24670)
When a name matches a compiled variable, zend_set_local_var() overwrites
its slot without releasing the previous refcounted value. The symbol table
path already releases the old value.
Save the old value, assign the new value, and only then destroy the old
one. Apply the same ordering to zend_set_local_var_str() so a destructor
that calls debug_backtrace() cannot observe a freed value in the CV.
Add a zend_test helper and regression test for value destruction and
reentrant backtraces. Disable JIT for the test because it changes a local
variable through the engine API.
Co-authored-by: yasu20200121web-prog <258152685+yasu20200121web-prog@users.noreply.github.com>
Closes #24670
diff --git a/NEWS b/NEWS
index e39ef7ac85a..f74b0b57f67 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,8 @@ PHP NEWS
for grouping. (Klaas Skelte van der Werf)
. Fixed bug GH-24265 (Unwind/GracefulExit discarded in
zend_fiber_execute()). (David Carlier)
+ . Fixed bug GH-24668 (zend_set_local_var() leaks the previous value of an
+ existing CV). (yasu20200121web-prog, lazerg)
. Fixed unmapping of an unrelated range when the aligned chunk mmap fails
(Marti2203).
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 30eb6ec8af3..01555e6165c 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -1960,7 +1960,10 @@ ZEND_API zend_result zend_set_local_var(zend_string *name, zval *value, bool for
if (ZSTR_H(*str) == h &&
zend_string_equal_content(*str, name)) {
zval *var = EX_VAR_NUM(str - op_array->vars);
+ zval garbage;
+ ZVAL_COPY_VALUE(&garbage, var);
ZVAL_COPY_VALUE(var, value);
+ zval_ptr_dtor(&garbage);
return SUCCESS;
}
str++;
@@ -2002,8 +2005,10 @@ ZEND_API zend_result zend_set_local_var_str(const char *name, size_t len, zval *
if (ZSTR_H(*str) == h &&
zend_string_equals_cstr(*str, name, len)) {
zval *var = EX_VAR_NUM(str - op_array->vars);
- zval_ptr_dtor(var);
+ zval garbage;
+ ZVAL_COPY_VALUE(&garbage, var);
ZVAL_COPY_VALUE(var, value);
+ zval_ptr_dtor(&garbage);
return SUCCESS;
}
str++;
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index 09940e664e1..d9d80bf6ccf 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -1056,6 +1056,24 @@ static ZEND_FUNCTION(zend_new_singleton_array)
array_init_singleton(return_value);
}
+static ZEND_FUNCTION(zend_test_set_local_var)
+{
+ zend_string *name;
+ zval *value;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(name)
+ Z_PARAM_ZVAL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ Z_TRY_ADDREF_P(value);
+ if (zend_set_local_var(name, value, false) == FAILURE) {
+ zval_ptr_dtor(value);
+ RETURN_FALSE;
+ }
+ RETURN_TRUE;
+}
+
static zend_object *zend_test_class_new(zend_class_entry *class_type)
{
zend_object *obj = zend_objects_new(class_type);
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 6ea169d315b..36d1be17a17 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -341,6 +341,8 @@ function zend_test_gh19792(): void {}
function zend_test_handle_post_data(mixed $stream, string $content_type, int $content_length): void {}
function zend_new_singleton_array(): array {}
+
+ function zend_test_set_local_var(string $name, mixed $value): bool {}
}
namespace ZendTestNS {
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index aa11ba1f77c..e2a388659ec 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/tests/gh24668.phpt b/ext/zend_test/tests/gh24668.phpt
new file mode 100644
index 00000000000..291632be334
--- /dev/null
+++ b/ext/zend_test/tests/gh24668.phpt
@@ -0,0 +1,33 @@
+--TEST--
+GH-24668 (zend_set_local_var() leaks the previous value of an existing CV)
+--EXTENSIONS--
+zend_test
+--INI--
+opcache.jit=0
+--FILE--
+<?php
+
+class Dtor {
+ public function __destruct() {
+ echo "dtor\n";
+ $GLOBALS['trace'] = debug_backtrace();
+ }
+}
+
+function test($a) {
+ var_dump(zend_test_set_local_var('a', 42));
+ var_dump($a);
+}
+
+test([new Dtor]);
+var_dump($trace[2]['args']);
+
+?>
+--EXPECT--
+dtor
+bool(true)
+int(42)
+array(1) {
+ [0]=>
+ int(42)
+}