public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Peter Lemenkov <lemenkov@gmail.com>
To: git-commits@fedoraproject.org
Subject: [rpms/erlang] f43: Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422
Date: Wed, 23 Sep 2026 20:29:16 GMT [thread overview]
Message-ID: <179019535684.1.3153612654807224891.rpms-erlang-a131da91b106@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/erlang
Branch : f43
Commit : a131da91b10624199ad49adee66c82cd27a06ede
Author : Peter Lemenkov <lemenkov@gmail.com>
Date : 2026-09-23T22:26:58+02:00
Stats : +1403/-1 in 4 file(s)
URL : https://src.fedoraproject.org/rpms/erlang/c/a131da91b10624199ad49adee66c82cd27a06ede?branch=f43
Log:
Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422
Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
---
diff --git a/erlang.spec b/erlang.spec
index 5481d2b..ccf6c1e 100644
--- a/erlang.spec
+++ b/erlang.spec
@@ -70,7 +70,7 @@
Name: erlang
Version: 26.2.5.21
-Release: 7%{?dist}
+Release: 8%{?dist}
Summary: General-purpose programming language and runtime environment
License: Apache-2.0
@@ -119,6 +119,9 @@ Patch17: otp-0017-ssl-Add-pre-TLS-1.3-client-cipher-suite-check.patch
Patch18: otp-0018-inets-Fix-rejection-of-invalid-chunk-sizes.patch
Patch19: otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch
Patch20: otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch
+Patch21: otp-0021-asn1-Mitigate-a-DoS-vector.patch
+Patch22: otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
+Patch23: otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
# end of autogenerated patch tag list
BuildRequires: gcc
@@ -1967,6 +1970,9 @@ ERL_TOP=${ERL_TOP} make TARGET=${TARGET} release_tests
%changelog
+* Wed Sep 23 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-8
+- Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422
+
* Mon Sep 7 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-7
- Backported fixes for CVE-2026-58227 and CVE-2026-59251
diff --git a/otp-0021-asn1-Mitigate-a-DoS-vector.patch b/otp-0021-asn1-Mitigate-a-DoS-vector.patch
new file mode 100644
index 0000000..d1b1985
--- /dev/null
+++ b/otp-0021-asn1-Mitigate-a-DoS-vector.patch
@@ -0,0 +1,217 @@
+From: =?UTF-8?q?John=20H=C3=B6gberg?= <john@erlang.org>
+Date: Tue, 28 Jul 2026 11:56:41 +0200
+Subject: [PATCH] asn1: Mitigate a DoS vector
+
+
+diff --git a/lib/asn1/src/asn1rtt_ber.erl b/lib/asn1/src/asn1rtt_ber.erl
+index f2458d25c5..cce748002e 100644
+--- a/lib/asn1/src/asn1rtt_ber.erl
++++ b/lib/asn1/src/asn1rtt_ber.erl
+@@ -536,21 +536,20 @@ decode_tag_and_length(<<Class:2, Form:1, 31:5, 1:1, TagPart1:7, 0:1, TagPartLast
+ << V:Length/binary, RestBuffer2/binary>> = RestBuffer,
+ {Form, (Class bsl 16) bor TagNo, V, RestBuffer2};
+ decode_tag_and_length(<<Class:2, Form:1, 31:5, Buffer/binary>>) ->
+- {TagNo, Buffer1} = decode_tag(Buffer, 0),
++ {TagNo, Buffer1} = decode_tag(Buffer),
+ {Length, RestBuffer} = decode_length(Buffer1),
+- << V:Length/binary, RestBuffer2/binary>> = RestBuffer,
++ <<V:Length/binary, RestBuffer2/binary>> = RestBuffer,
+ {Form, (Class bsl 16) bor TagNo, V, RestBuffer2}.
+
++decode_tag(Buffer) ->
++ decode_tag(Buffer, 0, 0).
+
+-
+-%% last partial tag
+-decode_tag(<<0:1,PartialTag:7, Buffer/binary>>, TagAck) ->
++decode_tag(<<0:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 ->
+ TagNo = (TagAck bsl 7) bor PartialTag,
+ {TagNo, Buffer};
+-%% more tags
+-decode_tag(<<_:1,PartialTag:7, Buffer/binary>>, TagAck) ->
++decode_tag(<<_:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 ->
+ TagAck1 = (TagAck bsl 7) bor PartialTag,
+- decode_tag(Buffer, TagAck1).
++ decode_tag(Buffer, N + 1, TagAck1).
+
+ %%=======================================================================
+ %%
+@@ -1172,7 +1171,7 @@ mk_object_val(Val, Ack, Len) ->
+
+ decode_object_identifier(Tlv, Tags) ->
+ Val = match_tags(Tlv, Tags),
+- [AddedObjVal|ObjVals] = dec_subidentifiers(Val,0,[]),
++ [AddedObjVal|ObjVals] = dec_subidentifiers(Val),
+ {Val1, Val2} = if
+ AddedObjVal < 40 ->
+ {0, AddedObjVal};
+@@ -1183,12 +1182,20 @@ decode_object_identifier(Tlv, Tags) ->
+ end,
+ list_to_tuple([Val1, Val2 | ObjVals]).
+
+-dec_subidentifiers(<<>>,_Av,Al) ->
+- lists:reverse(Al);
+-dec_subidentifiers(<<1:1,H:7,T/binary>>,Av,Al) ->
+- dec_subidentifiers(T,(Av bsl 7) + H,Al);
+-dec_subidentifiers(<<H,T/binary>>,Av,Al) ->
+- dec_subidentifiers(T,0,[((Av bsl 7) + H)|Al]).
++dec_subidentifiers(<<Octets/binary>>) ->
++ dec_subidentifiers_1(Octets, 0, 0).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++dec_subidentifiers_1(<<1:1, H:7, T/binary>>, N, Av0) when N < 16 ->
++ Av = (Av0 bsl 7) bor H,
++ dec_subidentifiers_1(T, N + 1, Av);
++dec_subidentifiers_1(<<H, T/binary>>, N, Av0) when N < 16 ->
++ Av = (Av0 bsl 7) bor H,
++ [Av | dec_subidentifiers_1(T, 0, 0)];
++dec_subidentifiers_1(<<>>, _N, _Av) ->
++ [].
+
+ %%============================================================================
+ %% RELATIVE-OID, ITU_T X.690 Chapter 8.20
+@@ -1215,7 +1222,7 @@ enc_relative_oid(Val) ->
+ %%============================================================================
+ decode_relative_oid(Tlv, Tags) ->
+ Val = match_tags(Tlv, Tags),
+- ObjVals = dec_subidentifiers(Val,0,[]),
++ ObjVals = dec_subidentifiers(Val),
+ list_to_tuple(ObjVals).
+
+ %%============================================================================
+@@ -1411,19 +1418,21 @@ dynamicsort_SETOF(ListOfEncVal) ->
+
+ %% multiple octet tag
+ dynsort_decode_tag(<<Class:2,_Form:1,31:5,Buffer/binary>>) ->
+- TagNum = dynsort_decode_tag(Buffer, 0),
++ TagNum = dynsort_decode_tag_1(Buffer, 0, 0),
+ {Class,TagNum};
+
+ %% single tag (< 31 tags)
+ dynsort_decode_tag(<<Class:2,_Form:1,TagNum:5,_/binary>>) ->
+ {Class,TagNum}.
+
+-dynsort_decode_tag(<<0:1,PartialTag:7,_/binary>>, TagAcc) ->
++%% Reject overlong tags to mitigate a DoS vector. This should be enough bits
++%% for all legitimate uses until someone can prove otherwise.
++dynsort_decode_tag_1(<<0:1,PartialTag:7,_/binary>>, N, TagAcc) when N < 16 ->
+ (TagAcc bsl 7) bor PartialTag;
+-dynsort_decode_tag(<<_:1,PartialTag:7,Buffer/binary>>, TagAcc0) ->
++dynsort_decode_tag_1(<<_:1,PartialTag:7,Buffer/binary>>, N, TagAcc0)
++ when N < 16 ->
+ TagAcc = (TagAcc0 bsl 7) bor PartialTag,
+- dynsort_decode_tag(Buffer, TagAcc).
+-
++ dynsort_decode_tag_1(Buffer, N + 1, TagAcc).
+
+ %%-------------------------------------------------------------------------
+ %% INTERNAL HELPER FUNCTIONS (not exported)
+diff --git a/lib/asn1/src/asn1rtt_jer.erl b/lib/asn1/src/asn1rtt_jer.erl
+index a6d6363eb6..665191b1ff 100644
+--- a/lib/asn1/src/asn1rtt_jer.erl
++++ b/lib/asn1/src/asn1rtt_jer.erl
+@@ -393,9 +393,16 @@ oid2json([],Acc) ->
+ list_to_binary(lists:reverse(Acc)).
+
+ json2oid(OidStr) when is_binary(OidStr) ->
+- OidList = binary:split(OidStr,[<<".">>],[global]),
+- OidNumList = [binary_to_integer(Num)||Num <- OidList],
+- list_to_tuple(OidNumList).
++ SubIds = binary:split(OidStr,[<<".">>],[global]),
++ list_to_tuple(json2oid_1(SubIds)).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++json2oid_1([SubId | SubIds]) when byte_size(SubId) < 32 ->
++ [binary_to_integer(SubId) | json2oid_1(SubIds)];
++json2oid_1([]) ->
++ [].
+
+ jer_bit_str2bitstr(Compact = {_Unused,_Binary}, _NamedBitList) ->
+ jer_compact2bitstr(Compact);
+diff --git a/lib/asn1/src/asn1rtt_per_common.erl b/lib/asn1/src/asn1rtt_per_common.erl
+index 5b5f47dfee..80022a90c9 100644
+--- a/lib/asn1/src/asn1rtt_per_common.erl
++++ b/lib/asn1/src/asn1rtt_per_common.erl
+@@ -90,7 +90,7 @@ decode_big_chars(Val, N) ->
+ decode_big_chars_1(decode_chars(Val, N)).
+
+ decode_oid(Octets) ->
+- [First|Rest] = dec_subidentifiers(Octets, 0, []),
++ [First|Rest] = dec_subidentifiers(Octets),
+ Idlist = if
+ First < 40 ->
+ [0,First|Rest];
+@@ -102,7 +102,7 @@ decode_oid(Octets) ->
+ list_to_tuple(Idlist).
+
+ decode_relative_oid(Octets) ->
+- list_to_tuple(dec_subidentifiers(Octets, 0, [])).
++ list_to_tuple(dec_subidentifiers(Octets)).
+
+ encode_chars(Val, NumBits) ->
+ << <<C:NumBits>> || C <- Val >>.
+@@ -372,12 +372,20 @@ decode_big_chars_1([H|T]) ->
+ [list_to_tuple(binary_to_list(<<H:32>>))|decode_big_chars_1(T)];
+ decode_big_chars_1([]) -> [].
+
+-dec_subidentifiers([H|T], Av, Al) when H >=16#80 ->
+- dec_subidentifiers(T, (Av bsl 7) bor (H band 16#7F), Al);
+-dec_subidentifiers([H|T], Av, Al) ->
+- dec_subidentifiers(T, 0, [(Av bsl 7) bor H|Al]);
+-dec_subidentifiers([], _Av, Al) ->
+- lists:reverse(Al).
++dec_subidentifiers(Octets) ->
++ dec_subidentifiers_1(Octets, 0, 0).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++dec_subidentifiers_1([H | T], N, Av0) when H >= 16#80, N < 16 ->
++ Av = (Av0 bsl 7) bor (H band 16#7F),
++ dec_subidentifiers_1(T, N + 1, Av);
++dec_subidentifiers_1([H | T], N, Av0) when N < 16 ->
++ Av = (Av0 bsl 7) bor H,
++ [Av | dec_subidentifiers_1(T, 0, 0)];
++dec_subidentifiers_1([], _N, _Av) ->
++ [].
+
+ enc_char(C0, Lb, Tab) ->
+ try element(C0-Lb, Tab) of
+diff --git a/lib/asn1/test/testPrim.erl b/lib/asn1/test/testPrim.erl
+index ce7a7f28fa..236d704135 100644
+--- a/lib/asn1/test/testPrim.erl
++++ b/lib/asn1/test/testPrim.erl
+@@ -163,7 +163,28 @@ obj_id(_) ->
+ %%==========================================================
+
+ [roundtrip('ObjId', V) ||
+- V <- [{0,22,3},{1,39,3},{2,100,3},{2,16303,3},{2,16304,3}]],
++ V <- [{0,22,3},
++ {1,39,3},
++ {2,100,3},
++ {2,16303,3},
++ {2,16304,3},
++ {2,1 bsl (14 * 7),3}]],
++
++ try
++ [roundtrip('ObjId', V) ||
++ V <- [{0,22,3},
++ {1,39,3},
++ {2,100,3},
++ {2,16303,3},
++ {2,16304,3},
++ {2,1 bsl (20 * 7),3}]]
++ of
++ [_|_]=Res -> ct:fail("Failed to reject overlong oid ~p", [Res])
++ catch
++ _:_ ->
++ ok
++ end,
++
+ ok.
+
+ rel_oid(_Rules) ->
diff --git a/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch b/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
new file mode 100644
index 0000000..293634d
--- /dev/null
+++ b/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
@@ -0,0 +1,765 @@
+From: =?UTF-8?q?Micha=C5=82=20W=C4=85sowski?= <michal@erlang.org>
+Date: Mon, 31 Aug 2026 19:01:07 +0200
+Subject: [PATCH] ssh: Limit size of channel cache entries
+
+(cherry picked from commit e2bbad0107bba41491a4d549686fd023db8740da)
+
+[Backport to OTP-26: dropped -doc attributes (docs not updated), kept
+ cache_find/2 in ssh_client_channel, and kept the simple
+ end_per_testcase/2 in ssh_connection_SUITE since the logger-event
+ verification framework does not exist on this branch.]
+
+Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
+Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
+
+diff --git a/lib/ssh/src/ssh_channel_sup.erl b/lib/ssh/src/ssh_channel_sup.erl
+index 44f8df753b..450b04cf03 100644
+--- a/lib/ssh/src/ssh_channel_sup.erl
++++ b/lib/ssh/src/ssh_channel_sup.erl
+@@ -25,9 +25,8 @@
+ -module(ssh_channel_sup).
+
+ -behaviour(supervisor).
+--include("ssh.hrl").
+
+--export([start_link/1, start_child/8]).
++-export([start_link/1, start_child/7]).
+
+ %% Supervisor callback
+ -export([init/1]).
+@@ -39,16 +38,11 @@ start_link(Args) ->
+ supervisor:start_link(?MODULE, [Args]).
+
+
+-start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec, _Opts) when is_pid(ConnRef) ->
++start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) ->
+ start_the_channel(ssh_client_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec);
+
+-start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts) when is_pid(ConnRef) ->
+- case max_num_channels_not_exceeded(ChannelSup, Opts) of
+- true ->
+- start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec);
+- false ->
+- {error, max_num_channels_exceeded}
+- end.
++start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) ->
++ start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec).
+
+
+ %%%=========================================================================
+@@ -64,14 +58,6 @@ init(_Args) ->
+ %%%=========================================================================
+ %%% Internal functions
+ %%%=========================================================================
+-max_num_channels_not_exceeded(ChannelSup, Opts) ->
+- MaxNumChannels = ?GET_OPT(max_channels, Opts),
+- NumChannels = length([x || {_,_,worker,[ssh_server_channel]} <-
+- supervisor:which_children(ChannelSup)]),
+- %% Note that NumChannels is BEFORE starting a new one
+- NumChannels < MaxNumChannels.
+-
+-
+ start_the_channel(ChanMod, ChannelSup, ConnRef, Callback, Id, Args, Exec) ->
+ ChildSpec =
+ #{id => make_ref(),
+diff --git a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl
+index 6b6623945f..b907553ce6 100644
+--- a/lib/ssh/src/ssh_client_channel.erl
++++ b/lib/ssh/src/ssh_client_channel.erl
+@@ -67,7 +67,7 @@
+ terminate/2, code_change/3]).
+
+ %% Internal application API
+--export([cache_create/0, cache_lookup/2, cache_update/2,
++-export([cache_create/0, cache_lookup/2, cache_insert/3, cache_update/2,
+ cache_delete/1, cache_delete/2, cache_foldl/3,
+ cache_info/2, cache_find/2,
+ get_print_info/1, get_print_info/2
+@@ -343,6 +343,20 @@ cache_lookup(Cache, Key) ->
+ undefined
+ end.
+
++cache_insert(Cache, #channel{local_id = Id} = Entry, infinity) when Id =/= undefined ->
++ true = ets:insert_new(Cache, Entry),
++ ok;
++cache_insert(Cache, #channel{local_id = Id} = Entry, Limit) when Id =/= undefined,
++ is_integer(Limit),
++ Limit > 0 ->
++ case cache_info(num_entries, Cache) of
++ Num when Num >= Limit ->
++ {error, max_num_channels_exceeded};
++ _Num ->
++ true = ets:insert_new(Cache, Entry),
++ ok
++ end.
++
+ cache_update(Cache, #channel{local_id = Id} = Entry) when Id =/= undefined ->
+ ets:insert(Cache, Entry).
+
+@@ -356,7 +370,7 @@ cache_foldl(Fun, Acc, Cache) ->
+ ets:foldl(Fun, Acc, Cache).
+
+ cache_info(num_entries, Cache) ->
+- proplists:get_value(size, ets:info(Cache)).
++ ets:info(Cache, size).
+
+ cache_find(ChannelPid, Cache) ->
+ case ets:match_object(Cache, #channel{user = ChannelPid}) of
+diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
+index cf2c434e08..fb0bc07f74 100644
+--- a/lib/ssh/src/ssh_connection.erl
++++ b/lib/ssh/src/ssh_connection.erl
+@@ -609,8 +609,9 @@ handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type,
+
+ if
+ MinAcceptedPackSz =< PacketSz ->
+- try setup_session(Connection0, RemoteId,
+- Type, WindowSz, PacketSz) of
++ Limit = ?GET_OPT(max_channels, SSHopts),
++ try setup_session(Connection0, RemoteId,
++ Type, WindowSz, PacketSz, Limit) of
+ Result ->
+ Result
+ catch _:_ ->
+@@ -640,7 +641,6 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+ channel_id_seed = ChId,
+ suggest_window_size = WinSz,
+ suggest_packet_size = PktSz,
+- options = Options,
+ connection_supervisor = ConnectionSup
+ } = C,
+ client, _SSH) ->
+@@ -650,20 +650,20 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+ case gen_tcp:connect(ConnectToHost, ConnectToPort, [{active,false}, binary]) of
+ {ok,Sock} ->
+ {ok,Pid} = ssh_connection_sup:start_channel(client, ConnectionSup, self(),
+- ssh_tcpip_forward_client, ChId,
+- [Sock], undefined, Options),
+- ssh_client_channel:cache_update(Cache,
+- #channel{type = "forwarded-tcpip",
+- sys = "none",
+- local_id = ChId,
+- remote_id = RemoteId,
+- user = Pid,
+- recv_window_size = WinSz,
+- recv_packet_size = PktSz,
+- send_window_size = WindowSize,
+- send_packet_size = PacketSize,
+- send_buf = queue:new()
+- }),
++ ssh_tcpip_forward_client, ChId,
++ [Sock], undefined),
++ Channel = #channel{type = "forwarded-tcpip",
++ sys = "none",
++ local_id = ChId,
++ remote_id = RemoteId,
++ user = Pid,
++ recv_window_size = WinSz,
++ recv_packet_size = PktSz,
++ send_window_size = WindowSize,
++ send_packet_size = PacketSize,
++ send_buf = queue:new()
++ },
++ ok = ssh_client_channel:cache_insert(Cache, Channel, infinity),
+ gen_tcp:controlling_process(Sock, Pid),
+ inet:setopts(Sock, [{active,once}]),
+ {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
+@@ -687,26 +687,26 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+ {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}};
+
+ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip",
+- sender_channel = RemoteId,
++ sender_channel = RemoteId,
+ initial_window_size = WindowSize,
+ maximum_packet_size = PacketSize,
+ data = <<?DEC_BIN(HostToConnect,_L1), ?UINT32(PortToConnect),
+ ?DEC_BIN(_OriginatorIPaddress,_L2), ?UINT32(_OrignatorPort)
+ >>
+- },
+- #connection{channel_cache = Cache,
++ } = Msg,
++ #connection{channel_cache = Cache,
+ channel_id_seed = ChId,
+ suggest_window_size = WinSz,
+ suggest_packet_size = PktSz,
+ options = Options,
+ connection_supervisor = ConnectionSup
+ } = C,
+- server, _SSH) ->
+- {ReplyMsg, NextChId} =
++ server, SSH) ->
++ Result =
+ case ?GET_OPT(tcpip_tunnel_in, Options) of
+ %% May add more to the option, like allowed ip/port pairs to connect to
+ false ->
+- {channel_open_failure_msg(RemoteId,
++ {channel_open_failure_msg(RemoteId,
+ ?SSH_OPEN_CONNECT_FAILED,
+ "Forwarding disabled", "en"),
+ ChId};
+@@ -715,36 +715,54 @@ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip",
+ case gen_tcp:connect(binary_to_list(HostToConnect), PortToConnect,
+ [{active,false}, binary]) of
+ {ok,Sock} ->
+- {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(),
+- ssh_tcpip_forward_srv, ChId,
+- [Sock], undefined, Options),
+- ssh_client_channel:cache_update(Cache,
+- #channel{type = "direct-tcpip",
+- sys = "none",
+- local_id = ChId,
+- remote_id = RemoteId,
+- user = Pid,
+- recv_window_size = WinSz,
+- recv_packet_size = PktSz,
+- send_window_size = WindowSize,
+- send_packet_size = PacketSize,
+- send_buf = queue:new()
+- }),
+- gen_tcp:controlling_process(Sock, Pid),
+- inet:setopts(Sock, [{active,once}]),
+-
+- {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
+- ChId + 1};
++ Limit = ?GET_OPT(max_channels, Options),
++ Channel = #channel{type = "direct-tcpip",
++ sys = "none",
++ local_id = ChId,
++ remote_id = RemoteId,
++ recv_window_size = WinSz,
++ recv_packet_size = PktSz,
++ send_window_size = WindowSize,
++ send_packet_size = PacketSize,
++ send_buf = queue:new()
++ },
++ case ssh_client_channel:cache_insert(Cache, Channel, Limit) of
++ ok ->
++ {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++ ssh_tcpip_forward_srv, ChId,
++ [Sock], undefined),
++ ssh_client_channel:cache_update(Cache, Channel#channel{user = Pid}),
++ gen_tcp:controlling_process(Sock, Pid),
++ inet:setopts(Sock, [{active,once}]),
++
++ {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
++ ChId + 1};
++ {error, max_num_channels_exceeded} ->
++ gen_tcp:close(Sock),
++ MsgFun = fun(M, L) ->
++ io_lib:format("Connection terminated. Message: ~w"
++ " reached a limit of: ~p", [M, L],
++ [{chars_limit, ssh_lib:max_log_len(SSH)}])
++ end,
++ ?LOG_DEBUG(MsgFun, [Msg, Limit]),
++ {send_disconnect, {?SSH_DISCONNECT_BY_APPLICATION, "Connection terminated. "
++ "Channel limit reached."}}
++ end;
+
+ {error,Error} ->
+- {channel_open_failure_msg(RemoteId,
++ {channel_open_failure_msg(RemoteId,
+ ?SSH_OPEN_CONNECT_FAILED,
+ io_lib:format("Forwarded connection refused: ~p",[Error]),
+ "en"),
+ ChId}
+ end
+ end,
+- {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}};
++ case Result of
++ {send_disconnect, Reason} ->
++ {send_disconnect, Reason, handle_stop(C)};
++ {ReplyMsg, NextChId} ->
++ {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}}
++ end;
+
+ handle_msg(#ssh_msg_channel_open{channel_type = "session",
+ sender_channel = RemoteId},
+@@ -1136,15 +1154,15 @@ encode_ip(Addr) when is_list(Addr) ->
+
+ %%%----------------------------------------------------------------
+ %%% Create the channel data when an ssh_msg_open_channel message
+-%%% of "session" typ is handled
++%%% of "session" type is handled
+ %%%
+ setup_session(#connection{channel_cache = Cache,
+ channel_id_seed = NewChannelID,
+ suggest_window_size = WinSz,
+ suggest_packet_size = PktSz
+- } = C,
+- RemoteId, Type, WindowSize, PacketSize) when is_integer(WinSz),
+- is_integer(PktSz) ->
++ } = C,
++ RemoteId, Type, WindowSize, PacketSize, ChannelLimit) when is_integer(WinSz),
++ is_integer(PktSz) ->
+ NextChannelID = NewChannelID + 1,
+ Channel =
+ #channel{type = Type,
+@@ -1157,7 +1175,7 @@ setup_session(#connection{channel_cache = Cache,
+ send_buf = queue:new(),
+ remote_id = RemoteId
+ },
+- ssh_client_channel:cache_update(Cache, Channel),
++ ok = ssh_client_channel:cache_insert(Cache, Channel, ChannelLimit),
+ OpenConfMsg = channel_open_confirmation_msg(RemoteId, NewChannelID,
+ WinSz,
+ PktSz),
+@@ -1168,15 +1186,15 @@ setup_session(#connection{channel_cache = Cache,
+ %%%----------------------------------------------------------------
+ %%% Start a cli or subsystem
+ %%%
+-start_cli(#connection{options = Options,
+- cli_spec = CliSpec,
++start_cli(#connection{cli_spec = CliSpec,
+ exec = Exec,
+ connection_supervisor = ConnectionSup}, ChannelId) ->
+ case CliSpec of
+ no_cli ->
+ {error, cli_disabled};
+ {CbModule, Args} ->
+- ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options)
++ ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++ CbModule, ChannelId, Args, Exec)
+ end.
+
+
+@@ -1186,7 +1204,8 @@ start_subsystem(BinName, #connection{options = Options,
+ Name = binary_to_list(BinName),
+ case check_subsystem(Name, Options) of
+ {Callback, Opts} when is_atom(Callback), Callback =/= none ->
+- ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options);
++ ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++ Callback, ChannelId, Opts, undefined);
+ {none, _} ->
+ {error, bad_subsystem};
+ {_, _} ->
+diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
+index e2311b119a..362633c991 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -190,16 +190,16 @@ open_channel(ConnectionHandler,
+ Timeout}).
+
+ %%--------------------------------------------------------------------
+-%%% Start a channel handling process in the superviser tree
++%%% Start a channel handling process in the supervisor tree
+ -spec start_channel(connection_ref(), atom(), channel_id(), list(), term()) ->
+ {ok, pid()} | {error, term()}.
+
+ %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+ start_channel(ConnectionHandler, CallbackModule, ChannelId, Args, Exec) ->
+- {ok, {ConnectionSup,Role,Opts}} = call(ConnectionHandler, get_misc),
++ {ok, {ConnectionSup,Role}} = call(ConnectionHandler, get_misc),
+ ssh_connection_sup:start_channel(Role, ConnectionSup,
+ ConnectionHandler, CallbackModule, ChannelId,
+- Args, Exec, Opts).
++ Args, Exec).
+
+ %%--------------------------------------------------------------------
+ handle_direct_tcpip(ConnectionHandler, ListenHost, ListenPort, ConnectToHost, ConnectToPort, Timeout) ->
+@@ -762,6 +762,13 @@ handle_event(internal, {conn_msg,Msg}, StateName, #data{connection_state = Conne
+ end,
+ {stop_and_reply, {shutdown,normal}, Repls, D};
+
++ {send_disconnect, {Code, Description}, RepliesConn} ->
++ {Replies, D1} = send_replies(RepliesConn, D0),
++ D = send_msg(#ssh_msg_disconnect{code = Code,
++ description = Description},
++ D1),
++ {stop_and_reply, {shutdown, Description}, Replies, D};
++
+ {Replies, Connection} when is_list(Replies) ->
+ {Repls, D} =
+ case StateName of
+@@ -1034,37 +1041,48 @@ handle_event({call,From}, {eof, ChannelId}, StateName, D0)
+ handle_event({call,From}, get_misc, StateName,
+ #data{connection_state = #connection{options = Opts}} = D) when ?CONNECTED(StateName) ->
+ ConnectionSup = ?GET_INTERNAL_OPT(connection_sup, Opts),
+- Reply = {ok, {ConnectionSup, ?role(StateName), Opts}},
++ Reply = {ok, {ConnectionSup, ?role(StateName)}},
+ {keep_state, D, [{reply,From,Reply}]};
+
+ handle_event({call,From},
+- {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout},
+- StateName,
+- D0 = #data{connection_state = C}) when ?CONNECTED(StateName) ->
+- erlang:monitor(process, ChannelPid),
+- {ChannelId, D1} = new_channel_id(D0),
++ {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout},
++ StateName,
++ D0 = #data{connection_state = C = #connection{channel_id_seed = ChannelId,
++ suggest_window_size = SuggestWindowSize,
++ suggest_packet_size = SuggestPacketSize}})
++ when ?CONNECTED(StateName) ->
+ WinSz = case InitialWindowSize of
+- undefined -> C#connection.suggest_window_size;
++ undefined -> SuggestWindowSize;
+ _ -> InitialWindowSize
+ end,
+ PktSz = case MaxPacketSize of
+- undefined -> C#connection.suggest_packet_size;
++ undefined -> SuggestPacketSize;
+ _ -> MaxPacketSize
+ end,
+- D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz, PktSz, Data),
+- D1),
+- ssh_client_channel:cache_update(cache(D2),
+- #channel{type = Type,
+- sys = "none",
+- user = ChannelPid,
+- local_id = ChannelId,
+- recv_window_size = WinSz,
+- recv_packet_size = PktSz,
+- send_buf = queue:new()
+- }),
+- D = add_request(true, ChannelId, From, D2),
+- start_channel_request_timer(ChannelId, From, Timeout),
+- {keep_state, D, cond_set_idle_timer(D)};
++ Limit = case ?role(StateName) of
++ server -> ?GET_OPT(max_channels, C#connection.options);
++ client -> infinity
++ end,
++ Channel = #channel{type = Type,
++ sys = "none",
++ user = ChannelPid,
++ local_id = ChannelId,
++ recv_window_size = WinSz,
++ recv_packet_size = PktSz,
++ send_buf = queue:new()
++ },
++ case ssh_client_channel:cache_insert(cache(D0), Channel, Limit) of
++ ok ->
++ erlang:monitor(process, ChannelPid),
++ D1 = D0#data{connection_state = C#connection{channel_id_seed = ChannelId + 1}},
++ D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz,PktSz, Data),
++ D1),
++ D = add_request(true, ChannelId, From, D2),
++ start_channel_request_timer(ChannelId, From, Timeout),
++ {keep_state, D, cond_set_idle_timer(D)};
++ Other ->
++ {keep_state, D0, [{reply,From,Other}]}
++ end;
+
+ handle_event({call,From}, {send_window, ChannelId}, StateName, D)
+ when ?CONNECTED(StateName) ->
+@@ -1360,16 +1378,39 @@ handle_event(info, check_cache, _, D) ->
+ {keep_state, D, cond_set_idle_timer(D)};
+
+ handle_event(info, {fwd_connect_received, Sock, ChId, ChanCB}, StateName, #data{connection_state = Connection}) ->
+- #connection{options = Options,
+- channel_cache = Cache,
++ #connection{channel_cache = Cache,
+ connection_supervisor = ConnectionSup} = Connection,
+ Channel = ssh_client_channel:cache_lookup(Cache, ChId),
+- {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined, Options),
++ {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined),
+ ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}),
+ gen_tcp:controlling_process(Sock, Pid),
+ inet:setopts(Sock, [{active,once}]),
+ keep_state_and_data;
+
++handle_event(info, {fwd_connect_failed, {error, max_num_channels_exceeded}}, StateName,
++ #data{connection_state = #connection{options = Opts}} = D0) ->
++ %% Keep same behavior as before, if channel limit is reached for port forwarding channels
++ %% we close the connection. Previously this was a crash in {fwd_connect_received, ...} on
++ %% {ok, Pid} = ssh_connection_sup:start_channel(...).
++ MsgFun =
++ fun(debug) ->
++ Limit = ?GET_OPT(max_channels, Opts),
++ io_lib:format("Connection terminated. Port forward request reached a "
++ "channel limit of: ~p",
++ [Limit]);
++ (_) ->
++ "Connection terminated. Channel limit reached."
++ end,
++ {Shutdown, D} =
++ ?send_disconnect(?SSH_DISCONNECT_BY_APPLICATION,
++ "Connection terminated. Channel limit reached.",
++ ?SELECT_MSG(MsgFun),
++ StateName, D0),
++ {stop, Shutdown, D};
++handle_event(info, {fwd_connect_failed, _Other}, _StateName, _D) ->
++ %% Ignore other reasons
++ keep_state_and_data;
++
+ handle_event({call,From},
+ {handle_direct_tcpip, ListenHost, ListenPort, ConnectToHost, ConnectToPort, _Timeout},
+ _StateName,
+@@ -1792,12 +1833,6 @@ add_request(Fun, ChannelId, From, #data{connection_state =
+ Requests = [{ChannelId, From, Fun} | Requests0],
+ State#data{connection_state = Connection#connection{requests = Requests}}.
+
+-new_channel_id(#data{connection_state = #connection{channel_id_seed = Id} =
+- Connection}
+- = State) ->
+- {Id, State#data{connection_state =
+- Connection#connection{channel_id_seed = Id + 1}}}.
+-
+
+ %%%----------------------------------------------------------------
+ start_rekeying(Role, D0) ->
+diff --git a/lib/ssh/src/ssh_connection_sup.erl b/lib/ssh/src/ssh_connection_sup.erl
+index 3d8ac4171e..a0ddfc6680 100644
+--- a/lib/ssh/src/ssh_connection_sup.erl
++++ b/lib/ssh/src/ssh_connection_sup.erl
+@@ -29,7 +29,7 @@
+ -include("ssh.hrl").
+
+ -export([start_link/4,
+- start_channel/8,
++ start_channel/7,
+ tcpip_fwd_supervisor/1
+ ]).
+
+@@ -47,9 +47,9 @@ start_link(Role, Id, Socket, Options) ->
+ Other
+ end.
+
+-start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) ->
++start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec) ->
+ ChannelSup = channel_supervisor(SupPid),
+- ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts).
++ ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec).
+
+ tcpip_fwd_supervisor(ConnectionSup) ->
+ find_child(tcpip_forward_acceptor_sup, ConnectionSup).
+diff --git a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
+index c3ebb902e3..20fade1297 100644
+--- a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
++++ b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
+@@ -87,8 +87,9 @@ acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB,
+ {ok,ChId} ->
+ gen_tcp:controlling_process(Sock, ConnPid),
+ ConnPid ! {fwd_connect_received, Sock, ChId, ChanCB};
+- _ ->
+- gen_tcp:close(Sock)
++ Other ->
++ gen_tcp:close(Sock),
++ ConnPid ! {fwd_connect_failed, Other}
+ end,
+ acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid);
+
+diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl
+index b9f5e6355e..40c141602e 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -73,6 +73,8 @@
+ kex_error/1,
+ interrupted_send/1,
+ max_channels_option/1,
++ max_channels_to_server/1,
++ max_channels_from_server/1,
+ no_sensitive_leak/1,
+ ptty_alloc/1,
+ ptty_alloc_default/1,
+@@ -183,6 +185,8 @@ all() ->
+ no_sensitive_leak,
+ start_subsystem_on_closed_channel,
+ max_channels_option,
++ max_channels_to_server,
++ max_channels_from_server,
+ handler_down_before_open
+ ].
+ groups() ->
+@@ -1768,19 +1772,22 @@ max_channels_option(Config) when is_list(Config) ->
+ {user_interaction, true},
+ {user_dir, UserDir}]),
+
+- %% Allocate a number of ChannelId:s to play with. (This operation is not
+- %% counted by the max_channel option).
++ %% Allocate a number of ChannelId:s to play with.
+ {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+ {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity),
+ {ok, ChannelId2} = ssh_connection:session_channel(ConnectionRef, infinity),
+- {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity),
+- {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity),
+- {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity),
+- {ok, ChannelId6} = ssh_connection:session_channel(ConnectionRef, infinity),
+- {ok, _ChannelId7} = ssh_connection:session_channel(ConnectionRef, infinity),
+-
+- %% Now start to open the channels (this is counted my max_channels) to check that
+- %% it gives a failure at right place
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
++
++ %% Now start subsystems in open channels.
+
+ %%%---- Channel 1(3): shell
+ ok = ssh_connection:shell(ConnectionRef,ChannelId0),
+@@ -1802,16 +1809,29 @@ max_channels_option(Config) when is_list(Config) ->
+ after 5000 ->
+ ct:fail("Exec #1 Timeout")
+ end,
++ %%%---- wait for exec to terminate
++ receive
++ {ssh_cm,ConnectionRef,{closed,ChannelId2}} -> ok
++ after 5000 ->
++ ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n"
++ "Message queue:~n~p",
++ [ConnectionRef,ChannelId2,erlang:process_info(self(),messages)]),
++ ct:fail("exit Timeout",[])
++ end,
++
++ %% Now ChannelId2 should be closed now, we can open ChannelId3
++ {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity),
+
+ %%%---- Channel 3(3): subsystem "echo_n" (Note that ChannelId2 should be closed now)
+ ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId3, "echo_n", infinity)),
+
+- %%%---- Channel 4(3) !: exec This should fail
+- failure = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity),
++ %%%---- Channel 4(3) !: creating channel should fail
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
+
+ %%%---- close the shell (Frees one channel)
+ ok = ssh_connection:send(ConnectionRef, ChannelId0, "exit().\n", 5000),
+-
++
+ %%%---- wait for the subsystem to terminate
+ receive
+ {ssh_cm,ConnectionRef,{closed,ChannelId0}} -> ok
+@@ -1822,15 +1842,114 @@ max_channels_option(Config) when is_list(Config) ->
+ ct:fail("exit Timeout",[])
+ end,
+
++ %%%---- Channel 3(3) !: exec This should succeed
++ {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity),
++ success = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity),
++ %%%---- wait for exec to terminate
++ receive
++ {ssh_cm,ConnectionRef,{closed,ChannelId4}} -> ok
++ after 5000 ->
++ ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n"
++ "Message queue:~n~p",
++ [ConnectionRef,ChannelId4,erlang:process_info(self(),messages)]),
++ ct:fail("exit Timeout",[])
++ end,
++
+ %%---- Try that we can open one channel instead of the closed one
++ {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity),
+ ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId5, "echo_n", infinity)),
+
+ %%---- But not a fourth one...
+- failure = ssh_connection:subsystem(ConnectionRef, ChannelId6, "echo_n", infinity),
++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++ ssh_connection:session_channel(ConnectionRef, infinity),
+
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid).
+
++%%--------------------------------------------------------------------
++max_channels_to_server(Config) when is_list(Config) ->
++ max_channels_forwarding_helper(Config, true).
++
++%%--------------------------------------------------------------------
++max_channels_from_server(Config) when is_list(Config) ->
++ max_channels_forwarding_helper(Config, false).
++
++max_channels_forwarding_helper(Config, ToServer) ->
++ PrivDir = proplists:get_value(priv_dir, Config),
++ UserDir = filename:join(PrivDir, nopubkey),
++ file:make_dir(UserDir),
++ SysDir = proplists:get_value(data_dir, Config),
++
++ {ok, TargetSock} = gen_tcp:listen(0, [{active, false}, binary, {reuseaddr, true}]),
++ {ok, {TargetHost, TargetPort}} = inet:sockname(TargetSock),
++
++ Options =
++ case ToServer of
++ true ->
++ [{tcpip_tunnel_in, true}];
++ false ->
++ [{tcpip_tunnel_out, true}]
++ end,
++
++ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
++ {user_dir, UserDir},
++ {password, "morot"},
++ {max_channels, 2}] ++
++ Options),
++
++ Ref = make_ref(),
++ Parent = self(),
++ ConnectionRef = ssh_test_lib:connect(Host, Port,
++ [{silently_accept_hosts, true},
++ {user, "foo"},
++ {password, "morot"},
++ {user_interaction, false},
++ {user_dir, UserDir},
++ {disconnectfun, fun(Reason) -> Parent ! {Ref, Reason} end}]),
++
++ {ok, ListenPort} =
++ case ToServer of
++ true ->
++ ssh:tcpip_tunnel_to_server(ConnectionRef,
++ {127,0,0,1},
++ 0,
++ TargetHost,
++ TargetPort,
++ timer:seconds(5));
++ false ->
++ ssh:tcpip_tunnel_from_server(ConnectionRef,
++ {127,0,0,1},
++ 0,
++ TargetHost,
++ TargetPort,
++ timer:seconds(5))
++ end,
++
++ {ok, Sock1} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++ {ok, Sock2} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++ {ok, _} = gen_tcp:accept(TargetSock),
++ {ok, _} = gen_tcp:accept(TargetSock),
++
++ %% Sockets are connected
++ {error, timeout} = gen_tcp:recv(Sock1, 0, 0),
++ {error, timeout} = gen_tcp:recv(Sock2, 0, 0),
++
++ %% Upon hitting the channel limit, server will close all sockets
++ {ok, Sock3} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++ receive
++ {Ref, "Received disconnect: Connection terminated. Channel limit reached."} ->
++ ok
++ after 2000 ->
++ ct:fail("Connection should be closed!")
++ end,
++ {error, closed} = gen_tcp:recv(Sock1, 0, timer:seconds(5)),
++ {error, closed} = gen_tcp:recv(Sock2, 0, timer:seconds(5)),
++ {error, closed} = gen_tcp:recv(Sock3, 0, timer:seconds(5)),
++
++ gen_tcp:close(TargetSock),
++ ssh:stop_daemon(Pid).
++
++%%--------------------------------------------------------------------
+ handler_down_before_open(Config) ->
+ %% Start echo subsystem with a delay in init() - until a signal is received
+ %% One client opens a channel on the connection
+diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
+index 4d590072e3..57c9f1c908 100644
+--- a/lib/ssh/test/ssh_options_SUITE.erl
++++ b/lib/ssh/test/ssh_options_SUITE.erl
+@@ -28,6 +28,7 @@
+ -include_lib("common_test/include/ct.hrl").
+ -include_lib("kernel/include/file.hrl").
+ -include("ssh_test_lib.hrl").
++-include("ssh_connect.hrl").
+
+ %%% Test cases
+ -export([
+@@ -1197,7 +1198,7 @@ ssh_daemon_minimal_remote_max_packet_size_option(Config) ->
+
+ %% Try the limits of the minimal_remote_max_packet_size:
+ {ok, _ChannelId} = ssh_connection:session_channel(Conn, 100, 14, infinity),
+- {open_error,_,"Maximum packet size below 14 not supported",_} =
++ {open_error, ?SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "Maximum packet size below 14 not supported", <<"en">>} =
+ ssh_connection:session_channel(Conn, 100, 13, infinity),
+
+ ssh:close(Conn),
diff --git a/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch b/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
new file mode 100644
index 0000000..e9fbfac
--- /dev/null
+++ b/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
@@ -0,0 +1,414 @@
+From: Ingela Anderton Andin <ingela@erlang.org>
+Date: Wed, 16 Sep 2026 14:55:28 +0200
+Subject: [PATCH] ssl: Reject unsolicited TLS-1.3 pre_shared_key in client
+
+A TLS-1.3 client that offered no PSK accepted a ServerHello carrying a
+pre_shared_key extension: get_pre_shared_key/4 fell back to the all-zero
+"no PSK" value and handle_resumption/2 set resumption=true on the mere
+presence of the extension, routing the client past the certificate
+states (wait_cert_cr/wait_cert/wait_cv) straight to wait_finished. Since
+the handshake is keyed with the ordinary non-PSK schedule, any peer that
+completes ECDHE could impersonate any server to a verify_peer client
+without presenting a certificate (server-authentication bypass).
+
+Fix (RFC 8446 4.2.11): in get_pre_shared_key/4 every clause reachable
+only when the server DID send pre_shared_key now aborts with a fatal
+illegal_parameter alert (reason {unsolicited_pre_shared_key, _}) instead
+of returning the zero PSK - the client offered no identities, so
+selected_identity cannot be in range. The auto clause still unlocks
+tickets first. This alone closes the hole: handle_server_hello/2 fails
+in wait_sh before any keys are derived.
+
+Defence in depth: move the handle_resumption/2 call in
+handle_server_hello/2 to after get_pre_shared_key/4 has accepted the
+server's PSK selection, so resumption mode can never be entered for a
+PSK the client did not offer.
+
+Add regression test tls13_reject_unsolicited_psk (rogue_server_tests
+group in tls_api_SUITE): a certificate-less raw-TCP TLS-1.3 server sends
+an otherwise-valid flight with an unsolicited pre_shared_key. Verified
+the test fails (client connects) without the fix and passes
+(illegal_parameter) with it. Covers session_tickets=disabled and =auto.
+
+(cherry picked from commit fd1d9d07fc92ec0d59f96dfb66182195882bb7dd)
+
+[Backport to OTP-26: the rogue_server_tests group and its generic
+ helpers (rogue_observe/1, rogue_handshake/2, rogue_record/2,
+ rogue_read_record/1) come from earlier upstream commits not present
+ on this branch, so they are added here with only the
+ tls13_reject_unsolicited_psk test case.]
+
+Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
+Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
+
+diff --git a/lib/ssl/src/tls_client_connection_1_3.erl b/lib/ssl/src/tls_client_connection_1_3.erl
+index 928360374a..c4461c5adb 100644
+--- a/lib/ssl/src/tls_client_connection_1_3.erl
++++ b/lib/ssl/src/tls_client_connection_1_3.erl
+@@ -666,10 +666,6 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+ %% Go to state 'start' if server replies with 'HelloRetryRequest'.
+ Maybe(tls_handshake_1_3:maybe_hello_retry_request(ServerHello, State0)),
+
+- %% Resumption and PSK
+- State1 = tls_gen_connection_1_3:handle_resumption(State0,
+- ServerPreSharedKey),
+-
+ Maybe(validate_cipher_suite(SelectedCipherSuite, ClientCiphers)),
+ Maybe(validate_server_key_share(ClientGroups, ServerKeyShare)),
+
+@@ -681,7 +677,7 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+ client_private_key(SelectedGroup,
+ ClientKeyShare#key_share_client_hello.client_shares),
+ %% Update state
+- State2 = tls_handshake_1_3:update_start_state(State1,
++ State2 = tls_handshake_1_3:update_start_state(State0,
+ #{cipher => SelectedCipherSuite,
+ key_share => ClientKeyShare,
+ session_id => SessionId,
+@@ -698,13 +694,22 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+ UseTicket,
+ HKDFAlgo,
+ ServerPreSharedKey)),
+- State3 =
++
++ %% Resumption and PSK. Only enter resumption mode once the server's PSK
++ %% selection has been accepted by get_pre_shared_key/4 above; an
++ %% unsolicited pre_shared_key aborts before this point, so the
++ %% resumption flag (which skips the certificate states) can never be
++ %% set for a PSK the client did not offer. Defence in depth on top of
++ %% the get_pre_shared_key/4 check.
++ State3 = tls_gen_connection_1_3:handle_resumption(State2,
++ ServerPreSharedKey),
++ State4 =
+ tls_handshake_1_3:calculate_handshake_secrets(ServerPublicKey,
+ ClientPrivateKey,
+ SelectedGroup,
+- PSK, State2),
+- State4 = ssl_record:step_encryption_state_read(State3),
+- {State4, wait_ee}
++ PSK, State3),
++ State5 = ssl_record:step_encryption_state_read(State4),
++ {State5, wait_ee}
+ catch
+ {Ref, {State, StateName, ServerHello}} ->
+ {State, StateName, ServerHello};
+diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl
+index 0a0c34d8fb..58adef4161 100644
+--- a/lib/ssl/src/tls_handshake_1_3.erl
++++ b/lib/ssl/src/tls_handshake_1_3.erl
+@@ -1106,28 +1106,32 @@ get_pre_shared_key({_, PSK}, _) ->
+ %% Server initiates a full handshake
+ get_pre_shared_key(_, _, HKDFAlgo, undefined) ->
+ {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
+-%% Session resumption not configured
+-get_pre_shared_key(undefined, _, HKDFAlgo, _) ->
+- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
+-get_pre_shared_key(_, undefined, HKDFAlgo, _) ->
+- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++%% Session resumption not configured, i.e. we did not offer any PSK, so the
++%% server's selected_identity cannot be within the range we supplied.
++%% RFC 8446 Section 4.2.11: abort with an "illegal_parameter" alert instead of
++%% silently falling back to the (zero) "no PSK" value, which would skip server
++%% authentication.
++get_pre_shared_key(undefined, _, _, ServerPSK) ->
++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
++get_pre_shared_key(_, undefined, _, ServerPSK) ->
++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+ %% Session resumption
+-get_pre_shared_key(manual = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) ->
++get_pre_shared_key(manual = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) ->
+ TicketData = get_ticket_data(self(), SessionTickets, UseTicket),
+ case choose_psk(TicketData, ServerPSK) of
+- undefined -> %% full handshake, default PSK
+- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++ undefined -> %% No PSK was offered that matches the server selection
++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+ illegal_parameter ->
+ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)};
+ {_, PSK, _, _, _} ->
+ {ok, PSK}
+ end;
+-get_pre_shared_key(auto = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) ->
++get_pre_shared_key(auto = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) ->
+ TicketData = get_ticket_data(self(), SessionTickets, UseTicket),
+ case choose_psk(TicketData, ServerPSK) of
+- undefined -> %% full handshake, default PSK
++ undefined -> %% No PSK was offered that matches the server selection
+ tls_client_ticket_store:unlock_tickets(self(), UseTicket),
+- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+ illegal_parameter ->
+ tls_client_ticket_store:unlock_tickets(self(), UseTicket),
+ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)};
+diff --git a/lib/ssl/test/tls_api_SUITE.erl b/lib/ssl/test/tls_api_SUITE.erl
+index 04a1151f04..79ffef785f 100644
+--- a/lib/ssl/test/tls_api_SUITE.erl
++++ b/lib/ssl/test/tls_api_SUITE.erl
+@@ -30,6 +30,7 @@
+ -include_lib("ssl/src/tls_handshake.hrl").
+ -include_lib("ssl/src/ssl_alert.hrl").
+ -include_lib("ssl/src/ssl_cipher.hrl").
++-include_lib("ssl/src/tls_handshake_1_3.hrl").
+
+ %% Common test
+ -export([all/0,
+@@ -114,7 +115,9 @@
+ reuseaddr/0,
+ reuseaddr/1,
+ signature_algs/0,
+- signature_algs/1
++ signature_algs/1,
++ tls13_reject_unsolicited_psk/0,
++ tls13_reject_unsolicited_psk/1
+ ]).
+
+ %% Apply export
+@@ -130,6 +133,12 @@
+
+ -define(SLEEP, 500).
+ -define(CORRECT_PASSWORD, "hello test").
++
++%% TLS 1.3 constants for the unsolicited-pre_shared_key rogue-server test.
++-define(TLS13_KEY_SHARE_EXT, 51).
++-define(TLS13_PRE_SHARED_KEY_EXT, 41).
++-define(TLS13_SUPPORTED_VERSIONS_EXT, 43).
++-define(TLS13_GROUP_X25519, 16#001d).
+ -define(INCORRECT_PASSWORD, "hello").
+ -define(BADARG_PASSWORD, hello).
+
+@@ -142,7 +151,8 @@ all() ->
+ {group, 'tlsv1.3'},
+ {group, 'tlsv1.2'},
+ {group, 'tlsv1.1'},
+- {group, 'tlsv1'}
++ {group, 'tlsv1'},
++ {group, rogue_server_tests}
+ ].
+
+ groups() ->
+@@ -151,7 +161,8 @@ groups() ->
+ tls_13_middlebox_reject_change_cipher_spec_as_first_msg]) -- [sockname]},
+ {'tlsv1.2', [], api_tests()},
+ {'tlsv1.1', [], api_tests()},
+- {'tlsv1', [], api_tests()}
++ {'tlsv1', [], api_tests()},
++ {rogue_server_tests, [parallel], rogue_server_tests()}
+ ].
+
+ api_tests() ->
+@@ -191,6 +202,9 @@ api_tests() ->
+ reuseaddr
+ ].
+
++rogue_server_tests() ->
++ [tls13_reject_unsolicited_psk].
++
+ init_per_suite(Config0) ->
+ catch crypto:stop(),
+ try crypto:start() of
+@@ -754,6 +768,205 @@ tls_dont_crash_on_handshake_garbage(Config) ->
+ ssl_test_lib:check_server_alert(Server, handshake_failure)
+ end.
+
++%%--------------------------------------------------------------------
++tls13_reject_unsolicited_psk() ->
++ [{doc, "A malicious/on-path server sends a complete, otherwise-valid "
++ "TLS-1.3 flight (ServerHello, ChangeCipherSpec, encrypted "
++ "{EncryptedExtensions, Finished}) that carries a pre_shared_key "
++ "extension the client never offered, and NO Certificate/CertificateVerify. "
++ "RFC 8446 4.2.11 requires the client to abort with illegal_parameter. On "
++ "vulnerable code the verify_peer client instead accepts the unsolicited "
++ "PSK, keys the handshake with the zero PSK, skips the certificate states "
++ "and connects -- a complete server-authentication bypass "
++ "(ANT-2026-HEF8F3FY / OTP-20388). Checked for session_tickets=disabled "
++ "(default) and =auto with an empty ticket store."}].
++tls13_reject_unsolicited_psk(Config) when is_list(Config) ->
++ ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
++ ok = rogue_tls13_psk(ClientOpts, []),
++ ok = rogue_tls13_psk(ClientOpts, [{session_tickets, auto}]),
++ ok.
++
++%% Drive a real TLS-1.3 verify_peer client against a raw-TCP attacker that
++%% forges the full flight, and assert the mandated illegal_parameter alert.
++rogue_tls13_psk(ClientOpts0, ExtraOpts) ->
++ {ok, LSock} = gen_tcp:listen(0, [binary, {active, false},
++ {reuseaddr, true}, {packet, 0}]),
++ {ok, Port} = inet:port(LSock),
++ Parent = self(),
++ Attacker = spawn_link(fun() -> Parent ! {attacker, rogue_tls13_server(LSock)} end),
++ ClientOpts = ExtraOpts ++
++ [{versions, ['tlsv1.3']},
++ {ciphers, ["TLS_AES_128_GCM_SHA256"]},
++ {supported_groups, [x25519]},
++ {server_name_indication, disable},
++ {active, false} | ClientOpts0],
++ Result = ssl:connect("localhost", Port, ClientOpts, 5000),
++ gen_tcp:close(LSock),
++ Observed = receive {attacker, O} -> O after 5000 -> unlink(Attacker), timeout end,
++ ct:log("extra=~p~n ssl:connect -> ~p~n attacker -> ~p",
++ [ExtraOpts, Result, Observed]),
++ %% With a well-formed flight, a PATCHED client aborts with
++ %% illegal_parameter in wait_sh (the unsolicited-PSK check). A
++ %% VULNERABLE client instead accepts the unsolicited PSK, skips the
++ %% certificate states and returns {ok, Socket} (verified: without the
++ %% fix this case fails here with {ok,_}). The reason atom
++ %% {unsolicited_pre_shared_key,_} is not rendered into the tls_alert
++ %% description string, so we assert on the description + the fact that
++ %% a vulnerable client connects.
++ case Result of
++ {error, {tls_alert, {illegal_parameter, _}}} ->
++ ok;
++ {ok, Sock} ->
++ ssl:close(Sock),
++ ct:fail("SERVER-AUTHENTICATION BYPASS: verify_peer client connected "
++ "to a certificate-less server that sent an unsolicited "
++ "pre_shared_key (extra ~p). Attacker observed ~p.",
++ [ExtraOpts, Observed]);
++ Other ->
++ ct:fail("Expected illegal_parameter for unsolicited pre_shared_key "
++ "(extra ~p); got ~p (attacker ~p)",
++ [ExtraOpts, Other, Observed])
++ end.
++
++%% Raw-TCP rogue TLS-1.3 server. No certificate. Sends a valid ServerHello +
++%% CCS + encrypted {EncryptedExtensions, Finished} using the RFC 8446 7.1 key
++%% schedule so that an UNPATCHED client would complete the handshake.
++rogue_tls13_server(LSock) ->
++ {ok, Sock} = gen_tcp:accept(LSock, 5000),
++ {?HANDSHAKE, CHHandshake} = rogue_read_record(Sock),
++ ClientShare = rogue_tls13_client_x25519(CHHandshake),
++ SessionId = rogue_tls13_client_session_id(CHHandshake),
++
++ {ServerPub, ServerPriv} = crypto:generate_key(ecdh, x25519),
++ SHBody = rogue_tls13_server_hello_body(ServerPub, SessionId),
++ SHHandshake = rogue_handshake(?SERVER_HELLO, SHBody),
++
++ %% RFC 8446 7.1 handshake secrets (transcript = ClientHello ++ ServerHello).
++ Alg = sha256,
++ HashLen = 32,
++ Shared = crypto:compute_key(ecdh, ClientShare, ServerPriv, x25519),
++ Zero = binary:copy(<<0>>, HashLen),
++ EarlySecret = tls_v1:hkdf_extract(Alg, Zero, Zero),
++ DerivedEarly = rogue_derive_secret(Alg, HashLen, EarlySecret, <<"derived">>, <<>>),
++ HSSecret = tls_v1:hkdf_extract(Alg, DerivedEarly, Shared),
++ Transcript0 = <<CHHandshake/binary, SHHandshake/binary>>,
++ ServerHS = rogue_derive_secret(Alg, HashLen, HSSecret, <<"s hs traffic">>, Transcript0),
++ Key = tls_v1:hkdf_expand_label(ServerHS, <<"key">>, <<>>, 16, Alg),
++ IV = tls_v1:hkdf_expand_label(ServerHS, <<"iv">>, <<>>, 12, Alg),
++
++ EE = <<?ENCRYPTED_EXTENSIONS, 2:24, 0:16>>,
++ Transcript1 = <<Transcript0/binary, EE/binary>>,
++ FinKey = tls_v1:hkdf_expand_label(ServerHS, <<"finished">>, <<>>, HashLen, Alg),
++ THash = crypto:hash(Alg, Transcript1),
++ VerifyData = tls_v1:hmac_hash(Alg, FinKey, THash),
++ Finished = <<?FINISHED, (byte_size(VerifyData)):24, VerifyData/binary>>,
++
++ Inner = <<EE/binary, Finished/binary, ?HANDSHAKE>>,
++ EncRecord = rogue_tls13_encrypt(Key, IV, 0, Inner),
++
++ ok = gen_tcp:send(Sock, rogue_record(?HANDSHAKE, SHHandshake)),
++ ok = gen_tcp:send(Sock, <<20, 3, 3, 1:16, 1>>), %% ChangeCipherSpec
++ ok = gen_tcp:send(Sock, EncRecord),
++
++ Obs = rogue_observe(Sock),
++ gen_tcp:close(Sock),
++ Obs.
++
++rogue_tls13_server_hello_body(ServerPub, SessionId) ->
++ Random = crypto:strong_rand_bytes(32),
++ SVExt = <<?TLS13_SUPPORTED_VERSIONS_EXT:16, 2:16, 16#03, 16#04>>,
++ KSData = <<?TLS13_GROUP_X25519:16, (byte_size(ServerPub)):16, ServerPub/binary>>,
++ KSExt = <<?TLS13_KEY_SHARE_EXT:16, (byte_size(KSData)):16, KSData/binary>>,
++ PSKExt = <<?TLS13_PRE_SHARED_KEY_EXT:16, 2:16, 0:16>>, %% selected_identity = 0
++ Exts = <<SVExt/binary, KSExt/binary, PSKExt/binary>>,
++ %% RFC 8446 4.1.3: the ServerHello MUST echo the client's
++ %% legacy_session_id, otherwise the client aborts with
++ %% session_id_echo_mismatch before the pre_shared_key is examined.
++ <<16#03, 16#03, Random/binary,
++ (byte_size(SessionId)), SessionId/binary,
++ 16#13, 16#01, 0, %% TLS_AES_128_GCM_SHA256, compression=null
++ (byte_size(Exts)):16, Exts/binary>>.
++
++rogue_derive_secret(Alg, HashLen, Secret, Label, Messages) ->
++ Hash = crypto:hash(Alg, Messages),
++ tls_v1:hkdf_expand_label(Secret, Label, Hash, HashLen, Alg).
++
++%% Encrypt one TLS-1.3 record (outer type application_data), RFC 8446 5.2.
++rogue_tls13_encrypt(Key, IV, SeqNo, Inner) ->
++ TagLen = 16,
++ Len = byte_size(Inner) + TagLen,
++ AAD = <<?APPLICATION_DATA, 3, 3, Len:16>>,
++ Nonce = rogue_tls13_nonce(SeqNo, IV),
++ {Enc, Tag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, Nonce, Inner, AAD, TagLen, true),
++ Payload = <<Enc/binary, Tag/binary>>,
++ <<?APPLICATION_DATA, 3, 3, (byte_size(Payload)):16, Payload/binary>>.
++
++rogue_tls13_nonce(SeqNo, IV) ->
++ Padded = <<0:((byte_size(IV) - 8) * 8), SeqNo:64>>,
++ crypto:exor(Padded, IV).
++
++%% Parse client's legacy_session_id from the ClientHello handshake message.
++rogue_tls13_client_session_id(CHHandshake) ->
++ <<_HsType, _HsLen:24, Body/binary>> = CHHandshake,
++ <<_LegacyVsn:16, _Random:32/binary, SidLen, Sid:SidLen/binary, _/binary>> = Body,
++ Sid.
++
++%% Parse client's x25519 key_share from the ClientHello handshake message.
++rogue_tls13_client_x25519(CHHandshake) ->
++ <<_HsType, _HsLen:24, Body/binary>> = CHHandshake,
++ <<_LegacyVsn:16, _Random:32/binary, R0/binary>> = Body,
++ <<SidLen, R1/binary>> = R0,
++ <<_Sid:SidLen/binary, R2/binary>> = R1,
++ <<CsLen:16, R3/binary>> = R2,
++ <<_Cs:CsLen/binary, R4/binary>> = R3,
++ <<CompLen, R5/binary>> = R4,
++ <<_Comp:CompLen/binary, R6/binary>> = R5,
++ <<_ExtsLen:16, Exts/binary>> = R6,
++ rogue_tls13_find_key_share(Exts).
++
++rogue_tls13_find_key_share(<<?TLS13_KEY_SHARE_EXT:16, ExtLen:16, ExtData:ExtLen/binary, _/binary>>) ->
++ <<_SharesLen:16, Shares/binary>> = ExtData,
++ rogue_tls13_find_x25519(Shares);
++rogue_tls13_find_key_share(<<_Type:16, ExtLen:16, _ExtData:ExtLen/binary, Rest/binary>>) ->
++ rogue_tls13_find_key_share(Rest);
++rogue_tls13_find_key_share(<<>>) ->
++ ct:fail(client_offered_no_key_share).
++
++rogue_tls13_find_x25519(<<?TLS13_GROUP_X25519:16, KeLen:16, Ke:KeLen/binary, _/binary>>) ->
++ Ke;
++rogue_tls13_find_x25519(<<_Group:16, KeLen:16, _Ke:KeLen/binary, Rest/binary>>) ->
++ rogue_tls13_find_x25519(Rest);
++rogue_tls13_find_x25519(<<>>) ->
++ ct:fail(client_offered_no_x25519_share).
++
++rogue_observe(Sock) ->
++ case rogue_read_record(Sock) of
++ {?HANDSHAKE, <<MsgType, _/binary>>} ->
++ {client_handshake, MsgType};
++ {?ALERT, <<Level, Desc>>} ->
++ {client_alert, Level, Desc};
++ {error, closed} ->
++ client_closed;
++ Other ->
++ {other, Other}
++ end.
++
++rogue_handshake(Type, Body) ->
++ <<Type, (byte_size(Body)):24, Body/binary>>.
++
++rogue_record(ContentType, Payload) ->
++ <<ContentType, 3, 3, (byte_size(Payload)):16, Payload/binary>>.
++
++rogue_read_record(Sock) ->
++ case gen_tcp:recv(Sock, 5, 5000) of
++ {ok, <<CT, _Maj, _Min, Len:16>>} ->
++ case gen_tcp:recv(Sock, Len, 5000) of
++ {ok, Payload} -> {CT, Payload};
++ Err -> Err
++ end;
++ {error, _} = E -> E
++ end.
++
+ %%--------------------------------------------------------------------
+ tls_tcp_error_propagation_in_active_mode() ->
+ [{doc,"Test that process receives {ssl_error, Socket, closed} when tcp error ocurres"}].
reply other threads:[~2026-09-23 20:29 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=179019535684.1.3153612654807224891.rpms-erlang-a131da91b106@fedoraproject.org \
--to=lemenkov@gmail.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox