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)