[php-src] PHP-8.4: ext/standard: fix out-of-bounds access in the ftp:// directory stream

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-16T22:10:48-04:00

Commit: https://github.com/php/php-src/commit/b750803b766f1e2d8e5a480b2111e5500b1a63a2
Raw diff: https://github.com/php/php-src/commit/b750803b766f1e2d8e5a480b2111e5500b1a63a2.diff

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

Changed paths:
  A  ext/standard/tests/streams/ftp_dirstream_oob.phpt
  M  ext/ftp/tests/server.inc
  M  ext/standard/ftp_fopen_wrapper.c
  M  ext/standard/tests/streams/opendir-002.phpt
  M  ext/standard/tests/streams/opendir-004.phpt


Diff:

diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc
index 4c9d2a754bff..add8c3ae70ac 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 7b18e17f0d8f..fd7ecec02f19 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 000000000000..9e184f3c8ac6
--- /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 4978c8affa3d..0a5712564e2e 100644
--- a/ext/standard/tests/streams/opendir-002.phpt
+++ b/ext/standard/tests/streams/opendir-002.phpt
@@ -25,5 +25,5 @@ closedir($ds);
 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 9ccb86b4f4b4..3f076e0d8386 100644
--- a/ext/standard/tests/streams/opendir-004.phpt
+++ b/ext/standard/tests/streams/opendir-004.phpt
@@ -27,5 +27,5 @@ while ($fn=readdir($ds)) {
 resource(%d) of type (stream)
 string(5) "file1"
 string(5) "file1"
-string(3) "fil"
+string(4) "file"
 string(4) "b0rk"
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.