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)){
                 ;
             }