[PECL-CVS] [pecl-networking-ssh2] fix-channel-close-timeout: add timeout to close path

[email protected] (Rasmus Lerdorf) Sat, 11 Apr 2026 03:02:51 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-10T23:02:31-04:00

Commit: https://github.com/php/pecl-networking-ssh2/commit/e414d876beb6134a3b17217992458d8484dd9c63
Raw diff: https://github.com/php/pecl-networking-ssh2/commit/e414d876beb6134a3b17217992458d8484dd9c63.diff

add timeout to close path

Changed paths:
  A  tests/ssh2_close_timeout.phpt
  M  ssh2_fopen_wrappers.c


Diff:

diff --git a/ssh2_fopen_wrappers.c b/ssh2_fopen_wrappers.c
index f1b3ce1..c654893 100644
--- a/ssh2_fopen_wrappers.c
+++ b/ssh2_fopen_wrappers.c
@@ -141,8 +141,31 @@ static int php_ssh2_channel_stream_close(php_stream *stream, int close_handle)
 		if (abstract->refcount) {
 			efree(abstract->refcount);
 		}
+#ifdef PHP_SSH2_SESSION_TIMEOUT
+		/* Apply session timeout during channel close to prevent blocking
+		 * indefinitely when the remote command hasn't finished or the
+		 * connection has been lost. libssh2_channel_free() calls
+		 * libssh2_channel_close() internally which blocks waiting for
+		 * SSH_MSG_CHANNEL_CLOSE from the remote end. */
+		if (abstract->timeout > 0) {
+			LIBSSH2_SESSION *session;
+			session = (LIBSSH2_SESSION *)zend_fetch_resource(abstract->session_rsrc, PHP_SSH2_SESSION_RES_NAME, le_ssh2_session);
+			if (session) {
+				libssh2_session_set_timeout(session, abstract->timeout);
+			}
+		}
+#endif
 		libssh2_channel_eof(abstract->channel);
 		libssh2_channel_free(abstract->channel);
+#ifdef PHP_SSH2_SESSION_TIMEOUT
+		if (abstract->timeout > 0) {
+			LIBSSH2_SESSION *session;
+			session = (LIBSSH2_SESSION *)zend_fetch_resource(abstract->session_rsrc, PHP_SSH2_SESSION_RES_NAME, le_ssh2_session);
+			if (session) {
+				libssh2_session_set_timeout(session, 0);
+			}
+		}
+#endif
 		zend_list_delete(abstract->session_rsrc);
 	}
 	efree(abstract);
diff --git a/tests/ssh2_close_timeout.phpt b/tests/ssh2_close_timeout.phpt
new file mode 100644
index 0000000..e8079cb
--- /dev/null
+++ b/tests/ssh2_close_timeout.phpt
@@ -0,0 +1,34 @@
+--TEST--
+ssh2_exec() channel close respects stream timeout
+--SKIPIF--
+<?php
+require('ssh2_skip.inc');
+if (!function_exists('ssh2_set_timeout')) {
+    die("skip ssh2_set_timeout not available");
+}
+?>
+--FILE--
+<?php require('ssh2_test.inc');
+
+$ssh = ssh2_connect(TEST_SSH2_HOSTNAME, TEST_SSH2_PORT);
+ssh2_auth_pubkey_file($ssh, TEST_SSH2_USER, TEST_SSH2_PUB_KEY, TEST_SSH2_PRIV_KEY);
+
+$start = microtime(true);
+
+/* Run a command that takes much longer than our timeout */
+$stream = ssh2_exec($ssh, 'sleep 60');
+stream_set_timeout($stream, 3);
+
+/* Without the fix, fclose() blocks for 60s waiting for
+ * SSH_MSG_CHANNEL_CLOSE. With the fix, it respects the
+ * stream timeout and returns in ~3s. */
+fclose($stream);
+
+$elapsed = microtime(true) - $start;
+
+/* Should complete well under 10s. Without the fix this would take 60s. */
+var_dump($elapsed < 10);
+printf("Completed in %.1fs\n", $elapsed);
+--EXPECTF--
+bool(true)
+Completed in %fs