Skip to content

ext/ftp: clear ftp->nb when ftp_nb_get() bails out - #24478

Open
kubakozub wants to merge 2 commits into
php:PHP-8.4from
kubakozub:fix/ftp-nb-get-bail-clears-nb
Open

kubakozub wants to merge 2 commits into
php:PHP-8.4from
kubakozub:fix/ftp-nb-get-bail-clears-nb

Conversation

@kubakozub

Copy link
Copy Markdown

What

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 passes its
!ftp->nb gate and reaches

if (ret != PHP_FTP_MOREDATA && ftp->closestream) {
    php_stream_close(ftp->stream);

dereferencing the cleared pointer.

ftp_nb_continue_read()'s own bail: already clears the flag; this makes ftp_nb_get()
consistent with it.

Crash

nb_get(a) => FTP_MOREDATA
nb_get(b) => FTP_FAILED
now calling ftp_nb_continue()
Segmentation fault
_php_stream_free (...) at main/streams/streams.c:407
407     context = PHP_STREAM_CONTEXT(stream);
#1  zif_ftp_nb_continue (...) at ext/ftp/php_ftp.c:863

On a stripped build it faults earlier, in ftp_nb_continue_read() on data->fd
(mov 0x4(%r12),%esi with r12 == 0; offsetof(databuf_t, fd) is 4). Adding a NULL check
there alone only moves the crash to php_ftp.c:863 - I tried that first and it still exited
139 - so the flag is the right place to fix it.

Test

ext/ftp/tests/ftp_nb_get_rejected_abort.phpt, with a server.inc case that answers the
aborted transfer with 451. Without the change the test process dies at the ftp_nb_continue()
call; with it the call reports there is nothing to continue. Full ext/ftp suite: 62 passed,
0 failed, 4 skipped.

Note

php_ftp.c:862 could also take the ftp->stream && test that ftp.c:167 already uses, and
on this path the first transfer's stream is dropped without being closed. I left both alone to
keep the change minimal; happy to add either.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant