Commit b750803b766 for php.net

commit b750803b766f1e2d8e5a480b2111e5500b1a63a2
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Jul 16 08:01:05 2026 -0400

    ext/standard: fix out-of-bounds access in the ftp:// directory stream

    php_ftp_dirstream_read() sized the basename copy with
    MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1) and wrote the terminator at
    [tmp_len - 1]. An empty listing line leaves a basename of length 1, so
    tmp_len is 0 and the NUL lands one byte before d_name; a slash-only line
    leaves length 0, so the subtraction underflows to SIZE_MAX and memcpy() reads
    4096 bytes past the interned empty string. The same off-by-one truncated
    entry names that do not end in CRLF.

    Closes GH-22768

diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc
index 4c9d2a754bf..add8c3ae70a 100644
--- a/ext/ftp/tests/server.inc
+++ b/ext/ftp/tests/server.inc
@@ -290,7 +290,7 @@ if ($pid) {
             }

             if (empty($m[1]) || $m[1] !== 'emptydir') {
-                fputs($fs, "file1\r\nfile1\r\nfile\nb0rk\r\n");
+                fputs($fs, $nlst_data ?? "file1\r\nfile1\r\nfile\nb0rk\r\n");
             }

             fputs($s, "226 Closing data Connection.\r\n");
diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c
index 7b18e17f0d8..fd7ecec02f1 100644
--- a/ext/standard/ftp_fopen_wrapper.c
+++ b/ext/standard/ftp_fopen_wrapper.c
@@ -625,9 +625,9 @@ static ssize_t php_ftp_dirstream_read(php_stream *stream, char *buf, size_t coun

 	basename = php_basename(ent->d_name, tmp_len, NULL, 0);

-	tmp_len = MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1);
+	tmp_len = MIN(sizeof(ent->d_name) - 1, ZSTR_LEN(basename));
 	memcpy(ent->d_name, ZSTR_VAL(basename), tmp_len);
-	ent->d_name[tmp_len - 1] = '\0';
+	ent->d_name[tmp_len] = '\0';
 	zend_string_release_ex(basename, 0);
 	ent->d_type = DT_UNKNOWN;

diff --git a/ext/standard/tests/streams/ftp_dirstream_oob.phpt b/ext/standard/tests/streams/ftp_dirstream_oob.phpt
new file mode 100644
index 00000000000..9e184f3c8ac
--- /dev/null
+++ b/ext/standard/tests/streams/ftp_dirstream_oob.phpt
@@ -0,0 +1,33 @@
+--TEST--
+opendir() with 'ftp://' stream must not go out of bounds on an empty or slash-only listing line
+--SKIPIF--
+<?php
+if (array_search('ftp',stream_get_wrappers()) === FALSE) die("skip ftp wrapper not available.");
+if (!function_exists('pcntl_fork')) die("skip pcntl_fork() not available.");
+?>
+--FILE--
+<?php
+
+/* An empty line makes php_basename() return "\n" and a slash-only final line
+ * makes it return "", the two lengths the buffer math underflowed on. */
+$nlst_data = "file1\r\n\nb0rk\r\n/";
+
+require __DIR__ . "/../../../ftp/tests/server.inc";
+
+$path="ftp://localhost:" . $port."/";
+
+$ds=opendir($path);
+var_dump($ds);
+
+while (($fn=readdir($ds)) !== false) {
+      var_dump($fn);
+}
+
+closedir($ds);
+?>
+--EXPECTF--
+resource(%d) of type (stream)
+string(5) "file1"
+string(0) ""
+string(4) "b0rk"
+string(0) ""
diff --git a/ext/standard/tests/streams/opendir-002.phpt b/ext/standard/tests/streams/opendir-002.phpt
index 4978c8affa3..0a5712564e2 100644
--- a/ext/standard/tests/streams/opendir-002.phpt
+++ b/ext/standard/tests/streams/opendir-002.phpt
@@ -25,5 +25,5 @@
 resource(%d) of type (stream)
 string(5) "file1"
 string(5) "file1"
-string(3) "fil"
+string(4) "file"
 string(4) "b0rk"
diff --git a/ext/standard/tests/streams/opendir-004.phpt b/ext/standard/tests/streams/opendir-004.phpt
index 9ccb86b4f4b..3f076e0d838 100644
--- a/ext/standard/tests/streams/opendir-004.phpt
+++ b/ext/standard/tests/streams/opendir-004.phpt
@@ -27,5 +27,5 @@
 resource(%d) of type (stream)
 string(5) "file1"
 string(5) "file1"
-string(3) "fil"
+string(4) "file"
 string(4) "b0rk"