Commit 35fde02e424 for php.net
commit 35fde02e424faaddba2ebf27245e16697acaea4d
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Jun 15 20:34:20 2026 -0400
ext/ftp: fix out-of-bounds read in ftp_get() ASCII CRLF translation
In ASCII mode ftp_get() peeks at the byte after each '\r' to fold CRLF
into LF. When that '\r' is the last byte of a full FTP_BUFSIZE read, the
lookahead reads one byte past the buffer; a server placing '\r' at
offset 4095 of a 4096-byte read hits it (ASAN: heap-buffer-overflow
read of size 1). Bound the lookahead to the received data, as
ftp_readline() does. ftp_nb_continue_read() carries the trailing '\r'
across reads and isn't affected.
Closes GH-22328
diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c
index 36a0d1697ec..ca5e05ead81 100644
--- a/ext/ftp/ftp.c
+++ b/ext/ftp/ftp.c
@@ -946,7 +946,7 @@ ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_t pat
#else
while (e > ptr && (s = memchr(ptr, '\r', (e - ptr)))) {
php_stream_write(outstream, ptr, (s - ptr));
- if (*(s + 1) == '\n') {
+ if (s + 1 < e && *(s + 1) == '\n') {
s++;
php_stream_putc(outstream, '\n');
}
diff --git a/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt b/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt
new file mode 100644
index 00000000000..4c3a70b647c
--- /dev/null
+++ b/ext/ftp/tests/ftp_get_ascii_crlf_boundary.phpt
@@ -0,0 +1,25 @@
+--TEST--
+ftp_get() ASCII mode: CRLF straddling the FTP_BUFSIZE read boundary
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+$ftp = ftp_connect('127.0.0.1', $port);
+ftp_login($ftp, 'user', 'pass');
+$ftp or die("Couldn't connect to the server");
+
+$local = __DIR__ . "/crlf_boundary_out.txt";
+
+var_dump(ftp_get($ftp, $local, 'crlf_boundary', FTP_ASCII));
+var_dump(file_get_contents($local) === str_repeat("A", 4095) . "\n" . str_repeat("B", 10));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/crlf_boundary_out.txt");
+?>
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc
index c2c8449a068..4c9d2a754bf 100644
--- a/ext/ftp/tests/server.inc
+++ b/ext/ftp/tests/server.inc
@@ -391,6 +391,13 @@ if ($pid) {
// Just a side channel for getting the received file size.
fputs($s, "425 Can't open data connection (".$GLOBALS['rest_pos'].").\r\n");
break;
+ case "crlf_boundary":
+ // A CRLF whose CR lands on the final byte of the first
+ // FTP_BUFSIZE (4096) read, so the LF arrives in the next read.
+ fputs($s, "150 File status okay; about to open data connection.\r\n");
+ fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10));
+ fputs($s, "226 Closing data Connection.\r\n");
+ break;
default:
fputs($s, "550 {$matches[1]}: No such file or directory \r\n");