Commit b4a63ecd709 for php
commit b4a63ecd709ba6452be84c2842b475d8384f82c6
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Tue Sep 29 10:33:58 2026 +0200
ext/standard: Don't share the unserialize() context with enum autoloaders
Unlike the O: branch, the E: branch looked the class up and evaluated the
case value without BG(serialize_lock). An unserialize() call made by the
autoloader then reused the outer context: the back-references of the outer
payload resolved to the values it created, and its __unserialize() calls
were delayed until the outer call returned.
Closes GH-23981.
diff --git a/NEWS b/NEWS
index 4d970852134..8771d8eae54 100644
--- a/NEWS
+++ b/NEWS
@@ -108,6 +108,8 @@ PHP NEWS
bcrypt costs. (Ilia Alshanetsky)
. Fixed proc_open() null descriptor not being inheritable on Windows.
(Jorg Sowa)
+ . Don't share the unserialize() context with enum autoloaders.
+ (Nicolas Grekas)
- Tidy:
. Fixed tidy_repair_string(), tidy_repair_file(), tidy::repairString() and
@@ -360,6 +362,8 @@ PHP NEWS
(Nicolas Grekas)
. Fixed convert.quoted-printable-decode decoding lowercase hex digits to
the wrong byte. (Ilia Alshanetsky)
+ . Fixed unserialize() of enums sharing its state with the unserialize()
+ calls made by autoloaders. (nicolas-grekas)
- Tidy:
. Fixed a use-after-free when a tidyNode is used after its document is
diff --git a/Zend/tests/enum/unserialize-autoload-nested.phpt b/Zend/tests/enum/unserialize-autoload-nested.phpt
new file mode 100644
index 00000000000..296f7af3141
--- /dev/null
+++ b/Zend/tests/enum/unserialize-autoload-nested.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Enum unserialize runs the autoloader outside of the current unserialize() context
+--FILE--
+<?php
+
+class Init {
+ public bool $ready = false;
+
+ public function __unserialize(array $data): void {
+ $this->ready = true;
+ }
+}
+
+enum Backed: string {
+ case Foo = Value::FOO;
+}
+
+spl_autoload_register(function ($class) {
+ var_dump(unserialize('O:4:"Init":0:{}')->ready);
+ eval(match ($class) {
+ 'Pure' => 'enum Pure { case Foo; }',
+ 'Value' => 'class Value { const FOO = "foo"; }',
+ });
+});
+
+foreach (['E:8:"Pure:Foo";', 'E:10:"Backed:Foo";'] as $enum) {
+ $data = unserialize('a:3:{i:0;' . $enum . 'i:1;O:8:"stdClass":0:{}i:2;r:3;}');
+ var_dump($data[1] === $data[2]);
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index 353c7086d43..aedc2518eb3 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -1384,7 +1384,9 @@ object ":" uiv ":" ["] {
goto fail;
}
+ BG(serialize_lock)++;
zend_class_entry *ce = zend_lookup_class(enum_name);
+ BG(serialize_lock)--;
if (!ce) {
php_error_docref(NULL, E_WARNING, "Class '%s' not found", ZSTR_VAL(enum_name));
goto fail;
@@ -1413,7 +1415,10 @@ object ":" uiv ":" ["] {
zval *value = &c->value;
if (Z_TYPE_P(value) == IS_CONSTANT_AST) {
- if (zval_update_constant_ex(value, c->ce) == FAILURE) {
+ BG(serialize_lock)++;
+ zend_result result = zval_update_constant_ex(value, c->ce);
+ BG(serialize_lock)--;
+ if (result == FAILURE) {
return 0;
}
}