Your message dated Sat, 16 May 2026 10:23:16 +0000
with message-id <[email protected]>
and subject line Released with 13.5
has caused the Debian Bug report #1132321,
regarding trixie-pu: package erlang/1:27.3.4.1+dfsg-1+deb13u2
to be marked as done.

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

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


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

Hi, release team!

[ Reason ]
There was another set of vulnerabilities published for the
Erlang/OTP distribution, see [1] and [2]. The CVE affect
ithe tftp, inets and ssh Erlang applications. They do not warrant DSA
but I'd like to fix them in a stable point update. They are
already fixed in unstable and testing by uploading the
latest upstream release of Erlang 27.

[ Impact ]
1. Data access via TFTP or SFTP (if someone uses the tftp and/or ssh
   sftpd Erlang module)
2. Incorrectly interpreted Content-Length header by the inets httpd
   server may cause desynchronisation with a frontend (line Nginx)
   if Erlang's inets is used as a backend.
3. Improper handling of highly compressed data may cause denial
   of service for Erlang ssh implementation.

[ Tests ]
New Erlang package is manually tested, no new test errors are
detected. The newly introduced tests all went fine.

[ Risks ]
Low risk given that these patches add additional data checks
without changing core functionality.

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

[1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1128651
[2] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912

-- 
Sergei Golovan
diff -Nru erlang-27.3.4.1+dfsg/debian/changelog 
erlang-27.3.4.1+dfsg/debian/changelog
--- erlang-27.3.4.1+dfsg/debian/changelog       2025-07-08 10:27:28.000000000 
+0300
+++ erlang-27.3.4.1+dfsg/debian/changelog       2026-03-30 13:26:03.000000000 
+0300
@@ -1,5 +1,30 @@
+erlang (1:27.3.4.1+dfsg-1+deb13u2) trixie; urgency=medium
+
+  [ Lucas Kanashiro ]
+  * Fix CVE-2026-21620.
+    Relative Path Traversal, Improper Isolation or Compartmentalization
+    vulnerability in Erlang OTP (tftp_file modules). Closes: #1128651
+  * Fix CVE-2026-23941.
+    Inconsistent Interpretation of HTTP Requests ('HTTP Request Smuggling')
+    vulnerability in Erlang OTP (inets httpd module) allows HTTP Request
+    Smuggling.
+    - d/p/CVE-2026-23941.patch
+  * 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.
+    - d/p/CVE-2026-23942.patch
+  * 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.
+    - d/p/CVE-2026-23943.patch
+    Closes: #1130912
+
+ -- Sergei Golovan <[email protected]>  Mon, 30 Mar 2026 13:26:03 +0300
+
 erlang (1:27.3.4.1+dfsg-1+deb13u1) trixie; urgency=medium
 
   * Fix CVE-2025-48038: allocation of resources without limits or throttling
     vulnerability in the ssh_sftp module allows excessive allocation,
     resource leak exposure (closes: #1115093).
diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-21620.patch 
erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-21620.patch
--- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-21620.patch    1970-01-01 
03:00:00.000000000 +0300
+++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-21620.patch    2026-03-30 
13:26:03.000000000 +0300
@@ -0,0 +1,578 @@
+From: Erlang/OTP <[email protected]>
+Date: Thu, 19 Feb 2026 16:58:37 +0100
+Subject: Merge branch 'raimo/tftp/path-traversal-27/OTP-19981' into maint-27
+
+* raimo/tftp/path-traversal-27/OTP-19981:
+  Fix typos
+  Fix old timing sensitive test case
+  Document security considerations
+  Fix old timing sensitive test case
+  Test option root_dir
+  Rewrite old style catch
+  Validate initial options
+
+Origin: upstream, 
https://github.com/erlang/otp/commit/3970738f687325138eb75f798054fa8960ac354e
+Bug-Debian: https://bugs.debian.org/1128651
+
+More info about this CVE: 
https://github.com/erlang/otp/security/advisories/GHSA-hmrc-prh3-rpvp
+---
+ debian/patches/CVE-2026-21620.patch    | 572 +++++++++++++++++++++++++++++++++
+ debian/patches/series                  |   1 +
+ lib/tftp/doc/guides/getting_started.md |   5 +-
+ lib/tftp/doc/guides/introduction.md    |  15 +
+ lib/tftp/src/tftp.erl                  |  50 ++-
+ lib/tftp/src/tftp_file.erl             | 121 +++----
+ lib/tftp/test/tftp_SUITE.erl           | 119 +++++--
+ lib/tftp/test/tftp_test_lib.hrl        |   5 +-
+ 8 files changed, 807 insertions(+), 81 deletions(-)
+ create mode 100644 debian/patches/CVE-2026-21620.patch
+
+diff --git a/lib/tftp/doc/guides/getting_started.md 
b/lib/tftp/doc/guides/getting_started.md
+index e9112a8..fbdd203 100644
+--- a/lib/tftp/doc/guides/getting_started.md
++++ b/lib/tftp/doc/guides/getting_started.md
+@@ -30,13 +30,14 @@ a sample file using the TFTP client.
+ _Step 1._ Create a sample file to be used for the transfer:
+ 
+ ```text
+-      $ echo "Erlang/OTP 21" > file.txt
++      $ echo "Erlang/OTP 21" > /tmp/file.txt
+ ```
+ 
+ _Step 2._ Start the TFTP server:
+ 
+ ```erlang
+-      1> {ok, Pid} = tftp:start([{port, 19999}]).
++      1> Callback = {callback,{"",tftp_file,[{root_dir,"/tmp"}]}}.
++      2> {ok, Pid} = tftp:start([{port, 19999}, Callback]).
+       {ok,<0.65.0>}
+ ```
+ 
+diff --git a/lib/tftp/doc/guides/introduction.md 
b/lib/tftp/doc/guides/introduction.md
+index 55d35bd..d86f676 100644
+--- a/lib/tftp/doc/guides/introduction.md
++++ b/lib/tftp/doc/guides/introduction.md
+@@ -42,3 +42,18 @@ file system.  TFTP is often installed with controls such 
that only
+ files that have public read access are available via TFTP and writing
+ files via TFTP is disallowed."
+ 
++This essentially means that any machine on the network
++that can reach the TFTP server is able to read and write,
++without authentication, any file on the machine that runs
++the TFTP server, that the user (or group) that runs the TFTP server
++(in this case the Erlang VM) is allowed to read or write.
++The machine configuration has to be prepared for that.
++
++> #### Warning {: .warning }
++>
++> The default behavior mentioned above is in general very risky,
++> and as a remedy, this TFTP application's default callback
++> `tftp_file` implements an initial state option
++> `{root_dir,Dir}` that restricts the callback's file accesses
++> to `Dir` and subdirectories.  It is recommended
++> to use that option when starting start this TFTP server.
+diff --git a/lib/tftp/src/tftp.erl b/lib/tftp/src/tftp.erl
+index fdbe527..bd8c017 100644
+--- a/lib/tftp/src/tftp.erl
++++ b/lib/tftp/src/tftp.erl
+@@ -41,10 +41,11 @@ Interface module for the `tftp` application.
+ ## Overwiew
+ 
+ This is a complete implementation of the following IETF standards:
+-    RFC 1350, The TFTP Protocol (revision 2).
+-    RFC 2347, TFTP Option Extension.
+-    RFC 2348, TFTP Blocksize Option.
+-    RFC 2349, TFTP Timeout Interval and Transfer Size Options.
++
++* [RFC 1350][], The TFTP Protocol (revision 2).
++* [RFC 2347][], TFTP Option Extension.
++* [RFC 2348][], TFTP Blocksize Option.
++* [RFC 2349][], TFTP Timeout Interval and Transfer Size Options.
+ 
+ The only feature that not is implemented in this release is
+ the "netascii" transfer mode.
+@@ -60,6 +61,11 @@ with a TFTP daemon and performs the actual transfer of the 
file.
+ Most of the options are common for both the client and the server
+ side, but some of them differs a little.
+ 
++[RFC 1350]: https://datatracker.ietf.org/doc/html/rfc1350
++[RFC 2347]: https://datatracker.ietf.org/doc/html/rfc2347
++[RFC 2348]: https://datatracker.ietf.org/doc/html/rfc2348
++[RFC 2349]: https://datatracker.ietf.org/doc/html/rfc2349
++
+ ## Callbacks
+ 
+ A `tftp` callback module is to be implemented as a `tftp` behavior and export
+@@ -197,7 +203,7 @@ All options most of them common to the client and server.
+   Controls which features to reject. This is mostly useful for the server as 
it
+   can restrict the use of certain TFTP options or read/write access.
+ 
+-- **`{callback, {RegExp ::string(), Module::module(), State :: term()}}`**
++- **`{callback, {RegExp ::string(), Module::module(), InitialState :: 
term()}}`**
+ 
+   Registration of a callback module. When a file is to be transferred, its 
local
+   filename is matched to the regular expressions of the registered callbacks.
+@@ -207,6 +213,24 @@ All options most of them common to the client and server.
+   The callback module must implement the `tftp` behavior, see
+   [callbacks](`m:tftp#callbacks`).
+ 
++  At the end of the list of callbacks there are always the default callbacks
++  `tftp_file` and `tftp_binary` with the `RegExp = ""` and `InitialState = 
[]`.
++
++  The `InitialState` should be an option list, and the empty list
++  should be accepted by any callback module.  The `tftp_file`
++  callback module accepts an `InitialState = [{root_dir, Dir}]`
++  that restrict local file operations to files in `Dir` and subdirectories.
++  All file names received in protocol requests, relative or absolute,
++  are regarded as relative to this directory.
++
++  > #### Warning {: .warning }
++  >
++  > The default callback module configuration allows access to any file
++  > on any local filesystem that is readable or writable by the user
++  > running the Erlang VM.  This can be a security vulnerability.
++  > It is therefore recommended to explicitly configure the `tftp_file`
++  > callback module to use the `root_dir` option.
++
+ - **`{logger, module()}`**
+ 
+   Callback module for customized logging of errors, warnings, and info 
messages.
+@@ -390,6 +414,22 @@ Starts a daemon process listening for UDP packets on a 
port.
+ 
+ When it receives a request for read or write, it spawns a temporary
+ server process handling the actual transfer of the (virtual) file.
++
++The request filename is matched against the regexps of the registered
++callback modules, and the first match selects the callback
++to handle the request.
++
++If there are no registered callback modules, `tftp_file` is used,
++with the initial state `[]`.
++
++> #### Warning {: .warning }
++>
++> The default callback module configuration allows access to any file
++> on any local filesystem that is readable or writable by the user
++> running the Erlang VM.  This can be a security vulnerability.
++> See the [`{callback,_}` option](`t:connection_option/0`)
++> at the start of this module reference for a remedy.
++
+ """.
+ 
+ -spec start(Options) -> {ok, Pid} | {error, Reason} when
+diff --git a/lib/tftp/src/tftp_file.erl b/lib/tftp/src/tftp_file.erl
+index 27d2b9c..87f0c76 100644
+--- a/lib/tftp/src/tftp_file.erl
++++ b/lib/tftp/src/tftp_file.erl
+@@ -1,7 +1,7 @@
+ %%
+ %% %CopyrightBegin%
+ %% 
+-%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
++%% Copyright Ericsson AB 2005-2026. All Rights Reserved.
+ %% 
+ %% Licensed under the Apache License, Version 2.0 (the "License");
+ %% you may not use this file except in compliance with the License.
+@@ -44,10 +44,6 @@
+ 
+ -include_lib("kernel/include/file.hrl").
+ 
+--record(initial,
+-      {filename,
+-       is_native_ascii}).
+-
+ -record(state,
+       {access,
+        filename,
+@@ -96,8 +92,8 @@
+ 
+ prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when 
is_list(Initial) ->
+     %% Client side
+-    case catch handle_options(Access, Filename, Mode, SuggestedOptions, 
Initial) of
+-      {ok, Filename2, IsNativeAscii, IsNetworkAscii, AcceptedOptions} ->
++    try handle_options(Access, Filename, Mode, SuggestedOptions, Initial) of
++        {Filename2, IsNativeAscii, IsNetworkAscii, AcceptedOptions} ->
+           State = #state{access           = Access,
+                          filename         = Filename2,
+                          is_native_ascii  = IsNativeAscii,
+@@ -106,9 +102,9 @@ prepare(_Peer, Access, Filename, Mode, SuggestedOptions, 
Initial) when is_list(I
+                          blksize          = lookup_blksize(AcceptedOptions),
+                          count            = 0,
+                          buffer          =  []},
+-          {ok, AcceptedOptions, State};
+-      {error, {Code, Text}} ->
+-          {error, {Code, Text}}
++            {ok, AcceptedOptions, State}
++    catch throw : Error ->
++            {error, Error}
+     end.
+ 
+ %% ---------------------------------------------------------
+@@ -154,12 +150,12 @@ open(Peer, Access, Filename, Mode, SuggestedOptions, 
Initial) when is_list(Initi
+     end;
+ open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when 
is_record(State, state) ->
+     %% Both sides
+-    case catch handle_options(Access, Filename, Mode, NegotiatedOptions, 
State) of
+-      {ok, _Filename2, _IsNativeAscii, _IsNetworkAscii, Options} 
+-         when Options =:= NegotiatedOptions ->
+-          do_open(State);
+-      {error, {Code, Text}} ->
+-          {error, {Code, Text}}
++    try handle_options(Access, Filename, Mode, NegotiatedOptions, State) of
++        {_Filename2, _IsNativeAscii, _IsNetworkAscii, Options}
++          when Options =:= NegotiatedOptions ->
++            do_open(State)
++    catch throw : Error ->
++            {error, Error}
+     end;
+ open(Peer, Access, Filename, Mode, NegotiatedOptions, State) ->
+     %% Handle upgrade from old releases. Please, remove this clause in next 
release.
+@@ -295,45 +291,62 @@ abort(_Code, _Text, #state{fd = Fd, access = Access} = 
State) ->
+ %%-------------------------------------------------------------------
+ 
+ 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({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}})
++    {Filename2, IsNativeAscii, IsNetworkAscii, Options2}.
++
++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({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({badop, "Internal error. root_dir is not absolute"}),
++    filelib:is_dir(RootDir) orelse
++        throw({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({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" ->
+@@ -361,15 +374,15 @@ do_handle_options(_Access, _Filename, []) ->
+ 
+ 
+ handle_integer(Access, Filename, Key, Val, Options, Min, Max) ->
+-    case catch list_to_integer(Val) of
+-      {'EXIT', _} ->
+-          do_handle_options(Access, Filename, Options);
++    try list_to_integer(Val) of
+       Int when Int >= Min, Int =< Max ->
+           [{Key, Val} | do_handle_options(Access, Filename, Options)];
+       Int when Int >= Min, Max =:= infinity ->
+           [{Key, Val} | do_handle_options(Access, Filename, Options)];
+       _Int ->
+-          throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}})
++            throw({badopt, "Illegal " ++ Key ++ " value " ++ Val})
++    catch error : _ ->
++            do_handle_options(Access, Filename, Options)
+     end.
+ 
+ lookup_blksize(Options) ->
+diff --git a/lib/tftp/test/tftp_SUITE.erl b/lib/tftp/test/tftp_SUITE.erl
+index 7289242..655874c 100644
+--- 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 @@ suite() -> [{ct_hooks,[ts_install_cth]}].
+ all() ->
+     [
+      simple,
++     root_dir,
+      extra,
+      reuse_connection,
+      resend_client,
+@@ -126,7 +122,7 @@ simple(suite) ->
+ 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",
+@@ -152,6 +148,73 @@ simple(Config) when is_list(Config) ->
+     ?VERIFY(ok, application:stop(tftp)),
+     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
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+@@ -164,7 +227,7 @@ extra(Config) when is_list(Config) ->
+     ?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",
+@@ -298,7 +361,7 @@ resend_client(suite) ->
+     [];
+ 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)),
+@@ -418,6 +481,9 @@ resend_read_client(Host, Port, BlkSize) ->
+     Ack5Bin = <<0, 4, 0, 5>>,
+     ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack5Bin)),
+ 
++    %% Recv ACK #6
++    ?VERIFY({udp, Socket, Host, NewPort, <<0,3,0,6>>}, recv(Timeout)),
++
+     %% Close socket
+     ?VERIFY(ok, gen_udp:close(Socket)),
+ 
+@@ -693,11 +759,16 @@ resend_read_server(Host, BlkSize) ->
+     Data6Bin = list_to_binary([0, 3, 0, 6 | Block6]),
+     ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data6Bin)),
+ 
++    %% Recv ACK #6
++    Ack6Bin = <<0, 4, 0, 6>>,
++    ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack6Bin}, recv(Timeout)),
++
+     %% Close daemon and server sockets
+     ?VERIFY(ok, gen_udp:close(ServerSocket)),
+     ?VERIFY(ok, gen_udp:close(DaemonSocket)),
+ 
+-    ?VERIFY({ClientPid, {tftp_client_reply, {ok, Blob}}}, recv(Timeout)),
++    ?VERIFY({ClientPid, {tftp_client_reply, {ok, Blob}}},
++            recv(2 * (Timeout + timer:seconds(1)))),
+ 
+     ?VERIFY(timeout, recv(Timeout)),
+     ok.
+@@ -859,7 +930,7 @@ reuse_connection(suite) ->
+     [];
+ 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,
+@@ -933,7 +1004,7 @@ large_file(suite) ->
+ 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",
+@@ -968,3 +1039,15 @@ recv(Timeout) ->
+     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 --git a/lib/tftp/test/tftp_test_lib.hrl b/lib/tftp/test/tftp_test_lib.hrl
+index eb8ed77..743b9a5 100644
+--- a/lib/tftp/test/tftp_test_lib.hrl
++++ b/lib/tftp/test/tftp_test_lib.hrl
+@@ -1,7 +1,7 @@
+ %%
+ %% %CopyrightBegin%
+ %% 
+-%% Copyright Ericsson AB 2007-2018. All Rights Reserved.
++%% Copyright Ericsson AB 2007-2026. All Rights Reserved.
+ %% 
+ %% Licensed under the Apache License, Version 2.0 (the "License");
+ %% you may not use this file except in compliance with the License.
+@@ -24,7 +24,8 @@
+       tftp_test_lib:log(Format, Args, ?MODULE, ?LINE)).
+ 
+ -define(ERROR(Reason),
+-      tftp_test_lib:error(Reason, ?MODULE, ?LINE)).
++        erlang:error({?MODULE,?LINE,?FUNCTION_NAME,(Reason)})).
++      %% tftp_test_lib:error(Reason, ?MODULE, ?LINE)).
+ 
+ -define(VERIFY(Expected, Expr),
+       fun() ->
diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23941.patch 
erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23941.patch
--- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23941.patch    1970-01-01 
03:00:00.000000000 +0300
+++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23941.patch    2026-03-30 
13:26:03.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 f5f582d..6229928 100644
+--- a/lib/inets/src/http_server/httpd_request.erl
++++ b/lib/inets/src/http_server/httpd_request.erl
+@@ -211,7 +211,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)
+@@ -261,7 +261,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);
+@@ -430,23 +430,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 048e6c1..aeb4e2d 100644
+--- a/lib/inets/src/http_server/httpd_request_handler.erl
++++ b/lib/inets/src/http_server/httpd_request_handler.erl
+@@ -260,12 +260,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 d91b8e1..0ee9fcc 100644
+--- a/lib/inets/test/httpd_SUITE.erl
++++ b/lib/inets/test/httpd_SUITE.erl
+@@ -126,7 +126,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]},
+@@ -2027,6 +2027,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-27.3.4.1+dfsg/debian/patches/CVE-2026-23942.patch 
erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23942.patch
--- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23942.patch    1970-01-01 
03:00:00.000000000 +0300
+++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23942.patch    2026-03-30 
13:26:03.000000000 +0300
@@ -0,0 +1,196 @@
+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: upstream, 
https://github.com/erlang/otp/commit/9e0ac85d3485e7898e0da88a14be0ee2310a3b28
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912
+---
+ lib/ssh/doc/guides/hardening.md  | 25 +++++++++++++++++++++++
+ lib/ssh/src/ssh_sftpd.erl        | 25 ++++++++++++++++++-----
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 44 ++++++++++++++++++++++++++++------------
+ 3 files changed, 76 insertions(+), 18 deletions(-)
+
+--- a/lib/ssh/doc/guides/hardening.md
++++ b/lib/ssh/doc/guides/hardening.md
+@@ -241,3 +241,28 @@
+ The negotiation (session setup time) time can be limited with the _parameter_
+ `NegotiationTimeout` in a call establishing an ssh session, for example
+ `ssh:connect/3`.
++
++## SFTP Security
++
++### Root Directory Isolation
++
++The [`root`](`m:ssh_sftpd`) option restricts SFTP users to a
++specific directory tree, preventing access to files outside that directory.
++
++**Example:**
++
++```erlang
++ssh:daemon(Port, [
++    {subsystems, [ssh_sftpd:subsystem_spec([{root, "/home/sftpuser"}])]},
++    ...
++]).
++```
++
++**Important:** The `root` option is configured per daemon, not per user. All
++users connecting to the same daemon share the same root directory. For 
per-user
++isolation, consider running separate daemon instances on different ports or
++using OS-level mechanisms (PAM chroot, containers, file permissions).
++
++**Defense-in-depth:** For high-security deployments, combine the `root` option
++with OS-level isolation mechanisms such as chroot jails, containers, or
++mandatory access control (SELinux, AppArmor).
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -100,10 +100,15 @@
+     data provided by the SFTP client. (Note: limitations might be also
+     enforced by underlying operating system)
+ 
+-- **`root`** - Sets the SFTP root directory. Then the user cannot see any 
files
+-  above this root. If, for example, the root directory is set to `/tmp`, then
+-  the user sees this directory as `/`. If the user then writes `cd /etc`, the
+-  user moves to `/tmp/etc`.
++- **`root`** - Sets the SFTP root directory. The user cannot access files
++  outside this directory tree. If, for example, the root directory is set to
++  `/tmp`, then the user sees this directory as `/`. If the user then writes
++  `cd /etc`, the user moves to `/tmp/etc`.
++
++  Note: This provides application-level isolation. For additional security,
++  consider using OS-level chroot or similar mechanisms. See the
++  [SFTP Security](hardening.md#sftp-security) section in the Hardening guide
++  for deployment recommendations.
+ 
+ - **`sftpd_vsn`** - Sets the SFTP version to use. Defaults to 5. Version 6 is
+   under development and limited.
+@@ -922,7 +927,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
++         |-- access_outside_root (BaseDir)
++         |   |-- a (RootDir folder)
++         |   |   +-- b (CWD folder)
++         |   |-- a2 (sibling folder with name prefix equal to RootDir)
++         |   |   +-- secret.txt
++         |   +-- bad.txt
++    """,
++    ?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-27.3.4.1+dfsg/debian/patches/CVE-2026-23943.patch 
erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23943.patch
--- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23943.patch    1970-01-01 
03:00:00.000000000 +0300
+++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-23943.patch    2026-03-30 
13:26:03.000000000 +0300
@@ -0,0 +1,619 @@
+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: upstream, 
https://github.com/erlang/otp/commit/93073c3bd338c60cd2bae715ce6a1d4ffc1a8fd3
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130912
+---
+ lib/ssh/doc/guides/configurations.md   |   8 +-
+ lib/ssh/doc/guides/configure_algos.md  |  24 +++---
+ lib/ssh/doc/guides/hardening.md        |  20 +++++
+ lib/ssh/doc/ssh_app.md                 |   9 +-
+ 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 ++-
+ 9 files changed, 299 insertions(+), 56 deletions(-)
+
+--- a/lib/ssh/doc/guides/configurations.md
++++ b/lib/ssh/doc/guides/configurations.md
+@@ -185,8 +185,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ Note that the algorithms in the file `ex2.config` is not yet applied. They 
will
+@@ -202,8 +202,8 @@
+           {server2client,['aes192-ctr']}]},
+  {mac,[{client2server,['hmac-sha1']},
+        {server2client,['hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ 4>
+ ```
+ 
+--- a/lib/ssh/doc/guides/configure_algos.md
++++ b/lib/ssh/doc/guides/configure_algos.md
+@@ -78,7 +78,9 @@
+   This list is also divided into two for the both directions
+ 
+ - **`compression`** - If and how to compress the message. Examples are `none`,
+-  that is, no compression and `zlib`.
++  that is, no compression,
++  `zlib` for pre-authentication compression (disabled by default),
++  and `'[email protected]'` for post-authentication compression.
+ 
+   This list is also divided into two for the both directions
+ 
+@@ -120,8 +122,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ {: #example_default_algorithms }
+@@ -174,8 +176,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ Note that the unmentioned lists (`public_key`, `cipher`, `mac` and
+@@ -209,8 +211,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ Note that both lists in `cipher` has been changed to the provided value
+@@ -246,8 +248,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ ### Example 4
+@@ -341,8 +343,8 @@
+                        'hmac-sha1']},
+        {server2client,['hmac-sha2-256','hmac-sha2-512',
+                        'hmac-sha1']}]},
+- {compression,[{client2server,[none,'[email protected]',zlib]},
+-               {server2client,[none,'[email protected]',zlib]}]}]
++ {compression,[{client2server,[none,'[email protected]']},
++               {server2client,[none,'[email protected]']}]}]
+ ```
+ 
+ And the result shows that the Diffie-Hellman Group1 is added at the head of 
the
+--- a/lib/ssh/doc/guides/hardening.md
++++ b/lib/ssh/doc/guides/hardening.md
+@@ -93,6 +93,26 @@
+ 
+ ![SSH server timeouts](assets/ssh_timeouts.jpg "SSH server timeouts")
+ 
++### Resilience to compression-based attacks
++
++SSH supports compression of the data stream.
++
++Reasonable finite 
[max_sessions](`m:ssh#hardening_daemon_options-max_sessions`)
++option is highly recommended if compression is used to prevent excessive 
resource
++usage by the compression library.
++See [Counters and parallelism](#counters-and-parallelism).
++
++The `'[email protected]'` algorithm is recommended because it only activates
++after successful authentication.
++
++The `'zlib'` algorithm is not recommended because it activates before
++authentication completes, allowing unauthenticated clients to expose potential
++vulnerabilities in compression libraries, and increases attack surface of
++compression-based side-channel and traffic-analysis attacks.
++
++In both algorithms decompression is protected by a size limit that prevents
++excessive memory consumption.
++
+ ## Verifying the remote daemon (server) in an SSH client
+ 
+ Every SSH server presents a public key - the _host key_ \- to the client while
+--- a/lib/ssh/doc/ssh_app.md
++++ b/lib/ssh/doc/ssh_app.md
+@@ -231,7 +231,14 @@
+ **Compression algorithms**
+   - none
+   - [email protected]
+-  - zlib
++
++The following compression algorithm is disabled by default:
++
++- (zlib)
++
++It can be enabled with the
++[preferred_algorithms](`t:ssh:preferred_algorithms_common_option/0`) or
++[modify_algorithms](`t:ssh:modify_algorithms_common_option/0`) options.
+ 
+ ## Unicode support
+ 
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1228,6 +1228,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
+@@ -193,6 +193,9 @@
+                                       'ssh-dss'
+                                      ]);
+ 
++default_algorithms1(compression) ->
++    supported_algorithms(compression, same(['zlib']));
++
+ default_algorithms1(Alg) ->
+     supported_algorithms(Alg, []).
+ 
+@@ -1449,8 +1452,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.
+@@ -1966,15 +1973,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
+@@ -51,6 +51,10 @@
+          client_handles_keyboard_interactive_0_pwds/1,
+          client_handles_banner_keyboard_interactive/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,
+@@ -138,7 +142,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,
+@@ -231,6 +239,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).
+ 
+@@ -246,6 +256,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).
+ 
+@@ -683,6 +695,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).
+@@ -1528,12 +1672,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)},
+@@ -1543,14 +1689,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}].
++
+ channel_close_timeout(Config) ->
+     {User,_Pwd} = server_user_password(Config),
+     %% Create a listening socket as server socket:
+--- a/lib/ssh/test/ssh_trpt_test_lib.erl
++++ b/lib/ssh/test/ssh_trpt_test_lib.erl
+@@ -446,7 +446,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),
+@@ -512,6 +518,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-27.3.4.1+dfsg/debian/patches/series 
erlang-27.3.4.1+dfsg/debian/patches/series
--- erlang-27.3.4.1+dfsg/debian/patches/series  2025-07-08 10:27:28.000000000 
+0300
+++ erlang-27.3.4.1+dfsg/debian/patches/series  2026-03-30 13:26:03.000000000 
+0300
@@ -9,3 +9,7 @@
 CVE-2025-48039.patch
 CVE-2025-48040.patch
 CVE-2025-48041.patch
+CVE-2026-21620.patch
+CVE-2026-23941.patch
+CVE-2026-23942.patch
+CVE-2026-23943.patch

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

This update has been released as part of Debian 13.5.

--- End Message ---

Reply via email to