From b1c2c86f543e85a7a34677526e4891a284f2be4be9c16a963407acc052f01e1f Mon Sep 17 00:00:00 2001 From: Simon Lees Date: Fri, 19 Dec 2025 11:02:50 +1030 Subject: [PATCH] - Update the ssh component Take the latest update to the ssh component from the maint-27 branch, this contains multiple new fixes and no features as well as making the backport process easier * Multiple fixes for Excessive Resource Consumption (bsc#1249469, bsc#1249470 bsc#1249472, CVE-2025-48038, CVE-2025-48039, CVE-2025-48040) * Other Minor fixes * feature-fix-update-ssh-stack.patch --- erlang27.changes | 11 + erlang27.spec | 3 +- feature-fix-update-ssh-stack.patch | 5684 ++++++++++++++++++++++++++++ 3 files changed, 5697 insertions(+), 1 deletion(-) create mode 100644 feature-fix-update-ssh-stack.patch diff --git a/erlang27.changes b/erlang27.changes index 40d2cb2..86741a4 100644 --- a/erlang27.changes +++ b/erlang27.changes @@ -1,3 +1,14 @@ +------------------------------------------------------------------- +Mon Dec 15 12:05:33 UTC 2025 - Simon Lees + +- Update the ssh component to the latest in the maint-27 branch + * Multiple fixes for Excessive Resource Consumption + (bsc#1249469, bsc#1249470 bsc#1249472, CVE-2025-48038, + CVE-2025-48039, CVE-2025-48040) + * Other Minor fixes + * feature-fix-update-ssh-stack.patch + * fix-CVE-2025-48041.patch included in other patch + ------------------------------------------------------------------- Mon Nov 3 07:02:51 UTC 2025 - Simon Lees diff --git a/erlang27.spec b/erlang27.spec index d6c3151..508748d 100644 --- a/erlang27.spec +++ b/erlang27.spec @@ -52,7 +52,8 @@ Source10: epmd-user.conf Patch0: otp-R16B-rpath.patch # PATCH-FIX-OPENSUSE erlang-not-install-misc.patch - matwey.kornilov@gmail.com -- patch from Fedora, this removes unneeded magic Patch4: erlang-not-install-misc.patch -Patch5: fix-CVE-2025-48041.patch +# Take the latest ssh stack from the maint-27 branch +Patch5: feature-fix-update-ssh-stack.patch BuildRequires: Mesa-devel BuildRequires: autoconf BuildRequires: dejavu-fonts diff --git a/feature-fix-update-ssh-stack.patch b/feature-fix-update-ssh-stack.patch new file mode 100644 index 0000000..050ef51 --- /dev/null +++ b/feature-fix-update-ssh-stack.patch @@ -0,0 +1,5684 @@ +diff -ruN a/lib/ssh/doc/docs.exs b/lib/ssh/doc/docs.exs +--- a/lib/ssh/doc/docs.exs 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/doc/docs.exs 2025-12-17 17:25:02.074061554 +1030 +@@ -1,4 +1,12 @@ + [ ++ annotations_for_docs: fn ++ md -> ++ if md[:rfc] do ++ [md[:rfc]] ++ else ++ [] ++ end ++ end, + ## The order of these items determine + ## how they are listed in the docs + extras: [ +diff -ruN a/lib/ssh/doc/notes.md b/lib/ssh/doc/notes.md +--- a/lib/ssh/doc/notes.md 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/doc/notes.md 2025-12-17 17:25:02.074275138 +1030 +@@ -1,7 +1,7 @@ + + # SSH Release Notes + ++## Ssh 5.2.11.4 ++ ++### Fixed Bugs and Malfunctions ++ ++- With this change user space buffers are used to limit ssh hello message size instead of kernel buffers ++ ++ Own Id: OTP-19839 Aux Id: ERIERL-1273, [PR-10350] ++ ++[PR-10350]: https://github.com/erlang/otp/pull/10350 ++ ++## Ssh 5.2.11.3 ++ ++### Fixed Bugs and Malfunctions ++ ++- 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). ++ ++ *** POTENTIAL INCOMPATIBILITY *** ++ ++ Own Id: OTP-19701 Aux Id: [CVE-2025-48041], [PR-10157] ++ ++- 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. ++ ++ *** POTENTIAL INCOMPATIBILITY *** ++ ++ Own Id: OTP-19741 Aux Id: [CVE-2025-48040], [PR-10162] ++ ++- 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. ++ ++ *** POTENTIAL INCOMPATIBILITY *** ++ ++ Own Id: OTP-19742 Aux Id: [CVE-2025-48039], [PR-10155] ++ ++- Reject file handles exceeding size specified in RFCs (256 bytes). ++ ++ *** POTENTIAL INCOMPATIBILITY *** ++ ++ Own Id: OTP-19748 Aux Id: [CVE-2025-48038], [PR-10156] ++ ++[CVE-2025-48041]: https://nvd.nist.gov/vuln/detail/2025-48041 ++[PR-10157]: https://github.com/erlang/otp/pull/10157 ++[CVE-2025-48040]: https://nvd.nist.gov/vuln/detail/2025-48040 ++[PR-10162]: https://github.com/erlang/otp/pull/10162 ++[CVE-2025-48039]: https://nvd.nist.gov/vuln/detail/2025-48039 ++[PR-10155]: https://github.com/erlang/otp/pull/10155 ++[CVE-2025-48038]: https://nvd.nist.gov/vuln/detail/2025-48038 ++[PR-10156]: https://github.com/erlang/otp/pull/10156 ++ ++## Ssh 5.2.11.2 ++ ++### Fixed Bugs and Malfunctions ++ ++- Fix file handle id generation. ++ ++ Own Id: OTP-19691 Aux Id: [PR-10003] ++ ++- Fixes a badmatch error, when SFTP operation cannot be processed due to channel closed in parallel. ++ ++ Own Id: OTP-19707 Aux Id: [GH-9655], [PR-10035], [PR-10036] ++ ++[PR-10003]: https://github.com/erlang/otp/pull/10003 ++[GH-9655]: https://github.com/erlang/otp/issues/9655 ++[PR-10035]: https://github.com/erlang/otp/pull/10035 ++[PR-10036]: https://github.com/erlang/otp/pull/10036 ++ ++## Ssh 5.2.11.1 ++ ++### Fixed Bugs and Malfunctions ++ ++- 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. ++ ++ Own Id: OTP-19634 Aux Id: [GH-9102], [PR-9103] ++ ++- Improved interoperability with clients acting as Paramiko. ++ ++ Own Id: OTP-19637 Aux Id: [GH-6463], [PR-9838] ++ ++[GH-9102]: https://github.com/erlang/otp/issues/9102 ++[PR-9103]: https://github.com/erlang/otp/pull/9103 ++[GH-6463]: https://github.com/erlang/otp/issues/6463 ++[PR-9838]: https://github.com/erlang/otp/pull/9838 ++ ++## Ssh 5.2.11 ++ ++### Fixed Bugs and Malfunctions ++ ++- Fix KEX strict implementation according to draft-miller-sshm-strict-kex-01 document. ++ ++ Own Id: OTP-19625 Aux Id: CVE-2025-46712 ++ ++## Ssh 5.2.10 ++ ++### Fixed Bugs and Malfunctions ++ ++- Reception of wrong Unicode does not cause unnecessary processing. US-ASCII fields are not decoded as Unicode. ++ ++ Own Id: OTP-19582 Aux Id: [PR-9679] ++ ++- SSH daemon disconnects upon receiving connection protocol message for unauthenticated used. ++ ++ Thanks to Fabian Bäumer, Marcel Maehren, Marcus Brinkmann, Nurullah Erinola, Jörg Schwenk (Ruhr University Bochum). ++ ++ Own Id: OTP-19595 Aux Id: CVE-2025-32433 ++ ++[PR-9679]: https://github.com/erlang/otp/pull/9679 ++ ++## Ssh 5.2.9 ++ ++### Fixed Bugs and Malfunctions ++ ++- Reception of malicious KEX init message does not result with ssh daemon excessive memory usage. ++ ++ Own Id: OTP-19543 Aux Id: CVE-2025-30211 ++ ++- Call to ssh:daemon_replace_options does not crash when argument is not a valid daemon ref. ++ ++ Own Id: OTP-19559 Aux Id: [GH-9554], [PR-9545] ++ ++[GH-9554]: https://github.com/erlang/otp/issues/9554 ++[PR-9545]: https://github.com/erlang/otp/pull/9545 ++ ++## Ssh 5.2.8 ++ ++### Fixed Bugs and Malfunctions ++ ++- Minor documentation improvements. ++ ++ Own Id: OTP-19410 Aux Id: [PR-9188] ++ ++- Function specification for `ssh_sftp:start_channel/2` is fixed. ++ ++ Own Id: OTP-19475 Aux Id: [PR-9368], [GH-9359] ++ ++[PR-9188]: https://github.com/erlang/otp/pull/9188 ++[PR-9368]: https://github.com/erlang/otp/pull/9368 ++[GH-9359]: https://github.com/erlang/otp/issues/9359 ++ ++## Ssh 5.2.7 ++ ++### Fixed Bugs and Malfunctions ++ ++- SFTP packets exceeding max packet size are not processed and dropped. ++ ++ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618 ++ ++## Ssh 5.2.6 ++ ++### Fixed Bugs and Malfunctions ++ ++- With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value. ++ ++ Own Id: OTP-19388 Aux Id: ERIERL-1165, [PR-9161] ++ ++- With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication. ++ ++ Own Id: OTP-19392 Aux Id: [PR-9139], [GH-9065] ++ ++- With this change, large sftp transfers does not hang. Redundant window adjustment are not requested. ++ ++ Own Id: OTP-19435 Aux Id: [PR-9309] ++ ++[PR-9161]: https://github.com/erlang/otp/pull/9161 ++[PR-9139]: https://github.com/erlang/otp/pull/9139 ++[GH-9065]: https://github.com/erlang/otp/issues/9065 ++[PR-9309]: https://github.com/erlang/otp/pull/9309 ++ ++## Ssh 5.2.5 ++ ++### Fixed Bugs and Malfunctions ++ ++- Documentation is polished after OTP-27 migration to markdown. ++ ++ Own Id: OTP-19335 Aux Id: [PR-9021] ++ ++[PR-9021]: https://github.com/erlang/otp/pull/9021 ++ + ## Ssh 5.2.4 + + ### Fixed Bugs and Malfunctions +@@ -110,6 +285,50 @@ + [PR-7845]: https://github.com/erlang/otp/pull/7845 + [PR-8026]: https://github.com/erlang/otp/pull/8026 + ++## Ssh 5.1.4.6 ++ ++### Fixed Bugs and Malfunctions ++ ++* SFTP packets exceeding max packet size are not processed and dropped. ++ ++ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618 ++ ++## Ssh 5.1.4.5 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value. ++ ++ Own Id: OTP-19388 Aux Id: ERIERL-1165, PR-9161 ++* With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication. ++ ++ Own Id: OTP-19392 Aux Id: PR-9139, GH-9065 ++* With this change, large sftp transfers does not hang. Redundant window adjustment are not requested. ++ ++ Own Id: OTP-19435 Aux Id: PR-9309 ++ ++## Ssh 5.1.4.4 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, ssh connection does not crash upon receiving exit-signal message for an already terminated channel. ++ ++ Own Id: OTP-19326 Aux Id: PR-8995, GH-8929 ++ ++## Ssh 5.1.4.3 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, a race condition is removed from ssh client connection setup procedure. ++ ++ Own Id: OTP-19124 Aux Id: GH-7550, PR-8766 ++* With this change, ssh:connect is not affected by presence of EXIT message in queue. ++ ++ Own Id: OTP-19246 Aux Id: GH-8223, PR-8854 ++* With this change, ssh appends \{active, false\} option after socket options received from user - so that false value is always used. ++ ++ Own Id: OTP-19247 Aux Id: PR-8226 ++ + ## Ssh 5.1.4.2 + + ### Fixed Bugs and Malfunctions +@@ -179,6 +398,8 @@ + cost of affecting interoperability. See + [Configuring algorithms in SSH](configure_algos.md). + ++ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk. ++ + \*** POTENTIAL INCOMPATIBILITY \*** + + Own Id: OTP-18897 +@@ -247,6 +468,61 @@ + + Own Id: OTP-18490 Aux Id: OTP-18471, GH-6339, PR-6843 + ++## Ssh 4.15.3.10 ++ ++### Fixed Bugs and Malfunctions ++ ++* SFTP packets exceeding max packet size are not processed and dropped. ++ ++ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618 ++ ++## Ssh 4.15.3.9 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value. ++ ++ Own Id: OTP-19388 Aux Id: ERIERL-1165, PR-9161 ++* With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication. ++ ++ Own Id: OTP-19392 Aux Id: PR-9139, GH-9065 ++* With this change, large sftp transfers does not hang. Redundant window adjustment are not requested. ++ ++ Own Id: OTP-19435 Aux Id: PR-9309 ++ ++## Ssh 4.15.3.8 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, ssh connection does not crash upon receiving exit-signal message for an already terminated channel. ++ ++ Own Id: OTP-19326 Aux Id: PR-8995, GH-8929 ++ ++## Ssh 4.15.3.7 ++ ++### Fixed Bugs and Malfunctions ++ ++* With this change, a race condition is removed from ssh client connection setup procedure. ++ ++ Own Id: OTP-19124 Aux Id: GH-7550, PR-8766 ++* With this change, ssh:connect is not affected by presence of EXIT message in queue. ++ ++ Own Id: OTP-19246 Aux Id: GH-8223, PR-8854 ++* With this change, ssh appends \{active, false\} option after socket options received from user - so that false value is always used. ++ ++ Own Id: OTP-19247 Aux Id: PR-8226 ++ ++## Ssh 4.15.3.6 ++ ++### Fixed Bugs and Malfunctions ++ ++* The SSh daemon started with a TCP port number argument will now re-try obtaining a listen socket before returning an error to the user. ++ ++ Own Id: OTP-19170 Aux Id: GH-7746 ++* Robustness has been improved by monitoring the connection handler process before casting the socket control notification. ++ ++ Own Id: OTP-19173 Aux Id: PR-8310 ++ + ## Ssh 4.15.3.5 + + ### Fixed Bugs and Malfunctions +@@ -305,6 +581,8 @@ + cost of affecting interoperability. See + [Configuring algorithms in SSH](configure_algos.md). + ++ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk. ++ + \*** POTENTIAL INCOMPATIBILITY \*** + + Own Id: OTP-18897 +@@ -471,6 +749,8 @@ + cost of affecting interoperability. See + [Configuring algorithms in SSH](configure_algos.md). + ++ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk. ++ + \*** POTENTIAL INCOMPATIBILITY \*** + + Own Id: OTP-18897 +@@ -697,6 +977,8 @@ + + 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 Configuring algorithms in SSH User's Guide. + ++ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk. ++ + \*** POTENTIAL INCOMPATIBILITY *** + + Own Id: OTP-18897 +@@ -1081,6 +1363,8 @@ + + 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 Configuring algorithms in SSH User's Guide. + ++ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk. ++ + \*** POTENTIAL INCOMPATIBILITY *** + + Own Id: OTP-18897 +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/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl +--- a/lib/ssh/src/ssh_acceptor.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_acceptor.erl 2025-12-17 17:25:02.074556391 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -206,39 +206,63 @@ + 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. + + %%%---------------------------------------------------------------- +diff -ruN a/lib/ssh/src/ssh_agent.erl b/lib/ssh/src/ssh_agent.erl +--- a/lib/ssh/src/ssh_agent.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_agent.erl 2025-12-17 17:25:02.074556391 +1030 +@@ -58,7 +58,7 @@ + """. + -moduledoc(#{since => "OTP 23.0", + titles => +- [{type,<<"Options for the ssh_agent callback module">>}]}). ++ [{type,<<"Options">>}]}). + + -behaviour(ssh_client_key_api). + +@@ -72,19 +72,19 @@ + Sets the [socket path](`m:ssh_agent#SOCKET_PATH`) for the communication with the + agent. + """. +--doc(#{title => <<"Options for the ssh_agent callback module">>}). ++-doc(#{title => <<"Options">>}). + -type socket_path_option() :: {socket_path, string()}. + -doc """ + Sets the time-out in milliseconds when communicating with the agent via the + socket. The default value is `1000`. + """. +--doc(#{title => <<"Options for the ssh_agent callback module">>}). ++-doc(#{title => <<"Options">>}). + -type timeout_option() :: {timeout, integer()}. + -doc """ + The module which the `add_host_key` and `is_host_key` callbacks are delegated + to. Defaults to the `m:ssh_file` module. + """. +--doc(#{title => <<"Options for the ssh_agent callback module">>}). ++-doc(#{title => <<"Options">>}). + -type call_ssh_file_option() :: {call_ssh_file, atom()}. + + %% ssh_client_key_api implementation +diff -ruN a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl +--- a/lib/ssh/src/ssh_client_channel.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_client_channel.erl 2025-12-17 17:25:02.075556404 +1030 +@@ -22,16 +22,6 @@ + + -module(ssh_client_channel). + -moduledoc """ +-\-behaviour(ssh_client_channel). (Replaces ssh_channel) +- +-> #### Note {: .info } +-> +-> This module replaces ssh_channel. +-> +-> The old module is still available for compatibility, but should not be used +-> for new programs. The old module will not be maintained except for some error +-> corrections +- + SSH services (clients and servers) are implemented as channels that are + multiplexed over an SSH connection and communicates over the + [SSH Connection Protocol](http://www.ietf.org/rfc/rfc4254.txt). This module +@@ -45,6 +35,14 @@ + + > #### Note {: .info } + > ++> This module replaces ssh_channel. ++> ++> The old module is still available for compatibility, but should not be used ++> for new programs. The old module will not be maintained except for some error ++> corrections ++ ++> #### Note {: .info } ++> + > When implementing a `ssh` subsystem for daemons, use + > [\-behaviour(ssh_server_channel)](`m:ssh_server_channel`) (Replaces + > ssh_daemon_channel) instead. +@@ -60,8 +58,7 @@ + semantics as in a `m:gen_server`. If the time-out occurs, `c:handle_msg/2` is called as + handle_msg(timeout, State). + """. +--moduledoc(#{since => "OTP 21.0", +- titles => [{callback,<<"Callback Functions">>}]}). ++-moduledoc(#{since => "OTP 21.0"}). + + -include("ssh.hrl"). + -include("ssh_connect.hrl"). +@@ -73,7 +70,7 @@ + For more detailed information on time-outs, see Section + [Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`). + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback init(Args :: term()) -> + {ok, State :: term()} | {ok, State :: term(), timeout() | hibernate} | + {stop, Reason :: term()} | ignore. +@@ -83,7 +80,7 @@ + For more detailed information on time-outs,, see Section + [Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`). + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_call(Request :: term(), From :: {pid(), Tag :: term()}, + State :: term()) -> + {reply, Reply :: term(), NewState :: term()} | +@@ -98,7 +95,7 @@ + For more detailed information on time-outs, see Section + [Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`). + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_cast(Request :: term(), State :: term()) -> + {noreply, NewState :: term()} | + {noreply, NewState :: term(), timeout() | hibernate} | +@@ -112,7 +109,7 @@ + the channel process terminates with reason `Reason`. The return value is + ignored. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback terminate(Reason :: (normal | shutdown | {shutdown, term()} | + term()), + State :: term()) -> +@@ -136,7 +133,7 @@ + > handle two versions of the state, but this function cannot be used in the + > normal way. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback code_change(OldVsn :: (term() | {down, term()}), State :: term(), + Extra :: term()) -> + {ok, NewState :: term()} | {error, Reason :: term()}. +@@ -148,14 +145,14 @@ + Possible Erlang 'EXIT' messages is to be handled by this function and all + channels are to handle the following message. + +-- **`{ssh_channel_up, ``t:ssh:channel_id/0``, ``t:ssh:connection_ref/0``}`** - ++- **`{ssh_channel_up,` `t:ssh:channel_id/0` `,` `t:ssh:connection_ref/0` `}`** - + This is the first message that the channel receives. It is sent just before + the `init/1` function returns successfully. This is especially useful if the + server wants to send a message to the client without first receiving a message + from it. If the message is not useful for your particular scenario, ignore it + by immediately returning `{ok, State}`. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_msg(Msg ::term(), State :: term()) -> + {ok, State::term()} | {stop, ChannelId::ssh:channel_id(), State::term()}. + +@@ -165,11 +162,11 @@ + + The following message is taken care of by the `ssh_client_channel` behavior. + +-- **`{closed, ``t:ssh:channel_id/0``}`** - The channel behavior sends a close ++- **`{closed,` `t:ssh:channel_id/0` `}`** - The channel behavior sends a close + message to the other side, if such a message has not already been sent. Then + it terminates the channel with reason `normal`. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_ssh_msg(ssh_connection:event(), + State::term()) -> {ok, State::term()} | + {stop, ChannelId::ssh:channel_id(), +@@ -216,8 +213,6 @@ + call(ChannelPid, Msg, infinity). + + -doc """ +-call(ChannelRef, Msg, Timeout) -> Reply | {error, Reason} +- + Makes a synchronous call to the channel process by sending a message and waiting + until a reply arrives, or a time-out occurs. The channel calls + [Module:handle_call/3](`c:handle_call/3`) to handle the message. If the channel +@@ -248,8 +243,6 @@ + end. + + -doc """ +-cast(ChannelRef, Msg) -> ok +- + Sends an asynchronous message to the channel process and returns ok immediately, + ignoring if the destination node or channel process does not exist. The channel + calls [Module:handle_cast/2](`c:handle_cast/2`) to handle the message. +@@ -263,8 +256,6 @@ + + -opaque client() :: term(). + -doc """ +-reply(Client, Reply) -> \_ +- + This function can be used by a channel to send a reply to a client that called + `call/[2,3]` when the reply cannot be defined in the return value of + [Module:handle_call/3](`c:handle_call/3`). +@@ -307,9 +298,6 @@ + gen_server:start(?MODULE, [Options], []). + + -doc """ +-start_link(SshConnection, ChannelId, ChannelCb, CbInitArgs) -> {ok, ChannelRef} +-| {error, Reason} +- + Starts a process that handles an SSH channel. It is called internally, by the + `ssh` daemon, or explicitly by the `ssh` client implementations. The behavior + sets the `trap_exit` flag to `true`. +@@ -336,10 +324,10 @@ + gen_server:start_link(?MODULE, [Options], []). + + -doc """ +-enter*loop(State) -> * +- + Makes an existing process an `ssh_client_channel` (replaces ssh_channel) +-process. Does not return, instead the calling process enters the ++process. ++ ++Does not return, instead the calling process enters the + `ssh_client_channel` (replaces ssh_channel) process receive loop and become an + `ssh_client_channel` process. The process must have been started using one of + the start functions in `proc_lib`, see the `m:proc_lib` manual page in STDLIB. +@@ -363,7 +351,7 @@ + %% Description: Initiates the server + %%-------------------------------------------------------------------- + -doc """ +-init(Options) -> {ok, State} | {ok, State, Timeout} | {stop, Reason} ++Initiates a client channel. + + The following options must be present: + +@@ -372,10 +360,10 @@ + - **`{init_args(), list()}`** - The list of arguments to the `init` function of + the callback module. + +-- **`{cm, ``t:ssh:connection_ref/0``}`** - Reference to the `ssh` connection as ++- **`{cm,` `t:ssh:connection_ref/0` `}`** - Reference to the `ssh` connection as + returned by `ssh:connect/3`. + +-- **`{channel_id, ``t:ssh:channel_id/0``}`** - Id of the `ssh` channel as ++- **`{channel_id,` `t:ssh:channel_id/0` `}`** - Id of the `ssh` channel as + returned by + [ssh_connection:session_channel/2,4](`ssh_connection:session_channel/2`). + +diff -ruN a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl +--- a/lib/ssh/src/ssh_connection.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_connection.erl 2025-12-17 17:25:02.075556404 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -29,29 +29,31 @@ + This module provides API functions to send SSH Connection Protocol events to the + other side of an SSH channel. + +-The [SSH Connection Protocol](http://www.ietf.org/rfc/rfc4254.txt) is used by ++The [SSH Connection Protocol (RFC 4254)](http://www.ietf.org/rfc/rfc4254.txt) is used by + clients and servers, that is, SSH channels, to communicate over the SSH + connection. The API functions in this module send SSH Connection Protocol + events, which are received as messages by the remote channel handling the remote + channel. The Erlang format of thoose messages is (see also + [below](`t:event/0`)): + +-`{ssh_cm, ``t:ssh:connection_ref/0``, ``t:channel_msg/0``}` ++`{ssh_cm,` `t:ssh:connection_ref/0` `,` `t:channel_msg/0` `}` + + If the `m:ssh_client_channel` behavior is used to implement the channel process, + these messages are handled by + [handle_ssh_msg/2](`c:ssh_client_channel:handle_ssh_msg/2`). + """. + -moduledoc(#{titles => +- [{type,<<"SSH Connection Protocol: General">>}, +- {type,<<"Data Transfer (RFC 4254, section 5.2)">>}, +- {type,<<"Closing a Channel (RFC 4254, section 5.3)">>}, +- {type,<<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>}, +- {type,<<"Environment Variable Passing (RFC 4254, section 6.4)">>}, +- {type,<<"Starting a Shell or Command (RFC 4254, section 6.5)">>}, +- {type,<<"Window Dimension Change Message (RFC 4254, section 6.7)">>}, +- {type,<<"Signals (RFC 4254, section 6.9)">>}, +- {type,<<"Returning Exit Status (RFC 4254, section 6.10)">>}]}). ++ [{type,<<"General">>}, ++ {type,<<"Data Transfer">>}, ++ {type,<<"Closing a Channel">>}, ++ {type,<<"Pseudo-Terminal">>}, ++ {type,<<"Environment Variable">>}, ++ {type,<<"Shell or Command">>}, ++ {type,<<"Window Change">>}, ++ {type,<<"Signals">>}, ++ {type,<<"Exit Status">>}]}). ++ ++-include_lib("kernel/include/logger.hrl"). + + -include("ssh.hrl"). + -include("ssh_connect.hrl"). +@@ -122,7 +124,7 @@ + """. + -type reason() :: closed | timeout . + +--doc(#{equiv => reason/0}). ++-doc(#{}). + -type result() :: req_status() | {error, reason()} . + + -doc """ +@@ -150,8 +152,7 @@ + exec_ch_msg/0 + ]). + +--doc(#{title => <<"SSH Connection Protocol: General">>, +- equiv => channel_msg/0}). ++-doc(#{title => <<"General">>}). + -type event() :: {ssh_cm, ssh:connection_ref(), channel_msg()}. + -doc """ + As mentioned in the introduction, the +@@ -160,7 +161,7 @@ + support by the `m:ssh_client_channel` behavior the process must handle thoose + messages. + """. +--doc(#{title => <<"SSH Connection Protocol: General">>}). ++-doc(#{title => <<"General">>}). + -type channel_msg() :: data_ch_msg() + | eof_ch_msg() + | closed_ch_msg() +@@ -179,14 +180,15 @@ + [ssh_connection:reply_request/4](`reply_request/4`) with the boolean value of + `WantReply` as the second argument. + """. +--doc(#{title => <<"SSH Connection Protocol: General">>}). ++-doc(#{title => <<"General">>}). + -type want_reply() :: boolean(). + + -doc """ + Data has arrived on the channel. This event is sent as a result of calling + [ssh_connection:send/3,4,5](`send/3`). + """. +--doc(#{title => <<"Data Transfer (RFC 4254, section 5.2)">>}). ++-doc(#{title => <<"Data Transfer">>, ++ rfc => ~"RFC 4254, section 5.2"}). + -type data_ch_msg() :: {data, + ssh:channel_id(), + ssh_data_type_code(), +@@ -196,7 +198,8 @@ + Indicates that the other side sends no more data. This event is sent as a result + of calling [ssh_connection:send_eof/2](`send_eof/2`). + """. +--doc(#{title => <<"Closing a Channel (RFC 4254, section 5.3)">>}). ++-doc(#{title => <<"Closing a Channel">>, ++ rfc => ~"RFC 4254, section 5.3"}). + -type eof_ch_msg() :: {eof, + ssh:channel_id() + } . +@@ -207,7 +210,8 @@ + signals referred to are on OS-level and not something generated by an Erlang + program. + """. +--doc(#{title => <<"Signals (RFC 4254, section 6.9)">>}). ++-doc(#{title => <<"Signals">>, ++ rfc => ~"RFC 4254, section 6.9"}). + -type signal_ch_msg() :: {signal, + ssh:channel_id(), + SignalName :: string() +@@ -218,7 +222,8 @@ + [RFC 4254](https://tools.ietf.org/html/rfc4254#section-6.10) Section 6.10, which + shows a special case of these signals. + """. +--doc(#{title => <<"Returning Exit Status (RFC 4254, section 6.10)">>}). ++-doc(#{title => <<"Exit Status">>, ++ rfc => ~"RFC 4254, section 6.10"}). + -type exit_signal_ch_msg() :: {exit_signal, ssh:channel_id(), + ExitSignal :: string(), + ErrorMsg :: string(), +@@ -229,7 +234,8 @@ + means that the command terminated successfully. This event is sent as a result + of calling [ssh_connection:exit_status/3](`exit_status/3`). + """. +--doc(#{title => <<"Returning Exit Status (RFC 4254, section 6.10)">>}). ++-doc(#{title => <<"Exit Status">>, ++ rfc => ~"RFC 4254, section 6.10"}). + -type exit_status_ch_msg() :: {exit_status, + ssh:channel_id(), + ExitStatus :: non_neg_integer() +@@ -239,7 +245,8 @@ + Both the handling of this event and sending it are taken care of by the + `m:ssh_client_channel` behavior. + """. +--doc(#{title => <<"Closing a Channel (RFC 4254, section 5.3)">>}). ++-doc(#{title => <<"Closing a Channel">>, ++ rfc => ~"RFC 4254, section 5.3"}). + -type closed_ch_msg() :: {closed, + ssh:channel_id() + } . +@@ -247,15 +254,16 @@ + Environment variables can be passed to the shell/command to be started later. + This event is sent as a result of calling [ssh_connection:setenv/5](`setenv/5`). + """. +--doc(#{title => <<"Environment Variable Passing (RFC 4254, section 6.4)">>}). ++-doc(#{title => <<"Environment Variable">>, ++ rfc => ~"RFC 4254, section 6.4"}). + -type env_ch_msg() :: {env, + ssh:channel_id(), + want_reply(), + Var :: string(), + Value :: string() + } . +--doc(#{title => <<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>, +- equiv => term_mode/0}). ++-doc(#{title => <<"Pseudo-Terminal">>, ++ rfc => ~"RFC 4254, section 6.2"}). + -type pty_ch_msg() :: {pty, + ssh:channel_id(), + want_reply(), +@@ -280,7 +288,8 @@ + `OP code: 53, mnemonic name ECHO erlang atom: echo`. This event is sent as a + result of calling [ssh_connection:ptty_alloc/4](`ptty_alloc/4`). + """. +--doc(#{title => <<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>}). ++-doc(#{title => <<"Pseudo-Terminal">>, ++ rfc => ~"RFC 4254, section 6.2"}). + -type term_mode() :: {Opcode :: atom() | byte(), + Value :: non_neg_integer()} . + +@@ -288,7 +297,8 @@ + This message requests that the user default shell is started at the other end. + This event is sent as a result of calling [ssh_connection:shell/2](`shell/2`). + """. +--doc(#{title => <<"Starting a Shell or Command (RFC 4254, section 6.5)">>}). ++-doc(#{title => <<"Shell or Command">>, ++ rfc => ~"RFC 4254, section 6.5"}). + -type shell_ch_msg() :: {shell, + ssh:channel_id(), + want_reply() +@@ -298,7 +308,8 @@ + message to the server side to inform it of the new dimensions. No API function + generates this event. + """. +--doc(#{title => <<"Window Dimension Change Message (RFC 4254, section 6.7)">>}). ++-doc(#{title => <<"Window Change">>, ++ rfc => ~"RFC 4254, section 6.7"}). + -type window_change_ch_msg() :: {window_change, + ssh:channel_id(), + CharWidth :: non_neg_integer(), +@@ -310,7 +321,8 @@ + This message requests that the server starts execution of the given command. + This event is sent as a result of calling [ssh_connection:exec/4 ](`exec/4`). + """. +--doc(#{title => <<"Starting a Shell or Command (RFC 4254, section 6.5)">>}). ++-doc(#{title => <<"Shell or Command">>, ++ rfc => ~"RFC 4254, section 6.5"}). + -type exec_ch_msg() :: {exec, + ssh:channel_id(), + want_reply(), +@@ -340,8 +352,8 @@ + Timeout :: timeout(), + Result :: {ok, ssh:channel_id()} | {error, reason()} . + +-session_channel(ConnectionHandler, Timeout) -> +- session_channel(ConnectionHandler, undefined, undefined, Timeout). ++session_channel(ConnectionRef, Timeout) -> ++ session_channel(ConnectionRef, undefined, undefined, Timeout). + + + -doc """ +@@ -355,8 +367,8 @@ + Timeout :: timeout(), + Result :: {ok, ssh:channel_id()} | {error, reason()} . + +-session_channel(ConnectionHandler, InitialWindowSize, MaxPacketSize, Timeout) -> +- open_channel(ConnectionHandler, "session", <<>>, ++session_channel(ConnectionRef, InitialWindowSize, MaxPacketSize, Timeout) -> ++ open_channel(ConnectionRef, "session", <<>>, + InitialWindowSize, + MaxPacketSize, + Timeout). +@@ -365,11 +377,11 @@ + %% Description: Opens a channel for the given type. + %% -------------------------------------------------------------------- + -doc false. +-open_channel(ConnectionHandler, Type, ChanData, Timeout) -> +- open_channel(ConnectionHandler, Type, ChanData, undefined, undefined, Timeout). ++open_channel(ConnectionRef, Type, ChanData, Timeout) -> ++ open_channel(ConnectionRef, Type, ChanData, undefined, undefined, Timeout). + +-open_channel(ConnectionHandler, Type, ChanData, InitialWindowSize, MaxPacketSize, Timeout) -> +- case ssh_connection_handler:open_channel(ConnectionHandler, Type, ChanData, ++open_channel(ConnectionRef, Type, ChanData, InitialWindowSize, MaxPacketSize, Timeout) -> ++ case ssh_connection_handler:open_channel(ConnectionRef, Type, ChanData, + InitialWindowSize, MaxPacketSize, + Timeout) of + {open, Channel} -> +@@ -414,8 +426,8 @@ + Command :: string(), + Timeout :: timeout(). + +-exec(ConnectionHandler, ChannelId, Command, TimeOut) -> +- ssh_connection_handler:request(ConnectionHandler, self(), ChannelId, "exec", ++exec(ConnectionRef, ChannelId, Command, TimeOut) -> ++ ssh_connection_handler:request(ConnectionRef, self(), ChannelId, "exec", + true, [?string(Command)], TimeOut). + + %%-------------------------------------------------------------------- +@@ -437,8 +449,8 @@ + ChannelId :: ssh:channel_id(), + Result :: ok | success | failure | {error, timeout} . + +-shell(ConnectionHandler, ChannelId) -> +- ssh_connection_handler:request(ConnectionHandler, self(), ChannelId, ++shell(ConnectionRef, ChannelId) -> ++ ssh_connection_handler:request(ConnectionRef, self(), ChannelId, + "shell", false, <<>>, 0). + %%-------------------------------------------------------------------- + %% +@@ -457,8 +469,8 @@ + Subsystem :: string(), + Timeout :: timeout(). + +-subsystem(ConnectionHandler, ChannelId, SubSystem, TimeOut) -> +- ssh_connection_handler:request(ConnectionHandler, self(), ++subsystem(ConnectionRef, ChannelId, SubSystem, TimeOut) -> ++ ssh_connection_handler:request(ConnectionRef, self(), + ChannelId, "subsystem", + true, [?string(SubSystem)], TimeOut). + %%-------------------------------------------------------------------- +@@ -466,51 +478,51 @@ + %%-------------------------------------------------------------------- + -doc(#{equiv => send/5}). + -spec send(connection_ref(), channel_id(), iodata()) -> +- ok | {error, timeout | closed}. ++ ok | {error, reason()}. + +-send(ConnectionHandler, ChannelId, Data) -> +- send(ConnectionHandler, ChannelId, 0, Data, infinity). ++send(ConnectionRef, ChannelId, Data) -> ++ send(ConnectionRef, ChannelId, 0, Data, infinity). + + + -doc """ + send(ConnectionRef, ChannelId, Type, Data) + ++Depending on input arguments equivalent to one of `send/5` calls specified below. ++ + Equivalent to [send(ConnectionRef, ChannelId, 0, Data, TimeOut)](`send/5`) if + called with TimeOut being integer. + + Equivalent to [send(ConnectionRef, ChannelId, 0, Data, infinity)](`send/5`) if + called with TimeOut being infinity atom. + +-Equivalent to [send(ConnectionHandler, ChannelId, Type, Data, infinity)](`send/5`) if ++Equivalent to [send(ConnectionRef, ChannelId, Type, Data, infinity)](`send/5`) if + called with last argument which is not integer or infinity atom. + """. + +--spec send(connection_ref(), channel_id(), iodata(), timeout()) -> ok | {error, reason()}; +- (connection_ref(), channel_id(), ssh_data_type_code(), iodata()) -> ok | {error, reason()}. ++-spec send(connection_ref(), channel_id(), iodata(), timeout()) -> ok | {error, reason()}; ++ (connection_ref(), channel_id(), ssh_data_type_code(), iodata()) -> ok | {error, reason()}. + +-send(ConnectionHandler, ChannelId, Data, TimeOut) when is_integer(TimeOut) -> +- send(ConnectionHandler, ChannelId, 0, Data, TimeOut); ++send(ConnectionRef, ChannelId, Data, TimeOut) when is_integer(TimeOut) -> ++ send(ConnectionRef, ChannelId, 0, Data, TimeOut); + +-send(ConnectionHandler, ChannelId, Data, infinity) -> +- send(ConnectionHandler, ChannelId, 0, Data, infinity); ++send(ConnectionRef, ChannelId, Data, infinity) -> ++ send(ConnectionRef, ChannelId, 0, Data, infinity); + +-send(ConnectionHandler, ChannelId, Type, Data) -> +- send(ConnectionHandler, ChannelId, Type, Data, infinity). ++send(ConnectionRef, ChannelId, Type, Data) -> ++ send(ConnectionRef, ChannelId, Type, Data, infinity). + + + -doc """ +-send(ConnectionRef, ChannelId, Type, Data, TimeOut) -> ok | Error +- + Is to be called by client- and server-channel processes to send data to each + other. + + The function `subsystem/4` and subsequent calls of `send/3,4,5` must be executed + in the same process. + """. +--spec send(connection_ref(), channel_id(), ssh_data_type_code(), iodata(), timeout()) -> ok | {error, reason()}. ++-spec send(connection_ref(), channel_id(), ssh_data_type_code(), iodata(), timeout()) -> ok | {error, reason()}. + +-send(ConnectionHandler, ChannelId, Type, Data, TimeOut) -> +- ssh_connection_handler:send(ConnectionHandler, ChannelId, ++send(ConnectionRef, ChannelId, Type, Data, TimeOut) -> ++ ssh_connection_handler:send(ConnectionRef, ChannelId, + Type, Data, TimeOut). + %%-------------------------------------------------------------------- + -doc "Sends EOF on channel `ChannelId`.". +@@ -521,8 +533,8 @@ + %% + %% Description: Sends eof on the channel . + %%-------------------------------------------------------------------- +-send_eof(ConnectionHandler, Channel) -> +- ssh_connection_handler:send_eof(ConnectionHandler, Channel). ++send_eof(ConnectionRef, Channel) -> ++ ssh_connection_handler:send_eof(ConnectionRef, Channel). + + %%-------------------------------------------------------------------- + -doc """ +@@ -545,8 +557,8 @@ + %% + %% Description: Adjusts the ssh flowcontrol window. + %%-------------------------------------------------------------------- +-adjust_window(ConnectionHandler, Channel, Bytes) -> +- ssh_connection_handler:adjust_window(ConnectionHandler, Channel, Bytes). ++adjust_window(ConnectionRef, Channel, Bytes) -> ++ ssh_connection_handler:adjust_window(ConnectionRef, Channel, Bytes). + + %%-------------------------------------------------------------------- + -doc """ +@@ -563,11 +575,11 @@ + %% + %% Description: Environment variables may be passed to the shell/command to be + %% started later. +-setenv(ConnectionHandler, ChannelId, Var, Value, TimeOut) -> +- setenv(ConnectionHandler, ChannelId, true, Var, Value, TimeOut). ++setenv(ConnectionRef, ChannelId, Var, Value, TimeOut) -> ++ setenv(ConnectionRef, ChannelId, true, Var, Value, TimeOut). + +-setenv(ConnectionHandler, ChannelId, WantReply, Var, Value, TimeOut) -> +- case ssh_connection_handler:request(ConnectionHandler, ChannelId, ++setenv(ConnectionRef, ChannelId, WantReply, Var, Value, TimeOut) -> ++ case ssh_connection_handler:request(ConnectionRef, ChannelId, + "env", WantReply, + [?string(Var), ?string(Value)], TimeOut) of + ok when WantReply == false -> +@@ -593,8 +605,8 @@ + %% + %% Description: Sends a close message on the channel . + %%-------------------------------------------------------------------- +-close(ConnectionHandler, ChannelId) -> +- ssh_connection_handler:close(ConnectionHandler, ChannelId). ++close(ConnectionRef, ChannelId) -> ++ ssh_connection_handler:close(ConnectionRef, ChannelId). + + %%-------------------------------------------------------------------- + -doc """ +@@ -612,8 +624,8 @@ + %% + %% Description: Send status replies to requests that want such replies. + %%-------------------------------------------------------------------- +-reply_request(ConnectionHandler, true, Status, ChannelId) -> +- ssh_connection_handler:reply_request(ConnectionHandler, Status, ChannelId); ++reply_request(ConnectionRef, true, Status, ChannelId) -> ++ ssh_connection_handler:reply_request(ConnectionRef, Status, ChannelId); + reply_request(_,false, _, _) -> + ok. + +@@ -627,8 +639,8 @@ + ChannelId :: ssh:channel_id(), + Options :: proplists:proplist(). + +-ptty_alloc(ConnectionHandler, Channel, Options) -> +- ptty_alloc(ConnectionHandler, Channel, Options, infinity). ++ptty_alloc(ConnectionRef, Channel, Options) -> ++ ptty_alloc(ConnectionRef, Channel, Options, infinity). + + + -doc """ +@@ -659,11 +671,11 @@ + Options :: proplists:proplist(), + Timeout :: timeout(). + +-ptty_alloc(ConnectionHandler, Channel, Options0, TimeOut) -> ++ptty_alloc(ConnectionRef, Channel, Options0, TimeOut) -> + TermData = backwards_compatible(Options0, []), % FIXME + {Width, PixWidth} = pty_default_dimensions(width, TermData), + {Height, PixHeight} = pty_default_dimensions(height, TermData), +- pty_req(ConnectionHandler, Channel, ++ pty_req(ConnectionRef, Channel, + proplists:get_value(term, TermData, os:getenv("TERM", ?DEFAULT_TERMINAL)), + proplists:get_value(width, TermData, Width), + proplists:get_value(height, TermData, Height), +@@ -678,19 +690,19 @@ + %% Should they be documented and tested? + %%-------------------------------------------------------------------- + -doc false. +-window_change(ConnectionHandler, Channel, Width, Height) -> +- window_change(ConnectionHandler, Channel, Width, Height, 0, 0). ++window_change(ConnectionRef, Channel, Width, Height) -> ++ window_change(ConnectionRef, Channel, Width, Height, 0, 0). + -doc false. +-window_change(ConnectionHandler, Channel, Width, Height, ++window_change(ConnectionRef, Channel, Width, Height, + PixWidth, PixHeight) -> +- ssh_connection_handler:request(ConnectionHandler, Channel, ++ ssh_connection_handler:request(ConnectionRef, Channel, + "window-change", false, + [?uint32(Width), ?uint32(Height), + ?uint32(PixWidth), ?uint32(PixHeight)], 0). + + -doc false. +-signal(ConnectionHandler, Channel, Sig) -> +- ssh_connection_handler:request(ConnectionHandler, Channel, ++signal(ConnectionRef, Channel, Sig) -> ++ ssh_connection_handler:request(ConnectionRef, Channel, + "signal", false, [?string(Sig)], 0). + + +@@ -702,8 +714,8 @@ + ConnectionRef :: ssh:connection_ref(), + ChannelId :: ssh:channel_id(), + Status :: integer(). +-exit_status(ConnectionHandler, Channel, Status) -> +- ssh_connection_handler:request(ConnectionHandler, Channel, ++exit_status(ConnectionRef, Channel, Status) -> ++ ssh_connection_handler:request(ConnectionRef, Channel, + "exit-status", false, [?uint32(Status)], 0). + + %%-------------------------------------------------------------------- +@@ -746,23 +758,50 @@ + %%% + + -doc false. ++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, +@@ -811,6 +850,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; + +@@ -1026,14 +1069,24 @@ + ?DEC_BIN(Err, _ErrLen), + ?DEC_BIN(Lang, _LangLen)>> = Data, + case ssh_client_channel:cache_lookup(Cache, ChannelId) of +- #channel{remote_id = RemoteId} = Channel -> ++ #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)}), +- ChannelCloseMsg = channel_close_msg(RemoteId), +- {[{connection_reply, ChannelCloseMsg}|Reply], Connection}; ++ %% 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} +@@ -1250,12 +1303,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}}. + + + %%%---------------------------------------------------------------- +@@ -1555,9 +1603,9 @@ + %%% Pseudo terminal stuff + %%% + +-pty_req(ConnectionHandler, Channel, Term, Width, Height, ++pty_req(ConnectionRef, Channel, Term, Width, Height, + PixWidth, PixHeight, PtyOpts, TimeOut) -> +- ssh_connection_handler:request(ConnectionHandler, ++ ssh_connection_handler:request(ConnectionRef, + Channel, "pty-req", true, + [?string(Term), + ?uint32(Width), ?uint32(Height), +@@ -1825,7 +1873,6 @@ + WantedSize = Size - byte_size(Data), + ssh_client_channel:cache_update(Connection#connection.channel_cache, + Channel#channel{recv_window_size = WantedSize}), +- adjust_window(self(), ChannelId, byte_size(Data)), + reply_msg(Channel, Connection, {data, ChannelId, DataType, Data}); + undefined -> + {[], Connection} +@@ -1867,14 +1914,14 @@ + + %%%---------------------------------------------------------------- + -doc false. +-send_environment_vars(ConnectionHandler, Channel, VarNames) -> ++send_environment_vars(ConnectionRef, Channel, VarNames) -> + lists:foldl( + fun(Var, success) -> + case os:getenv(Var) of + false -> + success; + Value -> +- setenv(ConnectionHandler, Channel, false, ++ setenv(ConnectionRef, Channel, false, + Var, Value, infinity) + end + end, success, VarNames). +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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_connection_handler.erl 2025-12-17 17:25:02.075556404 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -35,7 +35,6 @@ + -include("ssh_transport.hrl"). + -include("ssh_auth.hrl"). + -include("ssh_connect.hrl"). +- + -include("ssh_fsm.hrl"). + + %%==================================================================== +@@ -614,17 +613,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", +@@ -653,11 +653,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, +@@ -675,17 +676,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, +@@ -728,16 +736,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; + +@@ -1096,12 +1094,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; + +@@ -1141,11 +1149,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 +@@ -1177,12 +1188,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; +@@ -1212,12 +1232,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; +@@ -1259,15 +1287,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 +@@ -1280,6 +1326,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), +@@ -2052,6 +2108,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.erl b/lib/ssh/src/ssh.erl +--- a/lib/ssh/src/ssh.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh.erl 2025-12-17 17:25:02.074556391 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2004-2024. 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. +@@ -61,7 +61,7 @@ + `m:ssh_client_channel`. For server channel handlers use `m:ssh_server_channel` + behaviour (replaces ssh_daemon_channel). + +-Both clients and daemons accepts options that controls the exact behaviour. Some ++Both clients and daemons accept options that control the exact behaviour. Some + options are common to both. The three sets are called + [Client Options](`t:client_options/0`), [Daemon Options](`t:daemon_options/0`) + and [Common Options](`t:common_options/0`). +@@ -133,7 +133,7 @@ + """. + -moduledoc(#{titles => + [{type,<<"Client Options">>}, +- {type,<<"Daemon Options (Server Options)">>}, ++ {type,<<"Daemon Options">>}, + {type,<<"Common Options">>}, + {type,<<"Other data types">>}]}). + +@@ -322,8 +322,6 @@ + bad_arg(PortOrOptions, OptionsOrTimeout). + + -doc """ +-connect(Host, Port, Options, NegotiationTimeout) -> Result +- + Connects to an SSH server at the `Host` on `Port`. + + As an alternative, an already open TCP socket could be passed to the function in +@@ -429,13 +427,13 @@ + %%-------------------------------------------------------------------- + %% Description: Retrieves information about a connection. + %%--------------------------------------------------------------------- +--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}). ++-doc(#{title => <<"Other data types">>}). + -type version() :: {protocol_version(), software_version()}. +--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}). ++-doc(#{title => <<"Other data types">>}). + -type protocol_version() :: {Major::pos_integer(), Minor::non_neg_integer()}. +--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}). ++-doc(#{title => <<"Other data types">>}). + -type software_version() :: string(). +--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}). ++-doc(#{title => <<"Other data types">>}). + -type conn_info_algs() :: [{kex, kex_alg()} + | {hkey, pubkey_alg()} + | {encrypt, cipher_alg()} +@@ -456,7 +454,7 @@ + -doc(#{title => <<"Other data types">>}). + -type conn_info_channels() :: [proplists:proplist()]. + +--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}). ++-doc(#{title => <<"Other data types">>}). + -type connection_info_tuple() :: + {client_version, version()} + | {server_version, version()} +@@ -466,15 +464,16 @@ + | {options, client_options()} + | {algorithms, conn_info_algs()} + | {channels, conn_info_channels()}. +- ++ + -doc(#{equiv => connection_info/2}). + -doc(#{since => <<"OTP 22.1">>}). +--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, []). + + -doc """ +@@ -482,7 +481,8 @@ + + When the `Key` is a single `Item`, the result is a single `InfoTuple` + """. +--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, +@@ -560,7 +560,7 @@ + + + -doc """ +-daemon(HostAddress, Port, Options) -> Result ++daemon(HostAddress, Port, Options) + + Starts a server listening for SSH connections on the given port. If the `Port` + is 0, a random free port is selected. See `daemon_info/1` about how to find the +@@ -682,9 +682,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. + + %%-------------------------------------------------------------------- + -doc """ +@@ -894,8 +898,6 @@ + + + -doc """ +-shell(Host, Port, Options) -> Result +- + Connects to an SSH server at `Host` and `Port` (defaults to 22) and starts an + interactive shell on that remote host. + +diff -ruN a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl +--- a/lib/ssh/src/ssh_file.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_file.erl 2025-12-17 17:25:02.075556404 +1030 +@@ -170,7 +170,7 @@ + """. + -moduledoc(#{since => "OTP 21.2", + titles => +- [{type,<<"Options for the default ssh_file callback module">>}]}). ++ [{type,<<"Options">>}]}). + + -include_lib("public_key/include/public_key.hrl"). + -include_lib("kernel/include/file.hrl"). +@@ -186,7 +186,7 @@ + -export([host_key/2, is_auth_key/3]). + -export_type([system_dir_daemon_option/0]). + -doc "Sets the [system directory](`m:ssh_file#SYSDIR`).". +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type system_dir_daemon_option() :: {system_dir, string()}. + + %%%--------------------- client exports --------------------------- +@@ -199,7 +199,7 @@ + + Note that EdDSA passhrases (Curves 25519 and 448) are not implemented. + """. +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type pubkey_passphrase_client_options() :: {dsa_pass_phrase, string()} + | {rsa_pass_phrase, string()} + %% Not yet implemented: | {ed25519_pass_phrase, string()} +@@ -218,16 +218,15 @@ + ]). + + -doc "Sets the [user directory](`m:ssh_file#USERDIR`).". +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type user_dir_common_option() :: {user_dir, string()}. +--doc(#{title => <<"Options for the default ssh_file callback module">>, +- equiv => user2dir/0}). ++-doc(#{title => <<"Options">>}). + -type user_dir_fun_common_option() :: {user_dir_fun, user2dir()}. + -doc """ + Sets the [user directory](`m:ssh_file#USERDIR`) dynamically by evaluating the + `user2dir` function. + """. +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type user2dir() :: fun((RemoteUserName::string()) -> UserDir :: string()) . + + -doc """ +@@ -239,17 +238,16 @@ + call of ["ssh:connect/3](`ssh:connect/3`), `ssh:daemon/2` or similar function + call that initiates an ssh connection. + """. +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type optimize_key_lookup() :: {optimize, time|space} . + + -doc "The key representation". +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type key() :: public_key:public_key() | public_key:private_key() . +--doc(#{title => <<"Options for the default ssh_file callback module">>, +- equiv => openssh_key_v1_attributes/0}). ++-doc(#{title => <<"Options">>}). + -type experimental_openssh_key_v1() :: [{key(), openssh_key_v1_attributes()}]. + -doc "Types for the experimental implementaition of the `openssh_key_v1` format.". +--doc(#{title => <<"Options for the default ssh_file callback module">>}). ++-doc(#{title => <<"Options">>}). + -type openssh_key_v1_attributes() :: [{atom(),term()}]. + + %%%================================================================ +diff -ruN a/lib/ssh/src/ssh_fsm.hrl b/lib/ssh/src/ssh_fsm.hrl +--- a/lib/ssh/src/ssh_fsm.hrl 2024-12-05 21:56:22.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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_fsm_kexinit.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -44,6 +44,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 + %%==================================================================== +@@ -54,8 +59,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), +@@ -68,11 +78,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), +@@ -82,6 +91,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), +@@ -90,24 +100,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), +@@ -117,16 +131,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), +@@ -134,20 +157,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), +@@ -163,6 +199,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), +@@ -184,7 +229,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 +@@ -218,3 +262,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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_fsm_userauth_client.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -106,7 +106,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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh.hrl 2025-12-17 17:25:02.074556391 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2004-2024. 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. +@@ -150,22 +150,19 @@ + default. The option can be set to the empty list if you do not want the daemon + to run any subsystems. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type subsystem_spec() :: {Name::string(), mod_args()} . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type algs_list() :: list( alg_entry() ). +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type alg_entry() :: {kex, [kex_alg()]} + | {public_key, [pubkey_alg()]} + | {cipher, double_algs(cipher_alg())} + | {mac, double_algs(mac_alg())} + | {compression, double_algs(compression_alg())} . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type kex_alg() :: 'curve25519-sha256' | + 'curve25519-sha256@libssh.org' | + 'curve448-sha512' | +@@ -181,8 +178,7 @@ + 'diffie-hellman-group1-sha1' + . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type pubkey_alg() :: 'ssh-ed25519' | + 'ssh-ed448' | + 'ecdsa-sha2-nistp521' | +@@ -194,8 +190,7 @@ + 'ssh-dss' + . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type cipher_alg() :: 'aes256-gcm@openssh.com' | + 'aes256-ctr' | + 'aes192-ctr' | +@@ -210,8 +205,7 @@ + '3des-cbc' + . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type mac_alg() :: 'hmac-sha2-512-etm@openssh.com' | + 'hmac-sha2-256-etm@openssh.com' | + 'hmac-sha2-512' | +@@ -223,8 +217,7 @@ + 'AEAD_AES_128_GCM' + . + +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type compression_alg() :: 'none' | + 'zlib' | + 'zlib@openssh.com' +@@ -324,23 +317,20 @@ + + -type internal_options() :: ssh_options:private_options(). + -type socket_options() :: [gen_tcp:connect_option() | gen_tcp:listen_option()]. +- +--doc(#{title => <<"Client Options">>,equiv => client_option/0}). ++ ++-doc(#{title => <<"Client Options">>}). + -type client_options() :: [ client_option() ] . +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => daemon_option/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type daemon_options() :: [ daemon_option() ]. +- + +--doc(#{title => <<"Common Options">>, +- equiv => common_option/0}). ++-doc(#{title => <<"Common Options">>}). + -type common_options() :: [ common_option() ]. + -doc """ + The options above can be used both in clients and in daemons (servers). They are + further explained below. + """. + -doc(#{title => <<"Common Options">>}). +--type common_option() :: ++-type common_option() :: + ssh_file:user_dir_common_option() + | profile_common_option() + | max_idle_time_common_option() +@@ -383,8 +373,7 @@ + """. + -doc(#{title => <<"Common Options">>}). + -type max_idle_time_common_option() :: {idle_time, timeout()}. +--doc(#{title => <<"Common Options">>, +- equiv => limit_time/0}). ++-doc(#{title => <<"Common Options">>}). + -type rekey_limit_common_option() :: {rekey_limit, Bytes::limit_bytes() | + {Minutes::limit_time(), Bytes::limit_bytes()} + }. +@@ -395,8 +384,7 @@ + -doc(#{title => <<"Common Options">>}). + -type max_log_item_len_common_option() :: {max_log_item_len, limit_bytes()} . + +--doc(#{title => <<"Common Options">>, +- equiv => limit_time/0}). ++-doc(#{title => <<"Common Options">>}). + -type limit_bytes() :: non_neg_integer() | infinity . % non_neg_integer due to compatibility + -doc """ + Sets the limit when rekeying is to be initiated. Both the max time and max +@@ -497,11 +485,9 @@ + """. + -doc(#{title => <<"Common Options">>}). + -type pref_public_key_algs_common_option() :: {pref_public_key_algs, [pubkey_alg()] } . +--doc(#{title => <<"Common Options">>, +- equiv => double_algs/1}). ++-doc(#{title => <<"Common Options">>}). + -type preferred_algorithms_common_option():: {preferred_algorithms, algs_list()}. +--doc(#{title => <<"Common Options">>, +- equiv => modify_algs_list/0}). ++-doc(#{title => <<"Common Options">>}). + -type modify_algorithms_common_option() :: {modify_algorithms, modify_algs_list()}. + -doc """ + Comma-separated string that determines which authentication methods that the +@@ -561,8 +547,7 @@ + | gen_tcp:connect_option() + | ?COMMON_OPTION . + +--doc(#{title => <<"Other data types">>, +- equiv => opaque_common_options/0}). ++-doc(#{title => <<"Other data types">>}). + -type opaque_client_options() :: + {keyboard_interact_fun, fun((Name::iodata(), + Instruction::iodata(), +@@ -572,25 +557,6 @@ + )} + | opaque_common_options(). + +--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}). +--type host_accepting_client_options() :: +- {silently_accept_hosts, accept_hosts()} +- | {user_interaction, boolean()} +- | {save_accepted_host, boolean()} +- | {quiet_mode, boolean()} . +- +--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}). +--type accept_hosts() :: boolean() +- | accept_callback() +- | {HashAlgoSpec::fp_digest_alg(), accept_callback()}. +- +--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}). +--type fp_digest_alg() :: 'md5' | crypto:sha1() | crypto:sha2() . +- +--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}). +--type accept_callback() :: fun((PeerName::string(), fingerprint() ) -> boolean()) % Old style +- | fun((PeerName::string(), Port::inet:port_number(), fingerprint() ) -> boolean()) % New style +- . + -doc """ + - **`silently_accept_hosts`{: #hardening_client_options-silently_accept_hosts + }** - This option guides the `connect` function on how to act when the +@@ -651,6 +617,26 @@ + Defaults to `false` + """. + -doc(#{title => <<"Client Options">>}). ++-type host_accepting_client_options() :: ++ {silently_accept_hosts, accept_hosts()} ++ | {user_interaction, boolean()} ++ | {save_accepted_host, boolean()} ++ | {quiet_mode, boolean()} . ++ ++-doc(#{title => <<"Client Options">>}). ++-type accept_hosts() :: boolean() ++ | accept_callback() ++ | {HashAlgoSpec::fp_digest_alg(), accept_callback()}. ++ ++-doc(#{title => <<"Client Options">>}). ++-type fp_digest_alg() :: 'md5' | crypto:sha1() | crypto:sha2() . ++ ++-doc(#{title => <<"Client Options">>}). ++-type accept_callback() :: fun((PeerName::string(), fingerprint() ) -> boolean()) % Old style ++ | fun((PeerName::string(), Port::inet:port_number(), fingerprint() ) -> boolean()) % New style ++ . ++ ++-doc(#{title => <<"Client Options">>}). + -type fingerprint() :: string() | [string()]. + + -doc """ +@@ -712,7 +698,7 @@ + a way that impacts the ssh deamon's behaviour negatively. You use it on your own + risk. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type daemon_option() :: + subsystem_daemon_option() + | shell_daemon_option() +@@ -732,23 +718,20 @@ + | gen_tcp:listen_option() + | ?COMMON_OPTION . + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => subsystem_spec/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type subsystem_daemon_option() :: {subsystems, subsystem_specs()}. +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => subsystem_spec/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type subsystem_specs() :: [ subsystem_spec() ]. + +--doc(#{title => <<"Daemon Options (Server Options)">>, ++-doc(#{title => <<"Daemon Options">>, + equiv => 'shell_fun/2'/0}). + -type shell_daemon_option() :: {shell, shell_spec()} . +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => 'shell_fun/2'/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type shell_spec() :: mod_fun_args() | shell_fun() | disabled . +--doc(#{title => <<"Daemon Options (Server Options)">>, ++-doc(#{title => <<"Daemon Options">>, + equiv => 'shell_fun/2'/0}). + -type shell_fun() :: 'shell_fun/1'() | 'shell_fun/2'() . +--doc(#{title => <<"Daemon Options (Server Options)">>, ++-doc(#{title => <<"Daemon Options">>, + equiv => 'shell_fun/2'/0}). + -type 'shell_fun/1'() :: fun((User::string()) -> pid()) . + -doc """ +@@ -759,23 +742,20 @@ + how the daemon executes shell-requests and exec-requests depending on the shell- + and exec-options. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type 'shell_fun/2'() :: fun((User::string(), PeerAddr::inet:ip_address()) -> pid()). + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => exec_spec/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type exec_daemon_option() :: {exec, exec_spec()} . +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt(). +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type exec_fun() :: 'exec_fun/1'() | 'exec_fun/2'() | 'exec_fun/3'(). +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => 'exec_fun/3'/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type 'exec_fun/1'() :: fun((Cmd::string()) -> exec_result()) . +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => 'exec_fun/3'/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type 'exec_fun/2'() :: fun((Cmd::string(), User::string()) -> exec_result()) . +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type 'exec_fun/3'() :: fun((Cmd::string(), User::string(), ClientAddr::ip_port()) -> exec_result()) . + -doc """ + This option changes how the daemon executes exec-requests from clients. The term +@@ -847,13 +827,13 @@ + > retained but obey the rules 1-6 above if conflicting. The old and undocumented + > style should not be used in new programs. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type exec_result() :: {ok,Result::term()} | {error,Reason::term()} . + -doc """ + Old-style exec specification that are kept for compatibility, but should not be + used in new programs + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type deprecated_exec_opt() :: fun() | mod_fun_args() . + + -doc """ +@@ -866,20 +846,20 @@ + [`shell`](`t:shell_daemon_option/0`) and [`exec`](`t:exec_daemon_option/0`) are + disabled and only subsystem channels are allowed. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type ssh_cli_daemon_option() :: {ssh_cli, mod_args() | no_cli }. + + -doc """ + Enables (`true`) or disables (`false`) the possibility to tunnel a TCP/IP + connection out of a [server](`daemon/2`). Disabled per default. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type tcpip_tunnel_out_daemon_option() :: {tcpip_tunnel_out, boolean()} . + -doc """ + Enables (`true`) or disables (`false`) the possibility to tunnel a TCP/IP + connection in to a [server](`daemon/2`). Disabled per default. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type tcpip_tunnel_in_daemon_option() :: {tcpip_tunnel_in, boolean()} . + + -doc """ +@@ -892,11 +872,10 @@ + Default value is `true` which is compatible with other implementations not + supporting ext-info. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type send_ext_info_daemon_option() :: {send_ext_info, boolean()} . + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => pwdfun_4/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type authentication_daemon_options() :: + ssh_file:system_dir_daemon_option() + | {auth_method_kb_interactive_data, prompt_texts() } +@@ -904,28 +883,22 @@ + | {pk_check_user, boolean()} + | {password, string()} + | {pwdfun, pwdfun_2() | pwdfun_4()} +- | {no_auth_needed, boolean()} +- . ++ | {no_auth_needed, boolean()}. + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => pwdfun_4/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type prompt_texts() :: + kb_int_tuple() + | kb_int_fun_3() +- | kb_int_fun_4() +- . ++ | kb_int_fun_4(). + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => pwdfun_4/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type kb_int_fun_3() :: fun((Peer::ip_port(), User::string(), Service::string()) -> kb_int_tuple()). +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => pwdfun_4/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type kb_int_fun_4() :: fun((Peer::ip_port(), User::string(), Service::string(), State::any()) -> kb_int_tuple()). +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => pwdfun_4/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type kb_int_tuple() :: {Name::string(), Instruction::string(), Prompt::string(), Echo::boolean()}. + +--doc(#{title => <<"Daemon Options (Server Options)">>, ++-doc(#{title => <<"Daemon Options">>, + equiv => pwdfun_4/0}). + -type pwdfun_2() :: fun((User::string(), Password::string()|pubkey) -> boolean()) . + -doc """ +@@ -1025,7 +998,7 @@ + + The default value is `false`. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type pwdfun_4() :: fun((User::string(), + Password::string()|pubkey, + PeerAddress::ip_port(), +@@ -1033,17 +1006,14 @@ + boolean() | disconnect | {boolean(),NewState::any()} + ) . + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => ssh_moduli_file/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type diffie_hellman_group_exchange_daemon_option() :: + {dh_gex_groups, [explicit_group()] | explicit_group_file() | ssh_moduli_file()} + | {dh_gex_limits, {Min::pos_integer(), Max::pos_integer()} } . + +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => ssh_moduli_file/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type explicit_group() :: {Size::pos_integer(),G::pos_integer(),P::pos_integer()} . +--doc(#{title => <<"Daemon Options (Server Options)">>, +- equiv => ssh_moduli_file/0}). ++-doc(#{title => <<"Daemon Options">>}). + -type explicit_group_file() :: {file,string()} . + -doc """ + - **`dh_gex_groups`** - Defines the groups the server may choose among when +@@ -1078,7 +1048,7 @@ + See [RFC 4419](https://tools.ietf.org/html/rfc4419) for the function of the + Max and Min values. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type ssh_moduli_file() :: {ssh_moduli_file,string()}. + + -doc """ +@@ -1089,7 +1059,7 @@ + [Timeouts section ](hardening.md#timeouts)in the User's Guide + [Hardening](hardening.md) chapter. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type max_initial_idle_time_daemon_option() :: {max_initial_idle_time, timeout()} . + -doc """ + Maximum time in milliseconds for the authentication negotiation. Defaults to +@@ -1100,7 +1070,7 @@ + [Timeouts section ](hardening.md#timeouts)in the User's Guide + [Hardening](hardening.md) chapter. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type negotiation_timeout_daemon_option() :: {negotiation_timeout, timeout()} . + -doc """ + Maximum time in milliseconds for the first part of the ssh session setup, the +@@ -1111,7 +1081,7 @@ + [Timeouts section ](hardening.md#timeouts)in the User's Guide + [Hardening](hardening.md) chapter. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type hello_timeout_daemon_option() :: {hello_timeout, timeout()} . + + -doc """ +@@ -1160,7 +1130,7 @@ + maximum packet size that the daemon will accept in channel open requests from + the client. The default value is 0. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type hardening_daemon_options() :: + {max_sessions, pos_integer()} + | {max_channels, pos_integer()} +@@ -1174,13 +1144,12 @@ + - **`failfun`** - Provides a fun to implement your own logging when a user fails + to authenticate. + """. +--doc(#{title => <<"Daemon Options (Server Options)">>}). ++-doc(#{title => <<"Daemon Options">>}). + -type callbacks_daemon_options() :: +- {failfun, fun((User::string(), PeerAddress::inet:ip_address(), Reason::term()) -> _)} +- | {connectfun, fun((User::string(), PeerAddress::inet:ip_address(), Method::string()) ->_)} . ++ {failfun, fun((User::string(), Peer::{inet:ip_address(), inet:port_number()}, Reason::term()) -> _)} ++ | {connectfun, fun((User::string(), Peer::{inet:ip_address(), inet:port_number()}, Method::string()) ->_)} . + +--doc(#{title => <<"Other data types">>, +- equiv => opaque_common_options/0}). ++-doc(#{title => <<"Other data types">>}). + -type opaque_daemon_options() :: + {infofun, fun()} + | opaque_common_options(). +@@ -1202,7 +1171,7 @@ + + -record(ssh, + { +- role :: client | role(), ++ role :: role(), + peer :: undefined | + {inet:hostname(),ip_port()}, %% string version of peer address + +@@ -1338,5 +1307,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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_info.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -274,6 +274,7 @@ + _:_ -> "?" + end. + ++ + format_address(#address{address=Addr, port=Port, profile=Prof}) -> + io_lib:format("~s (profile ~p)", [ssh_lib:format_address_port({Addr,Port}),Prof]); + format_address(A) -> +diff -ruN a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl +--- a/lib/ssh/src/ssh_lib.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_lib.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2004-2024. 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. +@@ -31,7 +31,9 @@ + format_time_ms/1, + comp/2, + set_label/1, +- set_label/2 ++ set_label/2, ++ trim_reason/1, ++ max_log_len/1 + ]). + + -include("ssh.hrl"). +@@ -97,3 +99,17 @@ + proc_lib:set_label({sshc, Details}); + set_label(server, Details) -> + proc_lib:set_label({sshd, Details}). ++ ++%% 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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_message.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2013-2024. 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. +@@ -25,6 +25,7 @@ + -moduledoc false. + + -include_lib("public_key/include/public_key.hrl"). ++-include_lib("kernel/include/logger.hrl"). + + -include("ssh.hrl"). + -include("ssh_connect.hrl"). +@@ -43,11 +44,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. +@@ -207,12 +209,12 @@ + encode(#ssh_msg_service_request{ + name = Service + }) -> +- <>; ++ <>; + + encode(#ssh_msg_service_accept{ + name = Service + }) -> +- <>; ++ <>; + + encode(#ssh_msg_ext_info{ + nr_extensions = N, +@@ -375,7 +377,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 + } +@@ -405,8 +407,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 + }; + +@@ -414,7 +416,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) + }; + +@@ -528,12 +530,12 @@ + + decode(<>) -> + #ssh_msg_service_request{ +- name = ?unicode_list(Service) ++ name = binary:bin_to_list(Service) + }; + + decode(<>) -> + #ssh_msg_service_accept{ +- name = ?unicode_list(Service) ++ name = binary:bin_to_list(Service) + }; + + decode(<>) -> +@@ -820,9 +822,33 @@ + %% See rfc 4253 7.1 + X = 0, + list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); +-decode_kex_init(<>, Acc, N) -> +- Names = string:tokens(?unicode_list(Data), ","), +- decode_kex_init(Rest, [Names | Acc], N -1). ++decode_kex_init(<>, 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(<>, _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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_options.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2004-2024. 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. +@@ -886,6 +886,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_server_channel.erl b/lib/ssh/src/ssh_server_channel.erl +--- a/lib/ssh/src/ssh_server_channel.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_server_channel.erl 2025-12-17 17:25:02.076556417 +1030 +@@ -51,8 +51,7 @@ + > When implementing a client subsystem handler, use + > [\-behaviour(ssh_client_channel)](`m:ssh_client_channel`) instead. + """. +--moduledoc(#{since => "OTP 21.0", +- titles => [{callback,<<"Callback Functions">>}]}). ++-moduledoc(#{since => "OTP 21.0"}). + + %% API to server side channel that can be plugged into the erlang ssh daemeon + -doc """ +@@ -63,7 +62,7 @@ + `m:gen_server`. If the time-out occurs, `c:handle_msg/2` is called as + [`handle_msg(timeout, State)`](`c:handle_msg/2`). + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback init(Args :: term()) -> + {ok, State :: term()} | {ok, State :: term(), timeout() | hibernate} | + {stop, Reason :: term()} | ignore. +@@ -76,7 +75,7 @@ + the channel process terminates with reason `Reason`. The return value is + ignored. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback terminate(Reason :: (normal | shutdown | {shutdown, term()} | + term()), + State :: term()) -> +@@ -89,13 +88,13 @@ + Possible Erlang 'EXIT' messages is to be handled by this function and all + channels are to handle the following message. + +-- **`{ssh_channel_up, ``t:ssh:channel_id/0``, ``t:ssh:connection_ref/0``}`** - ++- **`{ssh_channel_up,` `t:ssh:channel_id/0` `,` `t:ssh:connection_ref/0` `}`** - + This is the first message that the channel receives. This is especially useful + if the server wants to send a message to the client without first receiving a + message from it. If the message is not useful for your particular scenario, + ignore it by immediately returning `{ok, State}`. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_msg(Msg ::term(), State :: term()) -> + {ok, State::term()} | {stop, ChannelId::ssh:channel_id(), State::term()}. + -doc """ +@@ -104,11 +103,11 @@ + + The following message is taken care of by the `ssh_server_channel` behavior. + +-- **`{closed, ``t:ssh:channel_id/0``}`** - The channel behavior sends a close ++- **`{closed,` `t:ssh:channel_id/0` `}`** - The channel behavior sends a close + message to the other side, if such a message has not already been sent. Then + it terminates the channel with reason `normal`. + """. +--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}). ++-doc(#{since => <<"OTP 21.0">>}). + -callback handle_ssh_msg(ssh_connection:event(), + State::term()) -> {ok, State::term()} | + {stop, ChannelId::ssh:channel_id(), +diff -ruN a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +--- a/lib/ssh/src/ssh_sftpd.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_sftpd.erl 2025-12-17 17:25:02.077556430 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2005-2024. 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. +@@ -32,7 +32,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 +@@ -57,6 +57,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 {, directory, {Path, unread|eof}} or +@@ -86,6 +88,18 @@ + limit. If supplied, the number of filenames returned to the SFTP client per + `READDIR` request is limited to at most the given value. + ++- **`max_handles`** - The default value is `1000`. Positive integer ++ value represents the maximum number of file handles allowed for a ++ connection. ++ ++ (Note: separate limitation might be also enforced by underlying ++ operating system) ++ ++- **`max_path`** - The default value is `4096`. 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) ++ + - **`root`** - Sets the SFTP root directory. Then the user cannot see any files + above this root. If, for example, the root directory is set to `/tmp`, then + the user sees this directory as `/`. If the user then writes `cd /etc`, the +@@ -98,6 +112,8 @@ + Options :: [ {cwd, string()} | + {file_handler, CbMod | {CbMod, FileState}} | + {max_files, integer()} | ++ {max_handles, integer()} | ++ {max_path, integer()} | + {root, string()} | + {sftpd_vsn, integer()} + ], +@@ -149,8 +165,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 = []}}}. +@@ -163,9 +185,8 @@ + %%-------------------------------------------------------------------- + -doc false. + 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}; +@@ -224,24 +245,77 @@ + %%-------------------------------------------------------------------- + %%% Internal functions + %%-------------------------------------------------------------------- +-handle_data(0, <>, ++handle_data(0, ChannelId, <>, + State = #state{pending = <<>>}) -> + <> = 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, <>, +- 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 = <>, ++ 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, <>, 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, <>, ++ 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, <>, ++ 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]), +@@ -249,7 +323,7 @@ + ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <>), + State#state{xf = XF1}; + handle_op(?SSH_FXP_REALPATH, ReqId, +- <>, ++ <>, + State0) -> + RelPath = relate_file_name(RPath, State0, _Canonicalize=false), + {Res, State} = resolve_symlinks(RelPath, State0), +@@ -265,14 +339,16 @@ + end; + handle_op(?SSH_FXP_OPENDIR, ReqId, + <>, +- 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, +@@ -282,8 +358,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, + <>, +@@ -418,14 +498,12 @@ + send_status(Status, ReqId, State1); + + handle_op(?SSH_FXP_RENAME, ReqId, +- Bin = <>, ++ Bin = <>, + State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 -> + handle_op(?SSH_FXP_RENAME, ReqId, <>, State); + + handle_op(?SSH_FXP_RENAME, ReqId, +- <>, ++ <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + Path2 = relate_file_name(BPath2, State0), +@@ -463,23 +541,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; +@@ -734,7 +818,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 +@@ -746,7 +832,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}, +@@ -757,7 +843,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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_sftp.erl 2025-12-17 17:25:02.077556430 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2005-2024. 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. +@@ -30,8 +30,7 @@ + file transfer service available for SSH. + """. + -moduledoc(#{titles => +- [{type,<<"Error cause">>}, +- {type,<<"Crypto operations for open_tar">>}]}). ++ [{type,<<"Crypto open_tar">>}]}). + + -behaviour(ssh_client_channel). + +@@ -125,7 +124,7 @@ + + The `t:tuple/0` reason are other errors like for example `{exit_status,1}`. + """. +--doc(#{title => <<"Error cause">>}). ++-doc(#{}). + -type reason() :: atom() | string() | tuple() . + + %%==================================================================== +@@ -149,20 +148,12 @@ + %%% -spec:s are as if Dialyzer handled signatures for separate + %%% function clauses. + -doc(#{equiv => start_channel/3}). +--spec start_channel(ssh:open_socket(), +- [ssh:client_option() | sftp_option()] +- ) +- -> {ok,pid(),ssh:connection_ref()} | {error,reason()}; +- +- (ssh:connection_ref(), +- [sftp_option()] +- ) +- -> {ok,pid()} | {ok,pid(),ssh:connection_ref()} | {error,reason()}; +- +- (ssh:host(), +- [ssh:client_option() | sftp_option()] +- ) +- -> {ok,pid(),ssh:connection_ref()} | {error,reason()} . ++-spec start_channel(ssh:open_socket(), [ssh:client_option() | sftp_option()]) -> ++ {ok, pid(), ssh:connection_ref()} | {error,reason()}; ++ (ssh:connection_ref(), [ssh:client_option() | sftp_option()]) -> ++ {ok, pid()} | {ok, pid(), ssh:connection_ref()} | {error, reason()}; ++ (ssh:host(), [ssh:client_option() | sftp_option()]) -> ++ {ok, pid(), ssh:connection_ref()} | {error, reason()} . + start_channel(Cm, UserOptions0) when is_pid(Cm) -> + UserOptions = legacy_timeout(UserOptions0), + Timeout = proplists:get_value(timeout, UserOptions, infinity), +@@ -318,12 +309,10 @@ + call(Pid, {open, false, File, Mode}, FileOpTimeout). + + +--doc(#{title => <<"Crypto operations for open_tar">>, +- equiv => decrypt_spec/0}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type tar_crypto_spec() :: encrypt_spec() | decrypt_spec() . + +--doc(#{title => <<"Crypto operations for open_tar">>, +- equiv => decrypt_spec/0}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type encrypt_spec() :: {init_fun(), crypto_fun(), final_fun()} . + -doc """ + Specifies the encryption or decryption applied to tar files when using +@@ -336,16 +325,14 @@ + [Example with encryption](using_ssh.md#example-with-encryption) in the ssh Users + Guide. + """. +--doc(#{title => <<"Crypto operations for open_tar">>}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type decrypt_spec() :: {init_fun(), crypto_fun()} . + +--doc(#{title => <<"Crypto operations for open_tar">>, +- equiv => crypto_state/0}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type init_fun() :: fun(() -> {ok,crypto_state()}) + | fun(() -> {ok,crypto_state(),chunk_size()}) . + +--doc(#{title => <<"Crypto operations for open_tar">>, +- equiv => crypto_result/0}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type crypto_fun() :: fun((TextIn::binary(), crypto_state()) -> crypto_result()) . + -doc """ + The initial `t:crypto_state/0` returned from the `t:init_fun/0` is folded into +@@ -357,7 +344,7 @@ + If the `t:crypto_fun/0` reurns a `t:chunk_size/0`, that value is as block size + for further blocks in calls to `t:crypto_fun/0`. + """. +--doc(#{title => <<"Crypto operations for open_tar">>}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type crypto_result() :: {ok,TextOut::binary(),crypto_state()} + | {ok,TextOut::binary(),crypto_state(),chunk_size()} . + +@@ -367,11 +354,10 @@ + The `t:final_fun/0` is responsible for padding (if needed) and encryption of + that last piece. + """. +--doc(#{title => <<"Crypto operations for open_tar">>}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type final_fun() :: fun((FinalTextIn::binary(),crypto_state()) -> {ok,FinalTextOut::binary()}) . + +--doc(#{title => <<"Crypto operations for open_tar">>, +- equiv => crypto_state/0}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type chunk_size() :: undefined | pos_integer(). + -doc """ + The `t:init_fun/0` in the [tar_crypto_spec](`t:tar_crypto_spec/0`) is applied +@@ -388,7 +374,7 @@ + `t:chunk_size/0` can be changed in the return from the `t:crypto_fun/0`. The + value can be changed between `t:pos_integer/0` and `undefined`. + """. +--doc(#{title => <<"Crypto operations for open_tar">>}). ++-doc(#{title => <<"Crypto open_tar">>}). + -type crypto_state() :: any() . + + +@@ -414,7 +400,7 @@ + in the ssh Users Guide. + + The `crypto` mode option is explained in the data types section above, see +-[Crypto operations for open_tar](`m:ssh_sftp#types-crypto-operations-for-open_tar`). ++[Crypto operations for open_tar](`m:ssh_sftp#types-crypto-open_tar`). + Encryption is assumed if the `Mode` contains `write`, and decryption if the + `Mode` contains `read`. + """. +@@ -1038,14 +1024,12 @@ + 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 ++ maybe ++ {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 + end. + + read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, Acc) -> +@@ -1080,15 +1064,12 @@ + 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 ++ maybe ++ {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 + end. + + write_file_loop(_Pid, _Handle, _Pos, _Bin, 0, _PacketSz,_FileOpTimeout) -> +@@ -1910,8 +1891,10 @@ + + + read_repeat(Pid, Handle, Len, FileOpTimeout) -> +- {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout), +- read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>). ++ maybe ++ {ok, {_WindowSz, PacketSz}} ?= recv_window(Pid, FileOpTimeout), ++ read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>) ++ end. + + read_rpt(Pid, Handle, WantedLen, PacketSz, FileOpTimeout, Acc) when WantedLen > 0 -> + case read(Pid, Handle, min(WantedLen,PacketSz), FileOpTimeout) of +@@ -1929,8 +1912,10 @@ + 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). ++ maybe ++ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout), ++ write_file_loop(Pid, SftpHandle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout) ++ end. + + position_buf(Pid, SftpHandle, BufHandle, Pos, FileOpTimeout) -> + {ok,#bufinf{mode = Mode, +@@ -1967,18 +1952,19 @@ + 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. ++ maybe ++ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout), ++ {ok, B0} ?= call(Pid, {get_bufinf,BufHandle}, FileOpTimeout), ++ {ok, ResultBin, B} ?= do_the_read_buf(Pid, SftpHandle, WantedLen, Packet, FileOpTimeout, B0), ++ call(Pid, {put_bufinf, BufHandle, B}, FileOpTimeout), ++ {ok,ResultBin} ++ else ++ {error, Error} -> ++ {error,Error}; ++ {eof, BufInf} -> ++ call(Pid, {put_bufinf, BufHandle, BufInf}, FileOpTimeout), ++ eof ++ end. + + do_the_read_buf(_Pid, _SftpHandle, WantedLen, _Packet, _FileOpTimeout, + B=#bufinf{plain_text_buf=PlainBuf0, +@@ -2030,15 +2016,14 @@ + + + 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 = <>}) of +- {ok, B} -> +- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout), +- ok; +- {error,Error} -> +- {error,Error} ++ maybe ++ {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 = <>}), ++ call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout), ++ ok + end. + + do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout, +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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_system_sup.erl 2025-12-17 17:25:02.077556430 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -160,8 +160,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) -> +diff -ruN a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl +--- a/lib/ssh/src/ssh_transport.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/src/ssh_transport.erl 2025-12-17 17:25:02.077556430 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2004-2024. 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. +@@ -27,12 +27,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, +@@ -296,7 +295,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}) -> +@@ -400,8 +404,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; + +@@ -417,31 +422,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) -> +@@ -593,14 +605,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, +@@ -621,14 +638,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. + +@@ -654,7 +672,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; + +@@ -686,8 +705,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(_, _) -> +@@ -713,7 +732,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}}}}. + +@@ -744,19 +762,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, +@@ -781,20 +802,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. + + %%%---------------------------------------------------------------- +@@ -828,17 +847,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, +@@ -861,15 +888,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. + + +@@ -1081,7 +1107,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 +@@ -1106,7 +1132,6 @@ + _ -> + KexStrictNegotiated0 + end, +- + {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), + {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), + +@@ -1120,14 +1145,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), + +@@ -1147,7 +1167,7 @@ + ?GET_OPT(recv_ext_info,Opts), + + {ok, #alg{kex = Kex, +- hkey = HK, ++ hkey = HKey, + encrypt = Encrypt, + decrypt = Decrypt, + send_mac = SendMac, +@@ -1299,38 +1319,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/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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_connection_SUITE.erl 2025-12-17 17:25:02.079556456 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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. +@@ -109,6 +109,7 @@ + stop_listener/1, + trap_exit_connect/1, + trap_exit_daemon/1, ++ handler_down_before_open/1, + ssh_exec_echo/2 % called as an MFA + ]). + +@@ -180,7 +181,8 @@ + 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()}]. +@@ -348,7 +350,7 @@ + print_interesting_events([], Cnt) -> + {ok, Cnt}; + print_interesting_events([#{level := Level} = Event | Tail], Cnt) +- when Level /= info, Level /= notice -> ++ when Level /= info, Level /= notice, Level /= debug -> + ct:log("------------~nInteresting event found:~n~p~n==========~n", [Event]), + print_interesting_events(Tail, Cnt + 1); + print_interesting_events([_|Tail], Cnt) -> +@@ -623,6 +625,9 @@ + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + ++ %% pre-adjust receive window so the other end doesn't block ++ ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), ++ + ct:log("sending ~p byte binary~n",[size(Data)]), + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), +@@ -764,11 +769,8 @@ + %% 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 used to be interrupted when ssh_echo_server ran +-%% out of data window and closed channel +-%%- but with automatic window adjustment, above condition is not taking +-%% place, so ssh_echo_server continues sending data until it is done +-%%- so ssh_connection:send returns 'ok' ++%%- interrupted_send is interrupted when ssh_echo_server ran ++%% out of ssh data window and closed channel + small_interrupted_send(Config) -> + K = 1024, + SendSize = 10 * K * K, +@@ -807,7 +809,7 @@ + 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}, +@@ -840,6 +842,7 @@ + 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]), +@@ -858,7 +861,7 @@ + ct:log("~p:~p Not expected send result: ~p",[?MODULE,?LINE,Msg]), + {fail, "Not expected msg"} + end; +- {SenderPid, SenderResult} -> ++ {SenderPid, {error, closed}} -> + ct:log("~p:~p ~p - That's what we expect, " + "but client channel handler has not reported yet", + [?MODULE,?LINE, SenderResult]), +@@ -1293,7 +1296,7 @@ + + do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, Config) -> + DefaultReceiveFun = +- fun(ConnectionRef, ChannelId, Expect, ExpectType) -> ++ fun(ConnectionRef, ChannelId, _Expect, _ExpectType) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, ExpectType, Expect}} -> + ok +@@ -1630,6 +1633,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), +@@ -1650,6 +1655,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"}, +@@ -1667,7 +1676,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, +@@ -1685,12 +1694,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. + +@@ -1942,6 +1951,138 @@ + 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, ++ <>}} = 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 ------------------------------------------------ + %%-------------------------------------------------------------------- +@@ -1966,26 +2107,35 @@ + _ -> + receive_bytes(ConnectionRef, ChannelId0, N * byte_size(ExpectedBin), 0) + end, +- + %% receive close messages +- receive +- {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> +- 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, {exit_status, ChannelId0, 0}} -> +- ok +- after +- 10000 -> 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. + + + %%-------------------------------------------------------------------- +@@ -2122,6 +2272,7 @@ + {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)), + collect_data(ConnectionRef, ChannelId, EchoSize, [Data | Acc], Sum+size(Data)); + {ssh_cm, ConnectionRef, Msg={eof, ChannelId}} -> + collect_data_report_end(Acc, Msg, EchoSize); +@@ -2185,6 +2336,7 @@ + "~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 +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 2024-12-05 21:56:22.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_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl +--- a/lib/ssh/test/ssh_options_SUITE.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_options_SUITE.erl 2025-12-17 17:25:02.080556468 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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 +@@ -110,7 +111,7 @@ + + suite() -> + [{ct_hooks,[ts_install_cth]}, +- {timetrap,{seconds,60}}]. ++ {timetrap,{seconds,15}}]. + + all() -> + [connectfun_disconnectfun_server, +@@ -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} + ]. + +@@ -1563,7 +1565,7 @@ + + %%-------------------------------------------------------------------- + max_sessions_drops_tcp_connects() -> +- [{timetrap,{minutes,20}}]. ++ [{timetrap,{minutes,2}}]. + + max_sessions_drops_tcp_connects(Config) -> + MaxSessions = 20, +@@ -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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_protocol_SUITE.erl 2025-12-17 17:25:02.080556468 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2008-2024. 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 = <>)>>, ++ 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) -> +@@ -843,22 +1027,173 @@ + 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). +@@ -869,8 +1204,7 @@ + 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,12 +1217,7 @@ + {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), +@@ -1055,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 = <> ++ }}] ++ 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 ======================================== +@@ -1221,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 +diff -ruN a/lib/ssh/test/ssh_pubkey_SUITE.erl b/lib/ssh/test/ssh_pubkey_SUITE.erl +--- a/lib/ssh/test/ssh_pubkey_SUITE.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_pubkey_SUITE.erl 2025-12-17 17:25:02.080556468 +1030 +@@ -99,7 +99,7 @@ + + suite() -> + [{ct_hooks,[ts_install_cth]}, +- {timetrap,{seconds,40}}]. ++ {timetrap,{seconds,20}}]. + + all() -> + [{group, old_format}, +diff -ruN a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl +--- a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl 2025-12-17 17:25:02.082182205 +1030 +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2007-2021. All Rights Reserved. ++%% Copyright Ericsson AB 2007-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. +@@ -56,7 +56,7 @@ + + suite() -> + [{ct_hooks,[ts_install_cth]}, +- {timetrap,{seconds,40}}]. ++ {timetrap,{seconds,20}}]. + + all() -> + [close_file, +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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl 2025-12-17 17:25:02.082182205 +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"). +@@ -68,24 +69,25 @@ + -define(PASSWD, "Sesame"). + %% -define(XFER_PACKET_SIZE, 32768). + %% -define(XFER_WINDOW_SIZE, 4*?XFER_PACKET_SIZE). +--define(SSH_TIMEOUT, 10000). ++-define(SSH_TIMEOUT, 5000). + -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 ----------------------------------- + %%-------------------------------------------------------------------- + + suite() -> +- [{timetrap,{seconds,40}}]. ++ [{timetrap,{seconds,20}}]. + + all() -> + [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, <>, _} = ++ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, ++ ?SSH_FXF_OPEN_EXISTING), ++ R2 = req_id(), ++ {ok, <>, _} = ++ 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, <>, _} = ++ 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, <>, _} = +- 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, <>, _} = ++ open_file(LongFileName, Cm, Channel, ReqId1, + ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- +- NewReqId = 1, +- +- {ok, <>, _} = +- 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, <>, _} = +- open_dir(PrivDir, Cm, Channel, ReqId), +- ok = read_dir(Handle, Cm, Channel, ReqId). ++ [begin ++ R1 = req_id(), ++ {ok, <>, _} = ++ 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, <>, _} = ++ 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, <>, _} = +- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0), +- +- NewReqId = ReqId + 1, +- +- {ok, <>, _} = +- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6, +- ?SSH_FXP_RENAME_OVERWRITE), +- +- NewReqId1 = NewReqId + 1, +- file:copy(FileName, NewFileName), +- +- %% No overwrite +- {ok, <>, _} = +- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6, +- ?SSH_FXP_RENAME_NATIVE), +- +- NewReqId2 = NewReqId1 + 1, +- +- {ok, <>, _} = +- 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, <>, _} = ++ 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, <>, _} = +- open_file(FileName, Cm, Channel, ReqId, +- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- +- NewReqId = 1, +- +- {ok, <>, _} = +- 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, <>, _} = +- open_file(FileName ++ "0", Cm, Channel, ReqId0, +- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- ReqId1 = 1, +- {ok, <>, _} = +- open_file("./" ++ FileName ++ "1", Cm, Channel, ReqId1, +- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- ReqId2 = 2, +- {ok, <>, _} = +- 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, <>, _} = ++ 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, <>, _} = ++ 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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_sftp_SUITE.erl 2025-12-17 17:25:02.082182205 +1030 +@@ -1,7 +1,7 @@ + % + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2005-2024. 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, +@@ -82,8 +83,9 @@ + -include_lib("kernel/include/file.hrl"). + -include("ssh_test_lib.hrl"). + -include_lib("stdlib/include/assert.hrl"). ++ + %% Default timetrap timeout +--define(default_timeout, test_server:minutes(1)). ++-define(default_timeout, test_server:minutes(0.5)). + + %%-------------------------------------------------------------------- + %% Common Test interface functions ----------------------------------- +@@ -91,7 +93,7 @@ + + suite() -> + [{ct_hooks,[ts_install_cth]}, +- {timetrap,{seconds,40}}]. ++ {timetrap,{seconds,20}}]. + + all() -> + [{group, not_unicode}, +@@ -120,6 +122,7 @@ + + {unicode, [], [{group,erlang_server}, + {group,openssh_server}, ++ read_6GB, + sftp_nonexistent_subsystem + ]}, + +@@ -229,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)]), +@@ -288,7 +274,20 @@ + Config. + + %%-------------------------------------------------------------------- +- ++init_per_testcase(read_6GB, Config) -> ++ case verify_openssh(Config) of ++ Result = {skip, _} -> ++ Result; ++ _ -> ++ case {os:type(), erlang:system_info(system_architecture)} of ++ {{win32, _}, _} -> ++ {skip, "/dev/zero not available on Windows"}; ++ {_, "aarch64"} -> ++ {skip, "machine too slow for test"}; ++ _ -> ++ 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), +@@ -301,7 +300,6 @@ + [{User, Passwd}]} + ]), + [{sftpd, Sftpd} | Config]; +- + init_per_testcase(version_option, Config0) -> + Config = prepare(Config0), + TmpConfig0 = lists:keydelete(watchdog, 1, Config), +@@ -321,7 +319,6 @@ + ]), + Sftp = {ChannelPid, Connection}, + [{sftp,Sftp}, {watchdog, Dog} | TmpConfig]; +- + init_per_testcase(Case, Config00) -> + Config0 = prepare(Config00), + Config1 = lists:keydelete(watchdog, 1, Config0), +@@ -333,11 +330,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}, +@@ -352,18 +362,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 -> +@@ -713,6 +715,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(test_server:minutes(20)), ++ 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), +@@ -1271,4 +1296,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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_sup_SUITE.erl 2025-12-17 17:25:02.082182205 +1030 +@@ -61,7 +61,7 @@ + %%-------------------------------------------------------------------- + suite() -> + [{ct_hooks,[ts_install_cth]}, +- {timetrap,{seconds,100}}]. ++ {timetrap,{seconds,50}}]. + + all() -> + [default_tree, sshc_subtree, sshd_subtree, sshd_subtree_profile, +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 2024-12-05 21:56:22.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-2024. 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. +@@ -1344,6 +1344,7 @@ + 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, +diff -ruN a/lib/ssh/test/ssh_test_lib.hrl b/lib/ssh/test/ssh_test_lib.hrl +--- a/lib/ssh/test/ssh_test_lib.hrl 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_test_lib.hrl 2025-12-17 17:25:02.082182205 +1030 +@@ -9,7 +9,7 @@ + %%------------------------------------------------------------------------- + %% Timeout time in ms + %%------------------------------------------------------------------------- +--define(TIMEOUT, 27000). ++-define(TIMEOUT, 15000). + + %%------------------------------------------------------------------------- + %% Check for usable crypto +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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl 2025-12-17 17:25:02.082182205 +1030 +@@ -60,7 +60,7 @@ + %%-------------------------------------------------------------------- + + suite() -> +- [{timetrap,{seconds,60}}]. ++ [{timetrap,{seconds,30}}]. + + all() -> + case os:find_executable("ssh") of +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 2024-12-05 21:56:22.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 2024-12-05 21:56:22.000000000 +1030 ++++ b/lib/ssh/vsn.mk 2025-12-17 17:25:02.082556494 +1030 +@@ -1,4 +1,4 @@ + #-*-makefile-*- ; force emacs to enter makefile-mode + +-SSH_VSN = 5.2.4 ++SSH_VSN = 5.2.11.4 + APP_VSN = "ssh-$(SSH_VSN)" -- 2.51.1