Commit 0d780377e5b for php
commit 0d780377e5b031179c4744bb0ddc73addd253db4
Author: Alejandro Pacheco <alepacheco99@gmail.com>
Date: Thu Oct 15 04:37:24 2026 +0200
Fix double free in ext/ftp data_accept() on accept failure
When the server never opens the active-mode data connection, my_accept()
fails and data_accept() frees the databuf_t. ftp_getdata() has already
stored it in ftp->data, so the caller's bail path then runs data_close(),
which frees it a second time. This affects ftp_get(), ftp_put(),
ftp_append(), ftp_nb_get(), ftp_nb_put() and the listing functions.
data_accept() does not own the buffer: leave it in ftp->data and let
data_close() release it, as the SSL failure paths in data_accept()
already do.
Closes GH-24339.
diff --git a/NEWS b/NEWS
index c695f56584c..156aeb2533f 100644
--- a/NEWS
+++ b/NEWS
@@ -42,6 +42,9 @@ PHP NEWS
. Fixed bug GH-24289 (finfo::file() reports the wrong argument number for
an invalid $filename). (Lazizbek Ergashev)
+- FTP:
+ . Fix double free in ext/ftp data_accept() on accept failure. (alepacheco)
+
- MBString:
. Fixed bug GH-23958 (truncated encoding names resolving to full names). (Kamil Tekiela)
diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c
index 31ec7dc7343..fd0f295500b 100644
--- a/ext/ftp/ftp.c
+++ b/ext/ftp/ftp.c
@@ -1874,7 +1874,7 @@ data_accept(databuf_t *data, ftpbuf_t *ftp)
data->listener = -1;
if (data->fd == -1) {
- efree(data);
+ /* data is still owned by ftp->data, the caller's data_close() frees it */
return NULL;
}
diff --git a/ext/ftp/tests/ftp_get_data_accept_timeout.phpt b/ext/ftp/tests/ftp_get_data_accept_timeout.phpt
new file mode 100644
index 00000000000..bf0ece9a3fa
--- /dev/null
+++ b/ext/ftp/tests/ftp_get_data_accept_timeout.phpt
@@ -0,0 +1,39 @@
+--TEST--
+ftp_get()/ftp_nb_get() fail cleanly when the active data connection is never opened
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+$ftp = ftp_connect('127.0.0.1', $port, 1);
+if (!$ftp) die("Couldn't connect to the server");
+
+var_dump(ftp_login($ftp, 'anonymous', 'IEUser@'));
+var_dump(ftp_pasv($ftp, false));
+
+$local_file = __DIR__ . DIRECTORY_SEPARATOR . "ftp_get_data_accept_timeout.txt";
+
+var_dump(ftp_get($ftp, $local_file, 'no_data_connection', FTP_BINARY));
+var_dump(ftp_nb_get($ftp, $local_file, 'no_data_connection', FTP_BINARY) === FTP_FAILED);
+
+ftp_close($ftp);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . "ftp_get_data_accept_timeout.txt");
+?>
+--EXPECTF--
+bool(true)
+bool(true)
+
+Warning: ftp_get(): %s timed out in %s on line %d
+
+Warning: ftp_get(): File status okay; about to open data connection. in %s on line %d
+bool(false)
+
+Warning: ftp_nb_get(): %s timed out in %s on line %d
+
+Warning: ftp_nb_get(): File status okay; about to open data connection. in %s on line %d
+bool(true)
diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc
index add8c3ae70a..2d9697e4d35 100644
--- a/ext/ftp/tests/server.inc
+++ b/ext/ftp/tests/server.inc
@@ -328,6 +328,11 @@ if ($pid) {
break;
}
}elseif (preg_match('/^RETR ([\/]*[\w\h]+)/', $buf, $matches)) {
+ if ($matches[1] === "no_data_connection") {
+ // Never open the active data connection.
+ fputs($s, "150 File status okay; about to open data connection.\r\n");
+ continue;
+ }
if(!empty($pasv)){
;
}