[Pkg-erlang-devel] Bug#1127607: bookworm-pu: package erlang/1:25.2.3+dfsg-1+deb12u3
Jochen Sprickerhof
jspricke at debian.org
Tue Feb 10 10:45:23 GMT 2026
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: erlang at packages.debian.org, Sergei Golovan <sgolovan at debian.org>, Bastien Roucaries <rouca at debian.org>
Control: affects -1 + src:erlang
User: release.debian.org at packages.debian.org
Usertags: pu
[ Reason ]
There have been several CVEs published for the erlang programming
language that have been flagged as no DSA affecting the ssh server
implementation.
[ Impact ]
Mostly denial of service attacks.
[ Tests ]
Manually tested.
[ Risks ]
Low risk, given that those implementations are niche and the patches
mostly add safe guards.
[ Checklist ]
[X] *all* changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in (old)stable
[X] the issue is verified as fixed in unstable
[ Other info ]
@Sergei as with #1127606 (trixie) please write if you disagree.
-------------- next part --------------
diff --git a/debian/changelog b/debian/changelog
index 6738cb3c3a..b6b3272c74 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,20 @@
+erlang (1:25.2.3+dfsg-1+deb12u4) bookworm; urgency=medium
+
+ * Non-maintainer upload.
+ * Fix CVE-2025-48038: allocation of resources without limits or throttling
+ vulnerability in the ssh_sftp module allows excessive allocation,
+ resource leak exposure (closes: #1115093).
+ * Fix CVE-2025-48039: allocation of resources without limits or throttling
+ vulnerability in the ssh_sftp module allows excessive allocation,
+ resource leak exposure (closes: #1115092).
+ * Fix CVE-2025-48040: uncontrolled resource consumption vulnerability in
+ the ssh_sftp module allows excessive allocation, flooding (closes: 1115091).
+ * Fix CVE-2025-48041: allocation of resources without limits or throttling
+ vulnerability in the ssh_sftp module allows excessive allocation,
+ flooding (closes: #1115090).
+
+ -- Jochen Sprickerhof <jspricke at debian.org> Thu, 15 Jan 2026 10:56:30 +0100
+
erlang (1:25.2.3+dfsg-1+deb12u3) bookworm-proposed-updates; urgency=medium
* Fix FTBFS with newer xsltproc.
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000000..cec628c744
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,2 @@
+[DEFAULT]
+pristine-tar = True
diff --git a/debian/patches/CVE-2025-48038.patch b/debian/patches/CVE-2025-48038.patch
new file mode 100644
index 0000000000..800160e857
--- /dev/null
+++ b/debian/patches/CVE-2025-48038.patch
@@ -0,0 +1,34 @@
+From: Jakub Witczak <kuba at erlang.org>
+Date: Wed, 27 Aug 2025 17:49:08 +0200
+Subject: ssh: verify file handle size limit for client data
+
+- reject handles exceeding 256 bytes (as specified for SFTP)
+
+Origin: https://github.com/erlang/otp/commit/f09e0201ff701993dc24a08f15e524daf72db42f
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48038
+---
+ lib/ssh/src/ssh_sftpd.erl | 11 +++++++++++
+ 1 file changed, 11 insertions(+)
+
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 6bcad0d..cd24c3e 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -222,6 +222,17 @@ handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) ->
+ handle_data(Type, ChannelId, Data, State#state{pending = <<>>})
+ end.
+
++%% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be longer than 256 bytes."
++handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF})
++ when (Request == ?SSH_FXP_CLOSE orelse
++ Request == ?SSH_FXP_FSETSTAT orelse
++ Request == ?SSH_FXP_FSTAT orelse
++ Request == ?SSH_FXP_READ orelse
++ Request == ?SSH_FXP_READDIR orelse
++ Request == ?SSH_FXP_WRITE),
++ HLen > 256 ->
++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"),
++ State;
+ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
+ XF = State#state.xf,
+ Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
diff --git a/debian/patches/CVE-2025-48039.patch b/debian/patches/CVE-2025-48039.patch
new file mode 100644
index 0000000000..a683c023c0
--- /dev/null
+++ b/debian/patches/CVE-2025-48039.patch
@@ -0,0 +1,239 @@
+From: Jakub Witczak <kuba at erlang.org>
+Date: Fri, 11 Jul 2025 13:59:41 +0200
+Subject: ssh: ssh_sftpd verify path size for client data
+
+- reject max_path exceeding the 4096 limit or according to other option value
+
+Origin: https://github.com/erlang/otp/commit/043ee3c943e2977c1acdd740ad13992fd60b6bf0
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48039
+---
+ lib/ssh/doc/src/ssh_sftpd.xml | 8 ++++
+ lib/ssh/src/ssh_sftpd.erl | 32 +++++++++++++-
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 90 ++++++++++++++++++++++++++--------------
+ 3 files changed, 97 insertions(+), 33 deletions(-)
+
+diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
+index 49a23f4..efabf3f 100644
+--- a/lib/ssh/doc/src/ssh_sftpd.xml
++++ b/lib/ssh/doc/src/ssh_sftpd.xml
+@@ -65,6 +65,14 @@
+ If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c>
+ request is limited to at most the given value.</p>
+ </item>
++ <tag><c>max_path</c></tag>
++ <item>
++ <p>The default value is <c>4096</c>. Positive integer
++ value represents the maximum path length which cannot be
++ exceeded in data provided by the SFTP client. (Note:
++ limitations might be also enforced by underlying operating
++ system)</p>
++ </item>
+ <tag><c>root</c></tag>
+ <item>
+ <p>Sets the SFTP root directory. Then the user cannot see any files
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index cd24c3e..5632848 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -52,6 +52,7 @@
+ file_handler, % atom() - callback module
+ file_state, % state for the file callback module
+ max_files, % integer >= 0 max no files sent during READDIR
++ max_path, % integer > 0 - max length of path
+ options, % from the subsystem declaration
+ handles % list of open handles
+ %% handle is either {<int>, directory, {Path, unread|eof}} or
+@@ -65,6 +66,7 @@
+ Options :: [ {cwd, string()} |
+ {file_handler, CbMod | {CbMod, FileState}} |
+ {max_files, integer()} |
++ {max_path, integer()} |
+ {root, string()} |
+ {sftpd_vsn, integer()}
+ ],
+@@ -115,8 +117,12 @@ init(Options) ->
+ {Root0, State0}
+ end,
+ MaxLength = proplists:get_value(max_files, Options, 0),
++ MaxPath = proplists:get_value(max_path, Options, 4096),
+ Vsn = proplists:get_value(sftpd_vsn, Options, 5),
+- {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
++ {ok, State#state{cwd = CWD,
++ root = Root,
++ max_files = MaxLength,
++ max_path = MaxPath,
+ options = Options,
+ handles = [], pending = <<>>,
+ xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
+@@ -233,6 +239,30 @@ handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF})
+ HLen > 256 ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"),
+ State;
++handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>,
++ State = #state{max_path = MaxPath, xf = XF})
++ when (Request == ?SSH_FXP_LSTAT orelse
++ Request == ?SSH_FXP_MKDIR orelse
++ Request == ?SSH_FXP_OPEN orelse
++ Request == ?SSH_FXP_OPENDIR orelse
++ Request == ?SSH_FXP_READLINK orelse
++ Request == ?SSH_FXP_REALPATH orelse
++ Request == ?SSH_FXP_REMOVE orelse
++ Request == ?SSH_FXP_RMDIR orelse
++ Request == ?SSH_FXP_SETSTAT orelse
++ Request == ?SSH_FXP_STAT),
++ PLen > MaxPath ->
++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
++ "No such path"),
++ State;
++handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>,
++ State = #state{max_path = MaxPath, xf = XF})
++ when (Request == ?SSH_FXP_RENAME orelse
++ Request == ?SSH_FXP_SYMLINK),
++ (PLen > MaxPath orelse PLen2 > MaxPath) ->
++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
++ "No such path"),
++ State;
+ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
+ XF = State#state.xf,
+ Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
+diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl
+index 42677b7..f04cde3 100644
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -43,6 +43,7 @@
+ open_file_dir_v6/1,
+ read_dir/1,
+ read_file/1,
++ max_path/1,
+ real_path/1,
+ relative_path/1,
+ relpath/1,
+@@ -71,9 +72,8 @@
+ -define(SSH_TIMEOUT, 10000).
+ -define(REG_ATTERS, <<0,0,0,0,1>>).
+ -define(UNIX_EPOCH, 62167219200).
+-
+--define(is_set(F, Bits),
+- ((F) band (Bits)) == (F)).
++-define(MAX_PATH, 200).
++-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
+
+ %%--------------------------------------------------------------------
+ %% Common Test interface functions -----------------------------------
+@@ -86,6 +86,7 @@ all() ->
+ [open_close_file,
+ open_close_dir,
+ read_file,
++ max_path,
+ read_dir,
+ write_file,
+ rename_file,
+@@ -180,7 +181,8 @@ init_per_testcase(TestCase, Config) ->
+ {sftpd_vsn, 6}])],
+ ssh:daemon(0, [{subsystems, SubSystems}|Options]);
+ _ ->
+- SubSystems = [ssh_sftpd:subsystem_spec([])],
++ SubSystems = [ssh_sftpd:subsystem_spec(
++ [{max_path, ?MAX_PATH}])],
+ ssh:daemon(0, [{subsystems, SubSystems}|Options])
+ end,
+
+@@ -333,6 +335,23 @@ read_file(Config) when is_list(Config) ->
+
+ {ok, Data} = file:read_file(FileName).
+
++%%--------------------------------------------------------------------
++max_path(Config) when is_list(Config) ->
++ PrivDir = proplists:get_value(priv_dir, Config),
++ FileName = filename:join(PrivDir, "test.txt"),
++ {Cm, Channel} = proplists:get_value(sftp, Config),
++ %% verify max_path limit
++ LongFileName =
++ filename:join(PrivDir,
++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
++ {ok, _} = file:copy(FileName, LongFileName),
++ ReqId1 = req_id(),
++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH),
++ _/binary>>, _} =
++ open_file(LongFileName, Cm, Channel, ReqId1,
++ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
++ ?SSH_FXF_OPEN_EXISTING).
++
+ %%--------------------------------------------------------------------
+ read_dir(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+@@ -388,35 +407,33 @@ rename_file(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ FileName = filename:join(PrivDir, "test.txt"),
+ NewFileName = filename:join(PrivDir, "test1.txt"),
+- ReqId = 0,
++ LongFileName =
++ filename:join(PrivDir,
++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
+ {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+- {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
+- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
+- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0),
+-
+- NewReqId = ReqId + 1,
+-
+- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId),
+- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
+- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6,
+- ?SSH_FXP_RENAME_OVERWRITE),
+-
+- NewReqId1 = NewReqId + 1,
+- file:copy(FileName, NewFileName),
+-
+- %% No overwrite
+- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1),
+- ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} =
+- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6,
+- ?SSH_FXP_RENAME_NATIVE),
+-
+- NewReqId2 = NewReqId1 + 1,
+-
+- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2),
+- ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} =
+- rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6,
+- ?SSH_FXP_RENAME_ATOMIC).
++ Version = 6,
++ [begin
++ case Action of
++ {Code, AFile, BFile, Flags} ->
++ ReqId = req_id(),
++ ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p",
++ [ReqId, Code, AFile, BFile, Flags]),
++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} =
++ rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags);
++ {file_copy, AFile, BFile} ->
++ {ok, _} = file:copy(AFile, BFile)
++ end
++ end ||
++ Action <-
++ [{?SSH_FX_OK, FileName, NewFileName, 0},
++ {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE},
++ {file_copy, FileName, NewFileName},
++ %% no overwrite
++ {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE},
++ {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC},
++ %% max_path
++ {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]],
++ ok.
+
+ %%--------------------------------------------------------------------
+ mk_rm_dir(Config) when is_list(Config) ->
+@@ -1078,3 +1095,12 @@ encode_file_type(Type) ->
+
+ not_default_permissions() ->
+ 8#600. %% User read-write-only
++
++req_id() ->
++ ReqId =
++ case get(req_id) of
++ undefined -> 0;
++ I -> I
++ end,
++ put(req_id, ReqId + 1),
++ ReqId.
diff --git a/debian/patches/CVE-2025-48040.patch b/debian/patches/CVE-2025-48040.patch
new file mode 100644
index 0000000000..d27a182c56
--- /dev/null
+++ b/debian/patches/CVE-2025-48040.patch
@@ -0,0 +1,477 @@
+From: Jakub Witczak <kuba at erlang.org>
+Date: Wed, 20 Aug 2025 10:30:55 +0200
+Subject: ssh: key exchange robustness improvements
+
+- reduce untrusted data processing for non-debug logs
+- trim badmatch exceptions to avoid processing potentially malicious data
+- terminate with kexinit_error when too many algorithms are received in KEX init message
+
+Origin: https://github.com/erlang/otp/commit/548f1295d86d0803da884db8685cc16d461d0d5a
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48040
+---
+ lib/ssh/src/ssh_connection.erl | 3 +-
+ lib/ssh/src/ssh_connection_handler.erl | 35 +++++++---
+ lib/ssh/src/ssh_lib.erl | 15 ++++-
+ lib/ssh/src/ssh_message.erl | 42 +++++++-----
+ lib/ssh/src/ssh_transport.erl | 120 +++++++++++++++++++--------------
+ lib/ssh/test/ssh_connection_SUITE.erl | 12 +++-
+ 6 files changed, 147 insertions(+), 80 deletions(-)
+
+diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
+index badf3a2..b21d249 100644
+--- a/lib/ssh/src/ssh_connection.erl
++++ b/lib/ssh/src/ssh_connection.erl
+@@ -481,10 +481,9 @@ handle_msg(Msg, Connection, server, Ssh = #ssh{authenticated = false}) ->
+ %% respond by disconnecting, preferably with a proper disconnect message
+ %% sent to ease troubleshooting.
+ MsgFun = fun(M) ->
+- MaxLogItemLen = ?GET_OPT(max_log_item_len, Ssh#ssh.opts),
+ io_lib:format("Connection terminated. Unexpected message for unauthenticated user."
+ " Message: ~w", [M],
+- [{chars_limit, MaxLogItemLen}])
++ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
+ end,
+ ?LOG_DEBUG(MsgFun, [Msg]),
+ {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, handle_stop(Connection)};
+diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
+index ba46468..fa3b374 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1146,12 +1146,21 @@ handle_event(info, {Proto, Sock, NewData}, StateName,
+ {next_event, internal, Msg}
+ ]}
+ catch
+- C:E:ST ->
+- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
++ Class:Reason0:Stacktrace ->
++ Reason = ssh_lib:trim_reason(Reason0),
++ MsgFun =
++ fun(debug) ->
++ io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p",
++ [Class,Reason,Stacktrace],
++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
++ (_) ->
++ io_lib:format("Bad packet: Decrypted, but can't decode ~p:~p",
++ [Class, Reason],
++ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
++ end,
+ {Shutdown, D} =
+ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
+- io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p",
+- [C,E,ST], [{chars_limit, MaxLogItemLen}]),
++ ?SELECT_MSG(MsgFun),
+ StateName, D1),
+ {stop, Shutdown, D}
+ end;
+@@ -1181,12 +1190,20 @@ handle_event(info, {Proto, Sock, NewData}, StateName,
+ StateName, D0),
+ {stop, Shutdown, D}
+ catch
+- C:E:ST ->
+- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
++ Class:Reason0:Stacktrace ->
++ MsgFun =
++ fun(debug) ->
++ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",
++ [Class,Reason0,Stacktrace],
++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
++ (_) ->
++ Reason = ssh_lib:trim_reason(Reason0),
++ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p",
++ [Class,Reason],
++ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
++ end,
+ {Shutdown, D} =
+- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
+- io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",
+- [C,E,ST], [{chars_limit, MaxLogItemLen}]),
++ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun),
+ StateName, D0),
+ {stop, Shutdown, D}
+ end;
+diff --git a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl
+index 3d29b5e..c6791f1 100644
+--- a/lib/ssh/src/ssh_lib.erl
++++ b/lib/ssh/src/ssh_lib.erl
+@@ -28,7 +28,9 @@
+ format_address_port/2, format_address_port/1,
+ format_address/1,
+ format_time_ms/1,
+- comp/2
++ comp/2,
++ trim_reason/1,
++ max_log_len/1
+ ]).
+
+ -include("ssh.hrl").
+@@ -86,3 +88,14 @@ comp([], [], Truth) ->
+
+ comp(_, _, _) ->
+ false.
++%% We don't want to process badmatch details, potentially containing
++%% malicious data of unknown size
++trim_reason({badmatch, V}) when is_binary(V) ->
++ badmatch;
++trim_reason(E) ->
++ E.
++
++max_log_len(#ssh{opts = Opts}) ->
++ ?GET_OPT(max_log_item_len, Opts);
++max_log_len(Opts) when is_map(Opts) ->
++ ?GET_OPT(max_log_item_len, Opts).
+diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
+index e22a4e2..4d5ac74 100644
+--- a/lib/ssh/src/ssh_message.erl
++++ b/lib/ssh/src/ssh_message.erl
+@@ -43,7 +43,7 @@
+
+ -behaviour(ssh_dbg).
+ -export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, ssh_dbg_on/1, ssh_dbg_off/1, ssh_dbg_format/2]).
+--define(ALG_NAME_LIMIT, 64).
++-define(ALG_NAME_LIMIT, 64). % RFC4251 sec6
+
+ ucl(B) ->
+ try unicode:characters_to_list(B) of
+@@ -821,23 +821,33 @@ decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) ->
+ %% See rfc 4253 7.1
+ X = 0,
+ list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc]));
+-decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) ->
++decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) when
++ byte_size(Data) < ?MAX_NUM_ALGORITHMS * ?ALG_NAME_LIMIT ->
+ BinParts = binary:split(Data, <<$,>>, [global]),
+- Process =
+- fun(<<>>, PAcc) ->
+- PAcc;
+- (Part, PAcc) ->
+- case byte_size(Part) > ?ALG_NAME_LIMIT of
+- true ->
+- ?LOG_DEBUG("Ignoring too long name", []),
++ AlgCount = length(BinParts),
++ case AlgCount =< ?MAX_NUM_ALGORITHMS of
++ true ->
++ Process =
++ fun(<<>>, PAcc) ->
+ PAcc;
+- false ->
+- Name = binary:bin_to_list(Part),
+- [Name | PAcc]
+- end
+- end,
+- Names = lists:foldr(Process, [], BinParts),
+- decode_kex_init(Rest, [Names | Acc], N - 1).
++ (Part, PAcc) ->
++ case byte_size(Part) =< ?ALG_NAME_LIMIT of
++ true ->
++ Name = binary:bin_to_list(Part),
++ [Name | PAcc];
++ false ->
++ ?LOG_DEBUG("Ignoring too long name", []),
++ PAcc
++ end
++ end,
++ Names = lists:foldr(Process, [], BinParts),
++ decode_kex_init(Rest, [Names | Acc], N - 1);
++ false ->
++ throw({error, {kexinit_error, N, {alg_count, AlgCount}}})
++ end;
++decode_kex_init(<<?DEC_BIN(Data,__0), _Rest/binary>>, _Acc, N) ->
++ throw({error, {kexinit, N, {string_size, byte_size(Data)}}}).
++
+
+
+ %%%================================================================
+diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl
+index 6081a02..5abc11b 100644
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -405,8 +405,9 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own,
+ key_exchange_first_msg(Algos#alg.kex,
+ Ssh#ssh{algorithms = Algos})
+ catch
+- Class:Error ->
+- Msg = kexinit_error(Class, Error, client, Own, CounterPart),
++ Class:Reason0 ->
++ Reason = ssh_lib:trim_reason(Reason0),
++ Msg = kexinit_error(Class, Reason, client, Own, CounterPart, Ssh),
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
+ end;
+
+@@ -422,31 +423,38 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own,
+ Algos ->
+ {ok, Ssh#ssh{algorithms = Algos}}
+ catch
+- Class:Error ->
+- Msg = kexinit_error(Class, Error, server, Own, CounterPart),
++ Class:Reason0 ->
++ Reason = ssh_lib:trim_reason(Reason0),
++ Msg = kexinit_error(Class, Reason, server, Own, CounterPart, Ssh),
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
+ end.
+
+-kexinit_error(Class, Error, Role, Own, CounterPart) ->
++kexinit_error(Class, Error, Role, Own, CounterPart, Ssh) ->
+ {Fmt,Args} =
+ case {Class,Error} of
+ {error, {badmatch,{false,Alg}}} ->
+ {Txt,W,C} = alg_info(Role, Alg),
+- {"No common ~s algorithm,~n"
+- " we have:~n ~s~n"
+- " peer have:~n ~s~n",
+- [Txt,
+- lists:join(", ", element(W,Own)),
+- lists:join(", ", element(C,CounterPart))
+- ]};
++ MsgFun =
++ fun(debug) ->
++ {"No common ~s algorithm,~n"
++ " we have:~n ~s~n"
++ " peer have:~n ~s~n",
++ [Txt,
++ lists:join(", ", element(W,Own)),
++ lists:join(", ", element(C,CounterPart))]};
++ (_) ->
++ {"No common ~s algorithm", [Txt]}
++ end,
++ ?SELECT_MSG(MsgFun);
+ _ ->
+ {"Kexinit failed in ~p: ~p:~p", [Role,Class,Error]}
+ end,
+- try io_lib:format(Fmt, Args) of
++ try io_lib:format(Fmt, Args, [{chars_limit, ssh_lib:max_log_len(Ssh)}]) of
+ R -> R
+ catch
+ _:_ ->
+- io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error])
++ io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error],
++ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
+ end.
+
+ alg_info(client, Alg) ->
+@@ -598,14 +606,19 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E},
+ session_id = sid(Ssh1, H)}};
+ {error,unsupported_sign_alg} ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Unsupported algorithm ~p", [SignAlg])
+- )
++ io_lib:format("Unsupported algorithm ~p", [SignAlg],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
+ end;
+ true ->
+- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ MsgFun =
++ fun(debug) ->
+ io_lib:format("Kexdh init failed, received 'e' out of bounds~n E=~p~n P=~p",
+- [E,P])
+- )
++ [E,P], [{chars_limit, ssh_lib:max_log_len(Opts)}]);
++ (_) ->
++ io_lib:format("Kexdh init failed, received 'e' out of bounds", [],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}] )
++ end,
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
+ end.
+
+ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey,
+@@ -626,14 +639,15 @@ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey,
+ session_id = sid(Ssh, H)})};
+ Error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Kexdh init failed. Verify host key: ~p",[Error])
++ io_lib:format("Kexdh init failed. Verify host key: ~p",[Error],
++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
+ )
+ end;
+
+ true ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("Kexdh init failed, received 'f' out of bounds~n F=~p~n P=~p",
+- [F,P])
++ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
+ )
+ end.
+
+@@ -659,7 +673,8 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request{min = Min0,
+ }};
+ {error,_} ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
++ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}])
+ )
+ end;
+
+@@ -691,8 +706,8 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits},
+ }};
+ {error,_} ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
+- )
++ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
+ end;
+
+ handle_kex_dh_gex_request(_, _) ->
+@@ -718,7 +733,6 @@ handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) ->
+ {Public, Private} = generate_key(dh, [P,G,2*Sz]),
+ {SshPacket, Ssh1} =
+ ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), % Pub = G^Priv mod P (def)
+-
+ {ok, SshPacket,
+ Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}.
+
+@@ -749,19 +763,22 @@ handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E},
+ }};
+ {error,unsupported_sign_alg} ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Unsupported algorithm ~p", [SignAlg])
+- )
++ io_lib:format("Unsupported algorithm ~p", [SignAlg],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
+ end;
+ true ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- "Kexdh init failed, received 'k' out of bounds"
+- )
++ "Kexdh init failed, received 'k' out of bounds")
+ end;
+ true ->
+- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n E=~p~n P=~p",
+- [E,P])
+- )
++ MsgFun =
++ fun(debug) ->
++ io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n"
++ " E=~p~n P=~p", [E,P]);
++ (_) ->
++ io_lib:format("Kexdh gex init failed, received 'e' out of bounds", [])
++ end,
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
+ end.
+
+ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostKey,
+@@ -786,20 +803,18 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK
+ session_id = sid(Ssh, H)})};
+ Error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Kexdh gex reply failed. Verify host key: ~p",[Error])
+- )
++ io_lib:format("Kexdh gex reply failed. Verify host key: ~p",
++ [Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
+ end;
+
+ true ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- "Kexdh gex init failed, 'K' out of bounds"
+- )
++ "Kexdh gex init failed, 'K' out of bounds")
+ end;
+ true ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("Kexdh gex init failed, received 'f' out of bounds~n F=~p~n P=~p",
+- [F,P])
+- )
++ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
+ end.
+
+ %%%----------------------------------------------------------------
+@@ -833,17 +848,25 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic},
+ session_id = sid(Ssh1, H)}};
+ {error,unsupported_sign_alg} ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("Unsupported algorithm ~p", [SignAlg])
+- )
++ io_lib:format("Unsupported algorithm ~p", [SignAlg],
++ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
+ end
+ catch
+- Class:Error ->
+- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ Class:Reason0 ->
++ Reason = ssh_lib:trim_reason(Reason0),
++ MsgFun =
++ fun(debug) ->
+ io_lib:format("ECDH compute key failed in server: ~p:~p~n"
+ "Kex: ~p, Curve: ~p~n"
+ "PeerPublic: ~p",
+- [Class,Error,Kex,Curve,PeerPublic])
+- )
++ [Class,Reason,Kex,Curve,PeerPublic],
++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]);
++ (_) ->
++ io_lib:format("ECDH compute key failed in server: ~p:~p",
++ [Class,Reason],
++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
++ end,
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
+ end.
+
+ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey,
+@@ -866,15 +889,14 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey,
+ session_id = sid(Ssh, H)})};
+ Error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+- io_lib:format("ECDH reply failed. Verify host key: ~p",[Error])
+- )
++ io_lib:format("ECDH reply failed. Verify host key: ~p",[Error],
++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
+ end
+ catch
+ Class:Error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("Peer ECDH public key seem invalid: ~p:~p",
+- [Class,Error])
+- )
++ [Class,Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
+ end.
+
+
+diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl
+index 06d90cc..d529cf5 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -1345,6 +1345,8 @@ gracefull_invalid_long_start_no_nl(Config) when is_list(Config) ->
+ end.
+
+ kex_error(Config) ->
++ #{level := Level} = logger:get_primary_config(),
++ ok = logger:set_primary_config(level, debug),
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+@@ -1365,6 +1367,10 @@ kex_error(Config) ->
+ ok % Other msg
+ end,
+ self()),
++ Cleanup = fun() ->
++ ok = logger:remove_handler(kex_error),
++ ok = logger:set_primary_config(level, Level)
++ end,
+ try
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+@@ -1382,7 +1388,7 @@ kex_error(Config) ->
+ %% ok
+ receive
+ {Ref, ErrMsgTxt} ->
+- ok = logger:remove_handler(kex_error),
++ Cleanup(),
+ ct:log("ErrMsgTxt = ~n~s", [ErrMsgTxt]),
+ Lines = lists:map(fun string:trim/1, string:tokens(ErrMsgTxt, "\n")),
+ OK = (lists:all(fun(S) -> lists:member(S,Lines) end,
+@@ -1400,12 +1406,12 @@ kex_error(Config) ->
+ ct:fail("unexpected error text msg", [])
+ end
+ after 20000 ->
+- ok = logger:remove_handler(kex_error),
++ Cleanup(),
+ ct:fail("timeout", [])
+ end;
+
+ error:{badmatch,{error,_}} ->
+- ok = logger:remove_handler(kex_error),
++ Cleanup(),
+ ct:fail("unexpected error msg", [])
+ end.
+
diff --git a/debian/patches/CVE-2025-48041.patch b/debian/patches/CVE-2025-48041.patch
new file mode 100644
index 0000000000..45ee9b7d0f
--- /dev/null
+++ b/debian/patches/CVE-2025-48041.patch
@@ -0,0 +1,268 @@
+From: Jakub Witczak <kuba at erlang.org>
+Date: Wed, 20 Aug 2025 10:31:50 +0200
+Subject: ssh: max_handles option added to ssh_sftpd
+
+- add max_handles option and update tests (1000 by default)
+- remove sshd_read_file redundant testcase
+
+Origin: https://github.com/erlang/otp/commit/d49efa2d4fa9e6f7ee658719cd76ffe7a33c2401
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48041
+---
+ lib/ssh/doc/src/ssh_sftpd.xml | 4 ++
+ lib/ssh/src/ssh_sftpd.erl | 30 +++++++++++----
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 83 ++++++++++++++++++----------------------
+ 3 files changed, 64 insertions(+), 53 deletions(-)
+
+diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
+index efabf3f..7c250a9 100644
+--- a/lib/ssh/doc/src/ssh_sftpd.xml
++++ b/lib/ssh/doc/src/ssh_sftpd.xml
+@@ -65,6 +65,10 @@
+ If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c>
+ request is limited to at most the given value.</p>
+ </item>
++ <tag><c>max_handles</c></tag>
++ <item>
++ <p>The default value is <c>1000</c>. Positive integer value represents the maximum number of file handles allowed for a connection.</p>
++ </item>
+ <tag><c>max_path</c></tag>
+ <item>
+ <p>The default value is <c>4096</c>. Positive integer
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 5632848..dfa566a 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -52,6 +52,7 @@
+ file_handler, % atom() - callback module
+ file_state, % state for the file callback module
+ max_files, % integer >= 0 max no files sent during READDIR
++ max_handles, % integer > 0 - max number of file handles
+ max_path, % integer > 0 - max length of path
+ options, % from the subsystem declaration
+ handles % list of open handles
+@@ -66,6 +67,7 @@
+ Options :: [ {cwd, string()} |
+ {file_handler, CbMod | {CbMod, FileState}} |
+ {max_files, integer()} |
++ {max_handles, integer()} |
+ {max_path, integer()} |
+ {root, string()} |
+ {sftpd_vsn, integer()}
+@@ -117,11 +119,13 @@ init(Options) ->
+ {Root0, State0}
+ end,
+ MaxLength = proplists:get_value(max_files, Options, 0),
++ MaxHandles = proplists:get_value(max_handles, Options, 1000),
+ MaxPath = proplists:get_value(max_path, Options, 4096),
+ Vsn = proplists:get_value(sftpd_vsn, Options, 5),
+ {ok, State#state{cwd = CWD,
+ root = Root,
+ max_files = MaxLength,
++ max_handles = MaxHandles,
+ max_path = MaxPath,
+ options = Options,
+ handles = [], pending = <<>>,
+@@ -286,14 +290,16 @@ handle_op(?SSH_FXP_REALPATH, ReqId,
+ end;
+ handle_op(?SSH_FXP_OPENDIR, ReqId,
+ <<?UINT32(RLen), RPath:RLen/binary>>,
+- State0 = #state{xf = #ssh_xfer{vsn = Vsn},
+- file_handler = FileMod, file_state = FS0}) ->
++ State0 = #state{xf = #ssh_xfer{vsn = Vsn},
++ file_handler = FileMod, file_state = FS0,
++ max_handles = MaxHandles}) ->
+ RelPath = unicode:characters_to_list(RPath),
+ AbsPath = relate_file_name(RelPath, State0),
+
+ XF = State0#state.xf,
+ {IsDir, FS1} = FileMod:is_dir(AbsPath, FS0),
+ State1 = State0#state{file_state = FS1},
++ HandlesCnt = length(State0#state.handles),
+ case IsDir of
+ false when Vsn > 5 ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY,
+@@ -303,8 +309,12 @@ handle_op(?SSH_FXP_OPENDIR, ReqId,
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
+ "Not a directory"),
+ State1;
+- true ->
+- add_handle(State1, XF, ReqId, directory, {RelPath,unread})
++ true when HandlesCnt < MaxHandles ->
++ add_handle(State1, XF, ReqId, directory, {RelPath,unread});
++ true ->
++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
++ "max_handles limit reached"),
++ State1
+ end;
+ handle_op(?SSH_FXP_READDIR, ReqId,
+ <<?UINT32(HLen), BinHandle:HLen/binary>>,
+@@ -755,7 +765,9 @@ open(Vsn, ReqId, Data, State) when Vsn >= 4 ->
+ do_open(ReqId, State, Path, Flags).
+
+ do_open(ReqId, State0, Path, Flags) ->
+- #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}} = State0,
++ #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn},
++ max_handles = MaxHandles} = State0,
++ HandlesCnt = length(State0#state.handles),
+ AbsPath = relate_file_name(Path, State0),
+ {IsDir, _FS1} = FileMod:is_dir(AbsPath, FS0),
+ case IsDir of
+@@ -767,7 +779,7 @@ do_open(ReqId, State0, Path, Flags) ->
+ ssh_xfer:xf_send_status(State0#state.xf, ReqId,
+ ?SSH_FX_FAILURE, "File is a directory"),
+ State0;
+- false ->
++ false when HandlesCnt < MaxHandles ->
+ OpenFlags = [binary | Flags],
+ {Res, FS1} = FileMod:open(AbsPath, OpenFlags, FS0),
+ State1 = State0#state{file_state = FS1},
+@@ -778,7 +790,11 @@ do_open(ReqId, State0, Path, Flags) ->
+ ssh_xfer:xf_send_status(State1#state.xf, ReqId,
+ ssh_xfer:encode_erlang_status(Error)),
+ State1
+- end
++ end;
++ false ->
++ ssh_xfer:xf_send_status(State0#state.xf, ReqId,
++ ?SSH_FX_FAILURE, "max_handles limit reached"),
++ State0
+ end.
+
+ %% resolve all symlinks in a path
+diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl
+index f04cde3..fade45b 100644
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -52,7 +52,6 @@
+ retrieve_attributes/1,
+ root_with_cwd/1,
+ set_attributes/1,
+- sshd_read_file/1,
+ ver3_open_flags/1,
+ ver3_rename/1,
+ ver6_basic/1,
+@@ -72,6 +71,7 @@
+ -define(SSH_TIMEOUT, 10000).
+ -define(REG_ATTERS, <<0,0,0,0,1>>).
+ -define(UNIX_EPOCH, 62167219200).
++-define(MAX_HANDLES, 10).
+ -define(MAX_PATH, 200).
+ -define(is_set(F, Bits), ((F) band (Bits)) == (F)).
+
+@@ -98,8 +98,7 @@ all() ->
+ links,
+ ver3_rename,
+ ver3_open_flags,
+- relpath,
+- sshd_read_file,
++ relpath,
+ ver6_basic,
+ access_outside_root,
+ root_with_cwd,
+@@ -182,7 +181,8 @@ init_per_testcase(TestCase, Config) ->
+ ssh:daemon(0, [{subsystems, SubSystems}|Options]);
+ _ ->
+ SubSystems = [ssh_sftpd:subsystem_spec(
+- [{max_path, ?MAX_PATH}])],
++ [{max_handles, ?MAX_HANDLES},
++ {max_path, ?MAX_PATH}])],
+ ssh:daemon(0, [{subsystems, SubSystems}|Options])
+ end,
+
+@@ -318,22 +318,25 @@ open_close_dir(Config) when is_list(Config) ->
+ read_file(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ FileName = filename:join(PrivDir, "test.txt"),
+-
+- ReqId = 0,
+- {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+- open_file(FileName, Cm, Channel, ReqId,
+- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+- ?SSH_FXF_OPEN_EXISTING),
+-
+- NewReqId = 1,
+-
+- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
+- Data/binary>>, _} =
+- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
+-
+- {ok, Data} = file:read_file(FileName).
++ {Cm, Channel} = proplists:get_value(sftp, Config),
++ [begin
++ R1 = req_id(),
++ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
++ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
++ ?SSH_FXF_OPEN_EXISTING),
++ R2 = req_id(),
++ {ok, <<?SSH_FXP_DATA, ?UINT32(R2), ?UINT32(_Length), Data/binary>>, _} =
++ read_file(Handle, 100, 0, Cm, Channel, R2),
++ {ok, Data} = file:read_file(FileName)
++ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
++ ReqId = req_id(),
++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
++ ?UINT32(MsgLen), Msg:MsgLen/binary,
++ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
++ open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
++ ?SSH_FXF_OPEN_EXISTING),
++ ct:log("Message: ~s", [Msg]),
++ ok.
+
+ %%--------------------------------------------------------------------
+ max_path(Config) when is_list(Config) ->
+@@ -356,12 +359,21 @@ max_path(Config) when is_list(Config) ->
+ read_dir(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ {Cm, Channel} = proplists:get_value(sftp, Config),
+- ReqId = 0,
+- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+- open_dir(PrivDir, Cm, Channel, ReqId),
+- ok = read_dir(Handle, Cm, Channel, ReqId).
++ [begin
++ R1 = req_id(),
++ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
++ open_dir(PrivDir, Cm, Channel, R1),
++ R2 = req_id(),
++ ok = read_dir(Handle, Cm, Channel, R2)
++ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
++ ReqId = req_id(),
++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
++ ?UINT32(MsgLen), Msg:MsgLen/binary,
++ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
++ open_dir(PrivDir, Cm, Channel, ReqId),
++ ct:log("Message: ~s", [Msg]),
++ ok.
+
+-%%--------------------------------------------------------------------
+ write_file(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ FileName = filename:join(PrivDir, "test.txt"),
+@@ -661,27 +673,6 @@ relpath(Config) when is_list(Config) ->
+ Root = Path
+ end.
+
+-%%--------------------------------------------------------------------
+-sshd_read_file(Config) when is_list(Config) ->
+- PrivDir = proplists:get_value(priv_dir, Config),
+- FileName = filename:join(PrivDir, "test.txt"),
+-
+- ReqId = 0,
+- {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+- open_file(FileName, Cm, Channel, ReqId,
+- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+- ?SSH_FXF_OPEN_EXISTING),
+-
+- NewReqId = 1,
+-
+- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
+- Data/binary>>, _} =
+- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
+-
+- {ok, Data} = file:read_file(FileName).
+-%%--------------------------------------------------------------------
+ ver6_basic(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ %FileName = filename:join(PrivDir, "test.txt"),
diff --git a/debian/patches/series b/debian/patches/series
index 405ebf00f2..2fdd8154c8 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -16,3 +16,7 @@ ssh-early-RCE-fix.patch
ssh-strict-KEX-exchange-hardening.patch
zip-sanitize-paths.patch
xslt-for-each.patch
+CVE-2025-48038.patch
+CVE-2025-48039.patch
+CVE-2025-48040.patch
+CVE-2025-48041.patch
diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml
new file mode 100644
index 0000000000..f437b7ffa5
--- /dev/null
+++ b/debian/salsa-ci.yml
@@ -0,0 +1,6 @@
+---
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+variables:
+ RELEASE: 'bookworm'
More information about the Pkg-erlang-devel
mailing list