Commit 81e0367dbc2 for php

commit 81e0367dbc23afb35b7f0457eaa4f7ba2c7c15cd
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Oct 3 09:54:34 2026 -0400

    ext/standard: Close owned proc_open descriptors on setup failure

    Use the descriptor type to identify owned parent endpoints during failed
    setup. Descriptor zero is valid for pipes and sockets and must be closed;
    standard descriptors own only their duplicated child endpoint.

    Closes GH-24098

diff --git a/NEWS b/NEWS
index dacb602662f..40393c15fb7 100644
--- a/NEWS
+++ b/NEWS
@@ -203,6 +203,8 @@ PHP                                                                        NEWS
     read failure. (Ilia Alshanetsky)
   . Fixed iptcembed() corrupting JPEG headers when called recursively from an
     output handler. (Ilia Alshanetsky)
+  . Fixed proc_open() leaking descriptor zero when descriptor setup fails.
+    (Ilia Alshanetsky)
   . Fixed three Windows-only proc_open() defects: an uninitialized
     PROCESS_INFORMATION, an indeterminate comspec pointer after a failed
     lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky)
diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c
index 20f093f2183..54a523a4291 100644
--- a/ext/standard/proc_open.c
+++ b/ext/standard/proc_open.c
@@ -1189,8 +1189,9 @@ static void close_all_descriptors(descriptorspec_item *descriptors, int ndesc)
 {
 	for (int i = 0; i < ndesc; i++) {
 		close_descriptor(descriptors[i].childend);
-		if (descriptors[i].parentend)
+		if (descriptors[i].type != DESCRIPTOR_TYPE_STD) {
 			close_descriptor(descriptors[i].parentend);
+		}
 	}
 }

diff --git a/ext/standard/tests/general_functions/proc_open_descriptor_0_setup_failure.phpt b/ext/standard/tests/general_functions/proc_open_descriptor_0_setup_failure.phpt
new file mode 100644
index 00000000000..ea654b0f497
--- /dev/null
+++ b/ext/standard/tests/general_functions/proc_open_descriptor_0_setup_failure.phpt
@@ -0,0 +1,29 @@
+--TEST--
+proc_open() closes descriptor 0 when setup fails after pipe allocation
+--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
+$code = <<<'PHP'
+fclose(STDIN);
+var_dump(@proc_open("true", [0 => ["pipe", "w"], 1 => ["bogus_type"]], $pipes));
+$fd = fopen("/dev/null", "r");
+var_dump(readlink("/proc/self/fd/0"));
+fclose($fd);
+PHP;
+$process = proc_open([PHP_BINARY, "-n", "-r", $code], [1 => ["pipe", "w"]], $pipes);
+echo stream_get_contents($pipes[1]);
+fclose($pipes[1]);
+var_dump(proc_close($process));
+?>
+--EXPECT--
+bool(false)
+string(9) "/dev/null"
+int(0)
diff --git a/ext/standard/tests/general_functions/proc_open_socket_descriptor_0_setup_failure.phpt b/ext/standard/tests/general_functions/proc_open_socket_descriptor_0_setup_failure.phpt
new file mode 100644
index 00000000000..88e4eee1b20
--- /dev/null
+++ b/ext/standard/tests/general_functions/proc_open_socket_descriptor_0_setup_failure.phpt
@@ -0,0 +1,29 @@
+--TEST--
+proc_open() closes descriptor 0 when setup fails after socket allocation
+--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
+$code = <<<'PHP'
+fclose(STDIN);
+var_dump(@proc_open("true", [0 => ["socket"], 1 => ["bogus_type"]], $pipes));
+$fd = fopen("/dev/null", "r");
+var_dump(readlink("/proc/self/fd/0"));
+fclose($fd);
+PHP;
+$process = proc_open([PHP_BINARY, "-n", "-r", $code], [1 => ["pipe", "w"]], $pipes);
+echo stream_get_contents($pipes[1]);
+fclose($pipes[1]);
+var_dump(proc_close($process));
+?>
+--EXPECT--
+bool(false)
+string(9) "/dev/null"
+int(0)