diff --git a/SPECS/erlang/CVE-2025-48038.patch b/SPECS/erlang/CVE-2025-48038.patch
new file mode 100644
index 00000000000..686b75aeb9e
--- /dev/null
+++ b/SPECS/erlang/CVE-2025-48038.patch
@@ -0,0 +1,80 @@
+From 81eaa87eaf6b0064aebda2c142fde189b257ea36 Mon Sep 17 00:00:00 2001
+From: Jakub Witczak
+Date: Wed, 27 Aug 2025 17:49:08 +0200
+Subject: [PATCH 1/2] ssh: verify file handle size limit for client data
+
+- reject handles exceeding 256 bytes (as specified for SFTP)
+---
+ 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 f3d8053..5120884 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, <>, 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]),
+--
+2.45.4
+
+
+From 7380d99c3e69f0732276e4667d4260fbdbd4a5a3 Mon Sep 17 00:00:00 2001
+From: Jakub Witczak
+Date: Wed, 27 Aug 2025 17:49:53 +0200
+Subject: [PATCH 2/2] ssh: code formatting
+
+Signed-off-by: Azure Linux Security Servicing Account
+Upstream-reference: https://patch-diff.githubusercontent.com/raw/erlang/otp/pull/10156.patch
+---
+ lib/ssh/src/ssh_sftpd.erl | 8 +++-----
+ 1 file changed, 3 insertions(+), 5 deletions(-)
+
+diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
+index 5120884..fec6527 100644
+--- a/lib/ssh/src/ssh_sftpd.erl
++++ b/lib/ssh/src/ssh_sftpd.erl
+@@ -240,7 +240,7 @@ handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
+ ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <>),
+ State#state{xf = XF1};
+ handle_op(?SSH_FXP_REALPATH, ReqId,
+- <>,
++ <>,
+ State0) ->
+ RelPath = relate_file_name(RPath, State0, _Canonicalize=false),
+ {Res, State} = resolve_symlinks(RelPath, State0),
+@@ -409,14 +409,12 @@ handle_op(?SSH_FXP_RMDIR, ReqId, <>,
+ send_status(Status, ReqId, State1);
+
+ handle_op(?SSH_FXP_RENAME, ReqId,
+- Bin = <>,
++ Bin = <>,
+ State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 ->
+ handle_op(?SSH_FXP_RENAME, ReqId, <>, State);
+
+ handle_op(?SSH_FXP_RENAME, ReqId,
+- <>,
++ <>,
+ State0 = #state{file_handler = FileMod, file_state = FS0}) ->
+ Path = relate_file_name(BPath, State0),
+ Path2 = relate_file_name(BPath2, State0),
+--
+2.45.4
+
diff --git a/SPECS/erlang/CVE-2025-48040.patch b/SPECS/erlang/CVE-2025-48040.patch
new file mode 100644
index 00000000000..f14f46be3f3
--- /dev/null
+++ b/SPECS/erlang/CVE-2025-48040.patch
@@ -0,0 +1,481 @@
+From a4024eb3c83b3b5e119141e8f04c42b557db1623 Mon Sep 17 00:00:00 2001
+From: Jakub Witczak
+Date: Wed, 20 Aug 2025 10:30:55 +0200
+Subject: [PATCH] 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
+
+Signed-off-by: Azure Linux Security Servicing Account
+Upstream-reference: https://github.com/erlang/otp/commit/548f1295d86d0803da884db8685cc16d461d0d5a.patch
+---
+ 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 c82dd67..1a01626 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 15f98df..8c37e42 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -1169,12 +1169,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;
+@@ -1204,12 +1213,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 ec6193b..cc06779 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(<>, Acc, 0) ->
+ %% See rfc 4253 7.1
+ X = 0,
+ list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc]));
+-decode_kex_init(<>, Acc, N) ->
++decode_kex_init(<>, 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(<>, _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 7da6e1c..a03d07d 100644
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -403,8 +403,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;
+
+@@ -420,31 +421,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) ->
+@@ -596,14 +604,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,
+@@ -624,14 +637,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.
+
+@@ -657,7 +671,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;
+
+@@ -689,8 +704,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(_, _) ->
+@@ -716,7 +731,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}}}}.
+
+@@ -747,19 +761,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,
+@@ -784,20 +801,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.
+
+ %%%----------------------------------------------------------------
+@@ -831,17 +846,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,
+@@ -864,15 +887,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 8b94834..e067897 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -1439,6 +1439,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),
+@@ -1459,6 +1461,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"},
+@@ -1476,7 +1482,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,
+@@ -1494,12 +1500,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.
+
+--
+2.45.4
+
diff --git a/SPECS/erlang/CVE-2025-48041.patch b/SPECS/erlang/CVE-2025-48041.patch
new file mode 100644
index 00000000000..8e9d9a8d9c3
--- /dev/null
+++ b/SPECS/erlang/CVE-2025-48041.patch
@@ -0,0 +1,286 @@
+From d03cc5ad3def8de3d139a1036c35ec18b5b5d815 Mon Sep 17 00:00:00 2001
+From: Jakub Witczak
+Date: Wed, 20 Aug 2025 10:31:50 +0200
+Subject: [PATCH] ssh: max_handles option added to ssh_sftpd
+
+- add max_handles option and update tests (1000 by default)
+- remove sshd_read_file redundant testcase
+
+Signed-off-by: Azure Linux Security Servicing Account
+Upstream-reference: https://github.com/erlang/otp/commit/d49efa2d4fa9e6f7ee658719cd76ffe7a33c2401.patch
+---
+ lib/ssh/doc/src/ssh_sftpd.xml | 4 ++
+ lib/ssh/src/ssh_sftpd.erl | 34 ++++++++---
+ lib/ssh/test/ssh_sftpd_SUITE.erl | 96 +++++++++++++++-----------------
+ 3 files changed, 76 insertions(+), 58 deletions(-)
+
+diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml
+index 49a23f4..03e8dad 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 READDIR
+ request is limited to at most the given value.
+
++ max_handles
++ -
++
The default value is 1000. Positive integer value represents the maximum number of file handles allowed for a connection.
++
+ root
+ -
+
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 fec6527..0c64178 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
+ options, % from the subsystem declaration
+ handles % list of open handles
+ %% handle is either {, directory, {Path, unread|eof}} or
+@@ -65,6 +66,7 @@
+ Options :: [ {cwd, string()} |
+ {file_handler, CbMod | {CbMod, FileState}} |
+ {max_files, integer()} |
++ {max_handles, integer()} |
+ {root, string()} |
+ {sftpd_vsn, integer()}
+ ],
+@@ -115,8 +117,12 @@ init(Options) ->
+ {Root0, State0}
+ end,
+ MaxLength = proplists:get_value(max_files, Options, 0),
++ MaxHandles = proplists:get_value(max_handles, Options, 1000),
+ 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_handles = MaxHandles,
+ options = Options,
+ handles = [], pending = <<>>,
+ xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
+@@ -256,14 +262,16 @@ handle_op(?SSH_FXP_REALPATH, ReqId,
+ end;
+ handle_op(?SSH_FXP_OPENDIR, ReqId,
+ <>,
+- 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,
+@@ -273,8 +281,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,
+ <>,
+@@ -723,7 +735,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
+@@ -735,7 +749,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},
+@@ -746,7 +760,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 42677b7..9da2e41 100644
+--- a/lib/ssh/test/ssh_sftpd_SUITE.erl
++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl
+@@ -51,7 +51,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,
+@@ -71,9 +70,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_HANDLES, 10).
++-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
+
+ %%--------------------------------------------------------------------
+ %% Common Test interface functions -----------------------------------
+@@ -97,8 +95,7 @@ all() ->
+ links,
+ ver3_rename,
+ ver3_open_flags,
+- relpath,
+- sshd_read_file,
++ relpath,
+ ver6_basic,
+ access_outside_root,
+ root_with_cwd,
+@@ -180,7 +177,7 @@ 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_handles, ?MAX_HANDLES}])],
+ ssh:daemon(0, [{subsystems, SubSystems}|Options])
+ end,
+
+@@ -316,33 +313,44 @@ 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"),
++ {Cm, Channel} = proplists:get_value(sftp, Config),
++ [begin
++ R1 = req_id(),
++ {ok, <>, _} =
++ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
++ ?SSH_FXF_OPEN_EXISTING),
++ R2 = req_id(),
++ {ok, <>, _} =
++ 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, <>, _} =
++ open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
++ ?SSH_FXF_OPEN_EXISTING),
++ ct:log("Message: ~s", [Msg]),
++ ok.
+
+- ReqId = 0,
+- {Cm, Channel} = proplists:get_value(sftp, Config),
+-
+- {ok, <>, _} =
+- open_file(FileName, Cm, Channel, ReqId,
+- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+- ?SSH_FXF_OPEN_EXISTING),
+-
+- NewReqId = 1,
+-
+- {ok, <>, _} =
+- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
+-
+- {ok, Data} = file:read_file(FileName).
+-
+-%%--------------------------------------------------------------------
+ read_dir(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ {Cm, Channel} = proplists:get_value(sftp, Config),
+- ReqId = 0,
+- {ok, <>, _} =
+- open_dir(PrivDir, Cm, Channel, ReqId),
+- ok = read_dir(Handle, Cm, Channel, ReqId).
++ [begin
++ R1 = req_id(),
++ {ok, <>, _} =
++ 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, <>, _} =
++ 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"),
+@@ -644,27 +652,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, <>, _} =
+- open_file(FileName, Cm, Channel, ReqId,
+- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+- ?SSH_FXF_OPEN_EXISTING),
+-
+- NewReqId = 1,
+-
+- {ok, <>, _} =
+- 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"),
+@@ -1078,3 +1065,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.
+--
+2.45.4
+
diff --git a/SPECS/erlang/erlang.spec b/SPECS/erlang/erlang.spec
index fc1df85b8e7..ef156ae47d2 100644
--- a/SPECS/erlang/erlang.spec
+++ b/SPECS/erlang/erlang.spec
@@ -2,7 +2,7 @@
Summary: erlang
Name: erlang
Version: 25.3.2.21
-Release: 2%{?dist}
+Release: 3%{?dist}
License: Apache-2.0
Vendor: Microsoft Corporation
Distribution: Mariner
@@ -15,6 +15,9 @@ BuildRequires: unixODBC-devel
BuildRequires: unzip
Patch0: CVE-2025-4748.patch
+Patch1: CVE-2025-48038.patch
+Patch2: CVE-2025-48040.patch
+Patch3: CVE-2025-48041.patch
%description
erlang programming language
@@ -48,6 +51,9 @@ make
%{_libdir}/erlang/*
%changelog
+* Sat Sep 13 2025 Azure Linux Security Servicing Account - 25.3.2.21-3
+- Patch for CVE-2025-48041, CVE-2025-48040, CVE-2025-48038
+
* Thu Jun 19 2025 Kevin Lockwood - 25.3.2.21-2
- Patch CVE-2025-4748