Commit 087a08d9701 for php.net

commit 087a08d970109a80752f0114e242a76d15e36705
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Jun 20 21:47:27 2026 -0400

    Fix session save-handler argv leak on recursive rejection

    ps_call_handler() returned on the recursive-call rejection branch before
    reaching the argv cleanup loop, leaking one ref per owned argument. The
    read/write/destroy/validate_sid/update_timestamp callers copy the session
    id and data into argv and rely on ps_call_handler() to release them, so a
    handler that re-enters session machinery (for example calling
    session_destroy() from within a write handler) leaks those strings. Fold
    the handler call into an else branch so the cleanup loop always runs.

    Closes GH-22382

diff --git a/ext/session/mod_user.c b/ext/session/mod_user.c
index 78fb6260540..71b18612683 100644
--- a/ext/session/mod_user.c
+++ b/ext/session/mod_user.c
@@ -30,16 +30,16 @@ static void ps_call_handler(zval *func, int argc, zval *argv, zval *retval)
 		PS(in_save_handler) = 0;
 		ZVAL_UNDEF(retval);
 		php_error_docref(NULL, E_WARNING, "Cannot call session save handler in a recursive manner");
-		return;
-	}
-	PS(in_save_handler) = 1;
-	if (call_user_function(NULL, NULL, func, retval, argc, argv) == FAILURE) {
-		zval_ptr_dtor(retval);
-		ZVAL_UNDEF(retval);
-	} else if (Z_ISUNDEF_P(retval)) {
-		ZVAL_NULL(retval);
+	} else {
+		PS(in_save_handler) = 1;
+		if (call_user_function(NULL, NULL, func, retval, argc, argv) == FAILURE) {
+			zval_ptr_dtor(retval);
+			ZVAL_UNDEF(retval);
+		} else if (Z_ISUNDEF_P(retval)) {
+			ZVAL_NULL(retval);
+		}
+		PS(in_save_handler) = 0;
 	}
-	PS(in_save_handler) = 0;
 	for (i = 0; i < argc; i++) {
 		zval_ptr_dtor(&argv[i]);
 	}
diff --git a/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt b/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt
new file mode 100644
index 00000000000..2b954494e87
--- /dev/null
+++ b/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt
@@ -0,0 +1,31 @@
+--TEST--
+ps_call_handler() releases argv when a recursive save-handler call is rejected
+--INI--
+session.save_path=
+session.name=PHPSESSID
+--EXTENSIONS--
+session
+--FILE--
+<?php
+class H extends SessionHandler {
+    public bool $tripped = false;
+    public function write($id, $data): bool {
+        if (!$this->tripped) {
+            $this->tripped = true;
+            session_destroy();
+        }
+        return true;
+    }
+}
+
+session_set_save_handler(new H, true);
+session_start();
+$_SESSION['x'] = 1;
+session_write_close();
+echo "done\n";
+?>
+--EXPECTF--
+Warning: session_destroy(): Cannot call session save handler in a recursive manner in %s on line %d
+
+Warning: session_destroy(): Session object destruction failed in %s on line %d
+done