Commit 100938b40b0 for php.net

commit 100938b40b0346e0f7ded39ccaf11440f961d8fb
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sun Jun 14 12:06:23 2026 -0400

    Fix file descriptor leak when proc_open() descriptor setup fails

    When a descriptor spec entry fails to set up (unknown type, missing
    mode) after an earlier entry already opened a pipe or socket,
    proc_open() jumped to exit_fail without closing the descriptors it had
    already opened, leaking those fds; repeated calls exhaust the process
    descriptor table. Close the opened descriptors at exit_fail and drop
    the now-redundant per-call close before each spawn-failure goto.

    Closes GH-22311

diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c
index 278f7486e1a..d2d51de5a85 100644
--- a/ext/standard/proc_open.c
+++ b/ext/standard/proc_open.c
@@ -1371,7 +1371,6 @@ PHP_FUNCTION(proc_open)

 	if (newprocok == FALSE) {
 		DWORD dw = GetLastError();
-		close_all_descriptors(descriptors, ndesc);
 		char *msg = php_win32_error_to_msg(dw);
 		php_error_docref(NULL, E_WARNING, "CreateProcess failed: %s", msg);
 		php_win32_error_msg_free(msg);
@@ -1388,7 +1387,6 @@ PHP_FUNCTION(proc_open)

 	if (close_parentends_of_pipes(&factions, descriptors, ndesc) == FAILURE) {
 		posix_spawn_file_actions_destroy(&factions);
-		close_all_descriptors(descriptors, ndesc);
 		goto exit_fail;
 	}

@@ -1408,7 +1406,6 @@ PHP_FUNCTION(proc_open)
 	}
 	posix_spawn_file_actions_destroy(&factions);
 	if (r != 0) {
-		close_all_descriptors(descriptors, ndesc);
 		php_error_docref(NULL, E_WARNING, "posix_spawn() failed: %s", strerror(r));
 		goto exit_fail;
 	}
@@ -1450,7 +1447,6 @@ PHP_FUNCTION(proc_open)
 		_exit(127);
 	} else if (child < 0) {
 		/* Failed to fork() */
-		close_all_descriptors(descriptors, ndesc);
 		php_error_docref(NULL, E_WARNING, "Fork failed: %s", strerror(errno));
 		goto exit_fail;
 	}
@@ -1540,6 +1536,9 @@ PHP_FUNCTION(proc_open)
 	} else {
 exit_fail:
 		_php_free_envp(env);
+		if (descriptors) {
+			close_all_descriptors(descriptors, ndesc);
+		}
 		RETVAL_FALSE;
 	}

diff --git a/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt b/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt
new file mode 100644
index 00000000000..e072f75a82d
--- /dev/null
+++ b/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt
@@ -0,0 +1,20 @@
+--TEST--
+proc_open() does not leak file descriptors when descriptor setup fails mid-spec
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip proc_open() unavailable");
+if (!@is_dir("/proc/self/fd")) die("skip requires /proc/self/fd");
+?>
+--FILE--
+<?php
+$before = count(scandir("/proc/self/fd"));
+for ($i = 0; $i < 100; $i++) {
+    // Index 0 opens a real pipe; index 1 is invalid, so setup fails after the
+    // pipe is already open. The aborted call must release the pipe fds.
+    @proc_open("true", [0 => ["pipe", "r"], 1 => ["bogus_type"]], $pipes);
+}
+$after = count(scandir("/proc/self/fd"));
+var_dump($after <= $before + 2);
+?>
+--EXPECT--
+bool(true)