[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.