File feature-fix-update-ssh-stack.patch of Package erlang26.42057
diff -ruN a/lib/ssh/doc/html/.gitignore b/lib/ssh/doc/html/.gitignore
--- a/lib/ssh/doc/html/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/doc/html/.gitignore 2025-12-22 23:16:56.257838884 +1030
@@ -0,0 +1 @@
+*
diff -ruN a/lib/ssh/doc/specs/.gitignore b/lib/ssh/doc/specs/.gitignore
--- a/lib/ssh/doc/specs/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/doc/specs/.gitignore 2025-12-22 23:16:56.257838884 +1030
@@ -0,0 +1 @@
+specs_*.xml
diff -ruN a/lib/ssh/doc/src/notes.xml b/lib/ssh/doc/src/notes.xml
--- a/lib/ssh/doc/src/notes.xml 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/doc/src/notes.xml 2025-12-22 23:16:56.258838895 +1030
@@ -4,7 +4,7 @@
<chapter>
<header>
<copyright>
- <year>2004</year><year>2023</year>
+ <year>2004</year><year>2024</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
@@ -30,6 +30,382 @@
<file>notes.xml</file>
</header>
+<section><title>Ssh 5.1.4.13</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>With this change user space buffers are used to limit
+ ssh hello message size instead of kernel buffers</p>
+ <p>
+ Own Id: OTP-19839 Aux Id: ERIERL-1273, PR-10350 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.12</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Option max_handles can be configured for sshd running
+ SFTP. The positive integer value limits amount of file
+ handles opened for a connection (by default 4096 is
+ used).</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-19701 Aux Id: CVE-2025-48041, PR-10157 </p>
+ </item>
+ <item>
+ <p>Avoid decoding KEX messages providing too many
+ algorithms. This change does not introduce new limitation
+ but assures it is enforced earlier in processing chain.
+ Adjustments in error logging during handshake.</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-19741 Aux Id: CVE-2025-48040, PR-10162 </p>
+ </item>
+ <item>
+ <p>A new 'max_path' option is now available in the sshd
+ configuration, allowing administrators to set the maximum
+ allowable path length. By default, this value is set to
+ 4096 characters.</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-19742 Aux Id: CVE-2025-48039, PR-10155 </p>
+ </item>
+ <item>
+ <p>Reject file handles exceeding size specified in RFCs
+ (256 bytes).</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-19748 Aux Id: CVE-2025-48038, PR-10156 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.11</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Fix file handle id generation.</p>
+ <p>
+ Own Id: OTP-19691 Aux Id: PR-10003 </p>
+ </item>
+ <item>
+ <p>Fixes a badmatch error, when SFTP operation cannot be
+ processed due to channel closed in parallel.</p>
+ <p>
+ Own Id: OTP-19707 Aux Id: GH-9655, PR-10035, PR-10036 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.10</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Various channel closing robustness improvements. Avoid
+ crashes when channel handling process closes channel and
+ immediately exits. Avoid breaking the protocol by sending
+ duplicated channel-close messages. Cleanup channels which
+ timeout during closing procedure.</p>
+ <p>
+ Own Id: OTP-19634 Aux Id: GH-9102, PR-9103 </p>
+ </item>
+ <item>
+ <p>Improved interoperability with clients acting as
+ Paramiko.</p>
+ <p>
+ Own Id: OTP-19637 Aux Id: GH-6463, PR-9838 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.9</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Fix KEX strict implementation according to
+ draft-miller-sshm-strict-kex-01 document.</p>
+ <p>
+ Own Id: OTP-19625 Aux Id: CVE-2025-46712 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.8</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Reception of wrong Unicode does not cause unnecessary
+ processing. US-ASCII fields are not decoded as
+ Unicode.</p>
+ <p>
+ Own Id: OTP-19582 Aux Id: PR-9679 </p>
+ </item>
+ <item>
+ <p>SSH daemon disconnects upon receiving connection
+ protocol message for unauthenticated used.</p>
+ <p>Thanks to Fabian Bäumer, Marcel Maehren, Marcus
+ Brinkmann, Nurullah Erinola, Jörg Schwenk (Ruhr
+ University Bochum).</p>
+ <p>
+ Own Id: OTP-19595 Aux Id: CVE-2025-32433 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.7</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>Reception of malicious KEX init message does not
+ result with ssh daemon excessive memory usage.</p>
+ <p>
+ Own Id: OTP-19543 Aux Id: CVE-2025-30211 </p>
+ </item>
+ <item>
+ <p>Call to ssh:daemon_replace_options does not crash when
+ argument is not a valid daemon ref.</p>
+ <p>
+ Own Id: OTP-19559 Aux Id: GH-9554, PR-9545 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.6</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>SFTP packets exceeding max packet size are not
+ processed and dropped.</p>
+ <p>
+ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.5</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>With this change, type specs for
+ ssh:connection_info/1,2 functions are fixed so they
+ include {error, term()} return value.</p>
+ <p>
+ Own Id: OTP-19388 Aux Id: ERIERL-1165, PR-9161 </p>
+ </item>
+ <item>
+ <p>With this change, ssh client accepts a banner sent
+ during processing keyboard interactive user
+ authentication.</p>
+ <p>
+ Own Id: OTP-19392 Aux Id: PR-9139, GH-9065 </p>
+ </item>
+ <item>
+ <p> With this change, large sftp transfers does not hang.
+ Redundant window adjustment are not requested. </p>
+ <p>
+ Own Id: OTP-19435 Aux Id: PR-9309 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.4</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>With this change, ssh connection does not crash upon
+ receiving exit-signal message for an already terminated
+ channel.</p>
+ <p>
+ Own Id: OTP-19326 Aux Id: PR-8995, GH-8929 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.3</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, a race condition is removed from ssh
+ client connection setup procedure.</p>
+ <p>
+ Own Id: OTP-19124 Aux Id: GH-7550, PR-8766 </p>
+ </item>
+ <item>
+ <p>
+ With this change, ssh:connect is not affected by presence
+ of EXIT message in queue.</p>
+ <p>
+ Own Id: OTP-19246 Aux Id: GH-8223, PR-8854 </p>
+ </item>
+ <item>
+ <p>
+ With this change, ssh appends {active, false} option
+ after socket options received from user - so that false
+ value is always used.</p>
+ <p>
+ Own Id: OTP-19247 Aux Id: PR-8226 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.2</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, ssh daemon started with TCP port number
+ argument will re-try to obtain listen socket before
+ returning error to user.</p>
+ <p>
+ Own Id: OTP-19170 Aux Id: GH-7746 </p>
+ </item>
+ <item>
+ <p>
+ With this change, robustness is improved by monitoring
+ connection handler process before casting socket control
+ notification.</p>
+ <p>
+ Own Id: OTP-19173 Aux Id: PR-8310 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4.1</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, ssh client will automatically adjust
+ transfer window size for commands executed remotely over
+ SSH.</p>
+ <p>
+ Own Id: OTP-19057 Aux Id: PR-8345, GH-7483 </p>
+ </item>
+ <item>
+ <p>
+ With this change, race condition between connection
+ closing and automatic window adjustment is fixed.</p>
+ <p>
+ Own Id: OTP-19109 Aux Id: PR-8345 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.4</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, owner and group file attributes
+ decoding is fixed and results with value of integer type.</p>
+ <p>
+ Own Id: OTP-19013 Aux Id: GH-7897, PR-8220 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.3</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, acceptor_sup is not started for ssh
+ client as it is not needed in that role.</p>
+ <p>
+ Own Id: OTP-18974 </p>
+ </item>
+ <item>
+ <p>
+ With this change, more secure algorithms are preferred by
+ ssh and documentation is updated to reflect that.</p>
+ <p>
+ Own Id: OTP-18986 </p>
+ </item>
+ <item>
+ <p>
+ With this change, KEX strict terminal message is emitted
+ with debug verbosity.</p>
+ <p>
+ Own Id: OTP-19002 Aux Id: ERIERL-1041 </p>
+ </item>
+ <item>
+ <p>
+ Fix reading of password for ssh client when in
+ <c>user_interactive</c> mode.</p>
+ <p>
+ Own Id: OTP-19007 Aux Id: ERIERL-1049 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 5.1.2</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, Curve25519 and Curve448 KEX methods
+ become most preferred (related to RFC8731).</p>
+ <p>
+ Own Id: OTP-18964 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
<section><title>Ssh 5.1.1</title>
<section><title>Fixed Bugs and Malfunctions</title>
@@ -164,6 +540,59 @@
</section>
+<section><title>Ssh 4.15.3.2</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, Curve25519 and Curve448 KEX methods
+ become most preferred (related to RFC8731).</p>
+ <p>
+ Own Id: OTP-18964 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 4.15.3.1</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, connection handler does not execute
+ socket operations until it becomes socket owner.
+ Previously errors could occur if connection handler tried
+ to work with socket whose owner exited.</p>
+ <p>
+ Own Id: OTP-18869 Aux Id: PR-7849,GH-7571 </p>
+ </item>
+ <item>
+ <p>
+ With this change (being response to CVE-2023-48795), ssh
+ can negotiate "strict KEX" OpenSSH extension with peers
+ supporting it; also 'chacha20-poly1305@openssh.com'
+ algorithm becomes a less preferred cipher.</p>
+ <p>
+ If strict KEX availability cannot be ensured on both
+ connection sides, affected encryption modes(CHACHA and
+ CBC) can be disabled with standard ssh configuration.
+ This will provide protection against vulnerability, but
+ at a cost of affecting interoperability. See <seeguide
+ marker="configure_algos">Configuring algorithms in
+ SSH</seeguide>.</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-18897</p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
<section><title>Ssh 4.15.3</title>
<section><title>Fixed Bugs and Malfunctions</title>
@@ -357,6 +786,59 @@
</item>
</list>
</section>
+
+</section>
+
+<section><title>Ssh 4.13.2.5</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, Curve25519 and Curve448 KEX methods
+ become most preferred (related to RFC8731).</p>
+ <p>
+ Own Id: OTP-18964 </p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+<section><title>Ssh 4.13.2.4</title>
+
+ <section><title>Fixed Bugs and Malfunctions</title>
+ <list>
+ <item>
+ <p>
+ With this change, connection handler does not execute
+ socket operations until it becomes socket owner.
+ Previously errors could occur if connection handler tried
+ to work with socket whose owner exited.</p>
+ <p>
+ Own Id: OTP-18869 Aux Id: PR-7849,GH-7571 </p>
+ </item>
+ <item>
+ <p>
+ With this change (being response to CVE-2023-48795), ssh
+ can negotiate "strict KEX" OpenSSH extension with peers
+ supporting it; also 'chacha20-poly1305@openssh.com'
+ algorithm becomes a less preferred cipher.</p>
+ <p>
+ If strict KEX availability cannot be ensured on both
+ connection sides, affected encryption modes(CHACHA and
+ CBC) can be disabled with standard ssh configuration.
+ This will provide protection against vulnerability, but
+ at a cost of affecting interoperability. See <seeguide
+ marker="configure_algos">Configuring algorithms in
+ SSH</seeguide>.</p>
+ <p>
+ *** POTENTIAL INCOMPATIBILITY ***</p>
+ <p>
+ Own Id: OTP-18897</p>
+ </item>
+ </list>
+ </section>
</section>
diff -ruN a/lib/ssh/doc/src/SSH_app.xml b/lib/ssh/doc/src/SSH_app.xml
--- a/lib/ssh/doc/src/SSH_app.xml 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/doc/src/SSH_app.xml 2025-12-22 23:16:56.257838884 +1030
@@ -4,7 +4,7 @@
<appref>
<header>
<copyright>
- <year>2012</year><year>2021</year>
+ <year>2012</year><year>2024</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
@@ -172,16 +172,16 @@
<tag>Key exchange algorithms</tag>
<item>
<list type="bulleted">
- <item>ecdh-sha2-nistp384</item>
+ <item>curve25519-sha256</item>
+ <item>curve25519-sha256@libssh.org</item>
+ <item>curve448-sha512</item>
<item>ecdh-sha2-nistp521</item>
+ <item>ecdh-sha2-nistp384</item>
<item>ecdh-sha2-nistp256</item>
<item>diffie-hellman-group-exchange-sha256</item>
<item>diffie-hellman-group16-sha512</item>
<item>diffie-hellman-group18-sha512</item>
<item>diffie-hellman-group14-sha256</item>
- <item>curve25519-sha256</item>
- <item>curve25519-sha256@libssh.org</item>
- <item>curve448-sha512</item>
</list>
<p>The following unsecure <c>SHA1</c> algorithms are now disabled by default:</p>
<list>
@@ -199,13 +199,13 @@
<tag>Public key algorithms</tag>
<item>
<list type="bulleted">
- <item>ecdsa-sha2-nistp384</item>
- <item>ecdsa-sha2-nistp521</item>
- <item>ecdsa-sha2-nistp256</item>
- <item>ssh-ed25519</item>
+ <item>ssh-ed25519</item>
<item>ssh-ed448</item>
+ <item>ecdsa-sha2-nistp521</item>
+ <item>ecdsa-sha2-nistp384</item>
+ <item>ecdsa-sha2-nistp256</item>
+ <item>rsa-sha2-512</item>
<item>rsa-sha2-256</item>
- <item>rsa-sha2-512</item>
</list>
<p>The following unsecure <c>SHA1</c> algorithms are supported but disabled by default:</p>
<list>
@@ -227,11 +227,11 @@
<tag>MAC algorithms</tag>
<item>
<list type="bulleted">
- <item>hmac-sha2-256-etm@openssh.com</item>
<item>hmac-sha2-512-etm@openssh.com</item>
- <item>hmac-sha1-etm@openssh.com</item>
- <item>hmac-sha2-256</item>
+ <item>hmac-sha2-256-etm@openssh.com</item>
<item>hmac-sha2-512</item>
+ <item>hmac-sha2-256</item>
+ <item>hmac-sha1-etm@openssh.com</item>
<item>hmac-sha1</item>
</list>
<p>The following unsecure <c>SHA1</c> algorithm is disabled by default:</p>
diff -ruN a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
--- a/lib/ssh/doc/src/ssh_sftpd.xml 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/doc/src/ssh_sftpd.xml 2025-12-22 23:16:56.259838907 +1030
@@ -4,7 +4,7 @@
<erlref>
<header>
<copyright>
- <year>2005</year><year>2020</year>
+ <year>2005</year><year>2025</year>
<holder>Ericsson AB. All Rights Reserved.</holder>
</copyright>
<legalnotice>
@@ -65,6 +65,18 @@
If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c>
request is limited to at most the given value.</p>
</item>
+ <tag><c>max_handles</c></tag>
+ <item>
+ <p>The default value is <c>1000</c>. Positive integer value represents the maximum number of file handles allowed for a connection.</p>
+ </item>
+ <tag><c>max_path</c></tag>
+ <item>
+ <p>The default value is <c>4096</c>. Positive integer
+ value represents the maximum path length which cannot be
+ exceeded in data provided by the SFTP client. (Note:
+ limitations might be also enforced by underlying operating
+ system)</p>
+ </item>
<tag><c>root</c></tag>
<item>
<p>Sets the SFTP root directory. Then the user cannot see any files
diff -ruN a/lib/ssh/src/.gitignore b/lib/ssh/src/.gitignore
--- a/lib/ssh/src/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/src/.gitignore 2025-12-17 17:25:02.074275138 +1030
@@ -0,0 +1 @@
+deps
diff -ruN a/lib/ssh/src/Makefile b/lib/ssh/src/Makefile
--- a/lib/ssh/src/Makefile 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/Makefile 2025-12-22 23:16:56.259838907 +1030
@@ -1,7 +1,7 @@
#
# %CopyrightBegin%
#
-# Copyright Ericsson AB 2004-2022. All Rights Reserved.
+# Copyright Ericsson AB 2004-2024. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -80,7 +80,7 @@
ssh_sftpd \
ssh_sftpd_file\
ssh_shell \
- ssh_subsystem_sup \
+ ssh_connection_sup \
ssh_system_sup \
ssh_tcpip_forward_srv \
ssh_tcpip_forward_client \
diff -ruN a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl
--- a/lib/ssh/src/ssh_acceptor.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_acceptor.erl 2025-12-22 23:16:56.261838930 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@
%%%----------------------------------------------------------------
listen(Port, Options) ->
{_, Callback, _} = ?GET_OPT(transport, Options),
- SockOpts = [{active, false}, {reuseaddr,true} | ?GET_OPT(socket_options, Options)],
+ SockOpts = ?GET_OPT(socket_options, Options) ++ [{active, false}, {reuseaddr,true}],
case Callback:listen(Port, SockOpts) of
{error, nxdomain} ->
Callback:listen(Port, lists:delete(inet6, SockOpts));
@@ -83,7 +83,6 @@
proc_lib:init_ack(Parent, {ok, self()}),
request_ownership(LSock, SockOwner),
acceptor_loop(Port, Address, Opts, LSock, AcceptTimeout, SystemSup);
-
{error,_Error} ->
%% Not open, a restart
%% Allow gen_tcp:listen to fail 4 times if eaddrinuse (It is a bug fix):
@@ -96,10 +95,9 @@
proc_lib:init_fail(Parent, {error,Error}, {exit, normal})
end
end;
-
undefined ->
%% No listening socket (nor fd option) was provided; open a listening socket:
- case listen(Port, Opts) of
+ case try_listen(Port, Opts, 4) of
{ok,LSock} ->
proc_lib:init_ack(Parent, {ok, self()}),
acceptor_loop(Port, Address, Opts, LSock, AcceptTimeout, SystemSup);
@@ -108,7 +106,6 @@
end
end.
-
try_listen(Port, Opts, NtriesLeft) ->
try_listen(Port, Opts, 1, NtriesLeft).
@@ -121,7 +118,6 @@
Other
end.
-
request_ownership(LSock, SockOwner) ->
SockOwner ! {request_control,LSock,self()},
receive
@@ -137,7 +133,8 @@
MaxSessions = ?GET_OPT(max_sessions, Opts),
NumSessions = number_of_connections(SystemSup),
ParallelLogin = ?GET_OPT(parallel_login, Opts),
- case handle_connection(Address, Port, PeerName, Opts, Socket, MaxSessions, NumSessions, ParallelLogin) of
+ case handle_connection(Address, Port, PeerName, Opts, Socket,
+ MaxSessions, NumSessions, ParallelLogin) of
{error,Error} ->
catch close(Socket, Opts),
handle_error(Error, Address, Port, PeerName);
@@ -154,18 +151,19 @@
?MODULE:acceptor_loop(Port, Address, Opts, ListenSocket, AcceptTimeout, SystemSup).
%%%----------------------------------------------------------------
-handle_connection(_Address, _Port, _Peer, _Options, _Socket, MaxSessions, NumSessions, _ParallelLogin)
+handle_connection(_Address, _Port, _Peer, _Options, _Socket,
+ MaxSessions, NumSessions, _ParallelLogin)
when NumSessions >= MaxSessions->
{error,{max_sessions,MaxSessions}};
-
-handle_connection(_Address, _Port, {error,Error}, _Options, _Socket, _MaxSessions, _NumSessions, _ParallelLogin) ->
+handle_connection(_Address, _Port, {error,Error}, _Options, _Socket,
+ _MaxSessions, _NumSessions, _ParallelLogin) ->
{error,Error};
-
-handle_connection(Address, Port, _Peer, Options, Socket, _MaxSessions, _NumSessions, ParallelLogin)
+handle_connection(Address, Port, _Peer, Options, Socket,
+ _MaxSessions, _NumSessions, ParallelLogin)
when ParallelLogin == false ->
handle_connection(Address, Port, Options, Socket);
-
-handle_connection(Address, Port, _Peer, Options, Socket, _MaxSessions, _NumSessions, ParallelLogin)
+handle_connection(Address, Port, _Peer, Options, Socket,
+ _MaxSessions, _NumSessions, ParallelLogin)
when ParallelLogin == true ->
Ref = make_ref(),
Pid = spawn_link(
@@ -182,12 +180,10 @@
Pid ! {start,Ref},
ok.
-
-
handle_connection(Address, Port, Options0, Socket) ->
Options = ?PUT_INTERNAL_OPT([{user_pid, self()}
], Options0),
- ssh_system_sup:start_subsystem(server,
+ ssh_system_sup:start_connection(server,
#address{address = Address,
port = Port,
profile = ?GET_OPT(profile,Options)
@@ -206,44 +202,68 @@
handle_error(Reason, ToAddress, ToPort, FromAddress, FromPort) ->
case Reason of
{max_sessions, MaxSessions} ->
- error_logger:info_report(
- lists:concat(["Ssh login attempt to ",ssh_lib:format_address_port(ToAddress,ToPort),
- " from ",ssh_lib:format_address_port(FromAddress,FromPort),
- " denied due to option max_sessions limits to ",
- MaxSessions, " sessions."
- ])
- );
-
+ MsgFun =
+ fun(debug) ->
+ lists:concat(["Ssh login attempt to ",
+ ssh_lib:format_address_port(ToAddress,ToPort),
+ " from ",
+ ssh_lib:format_address_port(FromAddress,FromPort),
+ " denied due to option max_sessions limits to ",
+ MaxSessions, " sessions."]);
+ (_) ->
+ ["Ssh login attempt denied max_session limits"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
Limit when Limit==enfile ; Limit==emfile ->
%% Out of sockets...
- error_logger:info_report([atom_to_list(Limit),": out of accept sockets on ",
- ssh_lib:format_address_port(ToAddress, ToPort),
- " - retrying"]),
+ MsgFun =
+ fun(debug) ->
+ [atom_to_list(Limit),": out of accept sockets on ",
+ ssh_lib:format_address_port(ToAddress, ToPort),
+ " - retrying"];
+ (_) ->
+ ["Out of accept sockets on - retrying"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun)),
timer:sleep(?SLEEP_TIME);
-
closed ->
- error_logger:info_report(["The ssh accept socket on ",ssh_lib:format_address_port(ToAddress,ToPort),
- "was closed by a third party."]
- );
-
+ MsgFun =
+ fun(debug) ->
+ ["The ssh accept socket on ", ssh_lib:format_address_port(ToAddress,ToPort),
+ "was closed by a third party."];
+ (_) ->
+ ["The ssh accept socket on was closed by a third party"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
timeout ->
ok;
-
Error when is_list(Error) ->
ok;
Error when FromAddress=/=undefined,
FromPort=/=undefined ->
- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
- " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort),
- io_lib:format(": ~p", [Error])]);
+ MsgFun =
+ fun(debug) ->
+ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
+ " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort),
+ io_lib:format(": ~p", [Error])];
+ (_) ->
+ [io_lib:format("Accept failed on for connection: ~p", [Error])]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
Error ->
- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
- io_lib:format(": ~p", [Error])])
+ MsgFun =
+ fun(debug) ->
+ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
+ io_lib:format(": ~p", [Error])];
+ (_) ->
+ [io_lib:format("Accept failed on for connection: ~p", [Error])]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun))
end.
%%%----------------------------------------------------------------
number_of_connections(SysSupPid) ->
- lists:foldl(fun({_Ref,_Pid,supervisor,[ssh_subsystem_sup]}, N) -> N+1;
+ lists:foldl(fun({_Ref,_Pid,supervisor,[ssh_connection_sup]}, N) -> N+1;
(_, N) -> N
end, 0, supervisor:which_children(SysSupPid)).
diff -ruN a/lib/ssh/src/ssh_app.erl b/lib/ssh/src/ssh_app.erl
--- a/lib/ssh/src/ssh_app.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_app.erl 2025-12-22 23:16:56.261838930 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -23,11 +23,11 @@
%%%=========================================================================
%%% Purpose : Application master and top supervisors for SSH.
%%%
-%%% -----> ssh_sup -----+-----> sshc_sup --+--> "system sup" (etc)
+%%% -----> ssh_sup -----+-----> sshc_sup --+--> "connection sup" (etc)
%%% | |
-%%% | +--> "system sup" (etc)
+%%% | +--> "connection sup" (etc)
%%% | :
-%%% | +--> "system sup" (etc)
+%%% | +--> "connection sup" (etc)
%%% |
%%% +-----> sshc_sup --+--> "system sup" (etc)
%%% |
diff -ruN a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src
--- a/lib/ssh/src/ssh.app.src 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh.app.src 2025-12-22 23:16:56.259838907 +1030
@@ -36,7 +36,7 @@
ssh_sftpd,
ssh_sftpd_file,
ssh_sftpd_file_api,
- ssh_subsystem_sup,
+ ssh_connection_sup,
ssh_tcpip_forward_client,
ssh_tcpip_forward_srv,
ssh_tcpip_forward_acceptor_sup,
@@ -51,7 +51,7 @@
ssh_acceptor,
ssh_channel_sup,
ssh_connection_handler,
- ssh_subsystem_sup,
+ ssh_connection_sup,
ssh_system_sup
]},
{default_filter, rm} %% rm | filter
diff -ruN a/lib/ssh/src/ssh_connect.hrl b/lib/ssh/src/ssh_connect.hrl
--- a/lib/ssh/src/ssh_connect.hrl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_connect.hrl 2025-12-17 17:25:02.075556404 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -269,5 +269,5 @@
suggest_window_size,
suggest_packet_size,
exec,
- sub_system_supervisor
+ connection_supervisor
}).
diff -ruN a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
--- a/lib/ssh/src/ssh_connection.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_connection.erl 2025-12-22 23:16:56.262838942 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -26,6 +26,8 @@
-module(ssh_connection).
+-include_lib("kernel/include/logger.hrl").
+
-include("ssh.hrl").
-include("ssh_connect.hrl").
-include("ssh_transport.hrl").
@@ -468,23 +470,50 @@
%%% Replies {Reply, UpdatedConnection}
%%%
+handle_msg(#ssh_msg_disconnect{code = Code, description = Description}, Connection, _, _SSH) ->
+ {disconnect, {Code, Description}, handle_stop(Connection)};
+
+handle_msg(Msg, Connection, server, Ssh = #ssh{authenticated = false}) ->
+ %% See RFC4252 6.
+ %% Message numbers of 80 and higher are reserved for protocols running
+ %% after this authentication protocol, so receiving one of them before
+ %% authentication is complete is an error, to which the server MUST
+ %% respond by disconnecting, preferably with a proper disconnect message
+ %% sent to ease troubleshooting.
+ MsgFun = fun(M) ->
+ io_lib:format("Connection terminated. Unexpected message for unauthenticated user."
+ " Message: ~w", [M],
+ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
+ end,
+ ?LOG_DEBUG(MsgFun, [Msg]),
+ {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, handle_stop(Connection)};
+
handle_msg(#ssh_msg_channel_open_confirmation{recipient_channel = ChannelId,
sender_channel = RemoteId,
initial_window_size = WindowSz,
maximum_packet_size = PacketSz},
#connection{channel_cache = Cache} = Connection0, _, _SSH) ->
- #channel{remote_id = undefined} = Channel =
+ #channel{remote_id = undefined, user = U} = Channel =
ssh_client_channel:cache_lookup(Cache, ChannelId),
- ssh_client_channel:cache_update(Cache, Channel#channel{
- remote_id = RemoteId,
- recv_packet_size = max(32768, % rfc4254/5.2
- min(PacketSz, Channel#channel.recv_packet_size)
- ),
- send_window_size = WindowSz,
- send_packet_size = PacketSz}),
- reply_msg(Channel, Connection0, {open, ChannelId});
+ if U /= undefined ->
+ ssh_client_channel:cache_update(Cache, Channel#channel{
+ remote_id = RemoteId,
+ recv_packet_size = max(32768, % rfc4254/5.2
+ min(PacketSz, Channel#channel.recv_packet_size)
+ ),
+ send_window_size = WindowSz,
+ send_packet_size = PacketSz}),
+ reply_msg(Channel, Connection0, {open, ChannelId});
+ true ->
+ %% There is no user process so nobody cares about the channel
+ %% close it and remove from the cache, reply from the peer will be
+ %% ignored
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {[{connection_reply, CloseMsg}], Connection0}
+ end;
handle_msg(#ssh_msg_channel_open_failure{recipient_channel = ChannelId,
reason = Reason,
@@ -533,6 +562,10 @@
{Replies, Connection};
undefined ->
+ %% This may happen among other reasons
+ %% - we sent 'channel-close' %% and the peer failed to respond in time
+ %% - we tried to open a channel but the handler died prematurely
+ %% and the channel entry was removed from the cache
{[], Connection0}
end;
@@ -548,21 +581,23 @@
channel_data_reply_msg(ChannelId, Connection, DataType, Data);
handle_msg(#ssh_msg_channel_window_adjust{recipient_channel = ChannelId,
- bytes_to_add = Add},
+ bytes_to_add = Add},
#connection{channel_cache = Cache} = Connection, _, _SSH) ->
- #channel{send_window_size = Size, remote_id = RemoteId} =
- Channel0 = ssh_client_channel:cache_lookup(Cache, ChannelId),
-
- {SendList, Channel} = %% TODO: Datatype 0 ?
- update_send_window(Channel0#channel{send_window_size = Size + Add},
- 0, undefined, Connection),
-
- Replies = lists:map(fun({Type, Data}) ->
- {connection_reply, channel_data_msg(RemoteId, Type, Data)}
- end, SendList),
- FlowCtrlMsgs = flow_control(Channel, Cache),
- {Replies ++ FlowCtrlMsgs, Connection};
-
+ case ssh_client_channel:cache_lookup(Cache, ChannelId) of
+ Channel0 = #channel{send_window_size = Size,
+ remote_id = RemoteId} ->
+ {SendList, Channel} = %% TODO: Datatype 0 ?
+ update_send_window(Channel0#channel{send_window_size = Size + Add},
+ 0, undefined, Connection),
+ Replies = lists:map(fun({Type, Data}) ->
+ {connection_reply,
+ channel_data_msg(RemoteId, Type, Data)}
+ end, SendList),
+ FlowCtrlMsgs = flow_control(Channel, Cache),
+ {Replies ++ FlowCtrlMsgs, Connection};
+ undefined ->
+ {[], Connection}
+ end;
handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type,
sender_channel = RemoteId,
initial_window_size = WindowSz,
@@ -606,7 +641,7 @@
suggest_window_size = WinSz,
suggest_packet_size = PktSz,
options = Options,
- sub_system_supervisor = SubSysSup
+ connection_supervisor = ConnectionSup
} = C,
client, _SSH) ->
{ReplyMsg, NextChId} =
@@ -614,7 +649,7 @@
{ok, {ConnectToHost,ConnectToPort}} ->
case gen_tcp:connect(ConnectToHost, ConnectToPort, [{active,false}, binary]) of
{ok,Sock} ->
- {ok,Pid} = ssh_subsystem_sup:start_channel(client, SubSysSup, self(),
+ {ok,Pid} = ssh_connection_sup:start_channel(client, ConnectionSup, self(),
ssh_tcpip_forward_client, ChId,
[Sock], undefined, Options),
ssh_client_channel:cache_update(Cache,
@@ -664,7 +699,7 @@
suggest_window_size = WinSz,
suggest_packet_size = PktSz,
options = Options,
- sub_system_supervisor = SubSysSup
+ connection_supervisor = ConnectionSup
} = C,
server, _SSH) ->
{ReplyMsg, NextChId} =
@@ -680,7 +715,7 @@
case gen_tcp:connect(binary_to_list(HostToConnect), PortToConnect,
[{active,false}, binary]) of
{ok,Sock} ->
- {ok,Pid} = ssh_subsystem_sup:start_channel(server, SubSysSup, self(),
+ {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(),
ssh_tcpip_forward_srv, ChId,
[Sock], undefined, Options),
ssh_client_channel:cache_update(Cache,
@@ -739,21 +774,35 @@
handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId,
request_type = "exit-signal",
want_reply = false,
- data = Data},
+ data = Data},
#connection{channel_cache = Cache} = Connection0, _, _SSH) ->
<<?DEC_BIN(SigName, _SigLen),
- ?BOOLEAN(_Core),
+ ?BOOLEAN(_Core),
?DEC_BIN(Err, _ErrLen),
?DEC_BIN(Lang, _LangLen)>> = Data,
- Channel = ssh_client_channel:cache_lookup(Cache, ChannelId),
- RemoteId = Channel#channel.remote_id,
- {Reply, Connection} = reply_msg(Channel, Connection0,
- {exit_signal, ChannelId,
- binary_to_list(SigName),
- binary_to_list(Err),
- binary_to_list(Lang)}),
- CloseMsg = channel_close_msg(RemoteId),
- {[{connection_reply, CloseMsg}|Reply], Connection};
+ case ssh_client_channel:cache_lookup(Cache, ChannelId) of
+ #channel{remote_id = RemoteId, sent_close = SentClose} = Channel ->
+ {Reply, Connection} = reply_msg(Channel, Connection0,
+ {exit_signal, ChannelId,
+ binary_to_list(SigName),
+ binary_to_list(Err),
+ binary_to_list(Lang)}),
+ %% Send 'channel-close' only if it has not been sent yet
+ %% by e.g. our side also closing the channel or going down
+ %% and(!) update the cache
+ %% so that the 'channel-close' is not sent twice
+ if not SentClose ->
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_update(Cache,
+ Channel#channel{sent_close = true}),
+ {[{connection_reply, CloseMsg}|Reply], Connection};
+ true ->
+ {Reply, Connection}
+ end;
+ _ ->
+ %% Channel already closed by peer
+ {[], Connection0}
+ end;
handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId,
request_type = "xon-xoff",
@@ -912,8 +961,8 @@
{[{connection_reply, request_failure_msg()}], Connection};
true ->
- SubSysSup = ?GET_INTERNAL_OPT(subsystem_sup, Opts),
- FwdSup = ssh_subsystem_sup:tcpip_fwd_supervisor(SubSysSup),
+ ConnectionSup = ?GET_INTERNAL_OPT(connection_sup, Opts),
+ FwdSup = ssh_connection_sup:tcpip_fwd_supervisor(ConnectionSup),
ConnPid = self(),
case ssh_tcpip_forward_acceptor:supervised_start(FwdSup,
{ListenAddrStr, ListenPort},
@@ -966,12 +1015,7 @@
#connection{requests = [{_, From, Fun} | Rest]} = Connection0, _, _SSH) ->
Connection = Fun({success,Data}, Connection0),
{[{channel_request_reply, From, {success, Data}}],
- Connection#connection{requests = Rest}};
-
-handle_msg(#ssh_msg_disconnect{code = Code,
- description = Description},
- Connection, _, _SSH) ->
- {disconnect, {Code, Description}, handle_stop(Connection)}.
+ Connection#connection{requests = Rest}}.
%%%----------------------------------------------------------------
@@ -1127,22 +1171,22 @@
start_cli(#connection{options = Options,
cli_spec = CliSpec,
exec = Exec,
- sub_system_supervisor = SubSysSup}, ChannelId) ->
+ connection_supervisor = ConnectionSup}, ChannelId) ->
case CliSpec of
no_cli ->
{error, cli_disabled};
{CbModule, Args} ->
- ssh_subsystem_sup:start_channel(server, SubSysSup, self(), CbModule, ChannelId, Args, Exec, Options)
+ ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options)
end.
start_subsystem(BinName, #connection{options = Options,
- sub_system_supervisor = SubSysSup},
+ connection_supervisor = ConnectionSup},
#channel{local_id = ChannelId}, _ReplyMsg) ->
Name = binary_to_list(BinName),
case check_subsystem(Name, Options) of
{Callback, Opts} when is_atom(Callback), Callback =/= none ->
- ssh_subsystem_sup:start_channel(server, SubSysSup, self(), Callback, ChannelId, Opts, undefined, Options);
+ ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options);
{none, _} ->
{error, bad_subsystem};
{_, _} ->
diff -ruN a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
--- a/lib/ssh/src/ssh_connection_handler.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_connection_handler.erl 2025-12-22 23:16:56.264838965 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -34,7 +34,6 @@
-include("ssh_transport.hrl").
-include("ssh_auth.hrl").
-include("ssh_connect.hrl").
-
-include("ssh_fsm.hrl").
%%====================================================================
@@ -42,7 +41,7 @@
%%====================================================================
%%% Start and stop
--export([start_link/4, start_link/5,
+-export([start_link/3, start_link/4,
takeover/4,
stop/1
]).
@@ -98,10 +97,10 @@
%% Start / stop
%%====================================================================
-start_link(Role, Address, Socket, Options) ->
- start_link(Role, Address, undefined, Socket, Options).
+start_link(Role, Socket, Options) ->
+ start_link(Role, undefined, Socket, Options).
-start_link(Role, _Address=#address{}, Id, Socket, Options) ->
+start_link(Role, Id, Socket, Options) ->
case gen_statem:start_link(?MODULE,
[Role, Socket, Options],
[{spawn_opt, [{message_queue_data,off_heap}]}]) of
@@ -110,7 +109,7 @@
%% Announce the ConnectionRef to the system supervisor so it could
%% 1) initiate the socket handover, and
%% 2) be returned to whoever called for example ssh:connect; the Pid
- %% returned from this function is "consumed" by the subsystem
+ %% returned from this function is "consumed" by the connection
%% supervisor.
?GET_INTERNAL_OPT(user_pid,Options) ! {new_connection_ref, Id, Pid},
{ok, Pid};
@@ -119,21 +118,23 @@
Others
end.
-
-takeover(ConnPid, client, Socket, Options) ->
- group_leader(group_leader(), ConnPid),
- takeover(ConnPid, common, Socket, Options);
-
-takeover(ConnPid, _, Socket, Options) ->
+takeover(ConnPid, Role, Socket, Options) ->
+ case Role of
+ client ->
+ group_leader(group_leader(), ConnPid);
+ _ ->
+ ok
+ end,
{_, Callback, _} = ?GET_OPT(transport, Options),
case Callback:controlling_process(Socket, ConnPid) of
ok ->
+ Ref = erlang:monitor(process, ConnPid),
gen_statem:cast(ConnPid, socket_control),
NegTimeout = ?GET_INTERNAL_OPT(negotiation_timeout,
Options,
?GET_OPT(negotiation_timeout, Options)
),
- handshake(ConnPid, erlang:monitor(process,ConnPid), NegTimeout);
+ handshake(ConnPid, Role, Ref, NegTimeout);
{error, Reason} ->
{error, Reason}
end.
@@ -195,8 +196,8 @@
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
start_channel(ConnectionHandler, CallbackModule, ChannelId, Args, Exec) ->
- {ok, {SubSysSup,Role,Opts}} = call(ConnectionHandler, get_misc),
- ssh_subsystem_sup:start_channel(Role, SubSysSup,
+ {ok, {ConnectionSup,Role,Opts}} = call(ConnectionHandler, get_misc),
+ ssh_connection_sup:start_channel(Role, ConnectionSup,
ConnectionHandler, CallbackModule, ChannelId,
Args, Exec, Opts).
@@ -416,7 +417,7 @@
suggest_packet_size = PktSz,
requests = [],
options = Opts,
- sub_system_supervisor = ?GET_INTERNAL_OPT(subsystem_sup, Opts)
+ connection_supervisor = ?GET_INTERNAL_OPT(connection_sup, Opts)
},
case Role of
server ->
@@ -485,25 +486,41 @@
}
end.
-
-handshake(Pid, Ref, Timeout) ->
+handshake(ConnPid, server, Ref, Timeout) ->
receive
- {Pid, ssh_connected} ->
+ {ConnPid, ssh_connected} ->
erlang:demonitor(Ref, [flush]),
- {ok, Pid};
- {Pid, {not_connected, Reason}} ->
+ {ok, ConnPid};
+ {ConnPid, {not_connected, Reason}} ->
erlang:demonitor(Ref, [flush]),
{error, Reason};
- {'DOWN', Ref, process, Pid, {shutdown, Reason}} ->
+ {'DOWN', Ref, process, ConnPid, {shutdown, Reason}} ->
{error, Reason};
- {'DOWN', Ref, process, Pid, Reason} ->
+ {'DOWN', Ref, process, ConnPid, Reason} ->
{error, Reason};
{'EXIT',_,Reason} ->
- stop(Pid),
+ stop(ConnPid),
{error, {exit,Reason}}
after Timeout ->
erlang:demonitor(Ref, [flush]),
- ssh_connection_handler:stop(Pid),
+ ssh_connection_handler:stop(ConnPid),
+ {error, timeout}
+ end;
+handshake(ConnPid, client, Ref, Timeout) ->
+ receive
+ {ConnPid, ssh_connected} ->
+ erlang:demonitor(Ref, [flush]),
+ {ok, ConnPid};
+ {ConnPid, {not_connected, Reason}} ->
+ erlang:demonitor(Ref, [flush]),
+ {error, Reason};
+ {'DOWN', Ref, process, ConnPid, {shutdown, Reason}} ->
+ {error, Reason};
+ {'DOWN', Ref, process, ConnPid, Reason} ->
+ {error, Reason}
+ after Timeout ->
+ erlang:demonitor(Ref, [flush]),
+ ssh_connection_handler:stop(ConnPid),
{error, timeout}
end.
@@ -592,17 +609,18 @@
handle_event(internal, socket_ready, {hello,_}=StateName, #data{ssh_params = Ssh0} = D) ->
VsnMsg = ssh_transport:hello_version_msg(string_version(Ssh0)),
send_bytes(VsnMsg, D),
- case inet:getopts(Socket=D#data.socket, [recbuf]) of
- {ok, [{recbuf,Size}]} ->
+ case inet:getopts(Socket=D#data.socket, [buffer]) of
+ {ok, [{buffer,Size}]} ->
%% Set the socket to the hello text line handling mode:
inet:setopts(Socket, [{packet, line},
{active, once},
% Expecting the version string which might
% be max ?MAX_PROTO_VERSION bytes:
- {recbuf, ?MAX_PROTO_VERSION},
+ {buffer, ?MAX_PROTO_VERSION},
+ {packet_size, ?MAX_PROTO_VERSION},
{nodelay,true}]),
Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts, infinity),
- {keep_state, D#data{inet_initial_recbuf_size=Size}, [{state_timeout,Time,no_hello_received}] };
+ {keep_state, D#data{inet_initial_buffer_size=Size}, [{state_timeout,Time,no_hello_received}] };
Other ->
?call_disconnectfun_and_log_cond("Option return",
@@ -631,11 +649,12 @@
case handle_version(NumVsn, StrVsn, D0#data.ssh_params) of
{ok, Ssh1} ->
%% Since the hello part is finished correctly, we set the
- %% socket to the packet handling mode (including recbuf size):
+ %% socket to the packet handling mode (including buffer size):
inet:setopts(D0#data.socket, [{packet,0},
{mode,binary},
{active, once},
- {recbuf, D0#data.inet_initial_recbuf_size}]),
+ {buffer, D0#data.inet_initial_buffer_size},
+ {packet_size, 0}]),
{KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1),
send_bytes(SshPacket, D0),
D = D0#data{ssh_params = Ssh,
@@ -653,17 +672,24 @@
%%% timeout after tcp:connect but then nothing arrives
handle_event(state_timeout, no_hello_received, {hello,_Role}=StateName, D0 = #data{ssh_params = Ssh0}) ->
- Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts),
+ MsgFun =
+ fun (debug) ->
+ Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts),
+ lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]);
+ (_) ->
+ ["No HELLO received within hello_timeout"]
+ end,
{Shutdown, D} =
- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]),
- StateName, D0),
+ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun), StateName, D0),
{stop, Shutdown, D};
-%%% ######## {service_request, client|server} ####
-
-handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D0) ->
+%%% ######## {service_request, client|server} #### StateName ==
+%% {userauth,server} guard added due to interoperability with clients
+%% sending extra ssh_msg_service_request (e.g. Paramiko for Python,
+%% see GH-6463)
+handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName, D0)
+ when StateName == {service_request,server}; StateName == {userauth,server} ->
case ServiceName of
"ssh-userauth" ->
Ssh0 = #ssh{session_id=SessionId} = D0#data.ssh_params,
@@ -706,16 +732,6 @@
disconnect_fun("Received disconnect: "++Desc, D),
{stop_and_reply, {shutdown,Desc}, Actions, D};
-handle_event(internal, #ssh_msg_ignore{}, {_StateName, _Role, init},
- #data{ssh_params = #ssh{kex_strict_negotiated = true,
- send_sequence = SendSeq,
- recv_sequence = RecvSeq}}) ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("strict KEX violation: unexpected SSH_MSG_IGNORE "
- "send_sequence = ~p recv_sequence = ~p",
- [SendSeq, RecvSeq])
- );
-
handle_event(internal, #ssh_msg_ignore{}, _StateName, _) ->
keep_state_and_data;
@@ -1017,8 +1033,8 @@
handle_event({call,From}, get_misc, StateName,
#data{connection_state = #connection{options = Opts}} = D) when ?CONNECTED(StateName) ->
- SubSysSup = ?GET_INTERNAL_OPT(subsystem_sup, Opts),
- Reply = {ok, {SubSysSup, ?role(StateName), Opts}},
+ ConnectionSup = ?GET_INTERNAL_OPT(connection_sup, Opts),
+ Reply = {ok, {ConnectionSup, ?role(StateName), Opts}},
{keep_state, D, [{reply,From,Reply}]};
handle_event({call,From},
@@ -1074,12 +1090,22 @@
handle_event({call,From}, {close, ChannelId}, StateName, D0)
when ?CONNECTED(StateName) ->
+ %% Send 'channel-close' only if it has not been sent yet
+ %% e.g. when 'exit-signal' was received from the peer
+ %% and(!) we update the cache so that we remember what we've done
case ssh_client_channel:cache_lookup(cache(D0), ChannelId) of
- #channel{remote_id = Id} = Channel ->
+ #channel{remote_id = Id, sent_close = false} = Channel ->
D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
- ssh_client_channel:cache_update(cache(D1), Channel#channel{sent_close = true}),
- {keep_state, D1, [cond_set_idle_timer(D1), {reply,From,ok}]};
- undefined ->
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true}),
+ {keep_state, D1, [cond_set_idle_timer(D1),
+ channel_close_timer(D1, Id),
+ {reply,From,ok}]};
+ _ ->
+ %% Here we match a channel which has already sent 'channel-close'
+ %% AND possible cases of 'broken cache' i.e. when a channel
+ %% disappeared from the cache, but has not been properly shut down
+ %% The latter would be a bug, but hard to chase
{keep_state_and_data, [{reply,From,ok}]}
end;
@@ -1119,11 +1145,14 @@
of
{packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
D1 = D0#data{ssh_params =
- Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
- decrypted_data_buffer = <<>>,
- undecrypted_packet_length = undefined,
- aead_data = <<>>,
- encrypted_data_buffer = EncryptedDataRest},
+ Ssh1#ssh{recv_sequence =
+ ssh_transport:next_seqnum(StateName,
+ Ssh1#ssh.recv_sequence,
+ SshParams)},
+ decrypted_data_buffer = <<>>,
+ undecrypted_packet_length = undefined,
+ aead_data = <<>>,
+ encrypted_data_buffer = EncryptedDataRest},
try
ssh_message:decode(set_kex_overload_prefix(DecryptedBytes,D1))
of
@@ -1155,12 +1184,21 @@
{next_event, internal, Msg}
]}
catch
- C:E:ST ->
- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
+ Class:Reason0:Stacktrace ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p",
+ [Class,Reason,Stacktrace],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
+ (_) ->
+ io_lib:format("Bad packet: Decrypted, but can't decode ~p:~p",
+ [Class, Reason],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
+ end,
{Shutdown, D} =
?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~P",
- [C,E,ST,MaxLogItemLen]),
+ ?SELECT_MSG(MsgFun),
StateName, D1),
{stop, Shutdown, D}
end;
@@ -1190,12 +1228,20 @@
StateName, D0),
{stop, Shutdown, D}
catch
- C:E:ST ->
- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
+ Class:Reason0:Stacktrace ->
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",
+ [Class,Reason0,Stacktrace],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
+ (_) ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p",
+ [Class,Reason],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
+ end,
{Shutdown, D} =
- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~P",
- [C,E,ST,MaxLogItemLen]),
+ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun),
StateName, D0),
{stop, Shutdown, D}
end;
@@ -1237,15 +1283,33 @@
%%% Handle that ssh channels user process goes down
handle_event(info, {'DOWN', _Ref, process, ChannelPid, _Reason}, _, D) ->
Cache = cache(D),
- ssh_client_channel:cache_foldl(
- fun(#channel{user=U,
- local_id=Id}, Acc) when U == ChannelPid ->
- ssh_client_channel:cache_delete(Cache, Id),
- Acc;
- (_,Acc) ->
- Acc
- end, [], Cache),
- {keep_state, D, cond_set_idle_timer(D)};
+ %% Here we first collect the list of channel id's handled by the process
+ %% Do NOT remove them from the cache - they are not closed yet!
+ Channels = ssh_client_channel:cache_foldl(
+ fun(#channel{user=U} = Channel, Acc) when U == ChannelPid ->
+ [Channel | Acc];
+ (_,Acc) ->
+ Acc
+ end, [], Cache),
+ %% Then for each channel where 'channel-close' has not been sent yet
+ %% we send 'channel-close' and(!) update the cache so that we remember
+ %% what we've done.
+ %% Also set user as 'undefined' as there is no such process anyway
+ {D2, NewTimers} = lists:foldl(
+ fun(#channel{remote_id = Id, sent_close = false} = Channel,
+ {D0, Timers}) when Id /= undefined ->
+ D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true,
+ user = undefined}),
+ ChannelTimer = channel_close_timer(D1, Id),
+ {D1, [ChannelTimer | Timers]};
+ (Channel, {D0, _} = Acc) ->
+ ssh_client_channel:cache_update(cache(D0),
+ Channel#channel{user = undefined}),
+ Acc
+ end, {D, []}, Channels),
+ {keep_state, D2, [cond_set_idle_timer(D2) | NewTimers]};
handle_event({timeout,idle_time}, _Data, _StateName, D) ->
case ssh_client_channel:cache_info(num_entries, cache(D)) of
@@ -1258,6 +1322,16 @@
handle_event({timeout,max_initial_idle_time}, _Data, _StateName, _D) ->
{stop, {shutdown, "Timeout"}};
+handle_event({timeout, {channel_close, ChannelId}}, _Data, _StateName, D) ->
+ Cache = cache(D),
+ case ssh_client_channel:cache_lookup(Cache, ChannelId) of
+ #channel{sent_close = true} ->
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {keep_state, D, cond_set_idle_timer(D)};
+ _ ->
+ keep_state_and_data
+ end;
+
%%% So that terminate will be run when supervisor is shutdown
handle_event(info, {'EXIT', _Sup, Reason}, StateName, _D) ->
Role = ?role(StateName),
@@ -1281,9 +1355,9 @@
handle_event(info, {fwd_connect_received, Sock, ChId, ChanCB}, StateName, #data{connection_state = Connection}) ->
#connection{options = Options,
channel_cache = Cache,
- sub_system_supervisor = SubSysSup} = Connection,
+ connection_supervisor = ConnectionSup} = Connection,
Channel = ssh_client_channel:cache_lookup(Cache, ChId),
- {ok,Pid} = ssh_subsystem_sup:start_channel(?role(StateName), SubSysSup, self(), ChanCB, ChId, [Sock], undefined, Options),
+ {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined, Options),
ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}),
gen_tcp:controlling_process(Sock, Pid),
inet:setopts(Sock, [{active,once}]),
@@ -1292,8 +1366,8 @@
handle_event({call,From},
{handle_direct_tcpip, ListenHost, ListenPort, ConnectToHost, ConnectToPort, _Timeout},
_StateName,
- #data{connection_state = #connection{sub_system_supervisor=SubSysSup}}) ->
- case ssh_tcpip_forward_acceptor:supervised_start(ssh_subsystem_sup:tcpip_fwd_supervisor(SubSysSup),
+ #data{connection_state = #connection{connection_supervisor=ConnectionSup}}) ->
+ case ssh_tcpip_forward_acceptor:supervised_start(ssh_connection_sup:tcpip_fwd_supervisor(ConnectionSup),
{ListenHost, ListenPort},
{ConnectToHost, ConnectToPort},
"direct-tcpip", ssh_tcpip_forward_client,
@@ -2030,6 +2104,10 @@
_ -> {{timeout,idle_time}, infinity, none}
end.
+channel_close_timer(D, ChannelId) ->
+ {{timeout, {channel_close, ChannelId}},
+ ?GET_OPT(channel_close_timeout, (D#data.ssh_params)#ssh.opts), none}.
+
%%%----------------------------------------------------------------
start_channel_request_timer(_,_, infinity) ->
ok;
diff -ruN a/lib/ssh/src/ssh_connection_sup.erl b/lib/ssh/src/ssh_connection_sup.erl
--- a/lib/ssh/src/ssh_connection_sup.erl 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/src/ssh_connection_sup.erl 2025-12-22 23:16:56.264838965 +1030
@@ -0,0 +1,105 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+%%----------------------------------------------------------------------
+%% Purpose: The ssh connection supervisor
+%%----------------------------------------------------------------------
+
+-module(ssh_connection_sup).
+
+-behaviour(supervisor).
+
+-include("ssh.hrl").
+
+-export([start_link/4,
+ start_channel/8,
+ tcpip_fwd_supervisor/1
+ ]).
+
+%% Supervisor callback
+-export([init/1]).
+
+%%%=========================================================================
+%%% API
+%%%=========================================================================
+start_link(Role, Id, Socket, Options) ->
+ case supervisor:start_link(?MODULE, [Role, Id, Socket, Options]) of
+ {error, {shutdown, {failed_to_start_child, _, Error}}} ->
+ {error,Error};
+ Other ->
+ Other
+ end.
+
+start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) ->
+ ChannelSup = channel_supervisor(SupPid),
+ ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts).
+
+tcpip_fwd_supervisor(ConnectionSup) ->
+ find_child(tcpip_forward_acceptor_sup, ConnectionSup).
+
+
+%%%=========================================================================
+%%% Supervisor callback
+%%%=========================================================================
+init([Role, Id, Socket, Options]) ->
+ ConnectionSup = self(),
+ SupFlags = #{strategy => one_for_all,
+ auto_shutdown => any_significant,
+ intensity => 0,
+ period => 3600},
+ ChildSpecs =
+ [#{id => connection,
+ restart => temporary,
+ type => worker,
+ significant => true,
+ start => {ssh_connection_handler,
+ start_link,
+ [Role, Id, Socket,
+ ?PUT_INTERNAL_OPT([{connection_sup, ConnectionSup}], Options)]}
+ },
+ #{id => channel_sup,
+ restart => temporary,
+ type => supervisor,
+ start => {ssh_channel_sup, start_link, [Options]}
+ },
+
+ #{id => tcpip_forward_acceptor_sup,
+ restart => temporary,
+ type => supervisor,
+ start => {ssh_tcpip_forward_acceptor_sup, start_link, []}
+ }],
+ {ok, {SupFlags,ChildSpecs}}.
+
+%%%=========================================================================
+%%% Internal functions
+%%%=========================================================================
+channel_supervisor(ConnectionSup) -> find_child(channel_sup, ConnectionSup).
+
+find_child(Id, Sup) when is_pid(Sup) ->
+ try
+ {Id, Pid, _, _} = lists:keyfind(Id, 1, supervisor:which_children(Sup)),
+ Pid
+ catch
+ exit:{no_proc,_} ->
+ {error, no_proc};
+ _:_ ->
+ {error, {id_not_found,?MODULE,Id}}
+ end.
+
diff -ruN a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
--- a/lib/ssh/src/ssh.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh.erl 2025-12-22 23:16:56.261838930 +1030
@@ -1,7 +1,7 @@
%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -189,7 +189,7 @@
{error, Reason};
Options ->
- SocketOpts = [{active,false} | ?GET_OPT(socket_options,Options)],
+ SocketOpts = ?GET_OPT(socket_options,Options) ++ [{active,false}],
Host = mangle_connect_address(Host0, Options),
try
transport_connect(Host, Port, SocketOpts, Options)
@@ -248,7 +248,7 @@
port = SockPort,
profile = ?GET_OPT(profile,Options)
},
- ssh_system_sup:start_subsystem(client, Address, Socket, Options).
+ ssh_system_sup:start_connection(client, Address, Socket, Options).
%%--------------------------------------------------------------------
-spec close(ConnectionRef) -> ok | {error,term()} when
@@ -287,16 +287,18 @@
| {options, client_options()}
| {algorithms, conn_info_algs()}
| {channels, conn_info_channels()}.
-
--spec connection_info(ConnectionRef) -> InfoTupleList when
+
+-spec connection_info(ConnectionRef) ->
+ InfoTupleList | {error, term()} when
ConnectionRef :: connection_ref(),
InfoTupleList :: [InfoTuple],
InfoTuple :: connection_info_tuple().
-connection_info(ConnectionRef) ->
+connection_info(ConnectionRef) ->
connection_info(ConnectionRef, []).
--spec connection_info(ConnectionRef, ItemList|Item) -> InfoTupleList|InfoTuple when
+-spec connection_info(ConnectionRef, ItemList|Item) ->
+ InfoTupleList | InfoTuple | {error, term()} when
ConnectionRef :: connection_ref(),
ItemList :: [Item],
Item :: client_version | server_version | user | peer | sockname | options | algorithms | sockname,
@@ -343,7 +345,7 @@
profile = ?GET_OPT(profile,Options0)
},
Options = ?PUT_INTERNAL_OPT({connected_socket, Socket}, Options0),
- case ssh_system_sup:start_subsystem(server, Address, Socket, Options) of
+ case ssh_system_sup:start_connection(server, Address, Socket, Options) of
{ok,Pid} ->
{ok,Pid};
{error, {already_started, _}} ->
@@ -392,8 +394,7 @@
%% throws error:Error if no usable hostkey is found
ssh_connection_handler:available_hkey_algorithms(server, Options1),
- ssh_system_sup:start_system(server,
- #address{address = Host,
+ ssh_system_sup:start_system(#address{address = Host,
port = Port,
profile = ?GET_OPT(profile,Options1)},
Options1)
@@ -450,9 +451,13 @@
NewUserOptions :: daemon_options().
daemon_replace_options(DaemonRef, NewUserOptions) ->
- {ok,Os0} = ssh_system_sup:get_acceptor_options(DaemonRef),
- Os1 = ssh_options:merge_options(server, NewUserOptions, Os0),
- ssh_system_sup:replace_acceptor_options(DaemonRef, Os1).
+ case ssh_system_sup:get_acceptor_options(DaemonRef) of
+ {ok, Os0} ->
+ Os1 = ssh_options:merge_options(server, NewUserOptions, Os0),
+ ssh_system_sup:replace_acceptor_options(DaemonRef, Os1);
+ {error, _Reason} = Error ->
+ Error
+ end.
%%--------------------------------------------------------------------
-type daemon_info_tuple() ::
@@ -540,8 +545,7 @@
lists:foreach(fun({Sup,_Addr}) ->
stop_listener(Sup)
end,
- ssh_system_sup:addresses(server,
- #address{address=Address,
+ ssh_system_sup:addresses(#address{address=Address,
port=Port,
profile=Profile})).
@@ -552,7 +556,7 @@
-spec stop_daemon(DaemonRef::daemon_ref()) -> ok.
stop_daemon(SysSup) ->
- ssh_system_sup:stop_system(server, SysSup).
+ ssh_system_sup:stop_system(SysSup).
-spec stop_daemon(inet:ip_address(), inet:port_number()) -> ok.
@@ -567,8 +571,7 @@
lists:foreach(fun({Sup,_Addr}) ->
stop_daemon(Sup)
end,
- ssh_system_sup:addresses(server,
- #address{address=Address,
+ ssh_system_sup:addresses(#address{address=Address,
port=Port,
profile=Profile})).
diff -ruN a/lib/ssh/src/ssh_fsm.hrl b/lib/ssh/src/ssh_fsm.hrl
--- a/lib/ssh/src/ssh_fsm.hrl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm.hrl 2025-12-17 17:25:02.076556417 +1030
@@ -37,7 +37,7 @@
| undefined,
last_size_rekey = 0 :: non_neg_integer(),
event_queue = [] :: list(),
- inet_initial_recbuf_size :: pos_integer()
+ inet_initial_buffer_size :: pos_integer()
| undefined
}).
diff -ruN a/lib/ssh/src/ssh_fsm_kexinit.erl b/lib/ssh/src/ssh_fsm_kexinit.erl
--- a/lib/ssh/src/ssh_fsm_kexinit.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm_kexinit.erl 2025-12-22 23:16:56.264838965 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -43,6 +43,11 @@
-export([callback_mode/0, handle_event/4, terminate/3,
format_status/2, code_change/4]).
+-behaviour(ssh_dbg).
+-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1,
+ ssh_dbg_on/1, ssh_dbg_off/1,
+ ssh_dbg_format/2]).
+
%%====================================================================
%% gen_statem callbacks
%%====================================================================
@@ -53,8 +58,13 @@
%%--------------------------------------------------------------------
-%%% ######## {kexinit, client|server, init|renegotiate} ####
+handle_event(Type, Event = prepare_next_packet, StateName, D) ->
+ ssh_connection_handler:handle_event(Type, Event, StateName, D);
+handle_event(Type, Event = {send_disconnect, _, _, _, _}, StateName, D) ->
+ ssh_connection_handler:handle_event(Type, Event, StateName, D);
+
+%%% ######## {kexinit, client|server, init|renegotiate} ####
handle_event(internal, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg},
D = #data{key_exchange_init_msg = OwnKex}) ->
Ssh1 = ssh_transport:key_init(peer_role(Role), D#data.ssh_params, Payload),
@@ -67,11 +77,10 @@
end,
{next_state, {key_exchange,Role,ReNeg}, D#data{ssh_params=Ssh}};
-
%%% ######## {key_exchange, client|server, init|renegotiate} ####
-
%%%---- diffie-hellman
handle_event(internal, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexdhReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -81,6 +90,7 @@
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
@@ -89,24 +99,28 @@
%%%---- diffie-hellman group exchange
handle_event(internal, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(GexGroup, D),
Ssh = ssh_transport:parallell_gen_key(Ssh1),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(GexGroup, D),
Ssh = ssh_transport:parallell_gen_key(Ssh1),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexGexInit, D),
{next_state, {key_exchange_dh_gex_reply,client,ReNeg}, D#data{ssh_params=Ssh}};
%%%---- elliptic curve diffie-hellman
handle_event(internal, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexEcdhReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -116,16 +130,25 @@
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {key_exchange_dh_gex_init, server, init|renegotiate} ####
-
handle_event(internal, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexGexReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -133,20 +156,33 @@
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
-
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange_dh_gex_init,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {key_exchange_dh_gex_reply, client, init|renegotiate} ####
-
handle_event(internal, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
-
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange_dh_gex_reply,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {new_keys, client|server} ####
-
%% First key exchange round:
handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D0) ->
{ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D0#data.ssh_params),
@@ -162,6 +198,15 @@
%% ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}};
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {new_keys,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation (send_sequence = ~p recv_sequence = ~p)",
+ [SendSeq, RecvSeq]));
+
%% Subsequent key exchange rounds (renegotiation):
handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) ->
{ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
@@ -183,7 +228,6 @@
handle_event(internal, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) ->
{ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
{keep_state, D#data{ssh_params = Ssh}};
-
handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) ->
%% If something else arrives, goto next state and handle the event in that one
@@ -217,3 +261,70 @@
peer_role(client) -> server;
peer_role(server) -> client.
+check_kex_strict(Msg,
+ #data{ssh_params =
+ #ssh{algorithms =
+ #alg{
+ kex = Kex,
+ kex_strict_negotiated = KexStrictNegotiated},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ case check_msg_group(Msg, get_alg_group(Kex), KexStrictNegotiated) of
+ ok ->
+ ok;
+ error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]))
+ end.
+
+get_alg_group(Kex) when Kex == 'diffie-hellman-group16-sha512';
+ Kex == 'diffie-hellman-group18-sha512';
+ Kex == 'diffie-hellman-group14-sha256';
+ Kex == 'diffie-hellman-group14-sha1';
+ Kex == 'diffie-hellman-group1-sha1' ->
+ dh_alg;
+get_alg_group(Kex) when Kex == 'diffie-hellman-group-exchange-sha256';
+ Kex == 'diffie-hellman-group-exchange-sha1' ->
+ dh_gex_alg;
+get_alg_group(Kex) when Kex == 'curve25519-sha256';
+ Kex == 'curve25519-sha256@libssh.org';
+ Kex == 'curve448-sha512';
+ Kex == 'ecdh-sha2-nistp521';
+ Kex == 'ecdh-sha2-nistp384';
+ Kex == 'ecdh-sha2-nistp256' ->
+ ecdh_alg.
+
+check_msg_group(_Msg, _AlgGroup, false) -> ok;
+check_msg_group(#ssh_msg_kexdh_init{}, dh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kexdh_reply{}, dh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_request_old{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_request{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_group{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_init{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_reply{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_ecdh_init{}, ecdh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_ecdh_reply{}, ecdh_alg, true) -> ok;
+check_msg_group(_Msg, _AlgGroup, _) -> error.
+
+%%%################################################################
+%%%#
+%%%# Tracing
+%%%#
+
+ssh_dbg_trace_points() -> [connection_events].
+
+ssh_dbg_flags(connection_events) -> [c].
+
+ssh_dbg_on(connection_events) -> dbg:tp(?MODULE, handle_event, 4, x).
+
+ssh_dbg_off(connection_events) -> dbg:ctpg(?MODULE, handle_event, 4).
+
+ssh_dbg_format(connection_events, {call, {?MODULE,handle_event, [EventType, EventContent, State, _Data]}}) ->
+ ["Connection event\n",
+ io_lib:format("[~w] EventType: ~p~nEventContent: ~p~nState: ~p~n", [?MODULE, EventType, EventContent, State])
+ ];
+ssh_dbg_format(connection_events, {return_from, {?MODULE,handle_event,4}, Ret}) ->
+ ["Connection event result\n",
+ io_lib:format("[~w] ~p~n", [?MODULE, ssh_dbg:reduce_state(Ret, #data{})])
+ ].
diff -ruN a/lib/ssh/src/ssh_fsm_userauth_client.erl b/lib/ssh/src/ssh_fsm_userauth_client.erl
--- a/lib/ssh/src/ssh_fsm_userauth_client.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm_userauth_client.erl 2025-12-22 23:16:56.265838977 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -105,7 +105,10 @@
end;
%%---- banner to client
-handle_event(internal, #ssh_msg_userauth_banner{message = Msg}, {userauth,client}, D) ->
+handle_event(internal, #ssh_msg_userauth_banner{message = Msg}, {S,client}, D)
+ when S == userauth; S == userauth_keyboard_interactive;
+ S == userauth_keyboard_interactive_extra;
+ S == userauth_keyboard_interactive_info_response ->
case D#data.ssh_params#ssh.userauth_quiet_mode of
false -> io:format("~s", [Msg]);
true -> ok
diff -ruN a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
--- a/lib/ssh/src/ssh.hrl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh.hrl 2025-12-22 23:16:56.261838930 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -561,5 +561,11 @@
-define(CIRC_BUF_IN_ONCE(VALUE),
((fun(V) -> ?CIRC_BUF_IN(V), V end)(VALUE))
).
-
+
+-define(SELECT_MSG(__Fun),
+ (fun() ->
+ #{level := __Level} = logger:get_primary_config(),
+ __Fun(__Level)
+ end)()).
+
-endif. % SSH_HRL defined
diff -ruN a/lib/ssh/src/ssh_info.erl b/lib/ssh/src/ssh_info.erl
--- a/lib/ssh/src/ssh_info.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_info.erl 2025-12-22 23:16:56.265838977 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -131,24 +131,36 @@
walk_tree(server, Children, ?inc(Indent)),
io_lib:nl() % Separate system supervisors by an empty line
];
-format_sup(client, {{{ssh_system_sup,LocalAddress},Pid,supervisor,[ssh_system_sup]}, _Spec, Children}, Indent) ->
- [indent(Indent),
- io_lib:format("Local: ~s sys_sup=~s~n", [format_address(LocalAddress), print_pid(Pid)]),
- walk_tree(client, Children, ?inc(Indent)),
- io_lib:nl() % Separate system supervisors by an empty line
+format_sup(client,
+ {{Ref,ConnSup,supervisor,[ssh_connection_sup]}, _ConnSupSpec,
+ [{{connection,ConnPid,worker,[ssh_connection_handler]}, _ConnSpec}
+ | Children]
+ },
+ Indent) when is_reference(Ref) ->
+ [io_lib:format("~sLocal: ~s~n"
+ "~sRemote: ~s (Version: ~s)~n"
+ "~sConnectionRef=~s, connection_sup=~s~n",
+ [indent(Indent), local_addr(ConnPid),
+ indent(Indent), peer_addr(ConnPid), peer_version(client,ConnPid),
+ indent(Indent), print_pid(ConnPid), print_pid(ConnSup)
+ ]),
+ walk_tree(client,
+ [{H,{connref,ConnPid},Cs} || {H,_,Cs} <- Children],
+ ?inc(Indent)),
+ io_lib:nl() % Separate sub system supervisors by an empty line
];
-format_sup(Role,
- {{Ref,SubSysSup,supervisor,[ssh_subsystem_sup]}, _SubSysSpec,
- [{{connection,ConnPid,worker,[ssh_connection_handler]}, _ConnSpec}
+format_sup(server,
+ {{Ref,ConnSup,supervisor,[ssh_connection_sup]}, _ConnSupSpec,
+ [{{connection,ConnPid,worker,[ssh_connection_handler]}, _ConnSpec}
| Children]
},
Indent) when is_reference(Ref) ->
[io_lib:format("~sRemote: ~s (Version: ~s)~n"
- "~sConnectionRef=~s, subsys_sup=~s~n",
- [indent(Indent), peer_addr(ConnPid), peer_version(Role,ConnPid),
- indent(Indent), print_pid(ConnPid), print_pid(SubSysSup)
+ "~sConnectionRef=~s, connection_sup=~s~n",
+ [indent(Indent), peer_addr(ConnPid), peer_version(server,ConnPid),
+ indent(Indent), print_pid(ConnPid), print_pid(ConnSup)
]),
- walk_tree(Role,
+ walk_tree(server,
[{H,{connref,ConnPid},Cs} || {H,_,Cs} <- Children],
?inc(Indent)),
io_lib:nl() % Separate sub system supervisors by an empty line
@@ -250,7 +262,16 @@
catch
_:_ -> "?"
end.
-
+
+local_addr(Pid) ->
+ try
+ [{socket,Socket}] =
+ ssh_connection_handler:connection_info(Pid, [socket]),
+ {ok, AddrPort} = inet:sockname(Socket),
+ ssh_lib:format_address_port(AddrPort)
+ catch
+ _:_ -> "?"
+ end.
format_address(#address{address=Addr, port=Port, profile=Prof}) ->
io_lib:format("~s (profile ~p)", [ssh_lib:format_address_port({Addr,Port}),Prof]);
diff -ruN a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl
--- a/lib/ssh/src/ssh_lib.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_lib.erl 2025-12-22 23:16:56.267839000 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -28,7 +28,9 @@
format_address_port/2, format_address_port/1,
format_address/1,
format_time_ms/1,
- comp/2
+ comp/2,
+ trim_reason/1,
+ max_log_len/1
]).
-include("ssh.hrl").
@@ -86,3 +88,14 @@
comp(_, _, _) ->
false.
+%% We don't want to process badmatch details, potentially containing
+%% malicious data of unknown size
+trim_reason({badmatch, V}) when is_binary(V) ->
+ badmatch;
+trim_reason(E) ->
+ E.
+
+max_log_len(#ssh{opts = Opts}) ->
+ ?GET_OPT(max_log_item_len, Opts);
+max_log_len(Opts) when is_map(Opts) ->
+ ?GET_OPT(max_log_item_len, Opts).
diff -ruN a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
--- a/lib/ssh/src/ssh_message.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_message.erl 2025-12-22 23:16:56.267839000 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2013-2022. All Rights Reserved.
+%% Copyright Ericsson AB 2013-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
-module(ssh_message).
-include_lib("public_key/include/public_key.hrl").
+-include_lib("kernel/include/logger.hrl").
-include("ssh.hrl").
-include("ssh_connect.hrl").
@@ -42,11 +43,12 @@
-behaviour(ssh_dbg).
-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, ssh_dbg_on/1, ssh_dbg_off/1, ssh_dbg_format/2]).
+-define(ALG_NAME_LIMIT, 64). % RFC4251 sec6
ucl(B) ->
try unicode:characters_to_list(B) of
L when is_list(L) -> L;
- {error,_Matched,Rest} -> throw({error,{bad_unicode,Rest}})
+ {error,_Matched,_Rest} -> throw({error,bad_unicode})
catch
_:_ -> throw({error,bad_unicode})
end.
@@ -206,12 +208,12 @@
encode(#ssh_msg_service_request{
name = Service
}) ->
- <<?Ebyte(?SSH_MSG_SERVICE_REQUEST), ?Estring_utf8(Service)>>;
+ <<?Ebyte(?SSH_MSG_SERVICE_REQUEST), ?Estring(Service)>>;
encode(#ssh_msg_service_accept{
name = Service
}) ->
- <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring_utf8(Service)>>;
+ <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring(Service)>>;
encode(#ssh_msg_ext_info{
nr_extensions = N,
@@ -374,7 +376,7 @@
try
#ssh_msg_channel_request{
recipient_channel = Recipient,
- request_type = ?unicode_list(RequestType),
+ request_type = binary:bin_to_list(RequestType),
want_reply = erl_boolean(Bool),
data = Data
}
@@ -404,8 +406,8 @@
Data/binary>>) ->
#ssh_msg_userauth_request{
user = ?unicode_list(User),
- service = ?unicode_list(Service),
- method = ?unicode_list(Method),
+ service = binary:bin_to_list(Service),
+ method = binary:bin_to_list(Method),
data = Data
};
@@ -413,7 +415,7 @@
?DEC_BIN(Auths,__0),
?BYTE(Bool)>>) ->
#ssh_msg_userauth_failure {
- authentications = ?unicode_list(Auths),
+ authentications = binary:bin_to_list(Auths),
partial_success = erl_boolean(Bool)
};
@@ -527,12 +529,12 @@
decode(<<?SSH_MSG_SERVICE_REQUEST, ?DEC_BIN(Service,__0)>>) ->
#ssh_msg_service_request{
- name = ?unicode_list(Service)
+ name = binary:bin_to_list(Service)
};
decode(<<?SSH_MSG_SERVICE_ACCEPT, ?DEC_BIN(Service,__0)>>) ->
#ssh_msg_service_accept{
- name = ?unicode_list(Service)
+ name = binary:bin_to_list(Service)
};
decode(<<?BYTE(?SSH_MSG_DISCONNECT), ?UINT32(Code), ?DEC_BIN(Desc,__0), ?DEC_BIN(Lang,__1)>>) ->
@@ -819,9 +821,33 @@
%% See rfc 4253 7.1
X = 0,
list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc]));
-decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) ->
- Names = string:tokens(?unicode_list(Data), ","),
- decode_kex_init(Rest, [Names | Acc], N -1).
+decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) when
+ byte_size(Data) < ?MAX_NUM_ALGORITHMS * ?ALG_NAME_LIMIT ->
+ BinParts = binary:split(Data, <<$,>>, [global]),
+ AlgCount = length(BinParts),
+ case AlgCount =< ?MAX_NUM_ALGORITHMS of
+ true ->
+ Process =
+ fun(<<>>, PAcc) ->
+ PAcc;
+ (Part, PAcc) ->
+ case byte_size(Part) =< ?ALG_NAME_LIMIT of
+ true ->
+ Name = binary:bin_to_list(Part),
+ [Name | PAcc];
+ false ->
+ ?LOG_DEBUG("Ignoring too long name", []),
+ PAcc
+ end
+ end,
+ Names = lists:foldr(Process, [], BinParts),
+ decode_kex_init(Rest, [Names | Acc], N - 1);
+ false ->
+ throw({error, {kexinit_error, N, {alg_count, AlgCount}}})
+ end;
+decode_kex_init(<<?DEC_BIN(Data,__0), _Rest/binary>>, _Acc, N) ->
+ throw({error, {kexinit, N, {string_size, byte_size(Data)}}}).
+
%%%================================================================
diff -ruN a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl
--- a/lib/ssh/src/ssh_options.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_options.erl 2025-12-22 23:16:56.267839000 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -885,6 +885,12 @@
#{default => ?MAX_RND_PADDING_LEN,
chk => fun(V) -> check_non_neg_integer(V) end,
class => undoc_user_option
+ },
+
+ channel_close_timeout =>
+ #{default => 5 * 1000,
+ chk => fun(V) -> check_non_neg_integer(V) end,
+ class => undoc_user_option
}
}.
diff -ruN a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
--- a/lib/ssh/src/ssh_sftpd.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_sftpd.erl 2025-12-22 23:16:56.268839012 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@
-behaviour(ssh_server_channel).
-include_lib("kernel/include/file.hrl").
-
+-include_lib("kernel/include/logger.hrl").
-include("ssh.hrl").
-include("ssh_xfer.hrl").
-include("ssh_connect.hrl"). %% For ?DEFAULT_PACKET_SIZE and ?DEFAULT_WINDOW_SIZE
@@ -52,6 +52,8 @@
file_handler, % atom() - callback module
file_state, % state for the file callback module
max_files, % integer >= 0 max no files sent during READDIR
+ max_handles, % integer > 0 - max number of file handles
+ max_path, % integer > 0 - max length of path
options, % from the subsystem declaration
handles % list of open handles
%% handle is either {<int>, directory, {Path, unread|eof}} or
@@ -65,6 +67,8 @@
Options :: [ {cwd, string()} |
{file_handler, CbMod | {CbMod, FileState}} |
{max_files, integer()} |
+ {max_handles, integer()} |
+ {max_path, integer()} |
{root, string()} |
{sftpd_vsn, integer()}
],
@@ -115,8 +119,14 @@
{Root0, State0}
end,
MaxLength = proplists:get_value(max_files, Options, 0),
+ MaxHandles = proplists:get_value(max_handles, Options, 1000),
+ MaxPath = proplists:get_value(max_path, Options, 4096),
Vsn = proplists:get_value(sftpd_vsn, Options, 5),
- {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
+ {ok, State#state{cwd = CWD,
+ root = Root,
+ max_files = MaxLength,
+ max_handles = MaxHandles,
+ max_path = MaxPath,
options = Options,
handles = [], pending = <<>>,
xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
@@ -128,9 +138,8 @@
%% Description: Handles channel messages
%%--------------------------------------------------------------------
handle_ssh_msg({ssh_cm, _ConnectionManager,
- {data, _ChannelId, Type, Data}}, State) ->
- State1 = handle_data(Type, Data, State),
- {ok, State1};
+ {data, ChannelId, Type, Data}}, State) ->
+ handle_data(Type, ChannelId, Data, State);
handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) ->
{stop, ChannelId, State};
@@ -187,24 +196,77 @@
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-handle_data(0, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
+handle_data(0, ChannelId, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
State = #state{pending = <<>>}) ->
<<Op, ?UINT32(ReqId), Data/binary>> = Msg,
NewState = handle_op(Op, ReqId, Data, State),
case Rest of
<<>> ->
- NewState;
+ {ok, NewState};
_ ->
- handle_data(0, Rest, NewState)
+ handle_data(0, ChannelId, Rest, NewState)
end;
-
-handle_data(0, Data, State = #state{pending = <<>>}) ->
- State#state{pending = Data};
-
-handle_data(Type, Data, State = #state{pending = Pending}) ->
- handle_data(Type, <<Pending/binary, Data/binary>>,
- State#state{pending = <<>>}).
-
+handle_data(0, _ChannelId, Data, State = #state{pending = <<>>}) ->
+ {ok, State#state{pending = Data}};
+handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) ->
+ Data = <<Pending/binary, Data0/binary>>,
+ Size = byte_size(Data),
+ case Size > ?SSH_MAX_PACKET_SIZE of
+ true ->
+ ReportFun =
+ fun([S]) ->
+ Report =
+ #{label => {error_logger, error_report},
+ report =>
+ io_lib:format("SFTP packet size (~B) exceeds the limit!",
+ [S])},
+ Meta =
+ #{error_logger =>
+ #{tag => error_report,type => std_error},
+ report_cb => fun(#{report := Msg}) -> {Msg, []} end},
+ {Report, Meta}
+ end,
+ ?LOG_ERROR(ReportFun, [Size]),
+ {stop, ChannelId, State};
+ _ ->
+ handle_data(Type, ChannelId, Data, State#state{pending = <<>>})
+ end.
+
+%% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be longer than 256 bytes."
+handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF})
+ when (Request == ?SSH_FXP_CLOSE orelse
+ Request == ?SSH_FXP_FSETSTAT orelse
+ Request == ?SSH_FXP_FSTAT orelse
+ Request == ?SSH_FXP_READ orelse
+ Request == ?SSH_FXP_READDIR orelse
+ Request == ?SSH_FXP_WRITE),
+ HLen > 256 ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"),
+ State;
+handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>,
+ State = #state{max_path = MaxPath, xf = XF})
+ when (Request == ?SSH_FXP_LSTAT orelse
+ Request == ?SSH_FXP_MKDIR orelse
+ Request == ?SSH_FXP_OPEN orelse
+ Request == ?SSH_FXP_OPENDIR orelse
+ Request == ?SSH_FXP_READLINK orelse
+ Request == ?SSH_FXP_REALPATH orelse
+ Request == ?SSH_FXP_REMOVE orelse
+ Request == ?SSH_FXP_RMDIR orelse
+ Request == ?SSH_FXP_SETSTAT orelse
+ Request == ?SSH_FXP_STAT),
+ PLen > MaxPath ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
+ "No such path"),
+ State;
+handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>,
+ State = #state{max_path = MaxPath, xf = XF})
+ when (Request == ?SSH_FXP_RENAME orelse
+ Request == ?SSH_FXP_SYMLINK),
+ (PLen > MaxPath orelse PLen2 > MaxPath) ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
+ "No such path"),
+ State;
handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
XF = State#state.xf,
Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
@@ -212,7 +274,7 @@
ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <<?UINT32(Vsn)>>),
State#state{xf = XF1};
handle_op(?SSH_FXP_REALPATH, ReqId,
- <<?UINT32(Rlen), RPath:Rlen/binary>>,
+ <<?UINT32(RLen), RPath:RLen/binary>>,
State0) ->
RelPath = relate_file_name(RPath, State0, _Canonicalize=false),
{Res, State} = resolve_symlinks(RelPath, State0),
@@ -228,14 +290,16 @@
end;
handle_op(?SSH_FXP_OPENDIR, ReqId,
<<?UINT32(RLen), RPath:RLen/binary>>,
- State0 = #state{xf = #ssh_xfer{vsn = Vsn},
- file_handler = FileMod, file_state = FS0}) ->
+ State0 = #state{xf = #ssh_xfer{vsn = Vsn},
+ file_handler = FileMod, file_state = FS0,
+ max_handles = MaxHandles}) ->
RelPath = unicode:characters_to_list(RPath),
AbsPath = relate_file_name(RelPath, State0),
XF = State0#state.xf,
{IsDir, FS1} = FileMod:is_dir(AbsPath, FS0),
State1 = State0#state{file_state = FS1},
+ HandlesCnt = length(State0#state.handles),
case IsDir of
false when Vsn > 5 ->
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY,
@@ -245,8 +309,12 @@
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
"Not a directory"),
State1;
- true ->
- add_handle(State1, XF, ReqId, directory, {RelPath,unread})
+ true when HandlesCnt < MaxHandles ->
+ add_handle(State1, XF, ReqId, directory, {RelPath,unread});
+ true ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
+ "max_handles limit reached"),
+ State1
end;
handle_op(?SSH_FXP_READDIR, ReqId,
<<?UINT32(HLen), BinHandle:HLen/binary>>,
@@ -381,14 +449,12 @@
send_status(Status, ReqId, State1);
handle_op(?SSH_FXP_RENAME, ReqId,
- Bin = <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2),
- _:PLen2/binary>>,
+ Bin = <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _:PLen2/binary>>,
State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 ->
handle_op(?SSH_FXP_RENAME, ReqId, <<Bin/binary, 0:32>>, State);
handle_op(?SSH_FXP_RENAME, ReqId,
- <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2),
- BPath2:PLen2/binary, ?UINT32(Flags)>>,
+ <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2), BPath2:PLen2/binary, ?UINT32(Flags)>>,
State0 = #state{file_handler = FileMod, file_state = FS0}) ->
Path = relate_file_name(BPath, State0),
Path2 = relate_file_name(BPath2, State0),
@@ -426,23 +492,29 @@
State1 = State0#state{file_state = FS1},
send_status(Status, ReqId, State1).
-new_handle([], H) ->
- H;
-new_handle([{N, _,_} | Rest], H) when N =< H ->
- new_handle(Rest, N+1);
-new_handle([_ | Rest], H) ->
- new_handle(Rest, H).
+new_handle_id([]) -> 0;
+new_handle_id([{_, _, _} | _] = Handles) ->
+ {HandleIds, _, _} = lists:unzip3(Handles),
+ new_handle_id(lists:sort(HandleIds));
+new_handle_id(HandleIds) ->
+ find_gap(HandleIds).
+
+find_gap([Id]) -> % no gap found
+ Id + 1;
+find_gap([Id1, Id2 | _]) when Id2 - Id1 > 1 -> % gap found
+ Id1 + 1;
+find_gap([_, Id | Rest]) ->
+ find_gap([Id | Rest]).
add_handle(State, XF, ReqId, Type, DirFileTuple) ->
Handles = State#state.handles,
- Handle = new_handle(Handles, 0),
- ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(Handle)),
- %% OBS: If you change handles-tuple also change new_handle!
- %% Is this this the best way to implement new handle?
- State#state{handles = [{Handle, Type, DirFileTuple} | Handles]}.
+ HandleId = new_handle_id(Handles),
+ ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(HandleId)),
+ %% OBS: If you change handles-tuple also change new_handle_id!
+ State#state{handles = [{HandleId, Type, DirFileTuple} | Handles]}.
get_handle(Handles, BinHandle) ->
- case (catch list_to_integer(binary_to_list(BinHandle))) of
+ case (catch binary_to_integer(BinHandle)) of
I when is_integer(I) ->
case lists:keysearch(I, 1, Handles) of
{value, T} -> T;
@@ -697,7 +769,9 @@
do_open(ReqId, State, Path, Flags).
do_open(ReqId, State0, Path, Flags) ->
- #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}} = State0,
+ #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn},
+ max_handles = MaxHandles} = State0,
+ HandlesCnt = length(State0#state.handles),
AbsPath = relate_file_name(Path, State0),
{IsDir, _FS1} = FileMod:is_dir(AbsPath, FS0),
case IsDir of
@@ -709,7 +783,7 @@
ssh_xfer:xf_send_status(State0#state.xf, ReqId,
?SSH_FX_FAILURE, "File is a directory"),
State0;
- false ->
+ false when HandlesCnt < MaxHandles ->
OpenFlags = [binary | Flags],
{Res, FS1} = FileMod:open(AbsPath, OpenFlags, FS0),
State1 = State0#state{file_state = FS1},
@@ -720,7 +794,11 @@
ssh_xfer:xf_send_status(State1#state.xf, ReqId,
ssh_xfer:encode_erlang_status(Error)),
State1
- end
+ end;
+ false ->
+ ssh_xfer:xf_send_status(State0#state.xf, ReqId,
+ ?SSH_FX_FAILURE, "max_handles limit reached"),
+ State0
end.
%% resolve all symlinks in a path
diff -ruN a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl
--- a/lib/ssh/src/ssh_sftp.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_sftp.erl 2025-12-22 23:16:56.268839012 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -776,14 +776,15 @@
Timeout :: timeout(),
Error :: {error, reason()}.
read_file(Pid, Name, FileOpTimeout) ->
- case open(Pid, Name, [read, binary], FileOpTimeout) of
- {ok, Handle} ->
- {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
- Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []),
- close(Pid, Handle),
- Res;
- Error ->
- Error
+ try
+ {ok, Handle} = open(Pid, Name, [read, binary], FileOpTimeout),
+ {ok, {_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
+ Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []),
+ close(Pid, Handle),
+ Res
+ catch
+ error:{badmatch, Error = {error,_}} ->
+ Error
end.
read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, Acc) ->
@@ -813,15 +814,16 @@
write_file(Pid, Name, List, FileOpTimeout) when is_list(List) ->
write_file(Pid, Name, to_bin(List), FileOpTimeout);
write_file(Pid, Name, Bin, FileOpTimeout) ->
- case open(Pid, Name, [write, binary], FileOpTimeout) of
- {ok, Handle} ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- Res = write_file_loop(Pid, Handle, 0, Bin, byte_size(Bin), Packet,
- FileOpTimeout),
- close(Pid, Handle, FileOpTimeout),
- Res;
- Error ->
- Error
+ try
+ {ok, Handle} = open(Pid, Name, [write, binary], FileOpTimeout),
+ {ok, {_Window,Packet}} = send_window(Pid, FileOpTimeout),
+ Res = write_file_loop(Pid, Handle, 0, Bin, byte_size(Bin), Packet,
+ FileOpTimeout),
+ close(Pid, Handle, FileOpTimeout),
+ Res
+ catch
+ error:{badmatch, Error = {error, _}} ->
+ Error
end.
write_file_loop(_Pid, _Handle, _Pos, _Bin, 0, _PacketSz,_FileOpTimeout) ->
@@ -1634,8 +1636,13 @@
read_repeat(Pid, Handle, Len, FileOpTimeout) ->
- {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
- read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>).
+ try
+ {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
+ read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>)
+ catch
+ error:{badmatch, Error = {error, _}} ->
+ Error
+ end.
read_rpt(Pid, Handle, WantedLen, PacketSz, FileOpTimeout, Acc) when WantedLen > 0 ->
case read(Pid, Handle, min(WantedLen,PacketSz), FileOpTimeout) of
@@ -1653,8 +1660,13 @@
write_to_remote_tar(_Pid, _SftpHandle, <<>>, _FileOpTimeout) ->
ok;
write_to_remote_tar(Pid, SftpHandle, Bin, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- write_file_loop(Pid, SftpHandle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout).
+ try
+ {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
+ write_file_loop(Pid, SftpHandle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout)
+ catch
+ error:{badmatch, Error = {error, _}} ->
+ Error
+ end.
position_buf(Pid, SftpHandle, BufHandle, Pos, FileOpTimeout) ->
{ok,#bufinf{mode = Mode,
@@ -1691,18 +1703,24 @@
end.
read_buf(Pid, SftpHandle, BufHandle, WantedLen, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- {ok,B0} = call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
- case do_the_read_buf(Pid, SftpHandle, WantedLen, Packet, FileOpTimeout, B0) of
- {ok,ResultBin,B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- {ok,ResultBin};
- {error,Error} ->
- {error,Error};
- {eof,B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- eof
- end.
+ try
+ {ok, {_Window, Packet}} = send_window(Pid, FileOpTimeout),
+ {ok, B0} = call(Pid, {get_bufinf, BufHandle}, FileOpTimeout),
+ case do_the_read_buf(Pid, SftpHandle, WantedLen, Packet, FileOpTimeout, B0) of
+ {ok, ResultBin, B} ->
+ call(Pid, {put_bufinf, BufHandle, B}, FileOpTimeout),
+ {ok, ResultBin};
+ {eof, B} ->
+ call(Pid, {put_bufinf, BufHandle, B}, FileOpTimeout),
+ eof
+ end
+ catch
+ error:{badmatch, Error = {error, _}} ->
+ Error;
+ error:{case_clause, Error = {error, _}} ->
+ Error
+ end.
+
do_the_read_buf(_Pid, _SftpHandle, WantedLen, _Packet, _FileOpTimeout,
B=#bufinf{plain_text_buf=PlainBuf0,
@@ -1754,15 +1772,16 @@
write_buf(Pid, SftpHandle, BufHandle, PlainBin, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- {ok,B0=#bufinf{plain_text_buf=PTB}} = call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
- case do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
- B0#bufinf{plain_text_buf = <<PTB/binary,PlainBin/binary>>}) of
- {ok, B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- ok;
- {error,Error} ->
- {error,Error}
+ try
+ {ok, {_Window,Packet}} = send_window(Pid, FileOpTimeout),
+ {ok, B0=#bufinf{plain_text_buf=PTB}} = call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
+ {ok, B} = do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
+ B0#bufinf{plain_text_buf = <<PTB/binary,PlainBin/binary>>}),
+ call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
+ ok
+ catch
+ error:{badmatch, Error = {error, _}} ->
+ Error
end.
do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
diff -ruN a/lib/ssh/src/ssh_subsystem_sup.erl b/lib/ssh/src/ssh_subsystem_sup.erl
--- a/lib/ssh/src/ssh_subsystem_sup.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_subsystem_sup.erl 1970-01-01 09:30:00.000000000 +0930
@@ -1,110 +0,0 @@
-%%
-%% %CopyrightBegin%
-%%
-%% Copyright Ericsson AB 2008-2021. All Rights Reserved.
-%%
-%% Licensed under the Apache License, Version 2.0 (the "License");
-%% you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%% http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS,
-%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-%% See the License for the specific language governing permissions and
-%% limitations under the License.
-%%
-%% %CopyrightEnd%
-%%
-%%
-%%----------------------------------------------------------------------
-%% Purpose: The ssh subsystem supervisor
-%%----------------------------------------------------------------------
-
--module(ssh_subsystem_sup).
-
--behaviour(supervisor).
-
--include("ssh.hrl").
-
--export([start_link/5,
- start_channel/8,
- tcpip_fwd_supervisor/1
- ]).
-
-%% Supervisor callback
--export([init/1]).
-
-%%%=========================================================================
-%%% API
-%%%=========================================================================
-start_link(Role, Address=#address{}, Id, Socket, Options) ->
- case supervisor:start_link(?MODULE, [Role, Address, Id, Socket, Options]) of
- {error, {shutdown, {failed_to_start_child, _, Error}}} ->
- {error,Error};
- Other ->
- Other
- end.
-
-start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) ->
- ChannelSup = channel_supervisor(SupPid),
- ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts).
-
-tcpip_fwd_supervisor(SubSysSup) ->
- find_child(tcpip_forward_acceptor_sup, SubSysSup).
-
-
-%%%=========================================================================
-%%% Supervisor callback
-%%%=========================================================================
-init([Role, Address, Id, Socket, Options]) ->
- SubSysSup = self(),
- SupFlags = #{strategy => one_for_all,
- auto_shutdown => any_significant,
- intensity => 0,
- period => 3600
- },
- ChildSpecs = [#{id => connection,
- restart => temporary,
- type => worker,
- significant => true,
- start => {ssh_connection_handler,
- start_link,
- [Role, Address, Id, Socket,
- ?PUT_INTERNAL_OPT([
- {subsystem_sup, SubSysSup}
- ], Options)
- ]
- }
- },
- #{id => channel_sup,
- restart => temporary,
- type => supervisor,
- start => {ssh_channel_sup, start_link, [Options]}
- },
-
- #{id => tcpip_forward_acceptor_sup,
- restart => temporary,
- type => supervisor,
- start => {ssh_tcpip_forward_acceptor_sup, start_link, []}
- }
- ],
- {ok, {SupFlags,ChildSpecs}}.
-
-%%%=========================================================================
-%%% Internal functions
-%%%=========================================================================
-channel_supervisor(SubSysSup) -> find_child(channel_sup, SubSysSup).
-
-find_child(Id, Sup) when is_pid(Sup) ->
- try
- {Id, Pid, _, _} = lists:keyfind(Id, 1, supervisor:which_children(Sup)),
- Pid
- catch
- exit:{no_proc,_} ->
- {error, no_proc};
- _:_ ->
- {error, {id_not_found,?MODULE,Id}}
- end.
-
diff -ruN a/lib/ssh/src/ssh_system_sup.erl b/lib/ssh/src/ssh_system_sup.erl
--- a/lib/ssh/src/ssh_system_sup.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_system_sup.erl 2025-12-22 23:16:56.268839012 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -33,11 +33,11 @@
-export([start_link/3,
stop_listener/1,
- stop_system/2,
- start_system/3,
- start_subsystem/4,
+ stop_system/1,
+ start_system/2,
+ start_connection/4,
get_daemon_listen_address/1,
- addresses/2,
+ addresses/1,
get_options/2,
get_acceptor_options/1,
replace_acceptor_options/2
@@ -50,27 +50,27 @@
%%% API
%%%=========================================================================
-start_system(Role, Address0, Options) ->
- case find_system_sup(Role, Address0) of
+start_system(Address0, Options) ->
+ case find_system_sup(Address0) of
{ok,{SysPid,Address}} ->
restart_acceptor(SysPid, Address, Options);
{error,not_found} ->
- supervisor:start_child(sup(Role),
+ supervisor:start_child(sshd_sup,
#{id => {?MODULE,Address0},
- start => {?MODULE, start_link, [Role, Address0, Options]},
+ start => {?MODULE, start_link, [server, Address0, Options]},
restart => temporary,
type => supervisor
})
end.
%%%----------------------------------------------------------------
-stop_system(Role, SysSup) when is_pid(SysSup) ->
- case lists:keyfind(SysSup, 2, supervisor:which_children(sup(Role))) of
- {{?MODULE, Id}, SysSup, _, _} -> stop_system(Role, Id);
- false -> undefined % FIXME ssh:stop_daemon doc missing that ?
+stop_system(SysSup) when is_pid(SysSup) ->
+ case lists:keyfind(SysSup, 2, supervisor:which_children(sup(server))) of
+ {{?MODULE, Id}, SysSup, _, _} -> stop_system(Id);
+ false -> ok
end;
-stop_system(Role, Id) ->
- supervisor:terminate_child(sup(Role), {?MODULE, Id}).
+stop_system(Id) ->
+ supervisor:terminate_child(sup(server), {?MODULE, Id}).
%%%----------------------------------------------------------------
@@ -93,42 +93,49 @@
end.
%%%----------------------------------------------------------------
-%%% Start the subsystem child. It is a child of the system supervisor (callback = this module)
-start_subsystem(Role, Address=#address{}, Socket, Options0) ->
- Options = ?PUT_INTERNAL_OPT([{user_pid, self()}], Options0),
+%%% Start the connection child. It is a significant child of the system
+%%% supervisor (callback = this module) for server and non-significant
+%%% child of sshc_sup for client
+start_connection(Role = client, _, Socket, Options) ->
+ do_start_connection(Role, sup(client), false, Socket, Options);
+start_connection(Role = server, Address=#address{}, Socket, Options) ->
+ case get_system_sup(Address, Options) of
+ {ok, SysPid} ->
+ do_start_connection(Role, SysPid, true, Socket, Options);
+ Others ->
+ Others
+ end.
+
+do_start_connection(Role, SupPid, Significant, Socket, Options0) ->
Id = make_ref(),
- case get_system_sup(Role, Address, Options) of
- {ok,SysPid} ->
- case supervisor:start_child(SysPid,
- #{id => Id,
- start => {ssh_subsystem_sup, start_link,
- [Role,Address,Id,Socket,Options]
- },
- restart => temporary,
- significant => true,
- type => supervisor
- })
- of
- {ok,_SubSysPid} ->
- try
- receive
- {new_connection_ref, Id, ConnPid} ->
- ssh_connection_handler:takeover(ConnPid, Role, Socket, Options)
- after 10000 ->
- error(timeout)
- end
- catch
- error:{badmatch,{error,Error}} ->
- {error,Error};
- error:timeout ->
- %% The connection was started, but the takover procedure timed out,
- %% therefore it exists a subtree, but it is not quite ready and
- %% must be removed (by the supervisor above):
- supervisor:terminate_child(SysPid, Id),
- {error, connection_start_timeout}
- end;
- Others ->
- Others
+ Options = ?PUT_INTERNAL_OPT([{user_pid, self()}], Options0),
+ case supervisor:start_child(SupPid,
+ #{id => Id,
+ start => {ssh_connection_sup, start_link,
+ [Role,Id,Socket,Options]
+ },
+ restart => temporary,
+ significant => Significant,
+ type => supervisor
+ })
+ of
+ {ok,_ConnectionSupPid} ->
+ try
+ receive
+ {new_connection_ref, Id, ConnPid} ->
+ ssh_connection_handler:takeover(ConnPid, Role, Socket, Options)
+ after 10000 ->
+ error(timeout)
+ end
+ catch
+ error:{badmatch,{error,Error}} ->
+ {error,Error};
+ error:timeout ->
+ %% The connection was started, but the takover procedure timed out,
+ %% therefore it exists a subtree, but it is not quite ready and
+ %% must be removed (by the supervisor above):
+ supervisor:terminate_child(SupPid, Id),
+ {error, connection_start_timeout}
end;
Others ->
Others
@@ -139,9 +146,9 @@
supervisor:start_link(?MODULE, [Role, Address, Options]).
%%%----------------------------------------------------------------
-addresses(Role, #address{address=Address, port=Port, profile=Profile}) ->
+addresses(#address{address=Address, port=Port, profile=Profile}) ->
[{SysSup,A} || {{ssh_system_sup,A},SysSup,supervisor,_} <-
- supervisor:which_children(sup(Role)),
+ supervisor:which_children(sshd_sup),
Address == any orelse A#address.address == Address,
Port == any orelse A#address.port == Port,
Profile == any orelse A#address.profile == Profile].
@@ -152,8 +159,8 @@
case get_daemon_listen_address(SysPid) of
{ok,Address} ->
get_options(SysPid, Address);
- {error,Error} ->
- {error,Error}
+ {error,not_found} ->
+ {error,bad_daemon_ref}
end.
replace_acceptor_options(SysPid, NewOpts) ->
@@ -224,19 +231,20 @@
lookup(SupModule, SystemSup) ->
lists:keyfind([SupModule], 4, supervisor:which_children(SystemSup)).
-get_system_sup(Role, Address0, Options) ->
- case find_system_sup(Role, Address0) of
+get_system_sup(Address0, Options) ->
+ case find_system_sup(Address0) of
{ok,{SysPid,_Address}} ->
{ok,SysPid};
{error,not_found} ->
- start_system(Role, Address0, Options);
+ start_system(Address0, Options);
{error,Error} ->
{error,Error}
end.
-find_system_sup(Role, Address0) ->
- case addresses(Role, Address0) of
- [{SysSupPid,Address}] -> {ok,{SysSupPid,Address}};
+find_system_sup(Address0) ->
+ case addresses(Address0) of
+ [{SysSupPid,Address}] ->
+ {ok,{SysSupPid,Address}};
[] -> {error,not_found};
[_,_|_] -> {error,ambiguous}
end.
diff -ruN a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
--- a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl 2025-12-22 23:16:56.268839012 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -35,9 +35,8 @@
case get_fwd_listen_opts(ListenAddrStr) of
{ok,Opts} ->
%% start listening on Addr:BoundPort
- case gen_tcp:listen(ListenPort, [binary,
- {reuseaddr,true},
- {active,false} | Opts]) of
+ case gen_tcp:listen(ListenPort,
+ Opts ++ [binary, {reuseaddr,true}, {active,false}]) of
{ok,LSock} ->
{ok,{_, TrueListenPort}} = inet:sockname(LSock),
ssh_tcpip_forward_acceptor_sup:start_child(FwdSup,
diff -ruN a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl
--- a/lib/ssh/src/ssh_transport.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_transport.erl 2025-12-22 23:16:56.269839023 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -26,12 +26,11 @@
-include_lib("public_key/include/public_key.hrl").
-include_lib("kernel/include/inet.hrl").
-
-include("ssh_transport.hrl").
-include("ssh.hrl").
-export([versions/2, hello_version_msg/1]).
--export([next_seqnum/1,
+-export([next_seqnum/3,
supported_algorithms/0, supported_algorithms/1,
default_algorithms/0, default_algorithms/1,
clear_default_algorithms_env/0,
@@ -202,18 +201,16 @@
supported_algorithms(kex) ->
select_crypto_supported(
[
- {'ecdh-sha2-nistp384', [{public_keys,ecdh}, {curves,secp384r1}, {hashs,sha384}]},
+ {'curve25519-sha256', [{public_keys,ecdh}, {curves,x25519}, {hashs,sha256}]},
+ {'curve25519-sha256@libssh.org', [{public_keys,ecdh}, {curves,x25519}, {hashs,sha256}]},
+ {'curve448-sha512', [{public_keys,ecdh}, {curves,x448}, {hashs,sha512}]},
{'ecdh-sha2-nistp521', [{public_keys,ecdh}, {curves,secp521r1}, {hashs,sha512}]},
+ {'ecdh-sha2-nistp384', [{public_keys,ecdh}, {curves,secp384r1}, {hashs,sha384}]},
{'ecdh-sha2-nistp256', [{public_keys,ecdh}, {curves,secp256r1}, {hashs,sha256}]},
{'diffie-hellman-group-exchange-sha256', [{public_keys,dh}, {hashs,sha256}]},
{'diffie-hellman-group16-sha512', [{public_keys,dh}, {hashs,sha512}]}, % In OpenSSH 7.3.p1
{'diffie-hellman-group18-sha512', [{public_keys,dh}, {hashs,sha512}]}, % In OpenSSH 7.3.p1
{'diffie-hellman-group14-sha256', [{public_keys,dh}, {hashs,sha256}]}, % In OpenSSH 7.3.p1
- %% https://tools.ietf.org/html/draft-ietf-curdle-ssh-curves
- %% Secure Shell (SSH) Key Exchange Method using Curve25519 and Curve448
- {'curve25519-sha256', [{public_keys,ecdh}, {curves,x25519}, {hashs,sha256}]},
- {'curve25519-sha256@libssh.org', [{public_keys,ecdh}, {curves,x25519}, {hashs,sha256}]},
- {'curve448-sha512', [{public_keys,ecdh}, {curves,x448}, {hashs,sha512}]},
{'diffie-hellman-group14-sha1', [{public_keys,dh}, {hashs,sha}]},
{'diffie-hellman-group-exchange-sha1', [{public_keys,dh}, {hashs,sha}]},
{'diffie-hellman-group1-sha1', [{public_keys,dh}, {hashs,sha}]}
@@ -221,13 +218,13 @@
supported_algorithms(public_key) ->
select_crypto_supported(
[
- {'ecdsa-sha2-nistp384', [{public_keys,ecdsa}, {hashs,sha384}, {curves,secp384r1}]},
- {'ecdsa-sha2-nistp521', [{public_keys,ecdsa}, {hashs,sha512}, {curves,secp521r1}]},
- {'ecdsa-sha2-nistp256', [{public_keys,ecdsa}, {hashs,sha256}, {curves,secp256r1}]},
{'ssh-ed25519', [{public_keys,eddsa}, {curves,ed25519} ]},
{'ssh-ed448', [{public_keys,eddsa}, {curves,ed448} ]},
- {'rsa-sha2-256', [{public_keys,rsa}, {hashs,sha256} ]},
+ {'ecdsa-sha2-nistp521', [{public_keys,ecdsa}, {hashs,sha512}, {curves,secp521r1}]},
+ {'ecdsa-sha2-nistp384', [{public_keys,ecdsa}, {hashs,sha384}, {curves,secp384r1}]},
+ {'ecdsa-sha2-nistp256', [{public_keys,ecdsa}, {hashs,sha256}, {curves,secp256r1}]},
{'rsa-sha2-512', [{public_keys,rsa}, {hashs,sha512} ]},
+ {'rsa-sha2-256', [{public_keys,rsa}, {hashs,sha256} ]},
{'ssh-rsa', [{public_keys,rsa}, {hashs,sha} ]},
{'ssh-dss', [{public_keys,dss}, {hashs,sha} ]} % Gone in OpenSSH 7.3.p1
]);
@@ -253,15 +250,15 @@
supported_algorithms(mac) ->
same(
select_crypto_supported(
- [{'hmac-sha2-256-etm@openssh.com', [{macs,hmac}, {hashs,sha256}]},
- {'hmac-sha2-512-etm@openssh.com', [{macs,hmac}, {hashs,sha256}]},
- {'hmac-sha2-256', [{macs,hmac}, {hashs,sha256}]},
+ [{'hmac-sha2-512-etm@openssh.com', [{macs,hmac}, {hashs,sha256}]},
+ {'hmac-sha2-256-etm@openssh.com', [{macs,hmac}, {hashs,sha256}]},
{'hmac-sha2-512', [{macs,hmac}, {hashs,sha512}]},
+ {'hmac-sha2-256', [{macs,hmac}, {hashs,sha256}]},
{'hmac-sha1-etm@openssh.com', [{macs,hmac}, {hashs,sha256}]},
{'hmac-sha1', [{macs,hmac}, {hashs,sha}]},
{'hmac-sha1-96', [{macs,hmac}, {hashs,sha}]},
- {'AEAD_AES_128_GCM', [{ciphers,aes_128_gcm}]},
- {'AEAD_AES_256_GCM', [{ciphers,aes_256_gcm}]}
+ {'AEAD_AES_256_GCM', [{ciphers,aes_256_gcm}]},
+ {'AEAD_AES_128_GCM', [{ciphers,aes_128_gcm}]}
]
));
supported_algorithms(compression) ->
@@ -297,7 +294,12 @@
hello_version_msg(Data) ->
[Data,"\r\n"].
-next_seqnum(SeqNum) ->
+next_seqnum({State, _Role, init}, 16#ffffffff,
+ #ssh{algorithms = #alg{kex_strict_negotiated = true}})
+ when State == kexinit; State == key_exchange; State == new_keys ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: recv_sequence = 16#ffffffff", []));
+next_seqnum(_State, SeqNum, _) ->
(SeqNum + 1) band 16#ffffffff.
is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) ->
@@ -401,8 +403,9 @@
key_exchange_first_msg(Algos#alg.kex,
Ssh#ssh{algorithms = Algos})
catch
- Class:Error ->
- Msg = kexinit_error(Class, Error, client, Own, CounterPart),
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ Msg = kexinit_error(Class, Reason, client, Own, CounterPart, Ssh),
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
end;
@@ -418,31 +421,38 @@
Algos ->
{ok, Ssh#ssh{algorithms = Algos}}
catch
- Class:Error ->
- Msg = kexinit_error(Class, Error, server, Own, CounterPart),
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ Msg = kexinit_error(Class, Reason, server, Own, CounterPart, Ssh),
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
end.
-kexinit_error(Class, Error, Role, Own, CounterPart) ->
+kexinit_error(Class, Error, Role, Own, CounterPart, Ssh) ->
{Fmt,Args} =
case {Class,Error} of
{error, {badmatch,{false,Alg}}} ->
{Txt,W,C} = alg_info(Role, Alg),
- {"No common ~s algorithm,~n"
- " we have:~n ~s~n"
- " peer have:~n ~s~n",
- [Txt,
- lists:join(", ", element(W,Own)),
- lists:join(", ", element(C,CounterPart))
- ]};
+ MsgFun =
+ fun(debug) ->
+ {"No common ~s algorithm,~n"
+ " we have:~n ~s~n"
+ " peer have:~n ~s~n",
+ [Txt,
+ lists:join(", ", element(W,Own)),
+ lists:join(", ", element(C,CounterPart))]};
+ (_) ->
+ {"No common ~s algorithm", [Txt]}
+ end,
+ ?SELECT_MSG(MsgFun);
_ ->
{"Kexinit failed in ~p: ~p:~p", [Role,Class,Error]}
end,
- try io_lib:format(Fmt, Args) of
+ try io_lib:format(Fmt, Args, [{chars_limit, ssh_lib:max_log_len(Ssh)}]) of
R -> R
catch
_:_ ->
- io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error])
+ io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
end.
alg_info(client, Alg) ->
@@ -594,14 +604,19 @@
session_id = sid(Ssh1, H)}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
true ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ MsgFun =
+ fun(debug) ->
io_lib:format("Kexdh init failed, received 'e' out of bounds~n E=~p~n P=~p",
- [E,P])
- )
+ [E,P], [{chars_limit, ssh_lib:max_log_len(Opts)}]);
+ (_) ->
+ io_lib:format("Kexdh init failed, received 'e' out of bounds", [],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}] )
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey,
@@ -622,14 +637,15 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh init failed. Verify host key: ~p",[Error])
+ io_lib:format("Kexdh init failed. Verify host key: ~p",[Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
)
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Kexdh init failed, received 'f' out of bounds~n F=~p~n P=~p",
- [F,P])
+ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
)
end.
@@ -655,7 +671,8 @@
}};
{error,_} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
+ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}])
)
end;
@@ -687,8 +704,8 @@
}};
{error,_} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
- )
+ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
handle_kex_dh_gex_request(_, _) ->
@@ -714,7 +731,6 @@
{Public, Private} = generate_key(dh, [P,G,2*Sz]),
{SshPacket, Ssh1} =
ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), % Pub = G^Priv mod P (def)
-
{ok, SshPacket,
Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}.
@@ -745,19 +761,22 @@
}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- "Kexdh init failed, received 'k' out of bounds"
- )
+ "Kexdh init failed, received 'k' out of bounds")
end;
true ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n E=~p~n P=~p",
- [E,P])
- )
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n"
+ " E=~p~n P=~p", [E,P]);
+ (_) ->
+ io_lib:format("Kexdh gex init failed, received 'e' out of bounds", [])
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostKey,
@@ -782,20 +801,18 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh gex reply failed. Verify host key: ~p",[Error])
- )
+ io_lib:format("Kexdh gex reply failed. Verify host key: ~p",
+ [Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- "Kexdh gex init failed, 'K' out of bounds"
- )
+ "Kexdh gex init failed, 'K' out of bounds")
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Kexdh gex init failed, received 'f' out of bounds~n F=~p~n P=~p",
- [F,P])
- )
+ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end.
%%%----------------------------------------------------------------
@@ -829,17 +846,25 @@
session_id = sid(Ssh1, H)}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end
catch
- Class:Error ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ MsgFun =
+ fun(debug) ->
io_lib:format("ECDH compute key failed in server: ~p:~p~n"
"Kex: ~p, Curve: ~p~n"
"PeerPublic: ~p",
- [Class,Error,Kex,Curve,PeerPublic])
- )
+ [Class,Reason,Kex,Curve,PeerPublic],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]);
+ (_) ->
+ io_lib:format("ECDH compute key failed in server: ~p:~p",
+ [Class,Reason],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey,
@@ -862,15 +887,14 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("ECDH reply failed. Verify host key: ~p",[Error])
- )
+ io_lib:format("ECDH reply failed. Verify host key: ~p",[Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end
catch
Class:Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Peer ECDH public key seem invalid: ~p:~p",
- [Class,Error])
- )
+ [Class,Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end.
@@ -1082,7 +1106,7 @@
%% algorithm. Each string MUST contain at least one algorithm name.
select_algorithm(Role, Client, Server,
#ssh{opts = Opts,
- kex_strict_negotiated = KexStrictNegotiated0},
+ kex_strict_negotiated = KexStrictNegotiated0},
ReNeg) ->
KexStrictNegotiated =
case ReNeg of
@@ -1099,8 +1123,7 @@
end,
case Result of
true ->
- error_logger:info_report(
- lists:concat([Role, " will use strict KEX ordering"]));
+ logger:debug(lists:concat([Role, " will use strict KEX ordering"]));
_ ->
ok
end,
@@ -1108,7 +1131,6 @@
_ ->
KexStrictNegotiated0
end,
-
{Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server),
{SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server),
@@ -1122,14 +1144,9 @@
Server#ssh_msg_kexinit.languages_client_to_server),
S_Lng = select(Client#ssh_msg_kexinit.languages_server_to_client,
Server#ssh_msg_kexinit.languages_server_to_client),
- HKey = select_all(Client#ssh_msg_kexinit.server_host_key_algorithms,
- Server#ssh_msg_kexinit.server_host_key_algorithms),
- HK = case HKey of
- [] -> undefined;
- [HK0|_] -> HK0
- end,
- %% Fixme verify Kex against HKey list and algorithms
-
+ HKey = select(Client#ssh_msg_kexinit.server_host_key_algorithms,
+ Server#ssh_msg_kexinit.server_host_key_algorithms),
+ %% FIXME verify Kex against HKey list and algorithms (see RFC4253 sec 7.1)
Kex = select(Client#ssh_msg_kexinit.kex_algorithms,
Server#ssh_msg_kexinit.kex_algorithms),
@@ -1149,7 +1166,7 @@
?GET_OPT(recv_ext_info,Opts),
{ok, #alg{kex = Kex,
- hkey = HK,
+ hkey = HKey,
encrypt = Encrypt,
decrypt = Decrypt,
send_mac = SendMac,
@@ -1301,38 +1318,27 @@
{ok,SSH3} = decompress_final(SSH2),
SSH3.
-
-select_all(CL, SL) when length(CL) + length(SL) < ?MAX_NUM_ALGORITHMS ->
- %% algorithms only used by client
- %% NOTE: an algorithm occurring more than once in CL will still be present
- %% in CLonly. This is not a problem for nice clients.
- CLonly = CL -- SL,
-
- %% algorithms used by client and server (client pref)
- lists:foldr(fun(ALG, Acc) ->
- try [list_to_existing_atom(ALG) | Acc]
- catch
- %% If an malicious client uses the same non-existing algorithm twice,
- %% we will end up here
- _:_ -> Acc
- end
- end, [], (CL -- CLonly));
-
-select_all(CL, SL) ->
- Error = lists:concat(["Received too many algorithms (",length(CL),"+",length(SL)," >= ",?MAX_NUM_ALGORITHMS,")."]),
- ?DISCONNECT(?SSH_DISCONNECT_PROTOCOL_ERROR,
- Error).
-
-
select([], []) ->
none;
select(CL, SL) ->
- C = case select_all(CL,SL) of
- [] -> undefined;
- [ALG|_] -> ALG
- end,
- C.
-
+ select_first(CL, SL).
+
+select_first([ClientAlg | ClientRest], SL) ->
+ case lists:member(ClientAlg, SL) of
+ true ->
+ try list_to_existing_atom(ClientAlg) of
+ Alg when is_atom(Alg) ->
+ Alg
+ catch
+ error:badarg ->
+ select_first(ClientRest, SL)
+ end;
+ false ->
+ select_first(ClientRest, SL)
+ end;
+select_first([], _) ->
+ undefined.
+
ssh_packet(#ssh_msg_kexinit{} = Msg, Ssh0) ->
BinMsg = ssh_message:encode(Msg),
Ssh = key_init(Ssh0#ssh.role, Ssh0, BinMsg),
diff -ruN a/lib/ssh/src/ssh_xfer.erl b/lib/ssh/src/ssh_xfer.erl
--- a/lib/ssh/src/ssh_xfer.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/src/ssh_xfer.erl 2025-12-22 23:16:56.269839023 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -660,7 +660,7 @@
encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_UIDGID,
[?uint32(X) | Acc]);
ownergroup when Vsn>=5 ->
- X1 = list_to_binary(integer_to_list(X)), % TODO: check owner and group
+ X1 = integer_to_binary(X), % TODO: check owner and group
encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_OWNERGROUP,
[?binary(X1) | Acc]);
permissions ->
@@ -736,13 +736,11 @@
decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2);
ownergroup when ?is_set(?SSH_FILEXFER_ATTR_OWNERGROUP, Flags),Vsn>=5 ->
<<?UINT32(Len), Bin:Len/binary, Tail2/binary>> = Tail,
- X = binary_to_list(Bin),
+ X = binary_to_integer(Bin),
decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2);
-
permissions when ?is_set(?SSH_FILEXFER_ATTR_PERMISSIONS,Flags),Vsn>=5->
<<?UINT32(X), Tail2/binary>> = Tail,
decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2);
-
permissions when ?is_set(?SSH_FILEXFER_ATTR_PERMISSIONS,Flags),Vsn=<3->
<<?UINT32(X), Tail2/binary>> = Tail,
R1 = setelement(AField, R, X),
@@ -757,7 +755,6 @@
_ -> unknown
end,
decode_As(Vsn, As, R1#ssh_xfer_attr { type=Type}, Flags, Tail2);
-
acmodtime when ?is_set(?SSH_FILEXFER_ATTR_ACMODTIME,Flags),Vsn=<3 ->
<<?UINT32(X), Tail2/binary>> = Tail,
decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2);
diff -ruN a/lib/ssh/test/.gitignore b/lib/ssh/test/.gitignore
--- a/lib/ssh/test/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/test/.gitignore 2023-04-26 15:10:42.429802138 +0930
@@ -0,0 +1,7 @@
+*COVER.html
+
+ssh_sftp_SUITE_data/test_data*
+
+property_test/ssh_eqc_client_server_dirs/system
+property_test/ssh_eqc_client_server_dirs/user
+
diff -ruN a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl
--- a/lib/ssh/test/ssh_connection_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_connection_SUITE.erl 2025-12-22 23:16:56.269839023 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -21,9 +21,10 @@
%%
-module(ssh_connection_SUITE).
--include_lib("common_test/include/ct.hrl").
-include("ssh_connect.hrl").
-include("ssh_test_lib.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include_lib("stdlib/include/assert.hrl").
-export([
suite/0,
@@ -58,7 +59,7 @@
connect_timeout/1,
daemon_sock_not_passive/1,
daemon_sock_not_tcp/1,
- do_interrupted_send/3,
+ do_interrupted_send/4,
do_simple_exec/1,
encode_decode_pty_opts/1,
exec_disabled/1,
@@ -81,6 +82,7 @@
send_after_exit/1,
simple_eval/1,
simple_exec/1,
+ simple_exec_more_data/1,
simple_exec_sock/1,
simple_exec_two_socks/1,
small_cat/1,
@@ -97,6 +99,7 @@
start_shell_exec_direct_fun1_error_type/1,
start_shell_exec_direct_fun2/1,
start_shell_exec_direct_fun3/1,
+ start_shell_exec_direct_fun_more_data/1,
start_shell_exec_fun/1,
start_shell_exec_fun2/1,
start_shell_exec_fun3/1,
@@ -105,6 +108,9 @@
start_shell_sock_exec_fun/1,
start_subsystem_on_closed_channel/1,
stop_listener/1,
+ trap_exit_connect/1,
+ trap_exit_daemon/1,
+ handler_down_before_open/1,
ssh_exec_echo/2 % called as an MFA
]).
@@ -132,6 +138,8 @@
start_shell,
new_shell_dumb_term,
new_shell_xterm_term,
+ trap_exit_connect,
+ trap_exit_daemon,
start_shell_pty,
start_shell_exec,
start_shell_exec_fun,
@@ -140,6 +148,7 @@
start_shell_exec_direct_fun,
start_shell_exec_direct_fun2,
start_shell_exec_direct_fun3,
+ start_shell_exec_direct_fun_more_data,
start_shell_exec_direct_fun1_error,
start_shell_exec_direct_fun1_error_type,
start_exec_direct_fun1_read_write,
@@ -173,13 +182,15 @@
stop_listener,
no_sensitive_leak,
start_subsystem_on_closed_channel,
- max_channels_option
+ max_channels_option,
+ handler_down_before_open
].
groups() ->
[{openssh, [], payload() ++ ptty() ++ sock()}].
payload() ->
[simple_exec,
+ simple_exec_more_data,
simple_exec_sock,
simple_exec_two_socks,
small_cat,
@@ -228,7 +239,7 @@
%% end_per_testcase will be run!
end_per_testcase(any, Config),
ssh:start(),
- Config.
+ ssh_test_lib:verify_sanity_check(Config).
end_per_testcase(_TestCase, _Config) ->
ssh:stop().
@@ -240,6 +251,10 @@
ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, []),
do_simple_exec(ConnectionRef).
+simple_exec_more_data(Config) when is_list(Config) ->
+ ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, []),
+ %% more data received, SSH window adjust needs to be sent by client
+ do_simple_exec(ConnectionRef, 60000).
%%--------------------------------------------------------------------
simple_exec_sock(_Config) ->
{ok, Sock} = ssh_test_lib:gen_tcp_connect(?SSH_DEFAULT_PORT, [{active,false}]),
@@ -630,15 +645,24 @@
ssh:close(ConnectionRef).
%%--------------------------------------------------------------------
-small_interrupted_send(Config) ->
+%%- small_interrupted_send is interrupted by ssh_echo_server which is
+%% done with transferring data towards client and terminates the
+%% channel (this results with {error, closed} return value from
+%% ssh_connection:send on the client side)
+%%- interrupted_send is interrupted when ssh_echo_server ran
+%% out of ssh data window and closed channel
+small_interrupted_send(Config) ->
K = 1024,
- M = K*K,
- do_interrupted_send(Config, 10*M, 4*K).
+ SendSize = 10 * K * K,
+ EchoSize = 4 * K,
+ do_interrupted_send(Config, SendSize, EchoSize, {error, closed}).
interrupted_send(Config) ->
- M = 1024*1024,
- do_interrupted_send(Config, 10*M, 4*M).
+ K = 1024,
+ SendSize = 10 * K * K,
+ EchoSize = 4 * K * K,
+ do_interrupted_send(Config, SendSize, EchoSize, ok).
-do_interrupted_send(Config, SendSize, EchoSize) ->
+do_interrupted_send(Config, SendSize, EchoSize, SenderResult) ->
PrivDir = proplists:get_value(priv_dir, Config),
UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
file:make_dir(UserDir),
@@ -648,7 +672,6 @@
{user_dir, UserDir},
{password, "morot"},
{subsystems, [{"echo_n",EchoSS_spec}]}]),
-
ct:log("~p:~p connect", [?MODULE,?LINE]),
ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
{user, "foo"},
@@ -656,28 +679,24 @@
{user_interaction, false},
{user_dir, UserDir}]),
ct:log("~p:~p connected", [?MODULE,?LINE]),
-
%% build big binary
Data = << <<X:32>> || X <- lists:seq(1,SendSize div 4)>>,
-
%% expect remote end to send us EchoSize back
<<ExpectedData:EchoSize/binary, _/binary>> = Data,
-
%% Spawn listener. Otherwise we could get a deadlock due to filled buffers
Parent = self(),
ResultPid = spawn(
fun() ->
ct:log("~p:~p open channel",[?MODULE,?LINE]),
{ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
- ct:log("~p:~p start subsystem", [?MODULE,?LINE]),
+ ct:log("~p:~p start ssh subsystem", [?MODULE,?LINE]),
case ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", infinity) of
success ->
Parent ! {self(), channelId, ChannelId},
-
- Result =
+ Result =
try collect_data(ConnectionRef, ChannelId, EchoSize)
of
- ExpectedData ->
+ ExpectedData ->
ct:log("~p:~p got expected data",[?MODULE,?LINE]),
ok;
Other ->
@@ -692,40 +711,40 @@
Parent ! {self(), channelId, error, Other}
end
end),
-
receive
{ResultPid, channelId, error, Other} ->
ct:log("~p:~p channelId error ~p", [?MODULE,?LINE,Other]),
ssh:close(ConnectionRef),
ssh:stop_daemon(Pid),
{fail, "ssh_connection:subsystem"};
-
{ResultPid, channelId, ChannelId} ->
ct:log("~p:~p ~p going to send ~p bytes", [?MODULE,?LINE,self(),size(Data)]),
SenderPid = spawn(fun() ->
Parent ! {self(), ssh_connection:send(ConnectionRef, ChannelId, Data, 30000)}
end),
+ ct:log("SenderPid = ~p", [SenderPid]),
receive
{ResultPid, result, {fail, Fail}} ->
ct:log("~p:~p Listener failed: ~p", [?MODULE,?LINE,Fail]),
{fail, Fail};
-
{ResultPid, result, Result} ->
ct:log("~p:~p Got result: ~p", [?MODULE,?LINE,Result]),
ssh:close(ConnectionRef),
ssh:stop_daemon(Pid),
ct:log("~p:~p Check sender", [?MODULE,?LINE]),
receive
- {SenderPid, {error, closed}} ->
- ct:log("~p:~p {error,closed} - That's what we expect :)",[?MODULE,?LINE]),
+ {SenderPid, SenderResult} ->
+ ct:log("~p:~p ~p - That's what we expect :)",
+ [?MODULE,?LINE, SenderResult]),
ok;
Msg ->
ct:log("~p:~p Not expected send result: ~p",[?MODULE,?LINE,Msg]),
{fail, "Not expected msg"}
end;
-
{SenderPid, {error, closed}} ->
- ct:log("~p:~p {error,closed} - That's what we expect, but client channel handler has not reported yet",[?MODULE,?LINE]),
+ ct:log("~p:~p ~p - That's what we expect, "
+ "but client channel handler has not reported yet",
+ [?MODULE,?LINE, SenderResult]),
receive
{ResultPid, result, Result} ->
ct:log("~p:~p Now got the result: ~p", [?MODULE,?LINE,Result]),
@@ -736,7 +755,6 @@
ct:log("~p:~p Got an unexpected msg ~p",[?MODULE,?LINE,Msg]),
{fail, "Un-expected msg"}
end;
-
Msg ->
ct:log("~p:~p Got unexpected ~p",[?MODULE,?LINE,Msg]),
{fail, "Unexpected msg"}
@@ -1005,6 +1023,24 @@
"testing", <<"echo foo testing">>, 0,
Config).
+start_shell_exec_direct_fun_more_data(Config) ->
+ N = 60000,
+ ExpectedBin = <<"testing\n">>,
+ ReceiveFun =
+ fun(ConnectionRef, ChannelId, _Expect, _ExpectType) ->
+ receive_bytes(ConnectionRef, ChannelId,
+ N * byte_size(ExpectedBin), 0)
+ end,
+ do_start_shell_exec_fun({direct,
+ fun(_Cmd) ->
+ {ok,
+ [io_lib:format("testing~n",[]) ||
+ _ <- lists:seq(1, N)]}
+ end},
+ "not_relevant", <<"not_used\n">>, 0,
+ ReceiveFun,
+ Config).
+
start_shell_exec_direct_fun1_error(Config) ->
do_start_shell_exec_fun({direct, fun(_Cmd) -> {error, {bad}} end},
"testing", <<"**Error** {bad}">>, 1,
@@ -1139,6 +1175,28 @@
do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, Config) ->
+ DefaultReceiveFun =
+ fun(ConnectionRef, ChannelId, _Expect, _ExpectType) ->
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId, ExpectType, Expect}} ->
+ ok
+ after 5000 ->
+ receive
+ Other ->
+ ct:log("Received other:~n~p~nExpected: ~p~n",
+ [Other,
+ {ssh_cm, ConnectionRef,
+ {data, ChannelId, ExpectType, Expect}}]),
+ %% {data, '_ChannelId', ExpectType, Expect}}]),
+ ct:fail("Unexpected response")
+ after 0 ->
+ ct:fail("Exec Timeout")
+ end
+ end
+ end,
+ do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, DefaultReceiveFun, Config).
+
+do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, ReceiveFun, Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
file:make_dir(UserDir),
@@ -1154,26 +1212,64 @@
{user_interaction, true},
{user_dir, UserDir}]),
- {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+ {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
+ success = ssh_connection:exec(ConnectionRef, ChannelId, Command, infinity),
+ ReceiveFun(ConnectionRef, ChannelId, Expect, ExpectType),
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid).
- success = ssh_connection:exec(ConnectionRef, ChannelId0, Command, infinity),
+%%--------------------------------------------------------------------
+%% Issue GH-8223
+trap_exit_connect(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey),
+ file:make_dir(UserDir),
+ SysDir = proplists:get_value(data_dir, Config),
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, UserDir},
+ {password, "morot"}]),
+ %% Fake an EXIT message
+ ExitMsg = {'EXIT', self(), make_ref()},
+ self() ! ExitMsg,
+
+ {ok, ConnectionRef} = ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {save_accepted_host, false},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, true},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid),
+ %% Ensure the EXIT message is still there
receive
- {ssh_cm, ConnectionRef, {data, _ChannelId, ExpectType, Expect}} ->
- ok
- after 5000 ->
- receive
- Other ->
- ct:log("Received other:~n~p~nExpected: ~p~n",
- [Other, {ssh_cm, ConnectionRef, {data, '_ChannelId', ExpectType, Expect}} ]),
- ct:fail("Unexpected response")
- after 0 ->
- ct:fail("Exec Timeout")
- end
- end,
+ ExitMsg -> ok
+ after 0 ->
+ ct:fail("No EXIT message")
+ end.
- ssh:close(ConnectionRef),
- ssh:stop_daemon(Pid).
+%%--------------------------------------------------------------------
+%% Issue GH-8223
+trap_exit_daemon(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey),
+ file:make_dir(UserDir),
+ SysDir = proplists:get_value(data_dir, Config),
+
+ %% Fake an EXIT message
+ ExitMsg = {'EXIT', self(), make_ref()},
+ self() ! ExitMsg,
+
+ {ok, DaemonRef} = ssh:daemon(0, [{system_dir, SysDir},
+ {user_dir, UserDir}]),
+ ssh:stop_daemon(DaemonRef),
+
+ %% Ensure the EXIT message is still there
+ receive
+ ExitMsg -> ok
+ after 0 ->
+ ct:fail("No EXIT message")
+ end.
%%--------------------------------------------------------------------
start_shell_sock_exec_fun(Config) when is_list(Config) ->
@@ -1417,6 +1513,8 @@
end.
kex_error(Config) ->
+ #{level := Level} = logger:get_primary_config(),
+ ok = logger:set_primary_config(level, debug),
PrivDir = proplists:get_value(priv_dir, Config),
UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
file:make_dir(UserDir),
@@ -1437,6 +1535,10 @@
ok % Other msg
end,
self()),
+ Cleanup = fun() ->
+ ok = logger:remove_handler(kex_error),
+ ok = logger:set_primary_config(level, Level)
+ end,
try
ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
{user, "foo"},
@@ -1454,7 +1556,7 @@
%% ok
receive
{Ref, ErrMsgTxt} ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:log("ErrMsgTxt = ~n~s", [ErrMsgTxt]),
Lines = lists:map(fun string:trim/1, string:tokens(ErrMsgTxt, "\n")),
OK = (lists:all(fun(S) -> lists:member(S,Lines) end,
@@ -1472,12 +1574,12 @@
ct:fail("unexpected error text msg", [])
end
after 20000 ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:fail("timeout", [])
end;
error:{badmatch,{error,_}} ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:fail("unexpected error msg", [])
end.
@@ -1729,41 +1831,191 @@
ssh:close(ConnectionRef),
ssh:stop_daemon(Pid).
+handler_down_before_open(Config) ->
+ %% Start echo subsystem with a delay in init() - until a signal is received
+ %% One client opens a channel on the connection
+ %% the other client requests the echo subsystem on the second channel and then immediately goes down
+ %% the test monitors the client and when receiving 'DOWN' signals 'echo' to proceed
+ %% a) there should be no crash after 'channel-open-confirmation'
+ %% b) there should be proper 'channel-close' exchange
+ %% c) the 'exec' channel should not be affected after the 'echo' channel goes down
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = proplists:get_value(data_dir, Config),
+ Parent = self(),
+ EchoSS_spec = {ssh_echo_server, [8, [{dbg, true}, {parent, Parent}]]},
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, UserDir},
+ {password, "morot"},
+ {exec, fun ssh_exec_echo/1},
+ {subsystems, [{"echo_n",EchoSS_spec}]}]),
+ ct:log("~p:~p connect", [?MODULE,?LINE]),
+ ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ct:log("~p:~p connected", [?MODULE,?LINE]),
+
+ ExecChannelPid =
+ spawn(
+ fun() ->
+ {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+
+ %% This is to get peer's connection handler PID ({conn_peer ...} below) and suspend it
+ {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity),
+ ssh_connection:subsystem(ConnectionRef, ChannelId1, "echo_n", infinity),
+ ssh_connection:close(ConnectionRef, ChannelId1),
+ receive
+ {ssh_cm, ConnectionRef, {closed, 1}} -> ok
+ end,
+
+ Parent ! {self(), channelId, ChannelId0},
+ Result = receive
+ cmd ->
+ ct:log("~p:~p Channel ~p executing", [?MODULE, ?LINE, ChannelId0]),
+ success = ssh_connection:exec(ConnectionRef, ChannelId0, "testing", infinity),
+ Expect = <<"echo testing\n">>,
+ ExpSz = size(Expect),
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId0, 0,
+ <<Expect:ExpSz/binary, _/binary>>}} = R ->
+ ct:log("~p:~p Got expected ~p",[?MODULE,?LINE, R]),
+ ok;
+ Other ->
+ ct:log("~p:~p Got unexpected ~p~nExpect: ~p~n",
+ [?MODULE,?LINE, Other, {ssh_cm, ConnectionRef,
+ {data, ChannelId0, 0, Expect}}]),
+ {fail, "Unexpected data"}
+ after 5000 ->
+ {fail, "Exec Timeout"}
+ end;
+ stop -> {fail, "Stopped"}
+ end,
+ Parent ! {self(), Result}
+ end),
+ try
+ receive
+ {ExecChannelPid, channelId, ExId} ->
+ ct:log("~p:~p Channel that should stay: ~p pid ~p",
+ [?MODULE, ?LINE, ExId, ExecChannelPid]),
+ %% This is sent by the echo subsystem as a reaction to channel1 above
+ ConnPeer = receive {conn_peer, CM} -> CM end,
+ %% The sole purpose of this channel is to go down
+ %% before the opening procedure is complete
+ DownChannelPid = spawn(
+ fun() ->
+ ct:log("~p:~p open channel (incomplete)",[?MODULE,?LINE]),
+ Parent ! {self(), channelId, ok},
+ %% This is to prevent the peer from answering our 'channel-open' in time
+ sys:suspend(ConnPeer),
+ {ok, _} = ssh_connection:session_channel(ConnectionRef, infinity)
+ end),
+ MonRef = erlang:monitor(process, DownChannelPid),
+ receive
+ {DownChannelPid, channelId, ok} ->
+ ct:log("~p:~p Channel handler that won't continue: pid ~p",
+ [?MODULE, ?LINE, DownChannelPid]),
+ ensure_channels(ConnectionRef, 2),
+ channel_down_sequence(DownChannelPid, ExecChannelPid,
+ ExId, MonRef, ConnectionRef, ConnPeer)
+ end
+ end,
+ ensure_channels(ConnectionRef, 0)
+ after
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid)
+ end.
+
+ensure_channels(ConnRef, Expected) ->
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, Expected, length(ChannelList)).
+
+do_ensure_channels(_ConnRef, NumExpected, NumExpected) ->
+ ok;
+do_ensure_channels(ConnRef, NumExpected, _ChannelListLen) ->
+ ct:sleep(100),
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, NumExpected, length(ChannelList)).
+
+channel_down_sequence(DownChannelPid, ExecChannelPid, ExecChannelId, MonRef, ConnRef, Peer) ->
+ ct:log("~p:~p sending order to ~p to go down", [?MODULE, ?LINE, DownChannelPid]),
+ exit(DownChannelPid, die),
+ receive {'DOWN', MonRef, _, _, _} -> ok end,
+ ct:log("~p:~p order executed, sending order to ~p to proceed", [?MODULE, ?LINE, Peer]),
+ %% Resume the peer connection to let it clean up among its channels
+ sys:resume(Peer),
+ ensure_channels(ConnRef, 1),
+ ExecChannelPid ! cmd,
+ try
+ receive
+ {ExecChannelPid, ok} ->
+ ct:log("~p:~p expected exec result: ~p", [?MODULE, ?LINE, ok]),
+ ok;
+ {ExecChannelPid, Result} ->
+ ct:log("~p:~p Unexpected exec result: ~p", [?MODULE, ?LINE, Result]),
+ {fail, "Unexpected exec result"}
+ after 5000 ->
+ {fail, "Exec result timeout"}
+ end
+ after
+ ssh_connection:close(ConnRef, ExecChannelId)
+ end.
+
%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
-
do_simple_exec(ConnectionRef) ->
+ do_simple_exec(ConnectionRef, 1).
+
+do_simple_exec(ConnectionRef, N) ->
{ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
- success = ssh_connection:exec(ConnectionRef, ChannelId0,
- "echo testing", infinity),
- %% receive response to input
- receive
- {ssh_cm, ConnectionRef, {data, ChannelId0, 0, <<"testing\n">>}} ->
- ok
- after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ Cmd = "yes testing | head -n " ++ integer_to_list(N),
+ ct:log("Cmd to be invoked over SSH shell: ~p", [Cmd]),
+ success = ssh_connection:exec(ConnectionRef, ChannelId0, Cmd, infinity),
+ ExpectedBin = <<"testing\n">>,
+ case N of
+ 1 ->
+ %% receive response to input
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId0, 0, ExpectedBin}} ->
+ ok
+ after
+ 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ end;
+ _ ->
+ receive_bytes(ConnectionRef, ChannelId0, N * byte_size(ExpectedBin), 0)
end,
-
%% receive close messages
- receive
- {ssh_cm, ConnectionRef, {eof, ChannelId0}} ->
- ok
- after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
- end,
- receive
- {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} ->
- ok
+ CloseMessages =
+ [{ssh_cm, ConnectionRef, {eof, ChannelId0}},
+ {ssh_cm, ConnectionRef, {closed, ChannelId0}}],
+ Timeout = 10000,
+ [receive
+ M ->
+ ct:log("Received M = ~w", [M]),
+ ok
+ after
+ Timeout ->
+ ct:log("M = ~w not found !", [M]),
+ ct:log("Messages in queue =~n~p", [process_info(self(), messages)]),
+ ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ end || M <- CloseMessages],
+ receive
+ %% 141 is exit status of `yes testing | head -n 1` on tcsh
+ %% other shells return 0
+ ExitMsg = {ssh_cm, ConnectionRef, {exit_status, ChannelId0, ExitStatus}}
+ when ExitStatus == 0; ExitStatus == 141 ->
+ ct:log("Received M = ~w", [ExitMsg]),
+ ok
after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ Timeout ->
+ ct:log("Acceptable exit status not received"),
+ ct:log("Messages in queue =~n~p", [process_info(self(), messages)]),
+ ct:fail("timeout ~p:~p",[?MODULE,?LINE])
end,
- receive
- {ssh_cm, ConnectionRef,{closed, ChannelId0}} ->
- ok
- after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
- end.
+ ok.
%%--------------------------------------------------------------------
@@ -1866,6 +2118,7 @@
{ssh_cm, ConnectionRef, {data, ChannelId, 0, <<Expect:ExpSz/binary, _/binary>>}} = R ->
ct:log("~p:~p Got expected ~p",[?MODULE,?LINE,R]);
Other ->
+ %% FIXME - should this testcase fail when unexpected data is received?
ct:log("~p:~p Got unexpected ~p~nExpect: ~p~n",
[?MODULE,?LINE, Other, {ssh_cm, ConnectionRef, {data, ChannelId, 0, Expect}} ])
after 5000 ->
@@ -1879,8 +2132,6 @@
big_cat_rx(ConnectionRef, ChannelId, Acc) ->
receive
{ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} ->
- %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)),
- %% window was pre-adjusted, don't adjust again here
big_cat_rx(ConnectionRef, ChannelId, [Data | Acc]);
{ssh_cm, ConnectionRef, {eof, ChannelId}} ->
{ok, iolist_to_binary(lists:reverse(Acc))}
@@ -1889,7 +2140,8 @@
end.
collect_data(ConnectionRef, ChannelId, EchoSize) ->
- ct:log("~p:~p Listener ~p running! ConnectionRef=~p, ChannelId=~p",[?MODULE,?LINE,self(),ConnectionRef,ChannelId]),
+ ct:log("~p:~p Listener ~p running! ConnectionRef=~p, ChannelId=~p",
+ [?MODULE,?LINE,self(),ConnectionRef,ChannelId]),
collect_data(ConnectionRef, ChannelId, EchoSize, [], 0).
collect_data(ConnectionRef, ChannelId, EchoSize, Acc, Sum) ->
@@ -1898,18 +2150,15 @@
{ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} when is_binary(Data) ->
ct:log("~p:~p collect_data: received ~p bytes. total ~p bytes, want ~p more",
[?MODULE,?LINE,size(Data),Sum+size(Data),EchoSize-Sum]),
- ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)),
+ ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)),
collect_data(ConnectionRef, ChannelId, EchoSize, [Data | Acc], Sum+size(Data));
{ssh_cm, ConnectionRef, Msg={eof, ChannelId}} ->
collect_data_report_end(Acc, Msg, EchoSize);
-
{ssh_cm, ConnectionRef, Msg={closed,ChannelId}} ->
collect_data_report_end(Acc, Msg, EchoSize);
-
Msg ->
ct:log("~p:~p collect_data: ***** unexpected message *****~n~p",[?MODULE,?LINE,Msg]),
collect_data(ConnectionRef, ChannelId, EchoSize, Acc, Sum)
-
after TO ->
ct:log("~p:~p collect_data: ----- Nothing received for ~p seconds -----~n",[?MODULE,?LINE,TO]),
collect_data(ConnectionRef, ChannelId, EchoSize, Acc, Sum)
@@ -1953,3 +2202,24 @@
spawn(fun() ->
io:format("echo ~s ~s\n",[User,Cmd])
end).
+%% FIXME - upon refactoring this test suite, check if function below is reduntant to collect_data
+receive_bytes(_, _, 0, _) ->
+ ct:log("ALL DATA RECEIVED Budget = 0"),
+ ct:log("================================ ExpectBudget = 0 (reception completed)"),
+ ok;
+receive_bytes(ConnectionRef, ChannelId0, Budget, AccSize) when Budget > 0 ->
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId0, 0, D}} ->
+ Fmt = "================================ ExpectBudget = "
+ "~p bytes Received/Total = ~p/~p bytes",
+ Args = [Budget, byte_size(D), AccSize + byte_size(D)],
+ ct:log(Fmt, Args),
+ ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(D)),
+ receive_bytes(ConnectionRef, ChannelId0,
+ Budget - byte_size(D), AccSize + byte_size(D))
+ after
+ 10000 ->
+ ct:log("process_info(self(), messages) = ~p",
+ [process_info(self(), messages)]),
+ ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ end.
diff -ruN a/lib/ssh/test/ssh.cover b/lib/ssh/test/ssh.cover
--- a/lib/ssh/test/ssh.cover 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh.cover 2025-12-17 17:25:02.078556443 +1030
@@ -7,7 +7,7 @@
%% %% Supervisors
%% ssh_acceptor_sup, ssh_channel_sup,
- %% sshc_sup, sshd_sup, ssh_subsystem_sup, ssh_sup,
+ %% sshc_sup, sshd_sup, ssh_connection_sup, ssh_sup,
%% ssh_system_sup, ssh_tcpip_forward_acceptor_sup,
%% Test and/or info modules:
diff -ruN a/lib/ssh/test/ssh_dbg_SUITE.erl b/lib/ssh/test/ssh_dbg_SUITE.erl
--- a/lib/ssh/test/ssh_dbg_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_dbg_SUITE.erl 2025-12-17 17:25:02.079556456 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2018-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2018-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -336,6 +336,12 @@
?DBG_RECEIVE("Received SSH_MSG_KEXINIT:", Ref, C, Pid),
case atom_to_list( (ssh_connection_handler:alg(C))#alg.kex ) of
+ "curve"++_ ->
+ ?DBG_RECEIVE("Going to send SSH_MSG_KEX_ECDH_INIT:", Ref, C, Pid),
+ ?DBG_RECEIVE("Received SSH_MSG_KEX_ECDH_INIT:", Ref, D, Pid),
+ ?DBG_RECEIVE("Going to send SSH_MSG_KEX_ECDH_REPLY:", Ref, D, Pid),
+ ?DBG_RECEIVE("Received SSH_MSG_KEX_ECDH_REPLY:", Ref, C, Pid);
+
"ecdh-"++_ ->
?DBG_RECEIVE("Going to send SSH_MSG_KEX_ECDH_INIT:", Ref, C, Pid),
?DBG_RECEIVE("Received SSH_MSG_KEX_ECDH_INIT:", Ref, D, Pid),
diff -ruN a/lib/ssh/test/ssh_echo_server.erl b/lib/ssh/test/ssh_echo_server.erl
--- a/lib/ssh/test/ssh_echo_server.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_echo_server.erl 2025-12-17 17:25:02.079556456 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -27,7 +27,8 @@
n,
id,
cm,
- dbg = false
+ dbg = false,
+ parent
}).
-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]).
@@ -42,13 +43,19 @@
{ok, #state{n = N}};
init([N,Opts]) ->
State = #state{n = N,
- dbg = proplists:get_value(dbg,Opts,false)
+ dbg = proplists:get_value(dbg,Opts,false),
+ parent = proplists:get_value(parent, Opts)
},
?DBG(State, "init([~p])",[N]),
{ok, State}.
handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, State) ->
?DBG(State, "ssh_channel_up Cid=~p ConnMngr=~p",[ChannelId,ConnectionManager]),
+ Pid = State#state.parent,
+ if Pid /= undefined ->
+ Pid ! {conn_peer, ConnectionManager};
+ true -> ok
+ end,
{ok, State#state{id = ChannelId,
cm = ConnectionManager}}.
diff -ruN a/lib/ssh/test/ssh_limited.cover b/lib/ssh/test/ssh_limited.cover
--- a/lib/ssh/test/ssh_limited.cover 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_limited.cover 2025-12-17 17:25:02.080556468 +1030
@@ -9,7 +9,7 @@
%% Supervisors
ssh_acceptor_sup, ssh_channel_sup,
- sshc_sup, sshd_sup, ssh_subsystem_sup, ssh_sup,
+ sshc_sup, sshd_sup, ssh_connection_sup, ssh_sup,
ssh_system_sup, ssh_tcpip_forward_acceptor_sup,
%% Test and/or info modules:
diff -ruN a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
--- a/lib/ssh/test/ssh_options_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_options_SUITE.erl 2025-12-22 23:16:56.269839023 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2022. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -88,7 +88,8 @@
daemon_replace_options_simple/1,
daemon_replace_options_algs/1,
daemon_replace_options_algs_connect/1,
- daemon_replace_options_algs_conf_file/1
+ daemon_replace_options_algs_conf_file/1,
+ daemon_replace_options_not_found/1
]).
%%% Common test callbacks
@@ -159,6 +160,7 @@
daemon_replace_options_algs,
daemon_replace_options_algs_connect,
daemon_replace_options_algs_conf_file,
+ daemon_replace_options_not_found,
{group, hardening_tests}
].
@@ -2033,6 +2035,14 @@
end.
%%--------------------------------------------------------------------
+daemon_replace_options_not_found(_Config) ->
+ %% when the daemon doesn't exist the error should be the same
+ %% in daemon_info and daemon_replace_options
+ %% which is {error, bad_daemon_ref}
+ Error = ssh:daemon_info(self()),
+ Error = ssh:daemon_replace_options(self(), []).
+
+%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
diff -ruN a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl
--- a/lib/ssh/test/ssh_protocol_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_protocol_SUITE.erl 2025-12-22 23:16:56.270839035 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
-include_lib("kernel/include/inet.hrl").
-include("ssh.hrl"). % ?UINT32, ?BYTE, #ssh{} ...
-include("ssh_transport.hrl").
+-include("ssh_connect.hrl").
-include("ssh_auth.hrl").
-include("ssh_test_lib.hrl").
@@ -48,6 +49,7 @@
bad_service_name_then_correct/1,
bad_very_long_service_name/1,
client_handles_keyboard_interactive_0_pwds/1,
+ client_handles_banner_keyboard_interactive/1,
client_info_line/1,
do_gex_client_init/3,
do_gex_client_init_old/3,
@@ -55,7 +57,10 @@
ext_info_c/1,
ext_info_s/1,
kex_strict_negotiated/1,
- kex_strict_msg_ignore/1,
+ kex_strict_violation_key_exchange/1,
+ kex_strict_violation_new_keys/1,
+ kex_strict_violation/1,
+ kex_strict_violation_2/1,
kex_strict_msg_unknown/1,
gex_client_init_option_groups/1,
gex_client_init_option_groups_file/1,
@@ -73,6 +78,8 @@
modify_rm/1,
no_common_alg_client_disconnects/1,
no_common_alg_server_disconnects/1,
+ custom_kexinit/1,
+ early_rce/1,
no_ext_info_s1/1,
no_ext_info_s2/1,
packet_length_too_large/1,
@@ -80,7 +87,9 @@
preferred_algorithms/1,
service_name_length_too_large/1,
service_name_length_too_short/1,
- client_close_after_hello/1
+ client_close_after_hello/1,
+ channel_close_timeout/1,
+ extra_ssh_msg_service_request/1
]).
-define(NEWLINE, <<"\r\n">>).
@@ -94,11 +103,19 @@
[{client2server,Ciphs}, {server2client,Ciphs}]
end)()
).
-
-
-define(v(Key, Config), proplists:get_value(Key, Config)).
-define(v(Key, Config, Default), proplists:get_value(Key, Config, Default)).
-
+-define(HARDCODED_KEXDH_REPLY,
+ #ssh_msg_kexdh_reply{
+ public_host_key = {{{'ECPoint',<<73,72,235,162,96,101,154,59,217,114,123,192,96,105,250,29,214,76,60,63,167,21,221,118,246,168,152,2,7,172,137,125>>},
+ {namedCurve,{1,3,101,112}}},
+ 'ssh-ed25519'},
+ f = 18504393053016436370762156176197081926381112956345797067569792020930728564439992620494295053804030674742529174859108487694089045521619258420515443400605141150065440678508889060925968846155921972385560196703381004650914261218463420313738628465563288022895912907728767735629532940627575655703806353550720122093175255090704443612257683903495753071530605378193139909567971489952258218767352348904221407081210633467414579377014704081235998044497191940270966762124544755076128392259615566530695493013708460088312025006678879288856957348606386230195080105197251789635675011844976120745546472873505352732719507783227210178188,
+ h_sig = <<90,247,44,240,136,196,82,215,56,165,53,33,230,101,253,
+ 34,112,201,21,131,162,169,10,129,174,14,69,25,39,174,
+ 92,210,130,249,103,2,215,245,7,213,110,235,136,134,11,
+ 124,248,139,79,17,225,77,125,182,204,84,137,167,99,186,
+ 167,42,192,10>>}).
%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
@@ -111,6 +128,7 @@
all() ->
[{group,tool_tests},
client_info_line,
+ early_rce,
{group,kex},
{group,service_requests},
{group,authentication},
@@ -118,7 +136,8 @@
{group,field_size_error},
{group,ext_info},
{group,preferred_algorithms},
- {group,client_close_early}
+ {group,client_close_early},
+ {group,channel_close}
].
groups() ->
@@ -129,11 +148,10 @@
]},
{packet_size_error, [], [packet_length_too_large,
packet_length_too_short]},
-
{field_size_error, [], [service_name_length_too_large,
service_name_length_too_short]},
-
- {kex, [], [no_common_alg_server_disconnects,
+ {kex, [], [custom_kexinit,
+ no_common_alg_server_disconnects,
no_common_alg_client_disconnects,
gex_client_init_option_groups,
gex_server_gex_limit,
@@ -142,15 +160,20 @@
gex_client_old_request_exact,
gex_client_old_request_noexact,
kex_strict_negotiated,
- kex_strict_msg_ignore,
+ kex_strict_violation_key_exchange,
+ kex_strict_violation_new_keys,
+ kex_strict_violation,
+ kex_strict_violation_2,
kex_strict_msg_unknown]},
{service_requests, [], [bad_service_name,
bad_long_service_name,
bad_very_long_service_name,
empty_service_name,
- bad_service_name_then_correct
+ bad_service_name_then_correct,
+ extra_ssh_msg_service_request
]},
- {authentication, [], [client_handles_keyboard_interactive_0_pwds
+ {authentication, [], [client_handles_keyboard_interactive_0_pwds,
+ client_handles_banner_keyboard_interactive
]},
{ext_info, [], [no_ext_info_s1,
no_ext_info_s2,
@@ -163,8 +186,8 @@
modify_rm,
modify_combo
]},
- {client_close_early, [], [client_close_after_hello
- ]}
+ {client_close_early, [], [client_close_after_hello]},
+ {channel_close, [], [channel_close_timeout]}
].
@@ -174,7 +197,8 @@
end_per_suite(Config) ->
stop_apps(Config).
-init_per_testcase(no_common_alg_server_disconnects, Config) ->
+init_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects;
+ Tc == custom_kexinit ->
start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']},
{cipher,?DEFAULT_CIPHERS}
]}]);
@@ -220,7 +244,8 @@
init_per_testcase(_TestCase, Config) ->
check_std_daemon_works(Config, ?LINE).
-end_per_testcase(no_common_alg_server_disconnects, Config) ->
+end_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects;
+ Tc == custom_kexinit ->
stop_std_daemon(Config);
end_per_testcase(kex_strict_negotiated, Config) ->
Config;
@@ -381,6 +406,90 @@
]
).
+early_rce(Config) ->
+ {ok,InitialState} =
+ ssh_trpt_test_lib:exec([{set_options, [print_ops, print_seqnums, print_messages]}]),
+ TypeOpen = "session",
+ ChannelId = 0,
+ WinSz = 425984,
+ PktSz = 65536,
+ DataOpen = <<>>,
+ SshMsgChannelOpen = ssh_connection:channel_open_msg(TypeOpen, ChannelId, WinSz, PktSz, DataOpen),
+
+ Id = 0,
+ TypeReq = "exec",
+ WantReply = true,
+ DataReq = <<?STRING(<<"lists:seq(1,10).">>)>>,
+ SshMsgChannelRequest =
+ ssh_connection:channel_request_msg(Id, TypeReq, WantReply, DataReq),
+ {ok, _AfterKexState} =
+ ssh_trpt_test_lib:exec(
+ [{connect,
+ server_host(Config),server_port(Config),
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {silently_accept_hosts, true},
+ {recv_ext_info, false},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false}
+ | proplists:get_value(extra_options,Config,[])]},
+ receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, SshMsgChannelOpen},
+ {send, SshMsgChannelRequest},
+ {match, disconnect(), receive_msg}
+ ], InitialState),
+ ok.
+
+custom_kexinit(Config) ->
+ %% 16#C0 value causes unicode:characters_to_list to return a big error value
+ Trash = lists:duplicate(260_000, 16#C0),
+ FunnyAlg = "curve25519-sha256",
+ KexInit =
+ #ssh_msg_kexinit{cookie = <<"Ã/Ï!9zñKá:ñÀv¿JÜ">>,
+ kex_algorithms =
+ [FunnyAlg ++ Trash],
+ server_host_key_algorithms = ["ssh-rsa"],
+ encryption_algorithms_client_to_server =
+ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"],
+ encryption_algorithms_server_to_client =
+ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"],
+ mac_algorithms_client_to_server =
+ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com",
+ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"],
+ mac_algorithms_server_to_client =
+ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com",
+ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"],
+ compression_algorithms_client_to_server = ["none","zlib@openssh.com","zlib"],
+ compression_algorithms_server_to_client = ["none","zlib@openssh.com","zlib"],
+ languages_client_to_server = [],
+ languages_server_to_client = [],
+ first_kex_packet_follows = false,
+ reserved = 0
+ },
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, {print_messages,detail}]},
+ {connect,
+ server_host(Config),server_port(Config),
+ [{silently_accept_hosts, true},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false},
+ {preferred_algorithms,[{public_key,['ssh-rsa']},
+ {cipher,?DEFAULT_CIPHERS}
+ ]}
+ ]},
+ receive_hello,
+ {send, hello},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, KexInit}, % with server unsupported 'ssh-dss' !
+ {match, disconnect(), receive_msg}
+ ]
+ ).
+
%%--------------------------------------------------------------------
%%% Algo negotiation fail. This should result in a ssh_msg_disconnect
%%% being sent from the client.
@@ -689,7 +798,82 @@
]}]
).
+%%%--------------------------------------------------------------------
+%%% SSH_MSG_USERAUTH_BANNER can be sent at any time during user auth.
+%%% The following test mimics a SSH server implementation that sends the banner
+%%% immediately before sending SSH_MSG_USERAUTH_SUCCESS.
+client_handles_banner_keyboard_interactive(Config) ->
+ {User,_Pwd} = server_user_password(Config),
+ %% Create a listening socket as server socket:
+ {ok,InitialState} = ssh_trpt_test_lib:exec(listen),
+ HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+
+ %% Start a process handling one connection on the server side:
+ spawn_link(
+ fun() ->
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_messages]},
+ {accept, [{system_dir, system_dir(Config)},
+ {user_dir, user_dir(Config)}]},
+ receive_hello,
+ {send, hello},
+
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+
+ {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_reply},
+
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+
+ {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="none",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_failure{authentications = "keyboard-interactive",
+ partial_success = false}},
+
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="keyboard-interactive",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_info_request{name = "",
+ instruction = "",
+ language_tag = "",
+ num_prompts = 1,
+ data = <<0,0,0,10,80,97,115,115,119,111,114,100,58,32,0>>
+ }},
+ {match, #ssh_msg_userauth_info_response{num_responses = 1,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_info_request{name = "",
+ instruction = "",
+ language_tag = "",
+ num_prompts = 0,
+ data = <<>>
+ }},
+ {match, #ssh_msg_userauth_info_response{num_responses = 0,
+ data = <<>>,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_banner{message = "Banner\n"}},
+ {send, #ssh_msg_userauth_success{}},
+ close_socket,
+ print_state
+ ],
+ InitialState)
+ end),
+
+ %% and finally connect to it with a regular Erlang SSH client:
+ {ok,_} = std_connect(HostPort, Config,
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]}]
+ ).
%%%--------------------------------------------------------------------
client_info_line(Config) ->
@@ -828,49 +1012,199 @@
%%%--------------------------------------------------------------------
%%%
kex_strict_negotiated(Config0) ->
- {ok,Pid} = ssh_test_lib:add_report_handler(),
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
Config = start_std_daemon(Config0, []),
{Server, Host, Port} = proplists:get_value(server, Config),
- #{level := Level} = logger:get_primary_config(),
- logger:set_primary_config(level, notice),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
{ok, ConnRef} = std_connect({Host, Port}, Config, []),
- {algorithms, A} = ssh:connection_info(ConnRef, algorithms),
+ {algorithms, _A} = ssh:connection_info(ConnRef, algorithms),
ssh:stop_daemon(Server),
- {ok, Reports} = ssh_test_lib:get_reports(Pid),
- ct:log("Reports = ~p", [Reports]),
- true = ssh_test_lib:kex_strict_negotiated(client, Reports),
- true = ssh_test_lib:kex_strict_negotiated(server, Reports),
- logger:set_primary_config(Level),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ true = ssh_test_lib:kex_strict_negotiated(client, Events),
+ true = ssh_test_lib:kex_strict_negotiated(server, Events),
+ ssh_test_lib:set_log_level(Level),
+ ssh_test_lib:rm_log_handler(),
ok.
-%% Connect to an erlang server and inject unexpected SSH ignore
-kex_strict_msg_ignore(Config) ->
- ct:log("START: ~p~n=================================", [?FUNCTION_NAME]),
- ExpectedReason = "strict KEX violation: unexpected SSH_MSG_IGNORE",
- TestMessages =
- [{send, ssh_msg_ignore},
- {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
- {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
- kex_strict_helper(Config, TestMessages, ExpectedReason).
+%% Connect to an erlang server and inject unexpected SSH message
+%% ssh_fsm_kexinit in key_exchange state
+kex_strict_violation_key_exchange(Config) ->
+ ExpectedReason = "KEX strict violation",
+ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
+ TestProcedure =
+ fun(M) ->
+ ct:log(
+ "=================== START: ~p Message: ~p Expected Fail =================================",
+ [?FUNCTION_NAME, M]),
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, M},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
+ end,
+ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
+ Msg <- Injections],
+ ct:log("========== END ========"),
+ ok.
+
+%% Connect to an erlang server and inject unexpected SSH message
+%% ssh_fsm_kexinit in new_keys state
+kex_strict_violation_new_keys(Config) ->
+ ExpectedReason = "KEX strict violation",
+ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
+ TestProcedure =
+ fun(M) ->
+ ct:log(
+ "=================== START: ~p Message: ~p Expected Fail =================================",
+ [?FUNCTION_NAME, M]),
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {send, M},
+ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
+ end,
+ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
+ Msg <- Injections],
+ ct:log("========== END ========"),
+ ok.
+
+%% Connect to an erlang server and inject unexpected SSH message
+%% duplicated KEXINIT
+kex_strict_violation(Config) ->
+ TestFlows =
+ [{kexinit, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexinit},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {ssh_msg_kexdh_init, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init_dup},
+ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {new_keys, "Message ssh_msg_newkeys in wrong state",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+ {send, #ssh_msg_newkeys{}},
+ {match, disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR), receive_msg}]},
+ {ssh_msg_unexpected_dh_gex, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ %% dh_alg is expected but dh_gex_alg is provided
+ {send, #ssh_msg_kex_dh_gex_request{min = 1000, n = 3000, max = 4000}},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {wrong_role, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ %% client should not send message below
+ {send, ?HARDCODED_KEXDH_REPLY},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}],
+ TestProcedure =
+ fun({Msg, _, P}) ->
+ ct:log(
+ "==== START: ~p (duplicated ~p) Expected Fail ====~n~p",
+ [?FUNCTION_NAME, Msg, P]),
+ P
+ end,
+ [kex_strict_helper(Config, TestProcedure(Procedure), Reason) ||
+ Procedure = {_, Reason, _} <- TestFlows],
+ ct:log("==== END ====="),
+ ok.
+
+kex_strict_violation_2(Config) ->
+ ExpectedReason = "KEX strict violation",
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
+ %% Connect and negotiate keys
+ {ok, InitialState} = ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_seqnums, print_messages]}]),
+ {ok, UpToUnexpectedKexDHReply} =
+ ssh_trpt_test_lib:exec(
+ [{connect,
+ server_host(Config),server_port(Config),
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {silently_accept_hosts, true},
+ {recv_ext_info, false},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false}
+ | proplists:get_value(extra_options,Config,[])
+ ]}] ++
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ %% client should not send message below
+ {send, ?HARDCODED_KEXDH_REPLY},
+ {match, {'or', [#ssh_msg_newkeys{_='_'},
+ disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED)]},
+ receive_msg}],
+ InitialState),
+ case ssh_trpt_test_lib:return_value(UpToUnexpectedKexDHReply) of
+ {ssh_msg_newkeys} ->
+ ct:log("1st flow - extra match for disconnect needed"),
+ ssh_trpt_test_lib:exec(
+ [{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
+ UpToUnexpectedKexDHReply);
+ _ ->
+ ct:log("2nd flow disconnect already received")
+ end,
+ ct:sleep(100),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ ssh_test_lib:rm_log_handler(),
+ ct:log("Events = ~p", [Events]),
+ true = ssh_test_lib:kex_strict_negotiated(client, Events),
+ true = ssh_test_lib:kex_strict_negotiated(server, Events),
+ true = ssh_test_lib:event_logged(server, Events, ExpectedReason),
+ ssh_test_lib:set_log_level(Level),
+ ok.
%% Connect to an erlang server and inject unexpected non-SSH binary
kex_strict_msg_unknown(Config) ->
ct:log("START: ~p~n=================================", [?FUNCTION_NAME]),
ExpectedReason = "Bad packet: Size",
TestMessages =
- [{send, ssh_msg_unknown},
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {send, ssh_msg_unknown},
{match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
kex_strict_helper(Config, TestMessages, ExpectedReason).
kex_strict_helper(Config, TestMessages, ExpectedReason) ->
- {ok,HandlerPid} = ssh_test_lib:add_report_handler(),
- #{level := Level} = logger:get_primary_config(),
- logger:set_primary_config(level, notice),
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
%% Connect and negotiate keys
{ok, InitialState} = ssh_trpt_test_lib:exec(
- [{set_options, [print_ops, print_seqnums, print_messages]}]
- ),
+ [{set_options, [print_ops, print_seqnums, print_messages]}]),
{ok, _AfterKexState} =
ssh_trpt_test_lib:exec(
[{connect,
@@ -883,21 +1217,17 @@
{user_dir, user_dir(Config)},
{user_interaction, false}
| proplists:get_value(extra_options,Config,[])
- ]},
- receive_hello,
- {send, hello},
- {send, ssh_msg_kexinit},
- {match, #ssh_msg_kexinit{_='_'}, receive_msg},
- {send, ssh_msg_kexdh_init}] ++
+ ]}] ++
TestMessages,
InitialState),
ct:sleep(100),
- {ok, Reports} = ssh_test_lib:get_reports(HandlerPid),
- ct:log("HandlerPid = ~p~nReports = ~p", [HandlerPid, Reports]),
- true = ssh_test_lib:kex_strict_negotiated(client, Reports),
- true = ssh_test_lib:kex_strict_negotiated(server, Reports),
- true = ssh_test_lib:event_logged(server, Reports, ExpectedReason),
- logger:set_primary_config(Level),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ ssh_test_lib:rm_log_handler(),
+ ct:log("Events = ~p", [Events]),
+ true = ssh_test_lib:kex_strict_negotiated(client, Events),
+ true = ssh_test_lib:kex_strict_negotiated(server, Events),
+ true = ssh_test_lib:event_logged(server, Events, ExpectedReason),
+ ssh_test_lib:set_log_level(Level),
ok.
%%%----------------------------------------------------------------
@@ -1054,6 +1384,44 @@
{fail, no_handshakers}
end.
+%%% Connect to an erlang server and pretend client sending extra
+%%% ssh_msg_service_request (Paramiko client behavior)
+extra_ssh_msg_service_request(Config) ->
+ %% Connect and negotiate keys
+ {ok,InitialState} = ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_seqnums, print_messages]}]
+ ),
+ {ok,AfterKexState} = connect_and_kex(Config, InitialState),
+ %% Do the authentcation
+ {User,Pwd} = server_user_password(Config),
+ UserAuthFlow =
+ fun(P) ->
+ [{send, #ssh_msg_service_request{name = "ssh-userauth"}},
+ {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_userauth_request{user = User,
+ service = "ssh-connection",
+ method = "password",
+ data = <<?BOOLEAN(?FALSE),
+ ?STRING(unicode:characters_to_binary(P))>>
+ }}]
+ end,
+ {ok,EndState} =
+ ssh_trpt_test_lib:exec(
+ UserAuthFlow("WRONG") ++
+ [{match, #ssh_msg_userauth_failure{_='_'}, receive_msg}] ++
+ UserAuthFlow(Pwd) ++
+ [{match, #ssh_msg_userauth_success{_='_'}, receive_msg}],
+ AfterKexState),
+ %% Disconnect
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{send, #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION,
+ description = "End of the fun",
+ language = ""
+ }},
+ close_socket
+ ], EndState),
+ ok.
%%%================================================================
%%%==== Internal functions ========================================
@@ -1220,6 +1588,84 @@
],
InitialState).
+channel_close_timeout(Config) ->
+ {User,_Pwd} = server_user_password(Config),
+ %% Create a listening socket as server socket:
+ {ok,InitialState} = ssh_trpt_test_lib:exec(listen),
+ HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+ %% Start a process handling one connection on the server side:
+ spawn_link(
+ fun() ->
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_messages]},
+ {accept, [{system_dir, system_dir(Config)},
+ {user_dir, user_dir(Config)},
+ {idle_time, 50000}]},
+ receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_reply},
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+ {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="none",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_failure{authentications = "password",
+ partial_success = false}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="password",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_success{}},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=0,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 0,
+ sender_channel = 0,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024
+ }},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=1,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 1,
+ sender_channel = 1,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024}},
+ {match, #ssh_msg_channel_close{recipient_channel = 0}, receive_msg},
+ {match, disconnect(), receive_msg},
+ print_state],
+ InitialState)
+ end),
+ %% connect to it with a regular Erlang SSH client:
+ ChannelCloseTimeout = 3000,
+ {ok, ConnRef} = std_connect(HostPort, Config,
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {channel_close_timeout, ChannelCloseTimeout},
+ {idle_time, 50000}
+ ]
+ ),
+ {ok, Channel0} = ssh_connection:session_channel(ConnRef, 50000),
+ {ok, _Channel1} = ssh_connection:session_channel(ConnRef, 50000),
+ %% Close the channel from client side, the server does not reply with 'channel-close'
+ %% After the timeout, the client should drop the cache entry
+ _ = ssh_connection:close(ConnRef, Channel0),
+ receive
+ after ChannelCloseTimeout + 1000 ->
+ {channels, Channels} = ssh:connection_info(ConnRef, channels),
+ ct:log("Channel entries ~p", [Channels]),
+ %% Only one channel entry should be present, the other one should be dropped
+ 1 = length(Channels),
+ ssh:close(ConnRef)
+ end.
%%%----------------------------------------------------------------
%%% For matching peer disconnection
@@ -1249,18 +1695,23 @@
Port, {AccP,AccC,AccH}) ->
ParentHandshakers =
[{PidW,PidH} ||
- {{ssh_acceptor_sup,{address,_,Port1,_}}, PidW, worker, [ssh_acceptor]} <-
- supervisor:which_children(PidS),
+ {{ssh_acceptor_sup,{address,_,Port1,_}}, PidW, worker,
+ [ssh_acceptor]} <- supervisor:which_children(PidS),
Port1 == Port,
PidH <- element(2, process_info(PidW,links)),
is_pid(PidH),
- process_info(PidH,current_function) == {current_function,{ssh_connection_handler,handshake,3}}],
+ process_info(PidH,current_function) ==
+ {current_function,
+ {ssh_connection_handler,handshake,4}}],
{Parents,Handshakers} = lists:unzip(ParentHandshakers),
find_handshake_parent(T, Port, {AccP++Parents, AccC, AccH++Handshakers});
-find_handshake_parent([{_Ref,PidS,supervisor,[ssh_subsystem_sup]}|T], Port, {AccP,AccC,AccH}) ->
+find_handshake_parent([{_Ref,PidS,supervisor,[ssh_connection_sup]}|T],
+ Port, {AccP,AccC,AccH}) ->
Connections =
- [Pid || {connection,Pid,worker,[ssh_connection_handler]} <- supervisor:which_children(PidS)],
+ [Pid ||
+ {connection,Pid,worker,[ssh_connection_handler]} <-
+ supervisor:which_children(PidS)],
find_handshake_parent(T, Port, {AccP, AccC++Connections, AccH});
find_handshake_parent([_|T], Port, Acc) ->
diff -ruN a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl
--- a/lib/ssh/test/ssh_sftpd_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_sftpd_SUITE.erl 2025-12-22 23:16:56.270839035 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2022. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@
open_file_dir_v6/1,
read_dir/1,
read_file/1,
+ max_path/1,
real_path/1,
relative_path/1,
relpath/1,
@@ -51,7 +52,6 @@
retrieve_attributes/1,
root_with_cwd/1,
set_attributes/1,
- sshd_read_file/1,
ver3_open_flags/1,
ver3_rename/1,
ver6_basic/1,
@@ -60,6 +60,7 @@
-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").
+-include_lib("stdlib/include/assert.hrl").
-include("ssh_xfer.hrl").
-include("ssh.hrl").
-include("ssh_test_lib.hrl").
@@ -71,9 +72,9 @@
-define(SSH_TIMEOUT, 10000).
-define(REG_ATTERS, <<0,0,0,0,1>>).
-define(UNIX_EPOCH, 62167219200).
-
--define(is_set(F, Bits),
- ((F) band (Bits)) == (F)).
+-define(MAX_HANDLES, 10).
+-define(MAX_PATH, 200).
+-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
@@ -86,6 +87,7 @@
[open_close_file,
open_close_dir,
read_file,
+ max_path,
read_dir,
write_file,
rename_file,
@@ -97,8 +99,7 @@
links,
ver3_rename,
ver3_open_flags,
- relpath,
- sshd_read_file,
+ relpath,
ver6_basic,
access_outside_root,
root_with_cwd,
@@ -180,7 +181,9 @@
{sftpd_vsn, 6}])],
ssh:daemon(0, [{subsystems, SubSystems}|Options]);
_ ->
- SubSystems = [ssh_sftpd:subsystem_spec([])],
+ SubSystems = [ssh_sftpd:subsystem_spec(
+ [{max_handles, ?MAX_HANDLES},
+ {max_path, ?MAX_PATH}])],
ssh:daemon(0, [{subsystems, SubSystems}|Options])
end,
@@ -316,33 +319,62 @@
read_file(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
+ {Cm, Channel} = proplists:get_value(sftp, Config),
+ [begin
+ R1 = req_id(),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
+ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ R2 = req_id(),
+ {ok, <<?SSH_FXP_DATA, ?UINT32(R2), ?UINT32(_Length), Data/binary>>, _} =
+ read_file(Handle, 100, 0, Cm, Channel, R2),
+ {ok, Data} = file:read_file(FileName)
+ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
+ ReqId = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
+ ?UINT32(MsgLen), Msg:MsgLen/binary,
+ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
+ open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ ct:log("Message: ~s", [Msg]),
+ ok.
- ReqId = 0,
+%%--------------------------------------------------------------------
+max_path(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ FileName = filename:join(PrivDir, "test.txt"),
{Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_file(FileName, Cm, Channel, ReqId,
+ %% verify max_path limit
+ LongFileName =
+ filename:join(PrivDir,
+ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
+ {ok, _} = file:copy(FileName, LongFileName),
+ ReqId1 = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH),
+ _/binary>>, _} =
+ open_file(LongFileName, Cm, Channel, ReqId1,
?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
-
- NewReqId = 1,
-
- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
- Data/binary>>, _} =
- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
-
- {ok, Data} = file:read_file(FileName).
+ ?SSH_FXF_OPEN_EXISTING).
%%--------------------------------------------------------------------
read_dir(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
{Cm, Channel} = proplists:get_value(sftp, Config),
- ReqId = 0,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_dir(PrivDir, Cm, Channel, ReqId),
- ok = read_dir(Handle, Cm, Channel, ReqId).
+ [begin
+ R1 = req_id(),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
+ open_dir(PrivDir, Cm, Channel, R1),
+ R2 = req_id(),
+ ok = read_dir(Handle, Cm, Channel, R2)
+ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
+ ReqId = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
+ ?UINT32(MsgLen), Msg:MsgLen/binary,
+ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
+ open_dir(PrivDir, Cm, Channel, ReqId),
+ ct:log("Message: ~s", [Msg]),
+ ok.
-%%--------------------------------------------------------------------
write_file(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
@@ -388,35 +420,33 @@
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
NewFileName = filename:join(PrivDir, "test1.txt"),
- ReqId = 0,
+ LongFileName =
+ filename:join(PrivDir,
+ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
{Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0),
-
- NewReqId = ReqId + 1,
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId),
- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6,
- ?SSH_FXP_RENAME_OVERWRITE),
-
- NewReqId1 = NewReqId + 1,
- file:copy(FileName, NewFileName),
-
- %% No overwrite
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1),
- ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6,
- ?SSH_FXP_RENAME_NATIVE),
-
- NewReqId2 = NewReqId1 + 1,
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2),
- ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6,
- ?SSH_FXP_RENAME_ATOMIC).
+ Version = 6,
+ [begin
+ case Action of
+ {Code, AFile, BFile, Flags} ->
+ ReqId = req_id(),
+ ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p",
+ [ReqId, Code, AFile, BFile, Flags]),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} =
+ rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags);
+ {file_copy, AFile, BFile} ->
+ {ok, _} = file:copy(AFile, BFile)
+ end
+ end ||
+ Action <-
+ [{?SSH_FX_OK, FileName, NewFileName, 0},
+ {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE},
+ {file_copy, FileName, NewFileName},
+ %% no overwrite
+ {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE},
+ {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC},
+ %% max_path
+ {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]],
+ ok.
%%--------------------------------------------------------------------
mk_rm_dir(Config) when is_list(Config) ->
@@ -644,27 +674,6 @@
Root = Path
end.
-%%--------------------------------------------------------------------
-sshd_read_file(Config) when is_list(Config) ->
- PrivDir = proplists:get_value(priv_dir, Config),
- FileName = filename:join(PrivDir, "test.txt"),
-
- ReqId = 0,
- {Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_file(FileName, Cm, Channel, ReqId,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
-
- NewReqId = 1,
-
- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
- Data/binary>>, _} =
- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
-
- {ok, Data} = file:read_file(FileName).
-%%--------------------------------------------------------------------
ver6_basic(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
%FileName = filename:join(PrivDir, "test.txt"),
@@ -728,25 +737,33 @@
FileName = "root_with_cwd.txt",
FilePath = filename:join(CWD, FileName),
ok = filelib:ensure_dir(FilePath),
- ok = file:write_file(FilePath ++ "0", <<>>),
- ok = file:write_file(FilePath ++ "1", <<>>),
- ok = file:write_file(FilePath ++ "2", <<>>),
{Cm, Channel} = proplists:get_value(sftp, Config),
- ReqId0 = 0,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId0), _Handle0/binary>>, _} =
- open_file(FileName ++ "0", Cm, Channel, ReqId0,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
- ReqId1 = 1,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId1), _Handle1/binary>>, _} =
- open_file("./" ++ FileName ++ "1", Cm, Channel, ReqId1,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
- ReqId2 = 2,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId2), _Handle2/binary>>, _} =
- open_file("/home/" ++ FileName ++ "2", Cm, Channel, ReqId2,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING).
+
+ %% repeat procedure to make sure uniq file handles are generated
+ FileHandles =
+ [begin
+ ReqIdStr = integer_to_list(ReqId),
+ ok = file:write_file(FilePath ++ ReqIdStr, <<>>),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+ open_file(FileName ++ ReqIdStr, Cm, Channel, ReqId,
+ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ Handle
+ end ||
+ ReqId <- lists:seq(0,2)],
+ ?assertEqual(length(FileHandles),
+ length(lists:uniq(FileHandles))),
+ %% create a gap in file handles
+ [_, MiddleHandle, _] = FileHandles,
+ close(MiddleHandle, 3, Cm, Channel),
+
+ %% check that gap in file handles is is re-used
+ GapReqId = 4,
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(GapReqId), MiddleHandle/binary>>, _} =
+ open_file(FileName ++ integer_to_list(1), Cm, Channel, GapReqId,
+ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ ok.
%%--------------------------------------------------------------------
relative_path(Config) when is_list(Config) ->
@@ -1078,3 +1095,12 @@
not_default_permissions() ->
8#600. %% User read-write-only
+
+req_id() ->
+ ReqId =
+ case get(req_id) of
+ undefined -> 0;
+ I -> I
+ end,
+ put(req_id, ReqId + 1),
+ ReqId.
diff -ruN a/lib/ssh/test/ssh_sftp_SUITE.erl b/lib/ssh/test/ssh_sftp_SUITE.erl
--- a/lib/ssh/test/ssh_sftp_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_sftp_SUITE.erl 2025-12-22 23:16:56.270839035 +1030
@@ -1,7 +1,7 @@
%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -55,6 +55,7 @@
pos_read/1,
pos_write/1,
position/1,
+ read_6GB/1,
read_crypto_tar/1,
read_dir/1,
read_file/1,
@@ -81,7 +82,9 @@
-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").
-include("ssh_test_lib.hrl").
- % Default timetrap timeout
+-include_lib("stdlib/include/assert.hrl").
+
+%% Default timetrap timeout
-define(default_timeout, test_server:minutes(1)).
%%--------------------------------------------------------------------
@@ -119,6 +122,7 @@
{unicode, [], [{group,erlang_server},
{group,openssh_server},
+ read_6GB,
sftp_nonexistent_subsystem
]},
@@ -228,24 +232,7 @@
[{peer, {fmt_host(HostX),PortX}}, {group, erlang_server}, {sftpd, Sftpd} | Config];
init_per_group(openssh_server, Config) ->
- ct:comment("Begin ~p",[grps(Config)]),
- Host = ssh_test_lib:hostname(),
- case (catch ssh_sftp:start_channel(Host,
- [{user_interaction, false},
- {silently_accept_hosts, true},
- {save_accepted_host, false}
- ])) of
- {ok, _ChannelPid, Connection} ->
- [{peer, {_HostName,{IPx,Portx}}}] = ssh:connection_info(Connection,[peer]),
- ssh:close(Connection),
- [{w2l, fun w2l/1},
- {peer, {fmt_host(IPx),Portx}}, {group, openssh_server} | Config];
- {error,"Key exchange failed"} ->
- {skip, "openssh server doesn't support the tested kex algorithm"};
- Other ->
- ct:log("No openssh server. Cause:~n~p~n",[Other]),
- {skip, "No openssh daemon (see log in testcase)"}
- end;
+ verify_openssh(Config);
init_per_group(remote_tar, Config) ->
ct:comment("Begin ~p",[grps(Config)]),
@@ -287,7 +274,18 @@
Config.
%%--------------------------------------------------------------------
-
+init_per_testcase(read_6GB, Config) ->
+ case verify_openssh(Config) of
+ Result = {skip, _} ->
+ Result;
+ _ ->
+ case os:type() of
+ {win32, _} ->
+ {skip, "/dev/zero not available on Windws"};
+ _ ->
+ init_per_testcase(read_6GB_prepare_openssh_server, Config)
+ end
+ end;
init_per_testcase(sftp_nonexistent_subsystem, Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
SysDir = proplists:get_value(data_dir, Config),
@@ -300,7 +298,6 @@
[{User, Passwd}]}
]),
[{sftpd, Sftpd} | Config];
-
init_per_testcase(version_option, Config0) ->
Config = prepare(Config0),
TmpConfig0 = lists:keydelete(watchdog, 1, Config),
@@ -320,7 +317,6 @@
]),
Sftp = {ChannelPid, Connection},
[{sftp,Sftp}, {watchdog, Dog} | TmpConfig];
-
init_per_testcase(Case, Config00) ->
Config0 = prepare(Config00),
Config1 = lists:keydelete(watchdog, 1, Config0),
@@ -332,11 +328,24 @@
undefined -> [];
Sz -> [{packet_size,Sz}]
end,
+ PrepareOpenSSHServer =
+ fun() ->
+ Host = ssh_test_lib:hostname(),
+ {ok, ChannelPid, Connection} =
+ ssh_sftp:start_channel(Host,
+ [{user_interaction, false},
+ {silently_accept_hosts, true},
+ {save_accepted_host, false}
+ | PktSzOpt
+ ]),
+ Sftp = {ChannelPid, Connection},
+ [{sftp, Sftp}, {watchdog, Dog} | Config2]
+ end,
Config =
case proplists:get_value(group,Config2) of
erlang_server ->
- {_,Host, Port} = proplists:get_value(sftpd, Config2),
- {ok, ChannelPid, Connection} =
+ {_,Host, Port} = proplists:get_value(sftpd, Config2),
+ {ok, ChannelPid, Connection} =
ssh_sftp:start_channel(Host, Port,
[{user, User},
{password, Passwd},
@@ -351,18 +360,10 @@
openssh_server when Case == links ->
{skip, "known bug in openssh"};
openssh_server ->
- Host = ssh_test_lib:hostname(),
- {ok, ChannelPid, Connection} =
- ssh_sftp:start_channel(Host,
- [{user_interaction, false},
- {silently_accept_hosts, true},
- {save_accepted_host, false}
- | PktSzOpt
- ]),
- Sftp = {ChannelPid, Connection},
- [{sftp, Sftp}, {watchdog, Dog} | Config2]
+ PrepareOpenSSHServer();
+ _ when Case == read_6GB_prepare_openssh_server ->
+ PrepareOpenSSHServer()
end,
-
case catch proplists:get_value(remote_tar,Config) of
%% The 'catch' is for the case of Config={skip,...}
true ->
@@ -563,26 +564,62 @@
retrieve_attributes(Config) when is_list(Config) ->
FileName = proplists:get_value(filename, Config),
SftpFileName = w2l(Config, FileName),
-
{Sftp, _} = proplists:get_value(sftp, Config),
{ok, FileInfo} = ssh_sftp:read_file_info(Sftp, SftpFileName),
{ok, NewFileInfo} = file:read_file_info(FileName),
-
- %% TODO comparison. There are some differences now is that ok?
- ct:log("SFTP: ~p FILE: ~p~n", [FileInfo, NewFileInfo]).
+ ct:log("ssh_sftp:read_file_info(~p): ~p~n"
+ "file:read_file_info(~p): ~p",
+ [SftpFileName, FileInfo, FileName, NewFileInfo]),
+ {ExpectedUid, ExpectedGid} =
+ case {os:type(), proplists:get_value(group,Config)} of
+ {{win32, _}, openssh_server} ->
+ %% Windows compiled Erlang is expected will return 0;
+ %% but when Erlang(Windows) client interacts with
+ %% OpenSSH server - value 1000 is received by client
+ %% over SFTP (because OpenSSH is compiled for Linux
+ %% and runs on WSL)
+ {1000, 1000};
+ _ ->
+ {FileInfo#file_info.uid, FileInfo#file_info.gid}
+ end,
+ ?assertEqual(ExpectedUid, NewFileInfo#file_info.uid),
+ ?assertEqual(ExpectedGid, NewFileInfo#file_info.gid),
+ ok.
%%--------------------------------------------------------------------
set_attributes(Config) when is_list(Config) ->
FileName = proplists:get_value(testfile, Config),
SftpFileName = w2l(Config, FileName),
-
{Sftp, _} = proplists:get_value(sftp, Config),
{ok,Fd} = file:open(FileName, write),
io:put_chars(Fd,"foo"),
- ok = ssh_sftp:write_file_info(Sftp, SftpFileName, #file_info{mode=8#400}),
- {error, eacces} = file:write_file(FileName, "hello again"),
- ok = ssh_sftp:write_file_info(Sftp, SftpFileName, #file_info{mode=8#600}),
- ok = file:write_file(FileName, "hello again").
+ TestWriting =
+ fun(FInfo) ->
+ ok = ssh_sftp:write_file_info(Sftp, SftpFileName,
+ FInfo#file_info{mode=8#400}),
+ {error, eacces} = file:write_file(FileName, "hello again"),
+ ok = ssh_sftp:write_file_info(Sftp, SftpFileName,
+ FInfo#file_info{mode=8#600}),
+ ok = file:write_file(FileName, "hello again")
+ end,
+ TestWriting(#file_info{}),
+ IsErlangServer =
+ fun() ->
+ TcGroupPath = proplists:get_value(tc_group_path, Config),
+ {_, Path} = lists:unzip(lists:flatten(TcGroupPath)),
+ lists:member(erlang_server, Path)
+ end,
+ case IsErlangServer() of
+ true ->
+ ct:log("Testing with writing a complete #file_info record"),
+ {ok, FileInfo} = file:read_file_info(SftpFileName),
+ TestWriting(FileInfo);
+ _ ->
+ %% with OpenSSH daemon started by other user above instruction end
+ %% up with permission denied
+ ok
+ end,
+ ok.
%%--------------------------------------------------------------------
file_owner_access(Config) when is_list(Config) ->
@@ -676,6 +713,29 @@
{ok, 1} = ssh_sftp:position(Sftp, Handle, cur),
{ok, "2"} = ssh_sftp:read(Sftp, Handle, 1).
+read_6GB(Config) when is_list(Config) ->
+ ct:timetrap(16*?default_timeout),
+ FileName = "/dev/zero",
+ SftpFileName = w2l(Config, FileName),
+ {SftpChannel, _ConnectionRef} = proplists:get_value(sftp, Config),
+ ChunkSize = 65535,
+ N = 100000,
+ {ok, Handle} = ssh_sftp:open(SftpChannel, SftpFileName, [read]),
+ ExpectedList = lists:duplicate(ChunkSize, 0),
+ [begin
+ MBTransferred = io_lib:format("~.2f", [I * ChunkSize / 1048576.0]),
+ case ssh_sftp:read(SftpChannel, Handle, ChunkSize, timer:minutes(1)) of
+ {ok, ExpectedList} ->
+ [ct:log("~n~s MB read~n", [MBTransferred]) || I rem 10000 == 0];
+ Result ->
+ ct:log("## After reading ~s MB~n## Unexpected result received = ~p",
+ [MBTransferred, Result]),
+ ct:fail(unexpected_reason)
+ end
+ end ||
+ I <- lists:seq(0, N)],
+ ok.
+
%%--------------------------------------------------------------------
pos_read(Config) when is_list(Config) ->
FileName = proplists:get_value(testfile, Config),
@@ -1234,4 +1294,22 @@
W2L = proplists:get_value(w2l, Config, fun(X) -> X end),
W2L(P).
-
+verify_openssh(Config) ->
+ ct:comment("Begin ~p",[grps(Config)]),
+ Host = ssh_test_lib:hostname(),
+ case (catch ssh_sftp:start_channel(Host,
+ [{user_interaction, false},
+ {silently_accept_hosts, true},
+ {save_accepted_host, false}
+ ])) of
+ {ok, _ChannelPid, Connection} ->
+ [{peer, {_HostName,{IPx,Portx}}}] = ssh:connection_info(Connection,[peer]),
+ ssh:close(Connection),
+ [{w2l, fun w2l/1},
+ {peer, {fmt_host(IPx),Portx}}, {group, openssh_server} | Config];
+ {error,"Key exchange failed"} ->
+ {skip, "openssh server doesn't support the tested kex algorithm"};
+ Other ->
+ ct:log("No openssh server. Cause:~n~p~n",[Other]),
+ {skip, "No openssh daemon (see log in testcase)"}
+ end.
diff -ruN a/lib/ssh/test/ssh_sup_SUITE.erl b/lib/ssh/test/ssh_sup_SUITE.erl
--- a/lib/ssh/test/ssh_sup_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_sup_SUITE.erl 2025-12-22 23:16:56.270839035 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2015-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2015-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@
-define(SSHD_SUP(Pid), {sshd_sup, Pid, supervisor, [supervisor]}).
-define(SYSTEM_SUP(Pid,Address),
{{ssh_system_sup, Address}, Pid, supervisor,[ssh_system_sup]}).
--define(SUB_SYSTEM_SUP(Pid), {_,Pid, supervisor,[ssh_subsystem_sup]}).
+-define(CONNECTION_SUP(Pid), {_,Pid, supervisor,[ssh_connection_sup]}).
-define(ACCEPTOR_SUP(Pid,Address),
{{ssh_acceptor_sup,Address},Pid,supervisor,[ssh_acceptor_sup]}).
-define(ACCEPTOR_WORKER(Pid,Address),
@@ -129,25 +129,22 @@
{user, ?USER},
{password, ?PASSWD},
{user_dir, UserDir}]),
- ?wait_match([?SYSTEM_SUP(SysSup,
- #address{address=LocalIP,
- port=LocalPort,
- profile=?DEFAULT_PROFILE})],
+ ?wait_match([?CONNECTION_SUP(ConnectionSup)],
supervisor:which_children(sshc_sup),
- [SysSup, LocalIP, LocalPort]),
- check_sshc_system_tree(SysSup, Pid1, LocalIP, LocalPort, Config),
+ [ConnectionSup]),
+ check_sshc_system_tree(ConnectionSup, Pid1, Config),
Pid2 = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
{save_accepted_host, false},
{user_interaction, false},
{user, ?USER},
{password, ?PASSWD},
{user_dir, UserDir}]),
- ?wait_match([?SYSTEM_SUP(_,_),
- ?SYSTEM_SUP(_,_)
+ ?wait_match([?CONNECTION_SUP(_),
+ ?CONNECTION_SUP(_)
],
supervisor:which_children(sshc_sup)),
ssh:close(Pid1),
- ?wait_match([?SYSTEM_SUP(_,_)
+ ?wait_match([?CONNECTION_SUP(_)
],
supervisor:which_children(sshc_sup)),
ssh:close(Pid2),
@@ -301,7 +298,7 @@
{user_interaction, true},
{user_dir, UserDir}]),
- [SubSysSup,_ChPid|_] = Sups0 = chk_empty_con_daemon(Daemon),
+ [ConnectionSup,_ChPid|_] = Sups0 = chk_empty_con_daemon(Daemon),
{ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
ok = ssh_connection:shell(ConnectionRef,ChannelId0),
@@ -311,7 +308,7 @@
{_,ChSup,supervisor,[ssh_channel_sup]},
{connection,_,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup]),
?wait_match([{_,GroupPid,worker,[ssh_server_channel]}
],
@@ -328,9 +325,9 @@
{ssh_cm,ConnectionRef, {data, ChannelId0, 0, <<"TimeoutShell started!",Rest/binary>>}} ->
ct:log("TimeoutShell started. Rest = ~p", [Rest]),
receive
- %%---- wait for the subsystem to terminate
+ %%---- wait for the connection to terminate
{ssh_cm,ConnectionRef,{closed,ChannelId0}} ->
- ct:log("Subsystem terminated",[]),
+ ct:log("Connection terminated",[]),
case {chk_empty_con_daemon(Daemon),
process_info(GroupPid),
process_info(ShellPid)} of
@@ -361,23 +358,23 @@
end.
chk_empty_con_daemon(Daemon) ->
- ?wait_match([?SUB_SYSTEM_SUP(SubSysSup),
+ ?wait_match([?CONNECTION_SUP(ConnectionSup),
?ACCEPTOR_SUP(AccSup,_)
],
supervisor:which_children(Daemon),
- [SubSysSup,AccSup]),
+ [ConnectionSup,AccSup]),
?wait_match([{_,FwdAccSup, supervisor,[ssh_tcpip_forward_acceptor_sup]},
{_,ChSup,supervisor,[ssh_channel_sup]},
{connection,ServerConnPid,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,FwdAccSup,ServerConnPid]),
?wait_match([], supervisor:which_children(FwdAccSup)),
?wait_match([], supervisor:which_children(ChSup)),
?wait_match([?ACCEPTOR_WORKER(_,_)],
supervisor:which_children(AccSup),
[]),
- [SubSysSup, ChSup, ServerConnPid, AccSup, FwdAccSup].
+ [ConnectionSup, ChSup, ServerConnPid, AccSup, FwdAccSup].
%%-------------------------------------------------------------------------
%% Help functions
@@ -389,14 +386,14 @@
{user, ?USER},
{password, ?PASSWD},
{user_dir, UserDir}]),
- ?wait_match([?SUB_SYSTEM_SUP(SubSysSup),
+ ?wait_match([?CONNECTION_SUP(ConnectionSup),
?ACCEPTOR_SUP(AccSup,_)],
supervisor:which_children(Daemon),
- [SubSysSup,AccSup]),
+ [ConnectionSup,AccSup]),
?wait_match([{_,FwdAccSup, supervisor,[ssh_tcpip_forward_acceptor_sup]},
{_,_,supervisor,[ssh_channel_sup]},
{connection,ServerConn,worker,[ssh_connection_handler]}],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[FwdAccSup,ServerConn]),
?wait_match([], supervisor:which_children(FwdAccSup)),
?wait_match([?ACCEPTOR_WORKER(_,_)], supervisor:which_children(AccSup)),
@@ -404,7 +401,7 @@
?wait_match([{_,FwdAccSup, supervisor,[ssh_tcpip_forward_acceptor_sup]},
{_,ChSup,supervisor,[ssh_channel_sup]},
{connection,ServerConn,worker,[ssh_connection_handler]}],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,ServerConn]),
?wait_match([{_,PidS,worker,[ssh_server_channel]}],
@@ -415,15 +412,15 @@
ssh:close(ClientConn).
-check_sshc_system_tree(SysSup, Connection, _LocalIP, _LocalPort, _Config) ->
- ?wait_match([?SUB_SYSTEM_SUP(SubSysSup)],
- supervisor:which_children(SysSup),
- [SubSysSup]),
+check_sshc_system_tree(ConnectionSup, Connection, _Config) ->
+ ?wait_match([?CONNECTION_SUP(ConnectionSup)],
+ supervisor:which_children(sshc_sup),
+ [ConnectionSup]),
?wait_match([{_,FwdAccSup, supervisor,[ssh_tcpip_forward_acceptor_sup]},
{_,_,supervisor,[ssh_channel_sup]},
{connection,Connection,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[FwdAccSup]),
?wait_match([], supervisor:which_children(FwdAccSup)),
@@ -432,7 +429,7 @@
{_,ChSup,supervisor, [ssh_channel_sup]},
{connection,Connection,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,FwdAccSup]),
?wait_match([{_,ChPid1,worker,[ssh_client_channel]}
@@ -445,7 +442,7 @@
{_,ChSup,supervisor, [ssh_channel_sup]},
{connection,Connection,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,FwdAccSup]),
?wait_match([{_,ChPid2,worker,[ssh_client_channel]},
@@ -460,7 +457,7 @@
{_,ChSup,supervisor, [ssh_channel_sup]},
{connection,Connection,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,FwdAccSup]),
?wait_match([{_,ChPid2,worker,[ssh_client_channel]}
@@ -474,7 +471,7 @@
{_,ChSup,supervisor, [ssh_channel_sup]},
{connection,Connection,worker,[ssh_connection_handler]}
],
- supervisor:which_children(SubSysSup),
+ supervisor:which_children(ConnectionSup),
[ChSup,FwdAccSup]),
?wait_match([], supervisor:which_children(ChSup)),
diff -ruN a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl
--- a/lib/ssh/test/ssh_test_lib.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_test_lib.erl 2025-12-17 17:25:02.082182205 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -64,6 +64,7 @@
del_dir_contents/1,
do_del_files/2,
openssh_sanity_check/1,
+verify_sanity_check/1,
default_algorithms/1,
default_algorithms/3,
default_algorithms/2,
@@ -122,17 +123,26 @@
setup_known_host/3,
get_addr_str/0,
file_base_name/2,
-add_report_handler/0,
-get_reports/1,
kex_strict_negotiated/2,
event_logged/3
]).
+%% logger callbacks and related helpers
+-export([log/2,
+ get_log_level/0, set_log_level/1, add_log_handler/0,
+ rm_log_handler/0, get_log_events/1]).
-include_lib("common_test/include/ct.hrl").
-include("ssh_transport.hrl").
-include_lib("kernel/include/file.hrl").
-include("ssh_test_lib.hrl").
+-define(SANITY_CHECK_NOTE,
+ "For enabling test, make sure following commands work:~n"
+ "ok = ssh:start(), "
+ "{ok, _} = ssh:connect(\"localhost\", 22, "
+ "[{password,\"\"},{silently_accept_hosts, true}, "
+ "{save_accepted_host, false}, {user_interaction, false}]).").
+
%%%----------------------------------------------------------------
connect(Port, Options) when is_integer(Port) ->
connect(hostname(), Port, Options).
@@ -566,7 +576,6 @@
end
end, Files).
-
openssh_sanity_check(Config) ->
ssh:start(),
case ssh:connect("localhost", ?SSH_DEFAULT_PORT,
@@ -578,11 +587,24 @@
{ok, Pid} ->
ssh:close(Pid),
ssh:stop(),
- Config;
+ [{sanity_check_result, ok} | Config];
Err ->
Str = lists:append(io_lib:format("~p", [Err])),
+ ct:log("Error = ~p", [Err]),
+ ct:log(?SANITY_CHECK_NOTE),
ssh:stop(),
- {skip, Str}
+ [{sanity_check_result, Str} | Config]
+ end.
+
+verify_sanity_check(Config) ->
+ SanityCheckResult = proplists:get_value(sanity_check_result, Config, ok),
+ case SanityCheckResult of
+ ok ->
+ Config;
+ Err ->
+ ct:log("Error = ~p", [Err]),
+ ct:log(?SANITY_CHECK_NOTE),
+ {fail, passwordless_connection_failed}
end.
%%%--------------------------------------------------------------------
@@ -1271,15 +1293,10 @@
file_base_name(system_src, Alg) -> file_base_name(system, Alg).
%%%----------------------------------------------------------------
-add_report_handler() ->
- ssh_eqc_event_handler:add_report_handler().
-
-get_reports(Pid) ->
- ssh_eqc_event_handler:get_reports(Pid).
-
-define(SEARCH_FUN(EXP),
begin
- fun({info_report, _, {_, std_info, EXP}}) ->
+ fun(#{msg := {string, EXP},
+ level := debug}) ->
true;
(_) ->
false
@@ -1287,19 +1304,20 @@
end).
-define(SEARCH_SUFFIX, " will use strict KEX ordering").
-kex_strict_negotiated(client, Reports) ->
- kex_strict_negotiated(?SEARCH_FUN("client" ++ ?SEARCH_SUFFIX), Reports);
-kex_strict_negotiated(server, Reports) ->
- kex_strict_negotiated(?SEARCH_FUN("server" ++ ?SEARCH_SUFFIX), Reports);
-kex_strict_negotiated(SearchFun, Reports) when is_function(SearchFun) ->
- case lists:search(SearchFun, Reports) of
+kex_strict_negotiated(client, Events) ->
+ kex_strict_negotiated(?SEARCH_FUN("client" ++ ?SEARCH_SUFFIX), Events);
+kex_strict_negotiated(server, Events) ->
+ kex_strict_negotiated(?SEARCH_FUN("server" ++ ?SEARCH_SUFFIX), Events);
+kex_strict_negotiated(SearchFun, Events) when is_function(SearchFun) ->
+ %% FIXME use event_logged?
+ case lists:search(SearchFun, Events) of
{value, _} -> true;
_ -> false
end.
-event_logged(Role, Reports, Reason) ->
+event_logged(Role, Events, Reason) ->
SearchF =
- fun({info_msg, _, {_, _Format, Args}}) ->
+ fun(#{msg := {report, #{args := Args}}}) ->
AnyF = fun (E) when is_list(E) ->
case string:find(E, Reason) of
nomatch -> false;
@@ -1310,10 +1328,48 @@
end,
lists:member(Role, Args) andalso
lists:any(AnyF, Args);
- (_) ->
+ (_Event) ->
false
end,
- case lists:search(SearchF, Reports) of
+ case lists:search(SearchF, Events) of
{value, _} -> true;
_ -> false
end.
+
+get_log_level() ->
+ #{level := Level} = logger:get_primary_config(),
+ Level.
+
+set_log_level(Level) ->
+ ok = logger:set_primary_config(level, Level).
+
+add_log_handler() ->
+ logger:remove_handler(?MODULE),
+ TestRef = make_ref(),
+ ok = logger:add_handler(?MODULE, ?MODULE,
+ #{level => debug,
+ filter_default => log,
+ recipient => self(),
+ test_ref => TestRef}),
+ {ok, TestRef}.
+
+rm_log_handler() ->
+ ok = logger:remove_handler(?MODULE).
+
+get_log_events(TestRef) ->
+ {ok, get_log_events(TestRef, [])}.
+
+get_log_events(TestRef, Acc) ->
+ receive
+ {TestRef, Event} ->
+ get_log_events(TestRef, [Event | Acc])
+ after
+ 500 ->
+ Acc
+ end.
+
+%% logger callbacks
+log(LogEvent = #{level:=_Level,msg:=_Msg,meta:=_Meta},
+ #{test_ref := TestRef, recipient := Recipient}) ->
+ Recipient ! {TestRef, LogEvent},
+ ok.
diff -ruN a/lib/ssh/test/ssh_to_openssh_SUITE.erl b/lib/ssh/test/ssh_to_openssh_SUITE.erl
--- a/lib/ssh/test/ssh_to_openssh_SUITE.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl 2025-12-22 23:16:56.271839046 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@
-include_lib("common_test/include/ct.hrl").
-include("ssh_test_lib.hrl").
--include_lib("ssh/src/ssh_transport.hrl").
+-include("ssh_transport.hrl").
-export([
suite/0,
@@ -131,12 +131,15 @@
init_per_testcase(erlang_server_openssh_client_renegotiate, Config) ->
case os:type() of
- {unix,_} -> ssh:start(), Config;
- Type -> {skip, io_lib:format("Unsupported test on ~p",[Type])}
+ {unix,_} ->
+ ssh:start(),
+ ssh_test_lib:verify_sanity_check(Config);
+ Type ->
+ {skip, io_lib:format("Unsupported test on ~p",[Type])}
end;
init_per_testcase(_TestCase, Config) ->
ssh:start(),
- Config.
+ ssh_test_lib:verify_sanity_check(Config).
end_per_testcase(_TestCase, _Config) ->
ssh:stop(),
@@ -146,29 +149,32 @@
%% Test Cases --------------------------------------------------------
%%--------------------------------------------------------------------
erlang_shell_client_openssh_server(Config) when is_list(Config) ->
- eclient_oserver_helper(Config).
+ eclient_oserver_helper2(eclient_oserver_helper1(), Config).
eclient_oserver_kex_strict(Config) when is_list(Config)->
case proplists:get_value(kex_strict, Config) of
true ->
- {ok, HandlerPid} = ssh_test_lib:add_report_handler(),
- #{level := Level} = logger:get_primary_config(),
- logger:set_primary_config(level, notice),
- Result = eclient_oserver_helper(Config),
- {ok, Reports} = ssh_test_lib:get_reports(HandlerPid),
- ct:pal("Reports = ~p", [Reports]),
- true = ssh_test_lib:kex_strict_negotiated(client, Reports),
- logger:set_primary_config(Level),
- Result;
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
+ HelperParams = eclient_oserver_helper1(),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ true = ssh_test_lib:kex_strict_negotiated(client, Events),
+ ssh_test_lib:set_log_level(Level),
+ ssh_test_lib:rm_log_handler(),
+ eclient_oserver_helper2(HelperParams, Config);
_ ->
{skip, "KEX strict not support by local OpenSSH"}
end.
-eclient_oserver_helper(Config) ->
+eclient_oserver_helper1() ->
process_flag(trap_exit, true),
IO = ssh_test_lib:start_io_server(),
Prev = lists:usort(supervisor:which_children(sshc_sup)),
Shell = ssh_test_lib:start_shell(?SSH_DEFAULT_PORT, IO),
+ {Shell, Prev, IO}.
+
+eclient_oserver_helper2({Shell, Prev, IO}, Config) ->
IO ! {input, self(), "echo Hej\n"},
case proplists:get_value(ptty_supported, Config) of
true ->
@@ -253,25 +259,28 @@
%%--------------------------------------------------------------------
%% Test that the Erlang/OTP server can renegotiate with openSSH
erlang_server_openssh_client_renegotiate(Config) ->
- eserver_oclient_renegotiate_helper(Config).
+ eserver_oclient_renegotiate_helper2(
+ eserver_oclient_renegotiate_helper1(Config)).
eserver_oclient_kex_strict(Config) ->
case proplists:get_value(kex_strict, Config) of
true ->
- {ok, HandlerPid} = ssh_test_lib:add_report_handler(),
- #{level := Level} = logger:get_primary_config(),
- logger:set_primary_config(level, notice),
- Result = eserver_oclient_renegotiate_helper(Config),
- {ok, Reports} = ssh_test_lib:get_reports(HandlerPid),
- ct:log("Reports = ~p", [Reports]),
- true = ssh_test_lib:kex_strict_negotiated(server, Reports),
- logger:set_primary_config(Level),
- Result;
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
+
+ HelperParams = eserver_oclient_renegotiate_helper1(Config),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ ct:log("Events = ~n~p", [Events]),
+ true = ssh_test_lib:kex_strict_negotiated(server, Events),
+ ssh_test_lib:set_log_level(Level),
+ ssh_test_lib:rm_log_handler(),
+ eserver_oclient_renegotiate_helper2(HelperParams);
_ ->
{skip, "KEX strict not support by local OpenSSH"}
end.
-eserver_oclient_renegotiate_helper(Config) ->
+eserver_oclient_renegotiate_helper1(Config) ->
_PubKeyAlg = ssh_rsa,
SystemDir = proplists:get_value(data_dir, Config),
PrivDir = proplists:get_value(priv_dir, Config),
@@ -295,7 +304,9 @@
OpenSsh = ssh_test_lib:open_port({spawn, Cmd++" < "++DataFile}),
+ {Data, OpenSsh, Pid}.
+eserver_oclient_renegotiate_helper2({Data, OpenSsh, Pid}) ->
Expect = fun({data,R}) ->
try
NonAlphaChars = [C || C<-lists:seq(1,255),
@@ -623,3 +634,4 @@
ct:log("KEX strict supported by local OpenSSH"),
true
end.
+
diff -ruN a/lib/ssh/test/ssh_trpt_test_lib.erl b/lib/ssh/test/ssh_trpt_test_lib.erl
--- a/lib/ssh/test/ssh_trpt_test_lib.erl 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/test/ssh_trpt_test_lib.erl 2025-12-17 17:25:02.082556494 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -24,7 +24,8 @@
-export([exec/1, exec/2,
instantiate/2,
format_msg/1,
- server_host_port/1
+ server_host_port/1,
+ return_value/1
]
).
@@ -91,7 +92,8 @@
report_trace(throw, Term, S1),
throw({Term,Op});
- error:Error ->
+ error:Error:St ->
+ ct:log("Stacktrace=~n~p", [St]),
report_trace(error, Error, S1),
error({Error,Op});
@@ -336,6 +338,17 @@
Msg = #ssh_msg_ignore{data = "unexpected_ignore_message"},
send(S0, Msg);
+send(S0, ssh_msg_debug) ->
+ Msg = #ssh_msg_debug{
+ always_display = true,
+ message = "some debug message",
+ language = "en"},
+ send(S0, Msg);
+
+send(S0, ssh_msg_unimplemented) ->
+ Msg = #ssh_msg_unimplemented{sequence = 123},
+ send(S0, Msg);
+
send(S0, ssh_msg_unknown) ->
Msg = binary:encode_hex(<<"0000000C060900000000000000000000">>),
send(S0, Msg);
@@ -383,6 +396,26 @@
end),
send_bytes(NextKexMsgBin, S#s{ssh = C});
+send(S0, ssh_msg_kexdh_init_dup) when ?role(S0) == client ->
+ {OwnMsg, PeerMsg} = S0#s.alg_neg,
+ {ok, NextKexMsgBin, C} =
+ try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh, init)
+ catch
+ Class:Exc ->
+ fail("Algorithm negotiation failed!",
+ {"Algorithm negotiation failed at line ~p:~p~n~p:~s~nPeer: ~s~n Own: ~s",
+ [?MODULE,?LINE,Class,format_msg(Exc),format_msg(PeerMsg),format_msg(OwnMsg)]},
+ S0)
+ end,
+ S = opt(print_messages, S0,
+ fun(X) when X==true;X==detail ->
+ #ssh{keyex_key = {{_Private, Public}, {_G, _P}}} = C,
+ Msg = #ssh_msg_kexdh_init{e = Public},
+ {"Send (reconstructed)~n~s~n",[format_msg(Msg)]}
+ end),
+ send_bytes(NextKexMsgBin, S#s{ssh = C}),
+ send_bytes(NextKexMsgBin, S#s{ssh = C});
+
send(S0, ssh_msg_kexdh_reply) ->
Bytes = proplists:get_value(ssh_msg_kexdh_reply, S0#s.reply),
S = opt(print_messages, S0,
@@ -532,7 +565,10 @@
S0#s.ssh)
of
{packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
- S1 = S0#s{ssh = Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
+ S1 = S0#s{ssh = Ssh1#ssh{recv_sequence =
+ ssh_transport:next_seqnum(undefined,
+ Ssh1#ssh.recv_sequence,
+ false)},
decrypted_data_buffer = <<>>,
undecrypted_packet_length = undefined,
aead_data = <<>>,
@@ -779,3 +815,6 @@
save_prints({Fmt,Args}, S) ->
S#s{prints = [{Fmt,Args}|S#s.prints]}.
+
+return_value(#s{return_value = ReturnValue}) ->
+ ReturnValue.
diff -ruN a/lib/ssh/vsn.mk b/lib/ssh/vsn.mk
--- a/lib/ssh/vsn.mk 2023-12-16 04:53:01.000000000 +1030
+++ b/lib/ssh/vsn.mk 2025-12-22 23:16:56.271839046 +1030
@@ -1,4 +1,4 @@
#-*-makefile-*- ; force emacs to enter makefile-mode
-SSH_VSN = 5.1.1
+SSH_VSN = 5.1.4.13
APP_VSN = "ssh-$(SSH_VSN)"