Your message dated Sat, 16 May 2026 11:07:42 +0000
with message-id <[email protected]>
and subject line Released with 12.14
has caused the Debian Bug report #1127607,
regarding bookworm-pu: package erlang/1:25.2.3+dfsg-1+deb12u3
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.)


-- 
1127607: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1127607
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
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 affecting the ssh server
implementation.

[ Impact ]
Mostly 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 ]
@Sergei as with #1127606 (trixie) please write if you disagree.
diff --git a/debian/changelog b/debian/changelog
index 6738cb3c3a..b6b3272c74 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,20 @@
+erlang (1:25.2.3+dfsg-1+deb12u4) bookworm; urgency=medium
+
+  * Non-maintainer upload.
+  * 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).
+
+ -- Jochen Sprickerhof <[email protected]>  Thu, 15 Jan 2026 10:56:30 +0100
+
 erlang (1:25.2.3+dfsg-1+deb12u3) bookworm-proposed-updates; urgency=medium
 
   * Fix FTBFS with newer xsltproc.
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-2025-48038.patch 
b/debian/patches/CVE-2025-48038.patch
new file mode 100644
index 0000000000..800160e857
--- /dev/null
+++ b/debian/patches/CVE-2025-48038.patch
@@ -0,0 +1,34 @@
+From: Jakub Witczak <[email protected]>
+Date: Wed, 27 Aug 2025 17:49:08 +0200
+Subject: ssh: verify file handle size limit for client data
+
+- reject handles exceeding 256 bytes (as specified for SFTP)
+
+Origin: 
https://github.com/erlang/otp/commit/f09e0201ff701993dc24a08f15e524daf72db42f
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48038
+---
+ lib/ssh/src/ssh_sftpd.erl | 11 +++++++++++
+ 1 file changed, 11 insertions(+)
+
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 6bcad0d..cd24c3e 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -222,6 +222,17 @@ handle_data(Type, ChannelId, Data0, State = 
#state{pending = Pending}) ->
+             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..a683c023c0
--- /dev/null
+++ b/debian/patches/CVE-2025-48039.patch
@@ -0,0 +1,239 @@
+From: Jakub Witczak <[email protected]>
+Date: Fri, 11 Jul 2025 13:59:41 +0200
+Subject: ssh: ssh_sftpd verify path size for client data
+
+- reject max_path exceeding the 4096 limit or according to other option value
+
+Origin: 
https://github.com/erlang/otp/commit/043ee3c943e2977c1acdd740ad13992fd60b6bf0
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48039
+---
+ lib/ssh/doc/src/ssh_sftpd.xml    |  8 ++++
+ lib/ssh/src/ssh_sftpd.erl        | 32 +++++++++++++-
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 90 ++++++++++++++++++++++++++--------------
+ 3 files changed, 97 insertions(+), 33 deletions(-)
+
+diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
+index 49a23f4..efabf3f 100644
+--- a/lib/ssh/doc/src/ssh_sftpd.xml
++++ b/lib/ssh/doc/src/ssh_sftpd.xml
+@@ -65,6 +65,14 @@
+           If supplied, the number of filenames returned to the SFTP client 
per <c>READDIR</c>
+           request is limited to at most the given value.</p>
+         </item>
++          <tag><c>max_path</c></tag>
++        <item>
++          <p>The default value is <c>4096</c>. Positive integer
++          value represents the maximum path length which cannot be
++          exceeded in data provided by the SFTP client. (Note:
++          limitations might be also enforced by underlying operating
++          system)</p>
++        </item>
+         <tag><c>root</c></tag>
+         <item>
+           <p>Sets the SFTP root directory. Then the user cannot see any files
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index cd24c3e..5632848 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -52,6 +52,7 @@
+         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
+@@ -65,6 +66,7 @@
+       Options :: [ {cwd, string()} |
+                    {file_handler, CbMod | {CbMod, FileState}} |
+                    {max_files, integer()} |
++                   {max_path, integer()} |
+                    {root, string()} |
+                    {sftpd_vsn, integer()}
+                  ],
+@@ -115,8 +117,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 = []}}}.
+@@ -233,6 +239,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]),
+diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl 
b/lib/ssh/test/ssh_sftpd_SUITE.erl
+index 42677b7..f04cde3 100644
+--- 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, 10000).
+ -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..d27a182c56
--- /dev/null
+++ b/debian/patches/CVE-2025-48040.patch
@@ -0,0 +1,477 @@
+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: 
https://github.com/erlang/otp/commit/548f1295d86d0803da884db8685cc16d461d0d5a
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48040
+---
+ lib/ssh/src/ssh_connection.erl         |   3 +-
+ lib/ssh/src/ssh_connection_handler.erl |  35 +++++++---
+ lib/ssh/src/ssh_lib.erl                |  15 ++++-
+ lib/ssh/src/ssh_message.erl            |  42 +++++++-----
+ lib/ssh/src/ssh_transport.erl          | 120 +++++++++++++++++++--------------
+ lib/ssh/test/ssh_connection_SUITE.erl  |  12 +++-
+ 6 files changed, 147 insertions(+), 80 deletions(-)
+
+diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
+index badf3a2..b21d249 100644
+--- a/lib/ssh/src/ssh_connection.erl
++++ b/lib/ssh/src/ssh_connection.erl
+@@ -481,10 +481,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 ba46468..fa3b374 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1146,12 +1146,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;
+@@ -1181,12 +1190,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 3d29b5e..c6791f1 100644
+--- a/lib/ssh/src/ssh_lib.erl
++++ b/lib/ssh/src/ssh_lib.erl
+@@ -28,7 +28,9 @@
+          format_address_port/2, format_address_port/1,
+          format_address/1,
+          format_time_ms/1,
+-         comp/2
++         comp/2,
++         trim_reason/1,
++         max_log_len/1
+         ]).
+ 
+ -include("ssh.hrl").
+@@ -86,3 +88,14 @@ comp([], [], Truth) ->
+ 
+ comp(_, _, _) ->
+     false.
++%% We don't want to process badmatch details, potentially containing
++%% malicious data of unknown size
++trim_reason({badmatch, V}) when is_binary(V) ->
++    badmatch;
++trim_reason(E) ->
++    E.
++
++max_log_len(#ssh{opts = Opts}) ->
++    ?GET_OPT(max_log_item_len, Opts);
++max_log_len(Opts) when is_map(Opts) ->
++    ?GET_OPT(max_log_item_len, Opts).
+diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
+index e22a4e2..4d5ac74 100644
+--- a/lib/ssh/src/ssh_message.erl
++++ b/lib/ssh/src/ssh_message.erl
+@@ -43,7 +43,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
+@@ -821,23 +821,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 6081a02..5abc11b 100644
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -405,8 +405,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;
+ 
+@@ -422,31 +423,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) ->
+@@ -598,14 +606,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,
+@@ -626,14 +639,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.
+ 
+@@ -659,7 +673,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;
+ 
+@@ -691,8 +706,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(_, _) ->
+@@ -718,7 +733,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}}}}.
+ 
+@@ -749,19 +763,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, 
+@@ -786,20 +803,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.
+ 
+ %%%----------------------------------------------------------------
+@@ -833,17 +848,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,
+@@ -866,15 +889,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 06d90cc..d529cf5 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -1345,6 +1345,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),
+@@ -1365,6 +1367,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"},
+@@ -1382,7 +1388,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,
+@@ -1400,12 +1406,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..45ee9b7d0f
--- /dev/null
+++ b/debian/patches/CVE-2025-48041.patch
@@ -0,0 +1,268 @@
+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: 
https://github.com/erlang/otp/commit/d49efa2d4fa9e6f7ee658719cd76ffe7a33c2401
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48041
+---
+ lib/ssh/doc/src/ssh_sftpd.xml    |  4 ++
+ lib/ssh/src/ssh_sftpd.erl        | 30 +++++++++++----
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 83 ++++++++++++++++++----------------------
+ 3 files changed, 64 insertions(+), 53 deletions(-)
+
+diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
+index efabf3f..7c250a9 100644
+--- a/lib/ssh/doc/src/ssh_sftpd.xml
++++ b/lib/ssh/doc/src/ssh_sftpd.xml
+@@ -65,6 +65,10 @@
+           If supplied, the number of filenames returned to the SFTP client 
per <c>READDIR</c>
+           request is limited to at most the given value.</p>
+         </item>
++        <tag><c>max_handles</c></tag>
++        <item>
++            <p>The default value is <c>1000</c>. Positive integer value 
represents the maximum number of file handles allowed for a connection.</p>     
       
++        </item>
+           <tag><c>max_path</c></tag>
+         <item>
+           <p>The default value is <c>4096</c>. Positive integer
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 5632848..dfa566a 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -52,6 +52,7 @@
+         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
+@@ -66,6 +67,7 @@
+       Options :: [ {cwd, string()} |
+                    {file_handler, CbMod | {CbMod, FileState}} |
+                    {max_files, integer()} |
++                   {max_handles, integer()} |
+                    {max_path, integer()} |
+                    {root, string()} |
+                    {sftpd_vsn, integer()}
+@@ -117,11 +119,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 = <<>>,
+@@ -286,14 +290,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,
+@@ -303,8 +309,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>>,
+@@ -755,7 +765,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 
+@@ -767,7 +779,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},
+@@ -778,7 +790,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 f04cde3..fade45b 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, 10000).
+ -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 405ebf00f2..2fdd8154c8 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -16,3 +16,7 @@ ssh-early-RCE-fix.patch
 ssh-strict-KEX-exchange-hardening.patch
 zip-sanitize-paths.patch
 xslt-for-each.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..f437b7ffa5
--- /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: 'bookworm'

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

This update has been released as part of Debian 12.14.

--- End Message ---

Reply via email to