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