Commit 28b643531a5 for php

commit 28b643531a5c39be1dee636023657cd45a0ad5e4
Author: Julien Voisin <jvoisin@users.noreply.github.com>
Date:   Mon Nov 2 12:03:32 2026 +0100

    Test that serialization state is reset before request input processing (#24479)

diff --git a/Zend/tests/arginfo_zpp_mismatch.inc b/Zend/tests/arginfo_zpp_mismatch.inc
index 9c03e719c2a..695486d2313 100644
--- a/Zend/tests/arginfo_zpp_mismatch.inc
+++ b/Zend/tests/arginfo_zpp_mismatch.inc
@@ -10,6 +10,7 @@ function skipFunction($function): bool {
      || $function === 'exit'
      || $function === 'die'
      || $function === 'zend_trigger_bailout'
+     || $function === 'zend_test_serialization_bailout'
         /* intentionally violate invariants */
      || $function === 'zend_create_unterminated_string'
      || $function === 'zend_test_array_return'
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index 846fd36a403..17312fca566 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -21,6 +21,7 @@
 #include "php.h"
 #include "php_ini.h"
 #include "ext/standard/info.h"
+#include "ext/standard/php_var.h"
 #include "php_test.h"
 #include "observer.h"
 #include "fiber.h"
@@ -212,6 +213,38 @@ static ZEND_FUNCTION(zend_leak_bytes)
 	emalloc(leakbytes);
 }

+static ZEND_FUNCTION(zend_test_serialization_bailout)
+{
+	bool unserialize, locked;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_BOOL(unserialize)
+		Z_PARAM_BOOL(locked)
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (unserialize) {
+		(void) php_var_unserialize_init();
+	} else {
+		(void) php_var_serialize_init();
+	}
+	if (locked) {
+		BG(serialize_lock)++;
+	}
+	zend_bailout();
+}
+
+static ZEND_FUNCTION(zend_test_serialization_state)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	array_init(return_value);
+	add_assoc_long(return_value, "lock", BG(serialize_lock));
+	add_assoc_long(return_value, "serialize_level", BG(serialize).level);
+	add_assoc_bool(return_value, "serialize_data", BG(serialize).data != NULL);
+	add_assoc_long(return_value, "unserialize_level", BG(unserialize).level);
+	add_assoc_bool(return_value, "unserialize_data", BG(unserialize).data != NULL);
+}
+
 /* Reallocate a block of old_size bytes to new_size bytes and return its block
  * size, along with the block size of a fresh allocation of new_size bytes. */
 static ZEND_FUNCTION(zend_test_erealloc_block_size)
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 8719c6935a3..62cc78cbe24 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -303,6 +303,10 @@ function zend_delref(mixed $variable): void {}

     function zend_test_erealloc_block_size(int $old_size, int $new_size): array {}

+    function zend_test_serialization_bailout(bool $unserialize, bool $locked): never {}
+
+    function zend_test_serialization_state(): array {}
+
 	function zend_bool(bool $param): bool {}
 	function zend_bool_or_null(bool|null $param): bool|null {}
 	function zend_bool_slow_zpp(bool $param): bool {}
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index c2509001e45..4d7bced1402 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/test_decl.h b/ext/zend_test/test_decl.h
index 291f7ab87af..8fc579dd687 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index d6e1f758e20..f654203e2a6 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ
diff --git a/sapi/cli/tests/serialization_context_request_reset.phpt b/sapi/cli/tests/serialization_context_request_reset.phpt
new file mode 100644
index 00000000000..caaf086ab17
--- /dev/null
+++ b/sapi/cli/tests/serialization_context_request_reset.phpt
@@ -0,0 +1,172 @@
+--TEST--
+Serialization contexts abandoned by a bailout are reset before processing the next request
+--EXTENSIONS--
+session
+zend_test
+--INI--
+allow_url_fopen=1
+--ENV--
+PHP_CLI_SERVER_WORKERS=
+--SKIPIF--
+<?php
+include "skipif.inc";
+if (!function_exists("proc_open")) {
+    die("skip proc_open is required");
+}
+?>
+--FILE--
+<?php
+include "php_cli_server.inc";
+
+$sessionDir = __DIR__ . "/serialization_context_request_reset-sessions-" . getmypid();
+mkdir($sessionDir);
+register_shutdown_function(function () use ($sessionDir) {
+    $path = $sessionDir . "/sess_serialization-reset";
+    if (file_exists($path)) {
+        unlink($path);
+    }
+    rmdir($sessionDir);
+});
+$args = [
+    "-d", "session.upload_progress.enabled=1",
+    "-d", "session.upload_progress.cleanup=0",
+    "-d", "session.upload_progress.freq=0",
+    "-d", "session.upload_progress.min_freq=0",
+    "-d", "session.use_strict_mode=0",
+    "-d", "session.save_handler=files",
+    "-d", "session.save_path=$sessionDir",
+];
+foreach (["session", "zend_test"] as $extension) {
+    $path = ini_get("extension_dir") . DIRECTORY_SEPARATOR . $extension . "." . PHP_SHLIB_SUFFIX;
+    if (file_exists($path)) {
+        $args[] = "-d";
+        $args[] = "extension=$path";
+    }
+}
+$server = php_cli_server_start(<<<'PHP'
+session_id("serialization-reset");
+session_start();
+echo getmypid(), "\n";
+if (isset($_GET["bailout"])) {
+    $_SESSION = ["value" => ["nested" => 42]];
+    session_write_close();
+    echo "bailout\n";
+    zend_test_serialization_bailout((bool) $_GET["unserialize"], (bool) $_GET["locked"]);
+}
+var_dump(zend_test_serialization_state());
+var_dump($_SESSION["value"]);
+var_dump($_SESSION["upload_progress_test"]["done"]);
+var_dump($_FILES["file"]["error"]);
+session_destroy();
+PHP, cmd_args: $args);
+
+$body = "--boundary\r\n"
+    . "Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\r\n\r\n"
+    . "test\r\n"
+    . "--boundary\r\n"
+    . "Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n"
+    . "Content-Type: text/plain\r\n\r\n"
+    . "contents\r\n"
+    . "--boundary--\r\n";
+
+foreach ([0, 1] as $unserialize) {
+    foreach ([0, 1] as $locked) {
+        [$pid, $response] = explode("\n", file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS
+            . "/?bailout=1&unserialize=$unserialize&locked=$locked"), 2);
+        echo $response;
+        $context = stream_context_create(["http" => [
+            "method" => "POST",
+            "header" => "Content-Type: multipart/form-data; boundary=boundary\r\n"
+                . "Cookie: PHPSESSID=serialization-reset\r\n",
+            "content" => $body,
+        ]]);
+        $response = file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS, false, $context);
+        if ($response === false) {
+            echo file_get_contents($server->outputFile);
+            break 2;
+        }
+        [$nextPid, $response] = explode("\n", $response, 2);
+        if ($nextPid !== $pid) {
+            throw new RuntimeException("Requests must use the same worker");
+        }
+        echo $response;
+    }
+}
+?>
+--EXPECT--
+bailout
+array(5) {
+  ["lock"]=>
+  int(0)
+  ["serialize_level"]=>
+  int(0)
+  ["serialize_data"]=>
+  bool(false)
+  ["unserialize_level"]=>
+  int(0)
+  ["unserialize_data"]=>
+  bool(false)
+}
+array(1) {
+  ["nested"]=>
+  int(42)
+}
+bool(true)
+int(0)
+bailout
+array(5) {
+  ["lock"]=>
+  int(0)
+  ["serialize_level"]=>
+  int(0)
+  ["serialize_data"]=>
+  bool(false)
+  ["unserialize_level"]=>
+  int(0)
+  ["unserialize_data"]=>
+  bool(false)
+}
+array(1) {
+  ["nested"]=>
+  int(42)
+}
+bool(true)
+int(0)
+bailout
+array(5) {
+  ["lock"]=>
+  int(0)
+  ["serialize_level"]=>
+  int(0)
+  ["serialize_data"]=>
+  bool(false)
+  ["unserialize_level"]=>
+  int(0)
+  ["unserialize_data"]=>
+  bool(false)
+}
+array(1) {
+  ["nested"]=>
+  int(42)
+}
+bool(true)
+int(0)
+bailout
+array(5) {
+  ["lock"]=>
+  int(0)
+  ["serialize_level"]=>
+  int(0)
+  ["serialize_data"]=>
+  bool(false)
+  ["unserialize_level"]=>
+  int(0)
+  ["unserialize_data"]=>
+  bool(false)
+}
+array(1) {
+  ["nested"]=>
+  int(42)
+}
+bool(true)
+int(0)