[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
Ilia Alshanetsky <[email protected]>
| Newsgroups | gmane.comp.php.cvs.general |
|---|---|
| Message-ID | <[email protected]> |
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-12T21:07:01-04:00
Commit: https://github.com/php/php-src/commit/765fe90b594b2b9fcbf55957b81a6565480c2e25
Raw diff: https://github.com/php/php-src/commit/765fe90b594b2b9fcbf55957b81a6565480c2e25.diff
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
Fix use-after-free on re-entrant ftp_close() during a transfer
Changed paths:
A ext/ftp/tests/ftp_close_during_transfer.phpt
A ext/ftp/tests/ftp_nb_close_during_transfer.phpt
A ext/ftp/tests/ftp_nb_get_during_nb_transfer.phpt
A ext/ftp/tests/ftp_nb_get_during_transfer.phpt
M ext/ftp/ftp.c
M ext/ftp/ftp.h
M ext/ftp/php_ftp.c
Diff:
diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c
index 5fcd41536ec9..88f7431cc32f 100644
--- a/ext/ftp/ftp.c
+++ b/ext/ftp/ftp.c
@@ -819,6 +819,11 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_
if (ftp == NULL) {
return false;
}
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return false;
+ }
+ ftp->in_use = true;
if (!ftp_type(ftp, type)) {
goto bail;
}
@@ -894,9 +899,11 @@ bool ftp_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const size_
goto bail;
}
+ ftp->in_use = false;
return true;
bail:
data_close(ftp);
+ ftp->in_use = false;
return false;
}
@@ -981,6 +988,11 @@ bool ftp_put(ftpbuf_t *ftp, const char *path, const size_t path_len, php_stream
if (ftp == NULL) {
return false;
}
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return false;
+ }
+ ftp->in_use = true;
if (!ftp_type(ftp, type)) {
goto bail;
}
@@ -1021,9 +1033,11 @@ bool ftp_put(ftpbuf_t *ftp, const char *path, const size_t path_len, php_stream
if (!ftp_getresp(ftp) || (ftp->resp != 226 && ftp->resp != 250 && ftp->resp != 200)) {
goto bail;
}
+ ftp->in_use = false;
return true;
bail:
data_close(ftp);
+ ftp->in_use = false;
return false;
}
@@ -1034,6 +1048,11 @@ bool ftp_append(ftpbuf_t *ftp, const char *path, const size_t path_len, php_stre
if (ftp == NULL) {
return false;
}
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return false;
+ }
+ ftp->in_use = true;
if (!ftp_type(ftp, type)) {
goto bail;
}
@@ -1061,9 +1080,11 @@ bool ftp_append(ftpbuf_t *ftp, const char *path, const size_t path_len, php_stre
if (!ftp_getresp(ftp) || (ftp->resp != 226 && ftp->resp != 250 && ftp->resp != 200)) {
goto bail;
}
+ ftp->in_use = false;
return true;
bail:
data_close(ftp);
+ ftp->in_use = false;
return false;
}
@@ -1920,6 +1941,10 @@ static char** ftp_genlist(ftpbuf_t *ftp, const char *cmd, const size_t cmd_len,
char **entry;
char *text;
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return NULL;
+ }
if ((tmpstream = php_stream_fopen_tmpfile()) == NULL) {
php_error_docref(NULL, E_WARNING, "Unable to create temporary file. Check permissions in temporary files directory.");
@@ -2018,6 +2043,11 @@ int ftp_nb_get(ftpbuf_t *ftp, php_stream *outstream, const char *path, const siz
return PHP_FTP_FAILED;
}
+ if (ftp->in_use) {
+ 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. */
@@ -2082,11 +2112,17 @@ int ftp_nb_continue_read(ftpbuf_t *ftp)
data = ftp->data;
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return PHP_FTP_FAILED;
+ }
+
/* check if there is already more data */
if (!data_available(ftp, data->fd, false)) {
return PHP_FTP_MOREDATA;
}
+ ftp->in_use = true;
type = ftp->type;
lastch = ftp->lastch;
@@ -2110,6 +2146,7 @@ int ftp_nb_continue_read(ftpbuf_t *ftp)
}
ftp->lastch = lastch;
+ ftp->in_use = false;
return PHP_FTP_MOREDATA;
}
@@ -2124,9 +2161,11 @@ int ftp_nb_continue_read(ftpbuf_t *ftp)
}
ftp->nb = 0;
+ ftp->in_use = false;
return PHP_FTP_FINISHED;
bail:
ftp->nb = 0;
+ ftp->in_use = false;
data_close(ftp);
return PHP_FTP_FAILED;
}
@@ -2139,6 +2178,10 @@ int ftp_nb_put(ftpbuf_t *ftp, const char *path, const size_t path_len, php_strea
if (ftp == NULL) {
return 0;
}
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return PHP_FTP_FAILED;
+ }
if (!ftp_type(ftp, type)) {
goto bail;
}
@@ -2182,16 +2225,24 @@ int ftp_nb_put(ftpbuf_t *ftp, const char *path, const size_t path_len, php_strea
int ftp_nb_continue_write(ftpbuf_t *ftp)
{
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ return PHP_FTP_FAILED;
+ }
+
/* check if we can write more data */
if (!data_writeable(ftp, ftp->data->fd)) {
return PHP_FTP_MOREDATA;
}
+ ftp->in_use = true;
+
if (ftp_send_stream_to_data_socket(ftp, ftp->data, ftp->stream, ftp->type, true) != SUCCESS) {
goto bail;
}
if (!php_stream_eof(ftp->stream)) {
+ ftp->in_use = false;
return PHP_FTP_MOREDATA;
}
@@ -2201,9 +2252,11 @@ int ftp_nb_continue_write(ftpbuf_t *ftp)
goto bail;
}
ftp->nb = 0;
+ ftp->in_use = false;
return PHP_FTP_FINISHED;
bail:
data_close(ftp);
ftp->nb = 0;
+ ftp->in_use = false;
return PHP_FTP_FAILED;
}
diff --git a/ext/ftp/ftp.h b/ext/ftp/ftp.h
index 241f92f57ec3..149534c92a08 100644
--- a/ext/ftp/ftp.h
+++ b/ext/ftp/ftp.h
@@ -72,6 +72,7 @@ typedef struct ftpbuf
databuf_t *data; /* Data connection for "nonblocking" transfers */
php_stream *stream; /* output stream for "nonblocking" transfers */
bool nb; /* "nonblocking" transfer in progress */
+ bool in_use; /* engine transfer in progress; blocks re-entrant ftp_close */
char lastch; /* last char of previous call */
bool direction; /* recv = 0 / send = 1 */
bool closestream;/* close or not close stream */
diff --git a/ext/ftp/php_ftp.c b/ext/ftp/php_ftp.c
index f19cc4b4b6b7..c89c1f07f7c2 100644
--- a/ext/ftp/php_ftp.c
+++ b/ext/ftp/php_ftp.c
@@ -654,6 +654,11 @@ PHP_FUNCTION(ftp_nb_fget)
}
/* configuration */
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ RETURN_FALSE;
+ }
+
ftp->direction = 0; /* recv */
ftp->closestream = 0; /* do not close */
@@ -767,6 +772,10 @@ PHP_FUNCTION(ftp_nb_get)
RETURN_THROWS();
}
GET_FTPBUF(ftp, z_ftp);
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ RETURN_FALSE;
+ }
XTYPE(xtype, mode);
/* ignore autoresume if autoseek is switched off */
@@ -804,8 +813,8 @@ PHP_FUNCTION(ftp_nb_get)
ftp->closestream = true; /* do close */
if ((ret = ftp_nb_get(ftp, outstream, remote, remote_len, xtype, resumepos)) == PHP_FTP_FAILED) {
- php_stream_close(outstream);
ftp->stream = NULL;
+ php_stream_close(outstream);
VCWD_UNLINK(local);
if (*ftp->inbuf) {
php_error_docref(NULL, E_WARNING, "%s", ftp->inbuf);
@@ -814,8 +823,8 @@ PHP_FUNCTION(ftp_nb_get)
}
if (ret == PHP_FTP_FINISHED){
- php_stream_close(outstream);
ftp->stream = NULL;
+ php_stream_close(outstream);
}
RETURN_LONG(ret);
@@ -953,6 +962,11 @@ PHP_FUNCTION(ftp_nb_fput)
}
/* configuration */
+ if (ftp->in_use) {
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ RETURN_FALSE;
+ }
+
ftp->direction = true; /* send */
ftp->closestream = false; /* do not close */
@@ -1093,6 +1107,12 @@ PHP_FUNCTION(ftp_nb_put)
}
}
+ if (ftp->in_use) {
+ php_stream_close(instream);
+ php_error_docref(NULL, E_WARNING, "FTP\\Connection is already in use");
+ RETURN_FALSE;
+ }
+
/* configuration */
ftp->direction = true; /* send */
ftp->closestream = true; /* do close */
@@ -1100,8 +1120,8 @@ PHP_FUNCTION(ftp_nb_put)
ret = ftp_nb_put(ftp, remote, remote_len, instream, xtype, startpos);
if (ret != PHP_FTP_MOREDATA) {
- php_stream_close(instream);
ftp->stream = NULL;
+ php_stream_close(instream);
}
if (ret == PHP_FTP_FAILED) {
@@ -1236,6 +1256,10 @@ PHP_FUNCTION(ftp_close)
obj = ftp_object_from_zend_object(Z_OBJ_P(z_ftp));
if (obj->ftp) {
+ if (obj->ftp->in_use) {
+ zend_throw_error(NULL, "Cannot close FTP\\Connection while a transfer is in progress");
+ RETURN_THROWS();
+ }
success = ftp_quit(obj->ftp);
ftp_close(obj->ftp);
obj->ftp = NULL;
diff --git a/ext/ftp/tests/ftp_close_during_transfer.phpt b/ext/ftp/tests/ftp_close_during_transfer.phpt
new file mode 100644
index 000000000000..72e410603921
--- /dev/null
+++ b/ext/ftp/tests/ftp_close_during_transfer.phpt
@@ -0,0 +1,44 @@
+--TEST--
+ftp_close() from a stream wrapper during a transfer throws instead of freeing the connection
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+class CloseDuringWrite {
+ public $context;
+ public static $ftp;
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ return true;
+ }
+ public function stream_write($data) {
+ ftp_close(self::$ftp);
+ return strlen($data);
+ }
+ public function stream_close() {}
+ public function stream_eof() {
+ return true;
+ }
+}
+
+stream_wrapper_register('reentrant', CloseDuringWrite::class);
+
+$ftp = ftp_connect('127.0.0.1', $port);
+var_dump(ftp_login($ftp, 'user', 'pass'));
+CloseDuringWrite::$ftp = $ftp;
+
+try {
+ @ftp_get($ftp, 'reentrant://sink', 'a story.txt', FTP_BINARY);
+} catch (\Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+ftp_close($ftp);
+echo "closed\n";
+?>
+--EXPECT--
+bool(true)
+Cannot close FTP\Connection while a transfer is in progress
+closed
diff --git a/ext/ftp/tests/ftp_nb_close_during_transfer.phpt b/ext/ftp/tests/ftp_nb_close_during_transfer.phpt
new file mode 100644
index 000000000000..702648f69592
--- /dev/null
+++ b/ext/ftp/tests/ftp_nb_close_during_transfer.phpt
@@ -0,0 +1,44 @@
+--TEST--
+ftp_close() from a stream wrapper during a non-blocking transfer throws instead of freeing the connection
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+class CloseDuringNbWrite {
+ public $context;
+ public static $ftp;
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ return true;
+ }
+ public function stream_write($data) {
+ ftp_close(self::$ftp);
+ return strlen($data);
+ }
+ public function stream_close() {}
+ public function stream_eof() {
+ return true;
+ }
+}
+
+stream_wrapper_register('reentrantnb', CloseDuringNbWrite::class);
+
+$ftp = ftp_connect('127.0.0.1', $port);
+var_dump(ftp_login($ftp, 'user', 'pass'));
+CloseDuringNbWrite::$ftp = $ftp;
+
+try {
+ @ftp_nb_get($ftp, 'reentrantnb://sink', 'a story.txt', FTP_BINARY);
+} catch (\Error $e) {
+ echo $e->getMessage(), "\n";
+}
+
+ftp_close($ftp);
+echo "closed\n";
+?>
+--EXPECT--
+bool(true)
+Cannot close FTP\Connection while a transfer is in progress
+closed
diff --git a/ext/ftp/tests/ftp_nb_get_during_nb_transfer.phpt b/ext/ftp/tests/ftp_nb_get_during_nb_transfer.phpt
new file mode 100644
index 000000000000..deb1698c77c5
--- /dev/null
+++ b/ext/ftp/tests/ftp_nb_get_during_nb_transfer.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Re-entrant ftp_nb_get() from a stream wrapper during an active non-blocking ftp_nb_get() must not corrupt the outer transfer
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+class NbGetDuringNbGet {
+ public $context;
+ public static $ftp;
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ return true;
+ }
+ public function stream_write($data) {
+ @ftp_nb_get(self::$ftp, 'php://memory', 'a story.txt', FTP_BINARY);
+ return strlen($data);
+ }
+ public function stream_close() {}
+ public function stream_eof() {
+ return true;
+ }
+}
+
+stream_wrapper_register('reentrantnbget', NbGetDuringNbGet::class);
+
+$ftp = ftp_connect('127.0.0.1', $port);
+var_dump(ftp_login($ftp, 'user', 'pass'));
+NbGetDuringNbGet::$ftp = $ftp;
+
+$r = @ftp_nb_get($ftp, 'reentrantnbget://sink', 'a story.txt', FTP_BINARY);
+while ($r == FTP_MOREDATA) {
+ $r = @ftp_nb_continue($ftp);
+}
+var_dump($r === FTP_FINISHED);
+
+ftp_close($ftp);
+echo "closed\n";
+?>
+--EXPECT--
+bool(true)
+bool(true)
+closed
diff --git a/ext/ftp/tests/ftp_nb_get_during_transfer.phpt b/ext/ftp/tests/ftp_nb_get_during_transfer.phpt
new file mode 100644
index 000000000000..c7920496feb4
--- /dev/null
+++ b/ext/ftp/tests/ftp_nb_get_during_transfer.phpt
@@ -0,0 +1,40 @@
+--TEST--
+Re-entrant ftp_nb_get() from a stream wrapper during a blocking ftp_get() must not free the active data connection
+--EXTENSIONS--
+ftp
+pcntl
+--FILE--
+<?php
+require 'server.inc';
+
+class NbGetDuringGet {
+ public $context;
+ public static $ftp;
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ return true;
+ }
+ public function stream_write($data) {
+ @ftp_nb_get(self::$ftp, 'php://memory', 'a story.txt', FTP_BINARY);
+ return strlen($data);
+ }
+ public function stream_close() {}
+ public function stream_eof() {
+ return true;
+ }
+}
+
+stream_wrapper_register('reentrantget', NbGetDuringGet::class);
+
+$ftp = ftp_connect('127.0.0.1', $port);
+var_dump(ftp_login($ftp, 'user', 'pass'));
+NbGetDuringGet::$ftp = $ftp;
+
+var_dump(@ftp_get($ftp, 'reentrantget://sink', 'a story.txt', FTP_BINARY));
+
+ftp_close($ftp);
+echo "closed\n";
+?>
+--EXPECT--
+bool(true)
+bool(true)
+closed