Hi!

Since there wasn't any upload of erlang/1:25.2.3+dfsg-1+deb12u4 I'd like to
add another set of fixes to it.

These are:
CVE-2026-23943 Improper Handling of Highly Compressed Data
(Compression Bomb) in sftpd
CVE-2026-23942 Improper Limitation of a Pathname to a Restricted
Directory in inets
CVE-2026-23941 Inconsistent Interpretation of HTTP Requests in inets
CVE-2026-21620 Improper Limitation of a Pathname to a Restricted
Directory in tftp

See [1] for detail on these CVEs. They are not too scary, but it'd be
better to fix them anyway.

Please, find the updated diff between 1:25.2.3+dfsg-1+deb12u4 and
1:25.2.3+dfsg-1+deb12u3 currently in oldstable.

All the changes come with additional tests, which were successful.

Similar patches were uploaded for trixie recently and for sid and
forky a while ago.

[1] https://security-tracker.debian.org/tracker/source-package/erlang

Cheers!
-- 
Sergei Golovan
diff -Nru erlang-25.2.3+dfsg/debian/changelog erlang-25.2.3+dfsg/debian/changelog
--- erlang-25.2.3+dfsg/debian/changelog	2025-08-31 13:57:31.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/changelog	2026-04-07 13:54:55.000000000 +0300
@@ -1,3 +1,43 @@
+erlang (1:25.2.3+dfsg-1+deb12u4) bookworm; urgency=medium
+
+  [ Jochen Sprickerhof ]
+  * Add salsa-ci
+  * Add gbp.conf.
+    Needed to reproduce the orig.tar with empty directories.
+  * 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).
+
+  [ Lucas Kanashiro ]
+  * Fix CVE-2026-23941.
+    Inconsistent Interpretation of HTTP Requests ('HTTP Request Smuggling')
+    vulnerability in Erlang OTP (inets httpd module) allows HTTP Request
+    Smuggling.
+  * Fix CVE-2026-23942.
+    Improper Limitation of a Pathname to a Restricted Directory ('Path
+    Traversal') vulnerability in Erlang OTP (ssh_sftpd module) allows Path
+    Traversal.
+  * Fix CVE-2026-23943.
+    Improper Handling of Highly Compressed Data (Compression Bomb)
+    vulnerability in Erlang OTP ssh (ssh_transport modules) allows Denial of
+    Service via Resource Depletion.
+    Closes: #1130912.
+
+  [ Sergei Golovan ]
+  * Fix CVE-2026-21620.
+    Relative Path Traversal, Improper Isolation or Compartmentalization
+    vulnerability in Erlang/OTP (tftp_file modules) (closes: 1128651).
+
+ -- Sergei Golovan <[email protected]>  Tue, 07 Apr 2026 13:54:55 +0300
+
 erlang (1:25.2.3+dfsg-1+deb12u3) bookworm-proposed-updates; urgency=medium
 
   * Fix FTBFS with newer xsltproc.
diff -Nru erlang-25.2.3+dfsg/debian/gbp.conf erlang-25.2.3+dfsg/debian/gbp.conf
--- erlang-25.2.3+dfsg/debian/gbp.conf	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/gbp.conf	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,2 @@
+[DEFAULT]
+pristine-tar = True
diff -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2025-48038.patch erlang-25.2.3+dfsg/debian/patches/CVE-2025-48038.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2025-48038.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2025-48038.patch	2026-04-07 13:54:55.000000000 +0300
@@ -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 -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2025-48039.patch erlang-25.2.3+dfsg/debian/patches/CVE-2025-48039.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2025-48039.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2025-48039.patch	2026-04-07 13:54:55.000000000 +0300
@@ -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 -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2025-48040.patch erlang-25.2.3+dfsg/debian/patches/CVE-2025-48040.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2025-48040.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2025-48040.patch	2026-04-07 13:54:55.000000000 +0300
@@ -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 -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2025-48041.patch erlang-25.2.3+dfsg/debian/patches/CVE-2025-48041.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2025-48041.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2025-48041.patch	2026-04-07 13:54:55.000000000 +0300
@@ -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 -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2026-21620.patch erlang-25.2.3+dfsg/debian/patches/CVE-2026-21620.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2026-21620.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2026-21620.patch	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,299 @@
+From: Upstream
+Date: Tue, 10 Feb 2026 18:13:21 +0100
+Subject: Patch fixes CVE-2026-21620
+
+Ensure that relative path components does not allow
+a requested file name to go outside the configured root_dir.
+
+root_dir should be checked to be a directory and absolute.
+
+If root_dir is used, Filename should be checked to be
+relative under root_dir.
+
+--- a/lib/tftp/src/tftp_file.erl
++++ b/lib/tftp/src/tftp_file.erl
+@@ -43,10 +43,6 @@
+ 
+ -include_lib("kernel/include/file.hrl").
+ 
+--record(initial,
+-	{filename,
+-	 is_native_ascii}).
+-
+ -record(state,
+ 	{access,
+ 	 filename,
+@@ -294,45 +290,62 @@
+ %%-------------------------------------------------------------------
+ 
+ handle_options(Access, Filename, Mode, Options, Initial) ->
+-    I = #initial{filename = Filename, is_native_ascii = is_native_ascii()},
+-    {Filename2, IsNativeAscii} = handle_initial(Initial, I),
+-    IsNetworkAscii = handle_mode(Mode, IsNativeAscii),
++    {Filename2, IsNativeAscii} = handle_initial(Initial, Filename),
++    IsNetworkAscii =
++        case Mode of
++            "netascii" when IsNativeAscii =:= true ->
++                true;
++            "octet" ->
++                false;
++            _ ->
++                throw({error, {badop, "Illegal mode " ++ Mode}})
++        end,
+     Options2 = do_handle_options(Access, Filename2, Options),
+     {ok, Filename2, IsNativeAscii, IsNetworkAscii, Options2}.
+ 
+-handle_mode(Mode, IsNativeAscii) ->
+-    case Mode of
+-	"netascii" when IsNativeAscii =:= true -> true;
+-	"octet" -> false;
+-	_ -> throw({error, {badop, "Illegal mode " ++ Mode}})
++handle_initial(
++  #state{filename = Filename, is_native_ascii = IsNativeAscii}, _FName) ->
++    {Filename, IsNativeAscii};
++handle_initial(Initial, Filename) when is_list(Initial) ->
++    Opts = get_initial_opts(Initial, #{}),
++    {case Opts of
++         #{ root_dir := RootDir } ->
++             safe_filename(Filename, RootDir);
++         #{} ->
++             Filename
++     end,
++     maps:get(is_native_ascii, Opts, is_native_ascii())}.
++
++get_initial_opts([], Opts) -> Opts;
++get_initial_opts([Opt | Initial], Opts) ->
++    case Opt of
++        {root_dir, RootDir} ->
++            is_map_key(root_dir, Opts) andalso
++                throw({error, {badop, "Internal error. root_dir already set"}}),
++            get_initial_opts(Initial, Opts#{ root_dir => RootDir });
++        {native_ascii, Bool} when is_boolean(Bool) ->
++            get_initial_opts(Initial, Opts#{ is_native_ascii => Bool })
+     end.
+ 
+-handle_initial([{root_dir, Dir} | Initial], I) ->
+-    case catch filename_join(Dir, I#initial.filename) of
+-	{'EXIT', _} ->
+-	    throw({error, {badop, "Internal error. root_dir is not a string"}});
+-	Filename2 ->
+-	    handle_initial(Initial, I#initial{filename = Filename2})
+-    end;
+-handle_initial([{native_ascii, Bool} | Initial], I) ->
+-    case Bool of
+-	true  -> handle_initial(Initial, I#initial{is_native_ascii = true});
+-	false -> handle_initial(Initial, I#initial{is_native_ascii = false})
+-    end;
+-handle_initial([], I) when is_record(I, initial) ->
+-    {I#initial.filename, I#initial.is_native_ascii};
+-handle_initial(State, _) when is_record(State, state) ->
+-    {State#state.filename, State#state.is_native_ascii}.
+-
+-filename_join(Dir, Filename) ->
+-    case filename:pathtype(Filename) of
+-	absolute ->
+-	    [_ | RelFilename] = filename:split(Filename),
+-	    filename:join([Dir, RelFilename]);
+-	_ ->
+-	    filename:join([Dir, Filename])
++safe_filename(Filename, RootDir) ->
++    absolute =:= filename:pathtype(RootDir) orelse
++        throw({error, {badop, "Internal error. root_dir is not absolute"}}),
++    filelib:is_dir(RootDir) orelse
++        throw({error, {badop, "Internal error. root_dir not a directory"}}),
++    RelFilename =
++        case filename:pathtype(Filename) of
++            absolute ->
++                filename:join(tl(filename:split(Filename)));
++            _ -> Filename
++        end,
++    case filelib:safe_relative_path(RelFilename, RootDir) of
++        unsafe ->
++	    throw({error, {badop, "Internal error. Filename out of bounds"}});
++        SafeFilename ->
++            filename:join(RootDir, SafeFilename)
+     end.
+ 
++
+ do_handle_options(Access, Filename, [{Key, Val} | T]) ->
+     case Key of
+ 	"tsize" ->
+--- a/lib/tftp/test/tftp_SUITE.erl
++++ b/lib/tftp/test/tftp_SUITE.erl
+@@ -20,7 +20,7 @@
+ 
+ -module(tftp_SUITE).
+ 
+--compile(export_all).
++-compile([export_all, nowarn_export_all]).
+ 
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %% Includes and defines
+@@ -29,18 +29,13 @@
+ -include_lib("common_test/include/ct.hrl").
+ -include("tftp_test_lib.hrl").
+ 
+--define(START_DAEMON(Port, Options),
++-define(START_DAEMON(Options),
+         begin
+-            {ok, Pid} = ?VERIFY({ok, _Pid}, tftp:start([{port, Port} | Options])),
+-            if
+-                Port == 0 ->
+-                    {ok, ActualOptions} = ?IGNORE(tftp:info(Pid)),
+-                    {value, {port, ActualPort}} =
+-                        lists:keysearch(port, 1, ActualOptions),
+-                    {ActualPort, Pid};
+-                true ->
+-                    {Port, Pid}
+-            end
++            {ok, Pid} = ?VERIFY({ok, _Pid}, tftp:start([{port, 0} | Options])),
++            {ok, ActualOptions} = ?IGNORE(tftp:info(Pid)),
++            {value, {port, ActualPort}} =
++                lists:keysearch(port, 1, ActualOptions),
++            {ActualPort, Pid}
+         end).
+ 
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+@@ -78,6 +73,7 @@
+ all() ->
+     [
+      simple,
++     root_dir,
+      extra,
+      reuse_connection,
+      resend_client,
+@@ -157,7 +153,7 @@
+ simple(Config) when is_list(Config) ->
+     ?VERIFY(ok, application:start(tftp)),
+ 
+-    {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])),
++    {Port, DaemonPid} = ?IGNORE(?START_DAEMON([{debug, brief}])),
+ 
+     %% Read fail
+     RemoteFilename = "tftp_temporary_remote_test_file.txt",
+@@ -184,6 +180,73 @@
+     ok.
+ 
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%% root_dir
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++
++root_dir(doc) ->
++    ["Start the daemon and check the root_dir option."];
++root_dir(suite) ->
++    [];
++root_dir(Config) when is_list(Config) ->
++    ?VERIFY(ok, application:start(tftp)),
++    PrivDir = get_conf(priv_dir, Config),
++    Root    = hd(filename:split(PrivDir)),
++    Up      = "..",
++    Remote  = "remote.txt",
++    Local   = "tftp_temporary_local_test_file.txt",
++    SideDir = fn_jn(PrivDir,tftp_side),
++    RootDir = fn_jn(PrivDir,tftp_root),
++    ?IGNORE(file:del_dir_r(RootDir)),
++    ?IGNORE(file:del_dir_r(SideDir)),
++    ok = filelib:ensure_path(fn_jn(RootDir,sub)),
++    ok = filelib:ensure_path(SideDir),
++    Blob = binary:copy(<<$1>>, 2000),
++    Size = byte_size(Blob),
++    ok = file:write_file(fn_jn(SideDir,Remote), Blob),
++    {Port, DaemonPid} =
++        ?IGNORE(?START_DAEMON([{debug, brief},
++                               {callback,
++                                {"", tftp_file, [{root_dir, RootDir}]}}])),
++    try
++        %% Outside root_dir
++        ?VERIFY({error, {client_open, badop, _}},
++                 tftp:read_file(
++                   fn_jn([Up,tftp_side,Remote]), binary, [{port, Port}])),
++        ?VERIFY({error, {client_open, badop, _}},
++                tftp:write_file(
++                  fn_jn([Up,tftp_side,Remote]), Blob, [{port, Port}])),
++        %% Nonexistent
++        ?VERIFY({error, {client_open, enoent, _}},
++                 tftp:read_file(
++                   fn_jn(sub,Remote), binary, [{port, Port}])),
++        ?VERIFY({error, {client_open, enoent, _}},
++                tftp:write_file(
++                  fn_jn(nonexistent,Remote), Blob, [{port, Port}])),
++        %% Write and read
++        ?VERIFY({ok, Size},
++                tftp:write_file(
++                  fn_jn(sub,Remote), Blob, [{port, Port}])),
++        ?VERIFY({ok, Blob},
++                tftp:read_file(
++                  fn_jn([Root,sub,Remote]), binary, [{port, Port}])),
++        ?VERIFY({ok, Size},
++                tftp:read_file(
++                  fn_jn(sub,Remote), Local, [{port, Port}])),
++        ?VERIFY({ok, Blob}, file:read_file(Local)),
++        ?VERIFY(ok, file:delete(Local)),
++        ?VERIFY(ok, application:stop(tftp))
++    after
++        %% Cleanup
++        unlink(DaemonPid),
++        exit(DaemonPid, kill),
++        ?IGNORE(file:del_dir_r(SideDir)),
++        ?IGNORE(file:del_dir_r(RootDir)),
++        ?IGNORE(application:stop(tftp))
++    end,
++    ok.
++
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %% Extra
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ 
+@@ -195,7 +258,7 @@
+     ?VERIFY({'EXIT', {badarg,{fake_key, fake_flag}}},
+             tftp:start([{port, 0}, {fake_key, fake_flag}])),
+ 
+-    {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])),
++    {Port, DaemonPid} = ?IGNORE(?START_DAEMON([{debug, brief}])),
+     
+     RemoteFilename = "tftp_extra_temporary_remote_test_file.txt",
+     LocalFilename = "tftp_extra_temporary_local_test_file.txt",
+@@ -329,7 +392,7 @@
+     [];
+ resend_client(Config) when is_list(Config) ->
+     Host = {127, 0, 0, 1},
+-    {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])),
++    {Port, DaemonPid} = ?IGNORE(?START_DAEMON([{debug, all}])),
+ 
+     ?VERIFY(ok, resend_read_client(Host, Port, 10)),
+     ?VERIFY(ok, resend_read_client(Host, Port, 512)),
+@@ -890,7 +953,7 @@
+     [];
+ reuse_connection(Config) when is_list(Config) ->
+     Host = {127, 0, 0, 1},
+-    {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])),
++    {Port, DaemonPid} = ?IGNORE(?START_DAEMON([{debug, all}])),
+ 
+     RemoteFilename = "reuse_connection.tmp",
+     BlkSize = 512,
+@@ -964,7 +1027,7 @@
+ large_file(Config) when is_list(Config) ->
+     ?VERIFY(ok, application:start(tftp)),
+ 
+-    {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])),
++    {Port, DaemonPid} = ?IGNORE(?START_DAEMON([{debug, brief}])),
+ 
+     %% Read fail
+     RemoteFilename = "tftp_temporary_large_file_remote_test_file.txt",
+@@ -999,3 +1062,15 @@
+     after Timeout ->
+             timeout
+     end.
++
++get_conf(Key, Config) ->
++    Default = make_ref(),
++    case proplists:get_value(Key, Config, Default) of
++        Default ->
++            erlang:error({no_key, Key});
++        Value ->
++            Value
++    end.
++
++fn_jn(A, B) -> filename:join(A, B).
++fn_jn(P) -> filename:join(P).
diff -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2026-23941.patch erlang-25.2.3+dfsg/debian/patches/CVE-2026-23941.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2026-23941.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2026-23941.patch	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,159 @@
+From: Erlang/OTP <[email protected]>
+Date: Thu, 12 Mar 2026 16:58:29 +0100
+Subject: Merge branch 'whaileee/inets/httpd/http-request-smuggling/OTP-20007'
+ into maint-27
+
+* whaileee/inets/httpd/http-request-smuggling/OTP-20007:
+  Prevent httpd from parsing HTTP requests when multiple Content-Length headers are present
+
+Origin: upstream, https://github.com/erlang/otp/commit/a761d391d8d08316cbd7d4a86733ba932b73c45b
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912
+---
+ lib/inets/src/http_server/httpd_request.erl        | 53 ++++++++++++++--------
+ .../src/http_server/httpd_request_handler.erl      | 10 ++--
+ lib/inets/test/httpd_SUITE.erl                     | 24 +++++++++-
+ 3 files changed, 63 insertions(+), 24 deletions(-)
+
+diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl
+index a16df69..f84f6ab 100644
+--- a/lib/inets/src/http_server/httpd_request.erl
++++ b/lib/inets/src/http_server/httpd_request.erl
+@@ -210,7 +210,7 @@ parse_headers(<<?CR,?LF,?CR,?LF,Body/binary>>, Header, Headers, _, _,
+ 					   Headers),
+ 	    {ok, list_to_tuple(lists:reverse([Body, {http_request:headers(FinalHeaders, #http_request_h{}), FinalHeaders} | Result]))};
+ 	NewHeader ->
+-	    case check_header(NewHeader, Options) of 
++	    case check_header(NewHeader, Headers, Options) of
+ 		ok ->
+ 		    FinalHeaders = lists:filtermap(fun(H) ->
+ 							   httpd_custom:customize_headers(Customize, request_header, H)
+@@ -260,7 +260,7 @@ parse_headers(<<?CR,?LF, Octet, Rest/binary>>, Header, Headers, Current, Max,
+ 	    parse_headers(Rest, [Octet], Headers, 
+ 			  Current, Max, Options, Result);
+ 	NewHeader ->
+-	    case check_header(NewHeader, Options) of 
++	    case check_header(NewHeader, Headers, Options) of
+ 		ok ->
+ 		    parse_headers(Rest, [Octet], [NewHeader | Headers], 
+ 				  Current, Max, Options, Result);
+@@ -429,23 +429,36 @@ get_persistens(HTTPVersion,ParsedHeader,ConfigDB)->
+ default_version()->
+     "HTTP/1.1".
+ 
+-check_header({"content-length", Value}, Maxsizes) ->
+-    Max = proplists:get_value(max_content_length, Maxsizes),
+-    MaxLen = length(integer_to_list(Max)),
+-    case length(Value) =< MaxLen of
+-	true ->
+-	    try 
+-		list_to_integer(Value)
+-	    of
+-		I when I>= 0 ->
+-		    ok;
+-		_ ->
+-		    {error, {size_error, Max, 411, "negative content-length"}}
+-	    catch _:_ ->
+-		    {error, {size_error, Max, 411, "content-length not an integer"}}
+-	    end;
+-	false ->
+-	    {error, {size_error, Max, 413, "content-length unreasonably long"}}
++check_header({"content-length", Value}, Headers, MaxSizes) ->
++    case check_parsed_content_length_values(Value, Headers) of
++        true ->
++            check_content_length_value(Value, MaxSizes);
++        false ->
++            {error, {bad_request, 400, "Multiple Content-Length headers with different values"}}
+     end;
+-check_header(_, _) ->
++
++check_header(_, _, _) ->
+     ok.
++
++check_parsed_content_length_values(CurrentValue, Headers) ->
++    ContentLengths = [V || {"content-length", _} = V <- Headers],
++    length([V || {"content-length", Value} = V <- ContentLengths, Value =:= CurrentValue]) =:= length(ContentLengths).
++
++check_content_length_value(Value, MaxSizes) ->
++    Max = proplists:get_value(max_content_length, MaxSizes),
++    MaxLen = length(integer_to_list(Max)),
++    case length(Value) =< MaxLen of
++        true ->
++            try
++                list_to_integer(Value)
++            of
++                I when I>= 0 ->
++                    ok;
++                _ ->
++                    {error, {size_error, Max, 411, "negative content-length"}}
++            catch _:_ ->
++                    {error, {size_error, Max, 411, "content-length not an integer"}}
++            end;
++        false ->
++            {error, {size_error, Max, 413, "content-length unreasonably long"}}
++    end.
+diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl
+index 33c07d3..5fd91b3 100644
+--- a/lib/inets/src/http_server/httpd_request_handler.erl
++++ b/lib/inets/src/http_server/httpd_request_handler.erl
+@@ -235,12 +235,16 @@ handle_info({Proto, Socket, Data},
+ 	    httpd_response:send_status(NewModData, ErrCode, ErrStr, {max_size, MaxSize}),
+ 	    {stop, normal, State#state{response_sent = true,
+ 				       mod = NewModData}};
+-
+-    {error, {version_error, ErrCode, ErrStr}, Version} ->
++        {error, {version_error, ErrCode, ErrStr}, Version} ->
+         NewModData =  ModData#mod{http_version = Version},
+ 	    httpd_response:send_status(NewModData, ErrCode, ErrStr),
+ 	    {stop, normal, State#state{response_sent = true,
+-				                   mod = NewModData}};
++				       mod = NewModData}};
++        {error, {bad_request, ErrCode, ErrStr}, Version} ->
++            NewModData =  ModData#mod{http_version = Version},
++            httpd_response:send_status(NewModData, ErrCode, ErrStr),
++            {stop, normal, State#state{response_sent = true,
++                                       mod = NewModData}};
+ 
+     {http_chunk = Module, Function, Args} when ChunkState =/= undefined ->
+         NewState = handle_chunk(Module, Function, Args, State),
+diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl
+index 5639a23..3fbecb8 100644
+--- a/lib/inets/test/httpd_SUITE.erl
++++ b/lib/inets/test/httpd_SUITE.erl
+@@ -122,7 +122,7 @@ groups() ->
+            disturbing_1_0,
+ 		   reload_config_file
+ 		  ]},
+-     {post, [], [chunked_post, chunked_chunked_encoded_post, post_204]},
++     {post, [], [chunked_post, chunked_chunked_encoded_post, post_204, multiple_content_length_header]},
+      {basic_auth, [], [basic_auth_1_1, basic_auth_1_0, verify_href_1_1]},
+      {auth_api, [], [auth_api_1_1, auth_api_1_0]},
+      {auth_api_dets, [], [auth_api_1_1, auth_api_1_0]},
+@@ -1862,6 +1862,28 @@ tls_alert(Config) when is_list(Config) ->
+     Port = proplists:get_value(port, Config),    
+     {error, {tls_alert, _}} = ssl:connect("localhost", Port, [{verify, verify_peer} | SSLOpts]).
+ 
++%%-------------------------------------------------------------------------
++multiple_content_length_header() ->
++    [{doc, "Test Content-Length header"}].
++
++multiple_content_length_header(Config) when is_list(Config) ->
++    ok = http_status("POST / ",
++                     {"Content-Length:0" ++ "\r\n",
++                      ""},
++                     [{http_version, "HTTP/1.1"} |Config],
++                     [{statuscode, 501}]),
++    ok = http_status("POST / ",
++                     {"Content-Length:0" ++ "\r\n" ++
++                      "Content-Length:0" ++ "\r\n",
++                      ""},
++                     [{http_version, "HTTP/1.1"} |Config],
++                     [{statuscode, 501}]),
++    ok = http_status("POST / ",
++                     {"Content-Length:1" ++ "\r\n" ++
++                      "Content-Length:0" ++ "\r\n",
++                      "Z"},
++                     [{http_version, "HTTP/1.1"} |Config],
++                     [{statuscode, 400}]).
+ %%--------------------------------------------------------------------
+ %% Internal functions -----------------------------------
+ %%--------------------------------------------------------------------
diff -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2026-23942.patch erlang-25.2.3+dfsg/debian/patches/CVE-2026-23942.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2026-23942.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2026-23942.patch	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,144 @@
+From: Erlang/OTP <[email protected]>
+Date: Thu, 12 Mar 2026 16:58:27 +0100
+Subject: Merge branch 'kuba/maint-27/ssh/sftp_path/OTP-20009' into maint-27
+
+* kuba/maint-27/ssh/sftp_path/OTP-20009:
+  ssh: Fix path traversal vulnerability in ssh_sftpd root directory validation
+
+Origin: backport, https://github.com/erlang/otp/commit/9e0ac85d3485e7898e0da88a14be0ee2310a3b28
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912
+---
+ lib/ssh/src/ssh_sftpd.erl        | 12 ++++++++++-
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 44 ++++++++++++++++++++++++++++------------
+ 2 files changed, 42 insertions(+), 14 deletions(-)
+
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -873,7 +873,17 @@
+     end.
+ 
+ is_within_root(Root, File) ->
+-    lists:prefix(Root, File).
++    RootParts = filename:split(Root),
++    FileParts = filename:split(File),
++    is_prefix_components(RootParts, FileParts).
++
++%% Verify if request file path is within configured root directory
++is_prefix_components([], _) ->
++    true;
++is_prefix_components([H|T1], [H|T2]) ->
++    is_prefix_components(T1, T2);
++is_prefix_components(_, _) ->
++    false.
+ 
+ %% Remove leading slash (/), if any, in order to make the filename
+ %% relative (to the root)
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -33,8 +33,7 @@
+          end_per_testcase/2
+         ]).
+ 
+--export([
+-         access_outside_root/1,
++-export([access_outside_root/1,
+          links/1,
+          mk_rm_dir/1,
+          open_close_dir/1,
+@@ -160,7 +159,7 @@
+                           RootDir = filename:join(BaseDir, a),
+                           CWD     = filename:join(RootDir, b),
+                           %% Make the directory chain:
+-                          ok = filelib:ensure_dir(filename:join(CWD, tmp)),
++                          ok = filelib:ensure_path(CWD),
+                           SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir},
+                                                                   {cwd, CWD}])],
+                           ssh:daemon(0, [{subsystems, SubSystems}|Options]);
+@@ -221,7 +220,12 @@
+     [{sftp, {Cm, Channel}}, {sftpd, Sftpd }| Config].
+ 
+ end_per_testcase(_TestCase, Config) ->
+-    catch ssh:stop_daemon(proplists:get_value(sftpd, Config)),
++    try
++        ssh:stop_daemon(proplists:get_value(sftpd, Config))
++    catch
++        Class:Error:_Stack ->
++            ?CT_LOG("Class = ~p Error = ~p", [Class, Error])
++    end,
+     {Cm, Channel} = proplists:get_value(sftp, Config),
+     ssh_connection:close(Cm, Channel),
+     ssh:close(Cm),
+@@ -688,33 +692,47 @@
+ access_outside_root(Config) when is_list(Config) ->
+     PrivDir  =  proplists:get_value(priv_dir, Config),
+     BaseDir  = filename:join(PrivDir, access_outside_root),
+-    %% A file outside the tree below RootDir which is BaseDir/a
+-    %% Make the file  BaseDir/bad :
+     BadFilePath = filename:join([BaseDir, bad]),
+     ok = file:write_file(BadFilePath, <<>>),
++    FileInSiblingDir = filename:join([BaseDir, a2, "secret.txt"]),
++    ok = filelib:ensure_dir(FileInSiblingDir),
++    ok = file:write_file(FileInSiblingDir, <<"secret">>),
++    TestFolderStructure = "" ++
++         "PrivDir\n" ++
++         "|-- access_outside_root (BaseDir)\n" ++
++         "|   |-- a (RootDir folder)\n" ++
++         "|   |   +-- b (CWD folder)\n" ++
++         "|   |-- a2 (sibling folder with name prefix equal to RootDir)\n" ++
++         "|   |   +-- secret.txt\n" ++
++         "|   +-- bad.txt\n" ++
++    "",
++    ?CT_LOG("TestFolderStructure = ~n~s", [TestFolderStructure]),
+     {Cm, Channel} = proplists:get_value(sftp, Config),
+-    %% Try to access a file parallel to the RootDir:
+-    try_access("/../bad",   Cm, Channel, 0),
++    %% Try to access a file parallel to the RootDir using parent traversal:
++    try_access("/../bad.txt",   Cm, Channel, 0),
+     %% Try to access the same file via the CWD which is /b relative to the RootDir:
+-    try_access("../../bad", Cm, Channel, 1).
+-
++    try_access("../../bad.txt", Cm, Channel, 1),
++    %% Try to access sibling folder name prefixed with root dir
++    try_access("/../a2/secret.txt", Cm, Channel, 2),
++    try_access("../../a2/secret.txt", Cm, Channel, 3).
+ 
+ try_access(Path, Cm, Channel, ReqId) ->
+     Return = 
+         open_file(Path, Cm, Channel, ReqId, 
+                   ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+                   ?SSH_FXF_OPEN_EXISTING),
+-    ct:log("Try open ~p -> ~p",[Path,Return]),
++    ?CT_LOG("Try open ~p -> ~w",[Path,Return]),
+     case Return of
+         {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), _Handle0/binary>>, _} ->
++            ?CT_LOG("Got the unexpected ?SSH_FXP_HANDLE",[]),
+             ct:fail("Could open a file outside the root tree!");
+         {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), Rest/binary>>, <<>>} ->
+             case Code of
+                 ?SSH_FX_FILE_IS_A_DIRECTORY ->
+-                    ct:log("Got the expected SSH_FX_FILE_IS_A_DIRECTORY status",[]),
++                    ?CT_LOG("Got the expected SSH_FX_FILE_IS_A_DIRECTORY status",[]),
+                     ok;
+                 ?SSH_FX_FAILURE ->
+-                    ct:log("Got the expected SSH_FX_FAILURE status",[]),
++                    ?CT_LOG("Got the expected SSH_FX_FAILURE status",[]),
+                     ok;
+                 _ ->
+                     case Rest of
+--- a/lib/ssh/test/ssh_test_lib.hrl
++++ b/lib/ssh/test/ssh_test_lib.hrl
+@@ -67,3 +67,14 @@
+                 ct:log("~p:~p Show file~n~s =~n~s~n",
+                        [?MODULE,?LINE,File__, Contents__])
+         end)(File)).
++
++-define(SSH_TEST_LIB_FORMAT, "(~s ~p:~p in ~p) ").
++-define(SSH_TEST_LIB_ARGS,
++        [erlang:pid_to_list(self()), ?MODULE, ?LINE, ?FUNCTION_NAME]).
++-define(CT_LOG(F),
++        (ct:log(?SSH_TEST_LIB_FORMAT ++ F, ?SSH_TEST_LIB_ARGS, [esc_chars]))).
++-define(CT_LOG(F, Args),
++        (ct:log(
++           ?SSH_TEST_LIB_FORMAT ++ F,
++           ?SSH_TEST_LIB_ARGS ++ Args,
++           [esc_chars]))).
diff -Nru erlang-25.2.3+dfsg/debian/patches/CVE-2026-23943.patch erlang-25.2.3+dfsg/debian/patches/CVE-2026-23943.patch
--- erlang-25.2.3+dfsg/debian/patches/CVE-2026-23943.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/CVE-2026-23943.patch	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,476 @@
+From: Erlang/OTP <[email protected]>
+Date: Thu, 12 Mar 2026 16:58:28 +0100
+Subject: Merge branch
+ 'michal/maint-27/ssh/fix-unbounded-zlib-inflate/OTP-20011' into maint-27
+
+* michal/maint-27/ssh/fix-unbounded-zlib-inflate/OTP-20011:
+  Add test for post-authentication compression
+  Add information about compression-based attacks to hardening guide
+  Adjust documentation to mention that zlib is disabled by default
+  Add tests that verify we disconnect on too large decompressed data
+  Always run compression test
+  Disable zlib by default and limit size of decompressed data
+
+Origin: backport, https://github.com/erlang/otp/commit/93073c3bd338c60cd2bae715ce6a1d4ffc1a8fd3
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912
+---
+ lib/ssh/src/ssh_connection_handler.erl |   7 ++
+ lib/ssh/src/ssh_transport.erl          |  64 +++++++++++++--
+ lib/ssh/test/ssh_basic_SUITE.erl       |  66 ++++++++-------
+ lib/ssh/test/ssh_protocol_SUITE.erl    | 146 ++++++++++++++++++++++++++++++++-
+ lib/ssh/test/ssh_trpt_test_lib.erl     |  11 ++-
+ 5 files changed, 254 insertions(+), 40 deletions(-)
+
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1188,6 +1188,13 @@
+                                  io_lib:format("Bad packet: Size (~p bytes) exceeds max size",
+                                                [PacketLen]),
+                                  StateName, D0),
++            {stop, Shutdown, D};
++
++    {error, exceeds_max_decompressed_size} ->
++            {Shutdown, D} =
++                ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
++                                 "Bad packet: Size after decompression exceeds max size",
++                                 StateName, D0),
+             {stop, Shutdown, D}
+     catch
+ 	Class:Reason0:Stacktrace ->
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -192,6 +192,9 @@
+                                       'ssh-dss'
+                                      ]);
+ 
++default_algorithms1(compression) ->
++    supported_algorithms(compression, same(['zlib']));
++
+ default_algorithms1(Alg) ->
+     supported_algorithms(Alg, []).
+ 
+@@ -1451,8 +1454,12 @@
+     case unpack(pkt_type(CryptoAlg), mac_type(MacAlg),
+                 DecryptedPfx, EncryptedBuffer, AEAD, TotalNeeded, Ssh0) of
+         {ok, Payload, NextPacketBytes, Ssh1} ->
+-            {Ssh, DecompressedPayload} = decompress(Ssh1, Payload),
+-            {packet_decrypted, DecompressedPayload, NextPacketBytes, Ssh};
++            case decompress(Ssh1, Payload) of
++                {ok, Ssh, DecompressedPayload} ->
++                    {packet_decrypted, DecompressedPayload, NextPacketBytes, Ssh};
++                Other ->
++                    Other
++            end;
+         Other ->
+             Other
+     end.
+@@ -1968,15 +1975,56 @@
+     {ok, Ssh#ssh{decompress = none, decompress_ctx = undefined}}.
+ 
+ decompress(#ssh{decompress = none} = Ssh, Data) ->
+-    {Ssh, Data};
++    {ok, Ssh, Data};
+ decompress(#ssh{decompress = zlib, decompress_ctx = Context} = Ssh, Data) ->
+-    Decompressed = zlib:inflate(Context, Data),
+-    {Ssh, list_to_binary(Decompressed)};
++    case safe_zlib_inflate(Context, Data) of
++        {ok, Decompressed} ->
++            {ok, Ssh, Decompressed};
++        Other ->
++            Other
++    end;
+ decompress(#ssh{decompress = '[email protected]', authenticated = false} = Ssh, Data) ->
+-    {Ssh, Data};
++    {ok, Ssh, Data};
+ decompress(#ssh{decompress = '[email protected]', decompress_ctx = Context, authenticated = true} = Ssh, Data) ->
+-    Decompressed = zlib:inflate(Context, Data),
+-    {Ssh, list_to_binary(Decompressed)}.
++    case safe_zlib_inflate(Context, Data) of
++        {ok, Decompressed} ->
++            {ok, Ssh, Decompressed};
++        Other ->
++            Other
++    end.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%% Safe decompression loop
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++
++safe_zlib_inflate(Context, Data) ->
++    safe_zlib_inflate_loop(Context, {0, []}, zlib:safeInflate(Context, Data)).
++
++safe_zlib_inflate_loop(Context, {AccLen0, AccData}, {Status, Chunk})
++  when Status == continue; Status == finished ->
++    ChunkLen = iolist_size(Chunk),
++    AccLen = AccLen0 + ChunkLen,
++    %% RFC 4253 section 6
++    %% Align with packets that don't use compression, we can process payloads with length
++    %% that required minimum padding.
++    %% From ?SSH_MAX_PACKET_SIZE subtract:
++    %% 1 byte for length of padding_length field
++    %% 4 bytes for minimum allowed length of padding
++    %% We don't subtract:
++    %% 4 bytes for packet_length field - not included in packet_length
++    %% x bytes for mac (size depends on type of used mac) - not included in packet_length
++    case AccLen > (?SSH_MAX_PACKET_SIZE - 5) of
++        true ->
++            {error, exceeds_max_decompressed_size};
++        false when Status == continue ->
++            Next = zlib:safeInflate(Context, []),
++            safe_zlib_inflate_loop(Context, {AccLen, [Chunk | AccData]}, Next);
++        false when Status == finished ->
++            Reversed = lists:reverse([Chunk | AccData]),
++            {ok, iolist_to_binary(Reversed)}
++    end;
++safe_zlib_inflate_loop(_Context, {_AccLen, _AccData}, {need_dictionary, Adler, _Chunk}) ->
++    erlang:error({need_dictionary, Adler}).
+ 
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%
+--- a/lib/ssh/test/ssh_basic_SUITE.erl
++++ b/lib/ssh/test/ssh_basic_SUITE.erl
+@@ -56,6 +56,7 @@
+          double_close/1,
+          exec/1,
+          exec_compressed/1,
++         exec_compressed_post_auth_compression/1,
+          exec_with_io_in/1,
+          exec_with_io_out/1,
+          host_equal/2,
+@@ -153,7 +154,7 @@
+                                            ]},
+      
+      {p_basic, [?PARALLEL], [send, peername_sockname,
+-                             exec, exec_compressed, 
++                             exec, exec_compressed, exec_compressed_post_auth_compression,
+                              exec_with_io_out, exec_with_io_in,
+                              cli, cli_exit_normal, cli_exit_status,
+                              idle_time_client, idle_time_server,
+@@ -401,37 +402,42 @@
+ %%--------------------------------------------------------------------
+ %%% Test that compression option works
+ exec_compressed(Config) when is_list(Config) ->
+-    case ssh_test_lib:ssh_supports(zlib, compression) of
+-	false ->
+-	    {skip, "zlib compression is not supported"};
++    exec_compressed_helper(Config, 'zlib').
+ 
+-	true ->
+-	    process_flag(trap_exit, true),
+-	    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
+-	    UserDir = proplists:get_value(priv_dir, Config), 
+-
+-	    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
+-						     {preferred_algorithms,[{compression, [zlib]}]},
+-						     {failfun, fun ssh_test_lib:failfun/2}]),
+-    
+-	    ConnectionRef =
+-		ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+-						  {user_dir, UserDir},
+-						  {user_interaction, false}]),
+-	    {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
+-	    success = ssh_connection:exec(ConnectionRef, ChannelId,
+-					  "1+1.", infinity),
+-	    Data = {ssh_cm, ConnectionRef, {data, ChannelId, 0, <<"2">>}},
+-	    case ssh_test_lib:receive_exec_result(Data) of
+-		expected ->
+-		    ok;
+-		Other ->
+-		    ct:fail(Other)
+-	    end,
+-	    ssh_test_lib:receive_exec_end(ConnectionRef, ChannelId),
+-	    ssh:close(ConnectionRef),
+-	    ssh:stop_daemon(Pid)
+-    end.
++%%--------------------------------------------------------------------
++%%% Test that post authentication compression option works
++exec_compressed_post_auth_compression(Config) when is_list(Config) ->
++    exec_compressed_helper(Config, '[email protected]').
++
++%%--------------------------------------------------------------------
++%%% Exec compressed helper
++exec_compressed_helper(Config, CompressAlgorithm) ->
++    process_flag(trap_exit, true),
++    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
++    UserDir = proplists:get_value(priv_dir, Config),
++
++    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
++                                             {preferred_algorithms,[{compression, [CompressAlgorithm]}]},
++                                             {failfun, fun ssh_test_lib:failfun/2}]),
++
++    ConnectionRef =
++        ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
++                                          {user_dir, UserDir},
++                                          {user_interaction, false},
++                                          {preferred_algorithms,[{compression, [CompressAlgorithm]}]}]),
++    {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
++    success = ssh_connection:exec(ConnectionRef, ChannelId,
++                                  "1+1.", infinity),
++    Data = {ssh_cm, ConnectionRef, {data, ChannelId, 0, <<"2">>}},
++    case ssh_test_lib:receive_exec_result(Data) of
++        expected ->
++            ok;
++        Other ->
++            ct:fail(Other)
++    end,
++    ssh_test_lib:receive_exec_end(ConnectionRef, ChannelId),
++    ssh:close(ConnectionRef),
++    ssh:stop_daemon(Pid).
+ 
+ %%--------------------------------------------------------------------
+ %%% Idle timeout test
+--- a/lib/ssh/test/ssh_protocol_SUITE.erl
++++ b/lib/ssh/test/ssh_protocol_SUITE.erl
+@@ -48,6 +48,10 @@
+          bad_very_long_service_name/1,
+          client_handles_keyboard_interactive_0_pwds/1,
+          client_info_line/1,
++         decompression_bomb_client/1,
++         decompression_bomb_client_after_auth/1,
++         decompression_bomb_server/1,
++         decompression_bomb_server_after_auth/1,
+          do_gex_client_init/3,
+          do_gex_client_init_old/3,
+          empty_service_name/1,
+@@ -132,7 +136,11 @@
+ 		       lib_no_match
+ 		      ]},
+      {packet_size_error, [], [packet_length_too_large,
+-			      packet_length_too_short]},
++			      packet_length_too_short,
++			      decompression_bomb_client,
++			      decompression_bomb_client_after_auth,
++			      decompression_bomb_server,
++			      decompression_bomb_server_after_auth]},
+      {field_size_error, [], [service_name_length_too_large,
+ 			     service_name_length_too_short]},
+      {kex, [], [custom_kexinit,
+@@ -223,6 +231,8 @@
+ 		     [{preferred_algorithms,[{cipher,?DEFAULT_CIPHERS}
+                                             ]}
+ 		      | Opts]);
++init_per_testcase(decompression_bomb_client, Config) ->
++    start_std_daemon(Config, [{preferred_algorithms, [{compression, ['zlib']}]}]);
+ init_per_testcase(_TestCase, Config) ->
+     check_std_daemon_works(Config, ?LINE).
+ 
+@@ -238,6 +248,8 @@
+ 				  TC == gex_client_old_request_exact ;
+ 				  TC == gex_client_old_request_noexact ->
+     stop_std_daemon(Config);
++end_per_testcase(decompression_bomb_client, Config) ->
++    stop_std_daemon(Config);
+ end_per_testcase(_TestCase, Config) ->
+     check_std_daemon_works(Config, ?LINE).
+ 
+@@ -675,6 +687,138 @@
+ 	  ], InitialState).
+ 
+ %%%--------------------------------------------------------------------
++decompression_bomb_client(Config) ->
++    {ok, InitialState} = connect_and_kex(Config, ssh_trpt_test_lib:exec([]),
++                                         [{kex, [?DEFAULT_KEX]},
++                                          {cipher, ?DEFAULT_CIPHERS},
++                                          {compression, ['zlib']}], dh),
++    %% ?SSH_MAX_PACKET_SIZE - 9 is enough to trigger disconnect because Payload of ssh packet becomes:
++    %% 1 byte message identifier
++    %% 4 bytes length of data field
++    %% ?SSH_MAX_PACKET_SIZE - 9 bytes of data
++    %% This is longer than max decompressed Payload length which is ?SSH_MAX_PACKET_SIZE - 5
++    %% See more in ssh_transport:safe_zlib_inflate_loop
++    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
++    {ok, _} =
++        ssh_trpt_test_lib:exec([
++                                {send, #ssh_msg_ignore{data = Data}},
++                                {match, disconnect(), receive_msg}
++                               ], InitialState).
++
++%%%--------------------------------------------------------------------
++decompression_bomb_client_after_auth(Config) ->
++    {ok, InitialState} = connect_and_kex(Config, ssh_trpt_test_lib:exec([]),
++                                         [{kex, [?DEFAULT_KEX]},
++                                          {cipher, ?DEFAULT_CIPHERS},
++                                          {compression, ['[email protected]']}], dh),
++    {User, Pwd} = server_user_password(Config),
++    {ok, AfterAuthState} =
++        ssh_trpt_test_lib:exec(
++          [{send, #ssh_msg_service_request{name = "ssh-userauth"}},
++           {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg},
++           {send, #ssh_msg_userauth_request{user = User,
++                                            service = "ssh-connection",
++                                            method = "password",
++                                            data = <<?BOOLEAN(?FALSE),
++                                                     ?STRING(unicode:characters_to_binary(Pwd))>>
++                                           }},
++           {match, #ssh_msg_userauth_success{_='_'}, receive_msg}
++          ], InitialState),
++    %% See explanation in decompression_bomb_client
++    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
++    {ok, _} =
++        ssh_trpt_test_lib:exec([
++                                {send, #ssh_msg_ignore{data = Data}},
++                                {match, disconnect(), receive_msg}
++                               ], AfterAuthState).
++
++%%%--------------------------------------------------------------------
++decompression_bomb_server(Config) ->
++    {ok, InitialState} = ssh_trpt_test_lib:exec(listen),
++    HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
++    %% See explanation in decompression_bomb_client
++    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
++    ServerPid =
++        spawn_link(
++          fun() ->
++                  {ok, _} =
++                      ssh_trpt_test_lib:exec(
++                        [{set_options, [print_ops, print_messages]},
++                         {accept, [{system_dir, system_dir(Config)},
++                                   {user_dir, user_dir(Config)},
++                                   {preferred_algorithms,[{kex, [?DEFAULT_KEX]},
++                                                          {cipher, ?DEFAULT_CIPHERS},
++                                                          {compression, ['zlib']}]}]},
++                         receive_hello,
++                         {send, hello},
++                         {send, ssh_msg_kexinit},
++                         {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++                         {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
++                         {send, ssh_msg_kexdh_reply},
++                         {send, #ssh_msg_newkeys{}},
++                         {match, #ssh_msg_newkeys{_='_'}, receive_msg},
++                         {send, #ssh_msg_ignore{data = Data}},
++                         {match, disconnect(), receive_msg}
++                        ], InitialState)
++          end),
++    Ref = monitor(process, ServerPid),
++    {error, "Protocol error"} =
++        std_connect(HostPort, Config,
++                    [{silently_accept_hosts, true},
++                     {user_dir, user_dir(Config)},
++                     {user_interaction, false},
++                     {preferred_algorithms, [{compression,['zlib']}]}]),
++    receive
++        {'DOWN', Ref, process, ServerPid, normal} -> ok
++    end.
++
++%%%--------------------------------------------------------------------
++decompression_bomb_server_after_auth(Config) ->
++    {ok, InitialState} = ssh_trpt_test_lib:exec(listen),
++    HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
++    %% See explanation in decompression_bomb_client
++    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
++    ServerPid =
++        spawn_link(
++          fun() ->
++                  {ok ,_} =
++                      ssh_trpt_test_lib:exec(
++                        [{set_options, [print_ops, print_messages]},
++                         {accept, [{system_dir, system_dir(Config)},
++                                   {user_dir, user_dir(Config)},
++                                   {preferred_algorithms,[{kex, [?DEFAULT_KEX]},
++                                                          {cipher, ?DEFAULT_CIPHERS},
++                                                          {compression, ['[email protected]']}]}]},
++                         receive_hello,
++                         {send, hello},
++                         {send, ssh_msg_kexinit},
++                         {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++                         {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
++                         {send, ssh_msg_kexdh_reply},
++                         {send, #ssh_msg_newkeys{}},
++                         {match, #ssh_msg_newkeys{_='_'}, receive_msg},
++                         {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
++                         {send, #ssh_msg_service_accept{name="ssh-userauth"}},
++                         {match, #ssh_msg_userauth_request{service="ssh-connection",
++                                                           method="none",
++                                                           _='_'}, receive_msg},
++                         {send, #ssh_msg_userauth_success{}},
++                         {send, #ssh_msg_ignore{data = Data}},
++                         {match, disconnect(), receive_msg}
++                        ], InitialState)
++          end),
++    Ref = monitor(process, ServerPid),
++    {ok, _} =
++        std_connect(HostPort, Config,
++                    [{silently_accept_hosts, true},
++                     {user_dir, user_dir(Config)},
++                     {user_interaction, false},
++                     {preferred_algorithms, [{compression, ['[email protected]']}]}]),
++    receive
++        {'DOWN', Ref, process, ServerPid, normal} -> ok
++    end.
++
++%%%--------------------------------------------------------------------
+ service_name_length_too_large(Config) -> bad_service_name_length(Config, +4).
+ 
+ service_name_length_too_short(Config) -> bad_service_name_length(Config, -4).
+@@ -1406,12 +1550,14 @@
+     connect_and_kex(Config, ssh_trpt_test_lib:exec([]) ).
+ 
+ connect_and_kex(Config, InitialState) ->
++    ClientAlgs = [{kex,[?DEFAULT_KEX]}, {cipher,?DEFAULT_CIPHERS}],
++    connect_and_kex(Config, InitialState, ClientAlgs, dh).
++
++connect_and_kex(Config, InitialState, ClientAlgs, Variant) ->
+     ssh_trpt_test_lib:exec(
+       [{connect,
+ 	server_host(Config),server_port(Config),
+-	[{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+-                                {cipher,?DEFAULT_CIPHERS}
+-                               ]},
++        [{preferred_algorithms,ClientAlgs},
+          {silently_accept_hosts, true},
+          {recv_ext_info, false},
+ 	 {user_dir, user_dir(Config)},
+@@ -1421,14 +1567,20 @@
+        receive_hello,
+        {send, hello},
+        {send, ssh_msg_kexinit},
+-       {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+-       {send, ssh_msg_kexdh_init},
+-       {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
+-       {send, #ssh_msg_newkeys{}},
+-       {match, #ssh_msg_newkeys{_='_'}, receive_msg}
+-      ],
++       {match, #ssh_msg_kexinit{_='_'}, receive_msg}] ++
++          get_kex_variant_ops(Variant) ++
++          [{send, #ssh_msg_newkeys{}},
++           {match, #ssh_msg_newkeys{_='_'}, receive_msg}
++          ],
+       InitialState).
+ 
++get_kex_variant_ops(dh) ->
++    [{send, ssh_msg_kexdh_init},
++     {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}];
++get_kex_variant_ops(ecdh) ->
++    [{send, ssh_msg_kex_ecdh_init},
++     {match, #ssh_msg_kex_ecdh_reply{_='_'}, receive_msg}].
++
+ %%%----------------------------------------------------------------
+ 
+ %%% For matching peer disconnection
+--- a/lib/ssh/test/ssh_trpt_test_lib.erl
++++ b/lib/ssh/test/ssh_trpt_test_lib.erl
+@@ -445,7 +445,13 @@
+ 	    fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
+     {ok, Packet, C} = ssh_transport:new_keys_message(S#s.ssh),
+     send_bytes(Packet, S#s{ssh = C});
+-    
++
++send(S0, #ssh_msg_userauth_success{} = Msg) ->
++    S = opt(print_messages, S0,
++        fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
++    {Packet, C} = ssh_transport:ssh_packet(Msg, S#s.ssh),
++    send_bytes(Packet, S#s{ssh = C#ssh{authenticated = true}, return_value = Msg});
++
+ send(S0, Msg) when is_tuple(Msg) ->
+     S = opt(print_messages, S0,
+ 	    fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
+@@ -511,6 +517,9 @@
+ 		#ssh_msg_newkeys{} ->
+ 		    {ok, C} = ssh_transport:handle_new_keys(PeerMsg, S#s.ssh),
+ 		    S#s{ssh=C};
++		#ssh_msg_userauth_success{} -> % Always the client
++		    C = S#s.ssh,
++		    S#s{ssh = C#ssh{authenticated = true}};
+ 		_ ->
+ 		    S
+ 	    end
diff -Nru erlang-25.2.3+dfsg/debian/patches/series erlang-25.2.3+dfsg/debian/patches/series
--- erlang-25.2.3+dfsg/debian/patches/series	2025-08-31 13:57:31.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/series	2026-04-07 13:54:55.000000000 +0300
@@ -16,3 +16,11 @@
 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
+CVE-2026-23941.patch
+CVE-2026-23942.patch
+CVE-2026-23943.patch
+CVE-2026-21620.patch
diff -Nru erlang-25.2.3+dfsg/debian/salsa-ci.yml erlang-25.2.3+dfsg/debian/salsa-ci.yml
--- erlang-25.2.3+dfsg/debian/salsa-ci.yml	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/salsa-ci.yml	2026-04-07 13:54:55.000000000 +0300
@@ -0,0 +1,6 @@
+---
+include:
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+variables:
+  RELEASE: 'bookworm'

Reply via email to