[Pkg-erlang-devel] Bug#1127606: trixie-pu: package erlang/1:27.3.4.1+dfsg-1

Jochen Sprickerhof jspricke at debian.org
Tue Feb 10 10:36:15 GMT 2026


Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: erlang at packages.debian.org, Sergei Golovan <sgolovan at debian.org>, Bastien Roucaries <rouca at debian.org>
Control: affects -1 + src:erlang
User: release.debian.org at packages.debian.org
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.
-------------- next part --------------
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 <sgolovan at debian.org>  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 <marcellanz at n-1.ch> and Konrad Pietrzak <konrad at erlang.org>)
+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 <kuba at erlang.org>)
+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 <kuba at erlang.org>)
+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 <kuba at erlang.org>
+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 <kuba at erlang.org>
+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'


More information about the Pkg-erlang-devel mailing list