From 4355de0f91cb0a4abcaf14e6f77e1a32df3c1fff Mon Sep 17 00:00:00 2001 From: Jakub Kozub Date: Wed, 21 Oct 2026 13:24:51 +0200 Subject: [PATCH 1/2] ext/ftp: abort a pending transfer in ftp_nb_put() ftp_nb_get() aborts a pending non-blocking transfer before it starts a new one; ftp_nb_put() did not. ftp_getdata() assigns ftp->data unconditionally, so every ftp_nb_put()/ftp_nb_fput() issued while a transfer was still pending replaced the pointer to the previous databuf_t, leaking it together with its socket, and read the pending transfer's response as the answer to its own command. The guard was added to the GET path by c962a96c347 (GH-10562, "Memory leak and invalid state with consecutive ftp_nb_fget") and was never applied to the PUT path. It is missing there on every branch. Co-Authored-By: Claude Opus 5 --- ext/ftp/ftp.c | 8 ++++ .../tests/ftp_nb_put_during_nb_transfer.phpt | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 ext/ftp/tests/ftp_nb_put_during_nb_transfer.phpt diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index fd0f295500b8..7bfe4c69c26a 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -2329,6 +2329,14 @@ ftp_nb_put(ftpbuf_t *ftp, const char *path, const size_t path_len, php_stream *i php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use"); return PHP_FTP_FAILED; } + if (ftp->data != NULL) { + /* If there is a transfer in action, abort it. + * If we don't, we get an invalid state and memory leaks when the new connection gets opened. */ + data_close(ftp); + if (!ftp_getresp(ftp) || (ftp->resp != 226 && ftp->resp != 250)) { + goto bail; + } + } if (!ftp_type(ftp, type)) { goto bail; } diff --git a/ext/ftp/tests/ftp_nb_put_during_nb_transfer.phpt b/ext/ftp/tests/ftp_nb_put_during_nb_transfer.phpt new file mode 100644 index 000000000000..ca7968813fe5 --- /dev/null +++ b/ext/ftp/tests/ftp_nb_put_during_nb_transfer.phpt @@ -0,0 +1,42 @@ +--TEST-- +ftp_nb_put() must abort a pending non-blocking transfer, like ftp_nb_get() does +--EXTENSIONS-- +ftp +pcntl +--FILE-- +data while the previous + * databuf_t is still owned by it, and reads the pending transfer's response + * as the answer to its own command. */ +$ret = ftp_nb_put($ftp, basename(__FILE__), __FILE__, FTP_ASCII); +while ($ret == FTP_MOREDATA) { + $ret = ftp_nb_continue($ftp); +} +var_dump($ret === FTP_FINISHED); + +ftp_close($ftp); +echo "closed\n"; +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +closed From 81429a6e02017b93100f4b7f3f424b4fa5ab186d Mon Sep 17 00:00:00 2001 From: Jakub Kozub Date: Wed, 21 Oct 2026 13:46:14 +0200 Subject: [PATCH 2/2] NEWS: abort a pending transfer in ftp_nb_put() --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 48afd654b2ef..4ea6485c80e2 100644 --- a/NEWS +++ b/NEWS @@ -51,6 +51,8 @@ PHP NEWS - FTP: . Fix double free in ext/ftp data_accept() on accept failure. (alepacheco) + . Fixed ftp_nb_put() not aborting a pending non-blocking transfer, which + leaked its data connection. (Jakub Kozub) - MBString: . Fixed bug GH-23958 (truncated encoding names resolving to full names). (Kamil Tekiela)