Your message dated Sat, 14 Mar 2026 11:48:35 +0000
with message-id <[email protected]>
and subject line Released with 13.4
has caused the Debian Bug report #1127606,
regarding trixie-pu: package erlang/1:27.3.4.1+dfsg-1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1127606: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1127606
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], Sergei Golovan <[email protected]>, 
Bastien Roucaries <[email protected]>
Control: affects -1 + src:erlang
User: [email protected]
Usertags: pu

[ Reason ]
There have been several CVEs published for the erlang programming
language that have been flagged as no DSA or unimportant affecting the
http and ssh server implementations.

[ Impact ]
Untrusted user data processing and denial of service attacks.

[ Tests ]
Manually tested.

[ Risks ]
Low risk, given that those implementations are niche and the patches
mostly add safe guards.

[ Checklist ]
  [X] *all* changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in (old)stable
  [X] the issue is verified as fixed in unstable

[ Other info ]
I reached out to Sergei before sending this PU but got no answer.
Bastien merged those patches in Salsa so I assume it is fine to send
this now. @Sergei please reply if you disagree.
diff --git a/debian/changelog b/debian/changelog
index 58b941444e..551ee27b24 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,22 @@
+erlang (1:27.3.4.1+dfsg-1+deb14u1) trixie-proposed-updates; urgency=medium
+
+  * Fix CVE-2025-48038: allocation of resources without limits or throttling
+    vulnerability in the ssh_sftp module allows excessive allocation,
+    resource leak exposure (closes: #1115093).
+  * Fix CVE-2025-48039: allocation of resources without limits or throttling
+    vulnerability in the ssh_sftp module allows excessive allocation,
+    resource leak exposure (closes: #1115092).
+  * Fix CVE-2025-48040: uncontrolled resource consumption vulnerability in
+    the ssh_sftp module allows excessive allocation, flooding (closes: 
1115091).
+  * Fix CVE-2025-48041: allocation of resources without limits or throttling
+    vulnerability in the ssh_sftp module allows excessive allocation,
+    flooding (closes: #1115090).
+  * Fix CVE-2016-1000107: inets does not protect applications from the presence
+    of untrusted client data in the HTTP_PROXY environment variable
+    (closes: #1115086).
+
+ -- Sergei Golovan <[email protected]>  Tue, 08 Jul 2025 10:27:28 +0300
+
 erlang (1:27.3.4.1+dfsg-1) unstable; urgency=medium
 
   * New upstream bugfix release.
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000000..cec628c744
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,2 @@
+[DEFAULT]
+pristine-tar = True
diff --git a/debian/patches/CVE-2016-1000107.patch 
b/debian/patches/CVE-2016-1000107.patch
new file mode 100644
index 0000000000..c598f7d60e
--- /dev/null
+++ b/debian/patches/CVE-2016-1000107.patch
@@ -0,0 +1,217 @@
+From: Upstream (Marcel Lanz <[email protected]> and Konrad Pietrzak 
<[email protected]>)
+Subject: A mix of patches to fix CVE-2016-1000107 and
+ to test for it.
+Date: Thu, 18 Sep 2025 11:12:13 +0300
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1115086
+Bug: https://github.com/erlang/otp/issues/3392
+
+--- a/lib/inets/examples/server_root/cgi-bin/printenv.bat
++++ b/lib/inets/examples/server_root/cgi-bin/printenv.bat
+@@ -1,8 +1,33 @@
++::
++:: %CopyrightBegin%
++::
++:: SPDX-License-Identifier: Apache-2.0
++::
++:: Copyright Ericsson AB 1997-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.
++:: You may obtain a copy of the License at
++::
++::     http://www.apache.org/licenses/LICENSE-2.0
++::
++:: Unless required by applicable law or agreed to in writing, software
++:: distributed under the License is distributed on an "AS IS" BASIS,
++:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++:: See the License for the specific language governing permissions and
++:: limitations under the License.
++::
++:: %CopyrightEnd%
++::
++::
++
++
+ @echo off
+ echo tomrad > c:\cygwin\tmp\hej
+ echo Content-type: text/html
+ echo.
+ echo ^<HTML^> ^<HEAD^> ^<TITLE^>OS Environment^</TITLE^> ^</HEAD^> 
^<BODY^>^<PRE^>
++set http_proxy=%HTTP_PROXY%
+ set
+ echo ^</PRE^>^</BODY^>^</HTML^>
+ 
+--- a/lib/inets/examples/server_root/cgi-bin/printenv.sh
++++ b/lib/inets/examples/server_root/cgi-bin/printenv.sh
+@@ -1,6 +1,31 @@
+ #!/bin/sh
++#
++# %CopyrightBegin%
++#
++# SPDX-License-Identifier: Apache-2.0
++#
++# Copyright Ericsson AB 1997-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.
++# You may obtain a copy of the License at
++#
++#     http://www.apache.org/licenses/LICENSE-2.0
++#
++# Unless required by applicable law or agreed to in writing, software
++# distributed under the License is distributed on an "AS IS" BASIS,
++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++# See the License for the specific language governing permissions and
++# limitations under the License.
++#
++# %CopyrightEnd%
++#
++#
++
++
+ echo "Content-type: text/html"
+ echo ""
+ echo "<HTML> <HEAD> <TITLE>OS Environment</TITLE> </HEAD> <BODY><PRE>"
++export http_proxy=$HTTP_PROXY
+ env
+-echo "</PRE></BODY></HTML>"
+\ No newline at end of file
++echo "</PRE></BODY></HTML>"
+--- a/lib/inets/src/http_server/httpd_script_env.erl
++++ b/lib/inets/src/http_server/httpd_script_env.erl
+@@ -42,6 +42,8 @@
+ %%
+ %% Description: Creates a list of cgi/esi environment variables and
+ %% there values.
++%%
++%% Note: "PROXY" header/variable is skipped because of CVE-2016-1000107
+ %%--------------------------------------------------------------------------
+ create_env(ScriptType, ModData, ScriptElements) ->
+     create_basic_elements(ScriptType, ModData) 
+@@ -149,6 +151,8 @@
+ create_http_header_elements(ScriptType, [{Name, Value} | Headers], Acc, 
OtherAcc) 
+   when is_list(Value) ->
+     try http_env_element(ScriptType, Name, Value) of
++        skipped ->
++            create_http_header_elements(ScriptType, Headers, Acc, OtherAcc);
+         Element ->
+             create_http_header_elements(ScriptType, Headers, [Element | Acc],
+                                        OtherAcc)
+@@ -158,9 +162,16 @@
+                                        [{Name, Value} | OtherAcc])
+     end.
+ 
+-http_env_element(cgi, VarName0, Value)  ->
+-    VarName = re:replace(VarName0,"-","_", [{return,list}, global]),
+-    {"HTTP_"++ http_util:to_upper(VarName), Value};
++http_env_element(cgi, VarName0, Value) ->
++  case http_util:to_upper(VarName0) of
++    "PROXY" ->
++      %% CVE-2016-1000107 – https://github.com/erlang/otp/issues/3392
++      skipped;
++    VarName1 ->
++      VarNameUpper = re:replace(VarName1, "-", "_", [{return, list}, global]),
++      {"HTTP_" ++ VarNameUpper, Value}
++  end;
++
+ http_env_element(esi, VarName0, Value)  ->
+     list_to_existing_atom(VarName0),
+     VarName = re:replace(VarName0,"-","_", [{return,list}, global]),
+--- a/lib/inets/test/httpd_SUITE.erl
++++ b/lib/inets/test/httpd_SUITE.erl
+@@ -42,7 +42,8 @@
+ %% Seconds before successful auths timeout.
+ -define(AUTH_TIMEOUT,5).
+ -define(URL_START, "http://";).
+-
++-define(URL_START_HTTPS, "https://";).
++-define(SSL_NO_VERIFY, {ssl, [{verify, verify_none}]}).
+ %%--------------------------------------------------------------------
+ %% Common Test interface functions -----------------------------------
+ %%--------------------------------------------------------------------
+@@ -133,13 +134,14 @@
+      {security, [], [security_1_1, security_1_0]},
+      {logging, [], [disk_log_internal, disk_log_exists,
+              disk_log_bad_size, disk_log_bad_file]},
+-     {http_1_1, [], [esi_propagate, esi_atom_leak, {group, 
http_1_1_parallel}] ++ load()},
++     {http_1_1, [], [esi_propagate, esi_atom_leak, {group, http_1_1_parallel},
++                     cgi_bin_env] ++ load()},
+      {http_1_1_parallel, [parallel],
+       [host, chunked, expect, cgi, cgi_chunked_encoding_test,
+        trace, range, if_modified_since, mod_esi_chunk_timeout,
+        esi_put, esi_patch, esi_post, esi_headers]
+       ++ http_head() ++ http_get()},
+-     {http_1_0, [], [{group, http_1_0_parallel} | load()]},
++     {http_1_0, [], [cgi_bin_env, {group, http_1_0_parallel} | load()]},
+      {http_1_0_parallel, [parallel], [host, cgi, trace] ++ http_head() ++ 
http_get()},
+      {http_rel_path_script_alias, [], [cgi]},
+      {esi, [], [erl_script_timeout_default,
+@@ -1291,6 +1293,51 @@
+     [Test301(T) || T <- TestURIs301],
+     ok.
+ 
++cgi_bin_env() ->
++[{doc, "Test whether HTTP_PROXY header is not applied to an environment
++that runs the cgi script"}].
++cgi_bin_env(Config) ->
++    Proto = case proplists:get_value(type, Config, undefined) =:= ssl of
++                true -> https;
++                _ -> http
++            end,
++    Cgi = case os:type() of
++        {win32, _} ->
++            "printenv.bat";
++        _ ->
++            "printenv.sh"
++    end,
++    HttpOpts = case Proto of
++                   https -> [?SSL_NO_VERIFY];
++                   _ -> []
++               end,
++    RandomString = base64:encode(crypto:strong_rand_bytes(9)),
++    Endpoint = "/cgi-bin/" ++ Cgi,
++    Env = os:env(),
++    %% Grab the value of HTTP_PROXY from the environment before the request
++    HttpProxyEnv = proplists:get_value("HTTP_PROXY", Env, undefined),
++    Url = url(Proto, Endpoint, Config),
++    {ok, {_Status, _Headers, Body}} = httpc:request(get, {Url, [{"PROXY", 
RandomString},
++                                                                {"proxy", 
RandomString}]},
++                                                    HttpOpts, []),
++    %% The script prints the system's environment to the body so we need to
++    %% grab the value of interest
++    HttpEnv = re:split(Body, "\n"),
++    BinSize = size(<<"HTTP_PROXY">>) * 8,
++    %% Filter keys of interest, while converting to proplist
++    EnvProp = [{binary_to_list(Key), binary_to_list(Val)} ||
++                  <<Key:BinSize/bitstring, "=", Val/bitstring>> <- HttpEnv,
++                  Key =:= <<"HTTP_PROXY">>],
++    %% EnvProp should only have HTTP_PROXY or be an empty list
++    RespHttpProxyEnv = proplists:get_value("HTTP_PROXY", EnvProp, undefined),
++    case HttpProxyEnv of
++        undefined ->
++            %% HTTP_PROXY was not set before the request
++            ?assertEqual([], EnvProp);
++        _ ->
++            %% HTTP_PROXY was set, so ensure it's the same in the body
++            ?assertEqual(HttpProxyEnv, RespHttpProxyEnv)
++    end.
+ %%-------------------------------------------------------------------------
+ actions() ->
+     [{doc, "Test mod_actions"}].
+@@ -1983,10 +2030,15 @@
+ %%--------------------------------------------------------------------
+ %% Internal functions -----------------------------------
+ %%--------------------------------------------------------------------
++url(https, End, Config) ->
++    ?URL_START_HTTPS ++ url(End, Config);
+ url(http, End, Config) ->
++    ?URL_START ++ url(End, Config).
++
++url(End, Config) ->
+     Port = proplists:get_value(port, Config),
+     {ok,Host} = inet:gethostname(),
+-    ?URL_START ++ Host ++ ":" ++ integer_to_list(Port) ++ End.
++    Host ++ ":" ++ integer_to_list(Port) ++ End.
+ 
+ http_get_url(Port0, HeaderDelay, ChunkDelay, BadChunkDelay) ->
+     {ok, Host} = inet:gethostname(),
diff --git a/debian/patches/CVE-2025-48038.patch 
b/debian/patches/CVE-2025-48038.patch
new file mode 100644
index 0000000000..3d28cbba13
--- /dev/null
+++ b/debian/patches/CVE-2025-48038.patch
@@ -0,0 +1,26 @@
+From: Upstream (Jakub Witczak <[email protected]>)
+Date: Wed, 27 Aug 2025 17:49:08 +0200
+Subject: [PATCH] ssh: verify file handle size limit for client data
+ - reject handles exceeding 256 bytes (as specified for SFTP)
+ - fixes CVE-2025-48038
+
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -259,6 +259,17 @@
+             handle_data(Type, ChannelId, Data, State#state{pending = <<>>})
+     end.
+ 
++%% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be 
longer than 256 bytes."
++handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = 
XF})
++  when (Request == ?SSH_FXP_CLOSE orelse
++        Request == ?SSH_FXP_FSETSTAT orelse
++        Request == ?SSH_FXP_FSTAT orelse
++        Request == ?SSH_FXP_READ orelse
++        Request == ?SSH_FXP_READDIR orelse
++        Request == ?SSH_FXP_WRITE),
++       HLen > 256 ->
++    ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid 
handle"),
++    State;
+ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
+     XF = State#state.xf,
+     Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
diff --git a/debian/patches/CVE-2025-48039.patch 
b/debian/patches/CVE-2025-48039.patch
new file mode 100644
index 0000000000..11072574d7
--- /dev/null
+++ b/debian/patches/CVE-2025-48039.patch
@@ -0,0 +1,220 @@
+From: Upstream (Jakub Witczak <[email protected]>)
+Date: Fri, 11 Jul 2025 13:59:41 +0200
+Subject: [PATCH] ssh: ssh_sftpd verify path size for client data
+ - reject max_path exceeding the 4096 limit or according to other option value
+ - fix CVE-2025-48039
+
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -57,6 +57,7 @@ Specifies a channel process to handle an SFTP subsystem.
+         file_handler,                 % atom() - callback module 
+         file_state,                   % state for the file callback module
+         max_files,                    % integer >= 0 max no files sent during 
READDIR
++        max_path,                     % integer > 0 - max length of path
+         options,                      % from the subsystem declaration
+         handles                       % list of open handles
+         %% handle is either {<int>, directory, {Path, unread|eof}} or
+@@ -86,6 +87,11 @@ Options:
+   limit. If supplied, the number of filenames returned to the SFTP client per
+   `READDIR` request is limited to at most the given value.
+ 
++- **`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 +104,7 @@ Options:
+       Options :: [ {cwd, string()} |
+                    {file_handler, CbMod | {CbMod, FileState}} |
+                    {max_files, integer()} |
++                   {max_path, integer()} |
+                    {root, string()} |
+                    {sftpd_vsn, integer()}
+                  ],
+@@ -149,8 +156,12 @@ init(Options) ->
+               {Root0, State0}
+       end,
+     MaxLength = proplists:get_value(max_files, Options, 0),
++    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_path = MaxPath,
+                     options = Options,
+                     handles = [], pending = <<>>,
+                     xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
+@@ -270,6 +281,30 @@ handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, 
State = #state{xf = XF})
+        HLen > 256 ->
+     ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid 
handle"),
+     State;
++handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>,
++          State = #state{max_path = MaxPath, xf = XF})
++  when (Request == ?SSH_FXP_LSTAT orelse
++        Request == ?SSH_FXP_MKDIR orelse
++        Request == ?SSH_FXP_OPEN orelse
++        Request == ?SSH_FXP_OPENDIR orelse
++        Request == ?SSH_FXP_READLINK orelse
++        Request == ?SSH_FXP_REALPATH orelse
++        Request == ?SSH_FXP_REMOVE orelse
++        Request == ?SSH_FXP_RMDIR orelse
++        Request == ?SSH_FXP_SETSTAT orelse
++        Request == ?SSH_FXP_STAT),
++       PLen > MaxPath ->
++    ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
++                            "No such path"),
++    State;
++handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), 
_/binary>>,
++          State = #state{max_path = MaxPath, xf = XF})
++  when (Request == ?SSH_FXP_RENAME orelse
++        Request == ?SSH_FXP_SYMLINK),
++       (PLen > MaxPath orelse PLen2 > MaxPath) ->
++    ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
++                            "No such path"),
++    State;
+ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
+     XF = State#state.xf,
+     Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -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,
+@@ -71,9 +72,8 @@
+ -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_PATH, 200).
++-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
+ 
+ %%--------------------------------------------------------------------
+ %% Common Test interface functions -----------------------------------
+@@ -86,6 +86,7 @@ all() ->
+     [open_close_file, 
+      open_close_dir, 
+      read_file, 
++     max_path,
+      read_dir,
+      write_file, 
+      rename_file, 
+@@ -180,7 +181,8 @@ init_per_testcase(TestCase, Config) ->
+                                                                 {sftpd_vsn, 
6}])],
+                         ssh:daemon(0, [{subsystems, SubSystems}|Options]);
+                     _ ->
+-                        SubSystems = [ssh_sftpd:subsystem_spec([])],
++                        SubSystems = [ssh_sftpd:subsystem_spec(
++                                          [{max_path, ?MAX_PATH}])],
+                         ssh:daemon(0, [{subsystems, SubSystems}|Options])
+                 end,
+ 
+@@ -333,6 +335,23 @@ read_file(Config) when is_list(Config) ->
+ 
+     {ok, Data} = file:read_file(FileName).
+ 
++%%--------------------------------------------------------------------
++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),
++    %% verify max_path limit
++    LongFileName =
++        filename:join(PrivDir,
++                      "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) 
++ "st.txt"),
++    {ok, _} = file:copy(FileName, LongFileName),
++    ReqId1 = req_id(),
++    {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH),
++        _/binary>>, _} =
++      open_file(LongFileName, Cm, Channel, ReqId1,
++                ?ACE4_READ_DATA  bor ?ACE4_READ_ATTRIBUTES,
++                ?SSH_FXF_OPEN_EXISTING).
++
+ %%--------------------------------------------------------------------
+ read_dir(Config) when is_list(Config) ->
+     PrivDir = proplists:get_value(priv_dir, Config),
+@@ -388,35 +407,33 @@ rename_file(Config) when is_list(Config) ->
+     PrivDir =  proplists:get_value(priv_dir, Config),
+     FileName = filename:join(PrivDir, "test.txt"),
+     NewFileName = filename:join(PrivDir, "test1.txt"),
+-    ReqId = 0,
++    LongFileName =
++        filename:join(PrivDir,
++                      "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) 
++ "st.txt"),
+     {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+-    {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
+-        ?UINT32(?SSH_FX_OK), _/binary>>, _} =
+-      rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0),
+-
+-    NewReqId = ReqId + 1,
+-
+-    {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId),
+-        ?UINT32(?SSH_FX_OK), _/binary>>, _} =
+-      rename(NewFileName, FileName, Cm, Channel, NewReqId, 6,
+-             ?SSH_FXP_RENAME_OVERWRITE),
+-
+-    NewReqId1 = NewReqId + 1,
+-    file:copy(FileName, NewFileName),
+-
+-    %% No overwrite
+-    {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1),
+-        ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} =
+-      rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6,
+-             ?SSH_FXP_RENAME_NATIVE),
+-
+-    NewReqId2 = NewReqId1 + 1,
+-
+-    {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2),
+-        ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} =
+-      rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6,
+-             ?SSH_FXP_RENAME_ATOMIC).
++    Version = 6,
++    [begin
++         case Action of
++             {Code, AFile, BFile, Flags} ->
++                 ReqId = req_id(),
++                 ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = 
~p,~nFlags = ~p",
++                        [ReqId, Code, AFile, BFile, Flags]),
++                 {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), 
_/binary>>, _} =
++                     rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags);
++             {file_copy, AFile, BFile} ->
++                 {ok, _} = file:copy(AFile, BFile)
++         end
++     end ||
++        Action <-
++            [{?SSH_FX_OK, FileName, NewFileName, 0},
++             {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE},
++             {file_copy, FileName, NewFileName},
++             %% no overwrite
++             {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, 
?SSH_FXP_RENAME_NATIVE},
++             {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, 
?SSH_FXP_RENAME_ATOMIC},
++             %% max_path
++             {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]],
++    ok.
+ 
+ %%--------------------------------------------------------------------
+ mk_rm_dir(Config) when is_list(Config) ->
+@@ -1078,3 +1095,12 @@ encode_file_type(Type) ->
+ 
+ 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 --git a/debian/patches/CVE-2025-48040.patch 
b/debian/patches/CVE-2025-48040.patch
new file mode 100644
index 0000000000..042c73fe7b
--- /dev/null
+++ b/debian/patches/CVE-2025-48040.patch
@@ -0,0 +1,488 @@
+From: Jakub Witczak <[email protected]>
+Date: Wed, 20 Aug 2025 10:30:55 +0200
+Subject: ssh: key exchange robustness improvements
+
+- reduce untrusted data processing for non-debug logs
+- trim badmatch exceptions to avoid processing potentially malicious data
+- terminate with kexinit_error when too many algorithms are received in KEX 
init message
+
+origin: backport, 
https://github.com/erlang/otp/commit/7cd7abb7e19e16b027eaee6a54e1f6fbbe21181a
+---
+ lib/ssh/src/ssh_connection.erl         |   3 +-
+ lib/ssh/src/ssh_connection_handler.erl |  35 +++++++---
+ lib/ssh/src/ssh_lib.erl                |  18 ++++-
+ lib/ssh/src/ssh_message.erl            |  42 +++++++-----
+ lib/ssh/src/ssh_transport.erl          | 120 +++++++++++++++++++--------------
+ lib/ssh/test/ssh_connection_SUITE.erl  |  14 ++--
+ 6 files changed, 151 insertions(+), 81 deletions(-)
+
+diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
+index bb644b6..d6be161 100644
+--- a/lib/ssh/src/ssh_connection.erl
++++ b/lib/ssh/src/ssh_connection.erl
+@@ -769,10 +769,9 @@ handle_msg(Msg, Connection, server, Ssh = 
#ssh{authenticated = false}) ->
+     %% respond by disconnecting, preferably with a proper disconnect message
+     %% sent to ease troubleshooting.
+     MsgFun = fun(M) ->
+-                     MaxLogItemLen = ?GET_OPT(max_log_item_len, Ssh#ssh.opts),
+                      io_lib:format("Connection terminated. Unexpected message 
for unauthenticated user."
+                                    " Message:  ~w", [M],
+-                                   [{chars_limit, MaxLogItemLen}])
++                                   [{chars_limit, ssh_lib:max_log_len(Ssh)}])
+              end,
+     ?LOG_DEBUG(MsgFun, [Msg]),
+     {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, 
handle_stop(Connection)};
+diff --git a/lib/ssh/src/ssh_connection_handler.erl 
b/lib/ssh/src/ssh_connection_handler.erl
+index d852e46..e32f42c 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1186,12 +1186,21 @@ handle_event(info, {Proto, Sock, NewData}, StateName,
+                                       {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], 
[{chars_limit, MaxLogItemLen}]),
++                                         ?SELECT_MSG(MsgFun),
+                                          StateName, D1),
+                     {stop, Shutdown, D}
+           end;
+@@ -1221,12 +1230,20 @@ handle_event(info, {Proto, Sock, NewData}, StateName,
+                                  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], [{chars_limit, 
MaxLogItemLen}]),
++                ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, 
?SELECT_MSG(MsgFun),
+                                  StateName, D0),
+             {stop, Shutdown, D}
+     end;
+diff --git a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl
+index 762bc3e..2c63a06 100644
+--- a/lib/ssh/src/ssh_lib.erl
++++ b/lib/ssh/src/ssh_lib.erl
+@@ -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 @@ set_label(client, Details) ->
+     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 --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
+index 1ab4100..de5eb8b 100644
+--- a/lib/ssh/src/ssh_message.erl
++++ b/lib/ssh/src/ssh_message.erl
+@@ -44,7 +44,7 @@
+ 
+ -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).
++-define(ALG_NAME_LIMIT, 64). % RFC4251 sec6
+ 
+ ucl(B) ->
+     try unicode:characters_to_list(B) of
+@@ -822,23 +822,33 @@ decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) ->
+     %% See rfc 4253 7.1
+     X = 0,
+     list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc]));
+-decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) ->
++decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) when
++      byte_size(Data) < ?MAX_NUM_ALGORITHMS * ?ALG_NAME_LIMIT ->
+     BinParts = binary:split(Data, <<$,>>, [global]),
+-    Process =
+-        fun(<<>>, PAcc) ->
+-                PAcc;
+-           (Part, PAcc) ->
+-                case byte_size(Part) > ?ALG_NAME_LIMIT of
+-                    true ->
+-                        ?LOG_DEBUG("Ignoring too long name", []),
++    AlgCount = length(BinParts),
++    case AlgCount =< ?MAX_NUM_ALGORITHMS of
++        true ->
++            Process =
++                fun(<<>>, PAcc) ->
+                         PAcc;
+-                    false ->
+-                        Name = binary:bin_to_list(Part),
+-                        [Name | PAcc]
+-                end
+-        end,
+-    Names = lists:foldr(Process, [], BinParts),
+-    decode_kex_init(Rest, [Names | Acc], N - 1).
++                   (Part, PAcc) ->
++                        case byte_size(Part) =< ?ALG_NAME_LIMIT of
++                            true ->
++                                Name = binary:bin_to_list(Part),
++                                [Name | PAcc];
++                            false ->
++                                ?LOG_DEBUG("Ignoring too long name", []),
++                                PAcc
++                        end
++                end,
++            Names = lists:foldr(Process, [], BinParts),
++            decode_kex_init(Rest, [Names | Acc], N - 1);
++        false ->
++            throw({error, {kexinit_error, N, {alg_count, AlgCount}}})
++    end;
++decode_kex_init(<<?DEC_BIN(Data,__0), _Rest/binary>>, _Acc, N) ->
++    throw({error, {kexinit, N, {string_size, byte_size(Data)}}}).
++
+ 
+ 
+ %%%================================================================
+diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl
+index ef42ef7..4debef1 100644
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -404,8 +404,9 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, 
#ssh_msg_kexinit{} = Own,
+           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;
+ 
+@@ -421,31 +422,38 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, 
#ssh_msg_kexinit{} = Own,
+       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) ->
+@@ -597,14 +605,19 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E},
+                                              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,
+@@ -625,14 +638,15 @@ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key 
= PeerPubHostKey,
+                                                              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.
+ 
+@@ -658,7 +672,8 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request{min 
= Min0,
+                   }};
+       {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;
+ 
+@@ -690,8 +705,8 @@ 
handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits},
+                   }};
+       {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(_, _) ->
+@@ -717,7 +732,6 @@ handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g 
= G}, Ssh0) ->
+     {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}}}}.
+ 
+@@ -748,19 +762,22 @@ handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E},
+                                                    }};
+                         {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, 
+@@ -785,20 +802,18 @@ 
handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK
+                                                                      
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.
+ 
+ %%%----------------------------------------------------------------
+@@ -832,17 +847,25 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = 
PeerPublic},
+                                              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,
+@@ -865,15 +888,14 @@ 
handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey,
+                                                              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.
+ 
+ 
+diff --git a/lib/ssh/test/ssh_connection_SUITE.erl 
b/lib/ssh/test/ssh_connection_SUITE.erl
+index ea5beeb..1fe966c 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -350,7 +350,7 @@ get_value(Key, List) ->
+ 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) ->
+@@ -1633,6 +1633,8 @@ gracefull_invalid_long_start_no_nl(Config) when 
is_list(Config) ->
+     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),
+@@ -1653,6 +1655,10 @@ kex_error(Config) ->
+                                    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"},
+@@ -1670,7 +1676,7 @@ kex_error(Config) ->
+             %% 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,
+@@ -1688,12 +1694,12 @@ kex_error(Config) ->
+                             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.
+ 
diff --git a/debian/patches/CVE-2025-48041.patch 
b/debian/patches/CVE-2025-48041.patch
new file mode 100644
index 0000000000..23ad66f04f
--- /dev/null
+++ b/debian/patches/CVE-2025-48041.patch
@@ -0,0 +1,265 @@
+From: Jakub Witczak <[email protected]>
+Date: Wed, 20 Aug 2025 10:31:50 +0200
+Subject: ssh: max_handles option added to ssh_sftpd
+
+- add max_handles option and update tests (1000 by default)
+- remove sshd_read_file redundant testcase
+
+origin: backport, 
https://github.com/erlang/otp/commit/5f9af63eec4657a37663828d206517828cb9f288
+---
+ lib/ssh/src/ssh_sftpd.erl        | 37 ++++++++++++++----
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 83 ++++++++++++++++++----------------------
+ 2 files changed, 67 insertions(+), 53 deletions(-)
+
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 60be295..385df56 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -57,6 +57,7 @@ Specifies a channel process to handle an SFTP subsystem.
+         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
+@@ -87,6 +88,13 @@ Options:
+   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
+@@ -104,6 +112,7 @@ Options:
+       Options :: [ {cwd, string()} |
+                    {file_handler, CbMod | {CbMod, FileState}} |
+                    {max_files, integer()} |
++                   {max_handles, integer()} |
+                    {max_path, integer()} |
+                    {root, string()} |
+                    {sftpd_vsn, integer()}
+@@ -156,11 +165,13 @@ init(Options) ->
+               {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,
++                      max_handles = MaxHandles,
+                       max_path = MaxPath,
+                     options = Options,
+                     handles = [], pending = <<>>,
+@@ -328,14 +339,16 @@ handle_op(?SSH_FXP_REALPATH, ReqId,
+     end;
+ handle_op(?SSH_FXP_OPENDIR, ReqId,
+        <<?UINT32(RLen), RPath:RLen/binary>>,
+-        State0 = #state{xf = #ssh_xfer{vsn = Vsn}, 
+-                        file_handler = FileMod, file_state = FS0}) ->
++        State0 = #state{xf = #ssh_xfer{vsn = Vsn},
++                        file_handler = FileMod, file_state = FS0,
++                          max_handles = MaxHandles}) ->
+     RelPath = unicode:characters_to_list(RPath),
+     AbsPath = relate_file_name(RelPath, State0),
+     
+     XF = State0#state.xf,
+     {IsDir, FS1} = FileMod:is_dir(AbsPath, FS0),
+     State1 = State0#state{file_state = FS1},
++    HandlesCnt = length(State0#state.handles),
+     case IsDir of
+       false when Vsn > 5 ->
+           ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY,
+@@ -345,8 +358,12 @@ handle_op(?SSH_FXP_OPENDIR, ReqId,
+           ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
+                                   "Not a directory"),
+           State1;
+-      true ->
+-          add_handle(State1, XF, ReqId, directory, {RelPath,unread})
++      true when HandlesCnt < MaxHandles ->
++          add_handle(State1, XF, ReqId, directory, {RelPath,unread});
++        true ->
++          ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
++                                  "max_handles limit reached"),
++          State1
+     end;
+ handle_op(?SSH_FXP_READDIR, ReqId,
+         <<?UINT32(HLen), BinHandle:HLen/binary>>,
+@@ -797,7 +814,9 @@ open(Vsn, ReqId, Data, State) when Vsn >= 4 ->
+     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 
+@@ -809,7 +828,7 @@ do_open(ReqId, State0, Path, Flags) ->
+           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},
+@@ -820,7 +839,11 @@ do_open(ReqId, State0, Path, Flags) ->
+                   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 --git a/lib/ssh/test/ssh_sftpd_SUITE.erl 
b/lib/ssh/test/ssh_sftpd_SUITE.erl
+index 850073f..a2ca29c 100644
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -52,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,
+@@ -72,6 +71,7 @@
+ -define(SSH_TIMEOUT, 5000).
+ -define(REG_ATTERS, <<0,0,0,0,1>>).
+ -define(UNIX_EPOCH,  62167219200).
++-define(MAX_HANDLES, 10).
+ -define(MAX_PATH, 200).
+ -define(is_set(F, Bits), ((F) band (Bits)) == (F)).
+ 
+@@ -98,8 +98,7 @@ all() ->
+      links,
+      ver3_rename,
+      ver3_open_flags,
+-     relpath, 
+-     sshd_read_file,
++     relpath,
+      ver6_basic,
+      access_outside_root,
+      root_with_cwd,
+@@ -182,7 +181,8 @@ init_per_testcase(TestCase, Config) ->
+                         ssh:daemon(0, [{subsystems, SubSystems}|Options]);
+                     _ ->
+                         SubSystems = [ssh_sftpd:subsystem_spec(
+-                                          [{max_path, ?MAX_PATH}])],
++                                          [{max_handles, ?MAX_HANDLES},
++                                        {max_path, ?MAX_PATH}])],
+                         ssh:daemon(0, [{subsystems, SubSystems}|Options])
+                 end,
+ 
+@@ -318,22 +318,25 @@ open_close_dir(Config) when is_list(Config) ->
+ read_file(Config) when is_list(Config) ->
+     PrivDir =  proplists:get_value(priv_dir, Config),
+     FileName = filename:join(PrivDir, "test.txt"),
+-
+-    ReqId = 0,
+-    {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+-    {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+-      open_file(FileName, Cm, Channel, ReqId,
+-                ?ACE4_READ_DATA  bor ?ACE4_READ_ATTRIBUTES,
+-                ?SSH_FXF_OPEN_EXISTING),
+-
+-    NewReqId = 1,
+-
+-    {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
+-        Data/binary>>, _} =
+-      read_file(Handle, 100, 0, Cm, Channel, NewReqId),
+-
+-    {ok, Data} = file:read_file(FileName).
++         {Cm, Channel} = proplists:get_value(sftp, Config),
++    [begin
++         R1 = req_id(),
++         {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
++             open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor 
?ACE4_READ_ATTRIBUTES,
++                       ?SSH_FXF_OPEN_EXISTING),
++         R2 = req_id(),
++         {ok, <<?SSH_FXP_DATA, ?UINT32(R2), ?UINT32(_Length), Data/binary>>, 
_} =
++             read_file(Handle, 100, 0, Cm, Channel, R2),
++         {ok, Data} = file:read_file(FileName)
++     end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
++    ReqId = req_id(),
++    {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
++           ?UINT32(MsgLen), Msg:MsgLen/binary,
++           ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
++        open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor 
?ACE4_READ_ATTRIBUTES,
++                  ?SSH_FXF_OPEN_EXISTING),
++    ct:log("Message: ~s", [Msg]),
++    ok.
+ 
+ %%--------------------------------------------------------------------
+ max_path(Config) when is_list(Config) ->
+@@ -356,12 +359,21 @@ max_path(Config) when is_list(Config) ->
+ read_dir(Config) when is_list(Config) ->
+     PrivDir = proplists:get_value(priv_dir, Config),
+     {Cm, Channel} = proplists:get_value(sftp, Config),
+-    ReqId = 0,
+-    {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+-      open_dir(PrivDir, Cm, Channel, ReqId),
+-    ok = read_dir(Handle, Cm, Channel, ReqId).
++    [begin
++         R1 = req_id(),
++         {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
++             open_dir(PrivDir, Cm, Channel, R1),
++         R2 = req_id(),
++         ok = read_dir(Handle, Cm, Channel, R2)
++     end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
++    ReqId = req_id(),
++    {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
++           ?UINT32(MsgLen), Msg:MsgLen/binary,
++           ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
++        open_dir(PrivDir, Cm, Channel, ReqId),
++    ct:log("Message: ~s", [Msg]),
++    ok.
+ 
+-%%--------------------------------------------------------------------
+ write_file(Config) when is_list(Config) ->
+     PrivDir =  proplists:get_value(priv_dir, Config),
+     FileName = filename:join(PrivDir, "test.txt"),
+@@ -661,27 +673,6 @@ relpath(Config) when is_list(Config) ->
+           Root = Path
+     end.
+ 
+-%%--------------------------------------------------------------------
+-sshd_read_file(Config) when is_list(Config) ->
+-    PrivDir =  proplists:get_value(priv_dir, Config),
+-    FileName = filename:join(PrivDir, "test.txt"),
+-
+-    ReqId = 0,
+-    {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+-    {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+-      open_file(FileName, Cm, Channel, ReqId,
+-                ?ACE4_READ_DATA  bor ?ACE4_READ_ATTRIBUTES,
+-                ?SSH_FXF_OPEN_EXISTING),
+-
+-    NewReqId = 1,
+-
+-    {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
+-        Data/binary>>, _} =
+-      read_file(Handle, 100, 0, Cm, Channel, NewReqId),
+-
+-    {ok, Data} = file:read_file(FileName).
+-%%--------------------------------------------------------------------
+ ver6_basic(Config) when is_list(Config) ->
+     PrivDir =  proplists:get_value(priv_dir, Config),
+     %FileName = filename:join(PrivDir, "test.txt"),
diff --git a/debian/patches/series b/debian/patches/series
index 8b5554fbcb..70cd2fcc6c 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -4,3 +4,8 @@ javascript.patch
 x32.patch
 doc.patch
 exdoc.patch
+CVE-2016-1000107.patch
+CVE-2025-48038.patch
+CVE-2025-48039.patch
+CVE-2025-48040.patch
+CVE-2025-48041.patch
diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml
new file mode 100644
index 0000000000..695edf0eb8
--- /dev/null
+++ b/debian/salsa-ci.yml
@@ -0,0 +1,6 @@
+---
+include:
+  - 
https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+variables:
+  RELEASE: 'trixie'

--- End Message ---
--- Begin Message ---
Package: release.debian.org
Version: 13.4

This update has been released as part of Debian 13.4.

--- End Message ---

Reply via email to