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;
 		}
 	}