From dac5bcbda108284a68e255a9372b313ce7ff00b9 Mon Sep 17 00:00:00 2001 From: Jakub Kozub Date: Wed, 21 Oct 2026 13:26:33 +0200 Subject: [PATCH 1/2] ext/ftp: clear ftp->nb when ftp_nb_get() bails out ftp_nb_get() sets ftp->nb = 1 only on success, but its bail: path left the flag set. Whether that path is taken is decided by the server: the guard at the top of the function aborts a pending transfer and then requires response 226 or 250. After such a bail, PHP_FUNCTION(ftp_nb_get) has already set ftp->stream to NULL and closed the stream it opened, while ftp->closestream stays true. ftp_nb_continue() then passed its !ftp->nb gate, reached if (ret != PHP_FTP_MOREDATA && ftp->closestream) { php_stream_close(ftp->stream); and dereferenced the cleared pointer. ftp_nb_continue_read()'s own bail: already clears the flag; this makes ftp_nb_get() consistent with it. Co-Authored-By: Claude Opus 5 --- ext/ftp/ftp.c | 1 + ext/ftp/tests/ftp_nb_get_rejected_abort.phpt | 39 ++++++++++++++++++++ ext/ftp/tests/server.inc | 9 +++++ 3 files changed, 49 insertions(+) create mode 100644 ext/ftp/tests/ftp_nb_get_rejected_abort.phpt diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index fd0f295500b8..e5ed836a186d 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -2239,6 +2239,7 @@ ftp_nb_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_t return (ftp_nb_continue_read(ftp)); bail: + ftp->nb = 0; data_close(ftp); return PHP_FTP_FAILED; } diff --git a/ext/ftp/tests/ftp_nb_get_rejected_abort.phpt b/ext/ftp/tests/ftp_nb_get_rejected_abort.phpt new file mode 100644 index 000000000000..8faa7f486fa0 --- /dev/null +++ b/ext/ftp/tests/ftp_nb_get_rejected_abort.phpt @@ -0,0 +1,39 @@ +--TEST-- +ftp_nb_get() must clear ftp->nb when aborting a pending transfer fails +--EXTENSIONS-- +ftp +pcntl +--FILE-- +nb was still + * set here, so this reached php_stream_close() on an already cleared + * ftp->stream and crashed. */ +var_dump(@ftp_nb_continue($ftp) === FTP_FAILED); + +ftp_close($ftp); +echo "closed\n"; +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +closed diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 2d9697e4d35d..cb1dad82cf18 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -404,6 +404,15 @@ if ($pid) { fputs($s, "226 Closing data Connection.\r\n"); break; + case "abort_rejected": + // Opens the data connection, then answers the transfer with + // something other than 226/250, so that a client aborting it + // from ftp_nb_get() takes the error path. + fputs($s, "150 File status okay; about to open data connection.\r\n"); + fputs($fs, "partial"); + fputs($s, "451 Requested action aborted: local error in processing.\r\n"); + break; + default: fputs($s, "550 {$matches[1]}: No such file or directory \r\n"); break; From 5617059a40c9188759f5945b7b6a6b2d9d391c83 Mon Sep 17 00:00:00 2001 From: Jakub Kozub Date: Wed, 21 Oct 2026 13:46:16 +0200 Subject: [PATCH 2/2] NEWS: clear ftp->nb when ftp_nb_get() bails out --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 48afd654b2ef..4ab0b10164d1 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 crash in ftp_nb_continue() after ftp_nb_get() failed to abort a + pending transfer. (Jakub Kozub) - MBString: . Fixed bug GH-23958 (truncated encoding names resolving to full names). (Kamil Tekiela)