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

Reply via email to