From b65c51ba9e46276f7fc7768136dd4fa33fe6e28b Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 9 Apr 2021 14:36:26 +0200 Subject: [PATCH 01/23] emqx: add 'emqx' to beam boot log --- erts/emulator/beam/erl_bif_info.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erts/emulator/beam/erl_bif_info.c b/erts/emulator/beam/erl_bif_info.c index b421071579f9..83ec8e65d532 100644 --- a/erts/emulator/beam/erl_bif_info.c +++ b/erts/emulator/beam/erl_bif_info.c @@ -89,9 +89,9 @@ static char erts_system_version[] = ("Erlang/OTP " ERLANG_OTP_RELEASE " [erts-" ERLANG_VERSION "]" #ifndef OTP_RELEASE #ifdef ERLANG_GIT_VERSION - " [source-" ERLANG_GIT_VERSION "]" + " [emqx-" ERLANG_GIT_VERSION "]" #else - " [source]" + " [emqx]" #endif #endif #if defined(ARCH_64) From 1fdb5781df7a5a364c1109b98d501aef70aa6455 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 9 Apr 2021 10:47:53 +0200 Subject: [PATCH 02/23] emqx: dynmaic ipv6 resolution for gen_tcp In this commit, a new option `ipv6_probe` is added for gen_tcp:connect API. The option can either be a proplist boolean flag or a timeout non-neg-integer or `infinity` When this option is provided, gen_tcp will try to connect the target with ipv6, then fallback to default options if failed to connect. --- lib/kernel/src/gen_tcp.erl | 89 +++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/lib/kernel/src/gen_tcp.erl b/lib/kernel/src/gen_tcp.erl index cd67a28d9e75..794f4345714a 100644 --- a/lib/kernel/src/gen_tcp.erl +++ b/lib/kernel/src/gen_tcp.erl @@ -361,7 +361,8 @@ this value is returned from `inet:getopts/2` when called with the option name recvtclass | recvttl | pktoptions | - ipv6_v6only. + ipv6_v6only | + ipv6_probe. -type connect_option() :: {fd, Fd :: non_neg_integer()} | inet:address_family() | @@ -372,6 +373,7 @@ this value is returned from `inet:getopts/2` when called with the option name {tcp_module, module()} | {netns, file:filename_all()} | {bind_to_device, binary()} | + {ipv6_probe, boolean() | timeout()} | option(). -type listen_option() :: {fd, Fd :: non_neg_integer()} | @@ -567,21 +569,86 @@ Defaults to `infinity`. Reason :: timeout | inet:posix(). connect(Address, Port, Opts0, Timeout) -> - case inet:gen_tcp_module(Opts0) of + %% When neither `inet` nor `inet6` is provided in Opts0, + %% and if `ipv6_probe` option is given, try to connect ipv6 first. + {TryIpv6, Ipv6T} = + case proplists:get_value(ipv6_probe, Opts0) of + true -> {true, 2000}; %% default 2 seconds + false -> {false, 0}; + undefined -> {false, 0}; + T -> {true, T} + end, + %% delete it to avoid interference + Opts1 = proplists:delete(ipv6_probe, Opts0), + case inet:gen_tcp_module(Opts1) of {?MODULE, Opts} -> - Timer = inet:start_timer(Timeout), - Res = (catch connect1(Address,Port,Opts,Timer)), - _ = inet:stop_timer(Timer), - case Res of - {ok,S} -> {ok,S}; - {error, einval} -> exit(badarg); - {'EXIT',Reason} -> exit(Reason); - Error -> Error - end; + connect_maybe_ipv6(Address, Port, Opts, Timeout, TryIpv6, Ipv6T); {GenTcpMod, Opts} -> GenTcpMod:connect(Address, Port, Opts, Timeout) end. +connect_maybe_ipv6(Address, Port, Opts, Timeout, TryIpv6, Ipv6T) -> + case maybe_ipv6(Address, Opts, TryIpv6) of + {probe, NewOpts} when TryIpv6 -> + try + {ok, _} = connect_0(Address, Port, NewOpts, Ipv6T) + catch + _ : _ -> + %% fallback + connect_0(Address, Port, Opts, Timeout) + end; + NewOpts -> + connect_0(Address, Port, NewOpts, Timeout) + end. + +connect_0(Address, Port, Opts, Timeout) -> + Timer = inet:start_timer(Timeout), + Res = (catch connect1(Address,Port,Opts,Timer)), + _ = inet:stop_timer(Timer), + case Res of + {ok,S} -> {ok,S}; + {error, einval} -> exit(badarg); + {'EXIT',Reason} -> exit(Reason); + Error -> Error + end. + +maybe_ipv6(Host, Opts, TryIpv6) -> + case lists:member(inet, Opts) orelse lists:member(inet6, Opts) of + true -> + Opts; %% caller has made the decision + false when is_tuple(Host) -> + %% ip tuple provided + maybe_ipv6_1(Host, Opts); + false when TryIpv6 -> + %% string host + maybe_ipv6_2(Host, Opts); + false -> + Opts + end. + +maybe_ipv6_1(Host, Opts) when tuple_size(Host) =:= 4 -> Opts; +maybe_ipv6_1(Host, Opts) when tuple_size(Host) =:= 8 -> [inet6 | Opts]. + +maybe_ipv6_2(Host, Opts) -> + case inet:parse_address(Host) of + {ok, Ip} when is_tuple(Ip) -> + %% ip string provided, parsed into tuple + maybe_ipv6_1(Ip, Opts); + _ -> + maybe_ipv6_3(Host, Opts) + end. + +maybe_ipv6_3(Host, Opts) -> + case inet:getaddr(Host, inet6) of + {ok, _} -> + %% the target has a resolvable v6 IP + %% maybe try to connect + {probe, [inet6 | Opts]}; + _ -> + %% the target has no resolvable v6 IP + Opts + end. + connect1(Address, Port, Opts0, Timer) -> {Mod, Opts} = inet:tcp_module(Opts0, Address), case Mod:getaddrs(Address, Timer) of From c81fd5528ecd8c917e9f59a94b217603a9af9005 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Mar 2021 11:14:38 +0200 Subject: [PATCH 03/23] Configurable send table batch size. Acked-by: William Yang --- lib/mnesia/src/mnesia_loader.erl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/mnesia/src/mnesia_loader.erl b/lib/mnesia/src/mnesia_loader.erl index fc8f19edffc8..6f4bf36bb584 100644 --- a/lib/mnesia/src/mnesia_loader.erl +++ b/lib/mnesia/src/mnesia_loader.erl @@ -928,7 +928,16 @@ get_chunk_func(Pid, Tab, {ext, Alias, Mod}, RemoteS) -> get_chunk_func(Pid, Tab, Storage, RemoteS) -> try TabSize = mnesia:table_info(Tab, size), - KeysPerTransfer = calc_nokeys(Storage, Tab), + KeysPerTransfer = + case ?catch_val(send_table_batch_size) of + {'EXIT', _} -> + mnesia_lib:set(send_table_batch_size, 0), + calc_nokeys(Storage, Tab); + 0 -> + calc_nokeys(Storage, Tab); + Val when is_integer(Val) -> + Val + end, ChunkData = dets:info(Tab, bchunk_format), UseDetsChunk = Storage == RemoteS andalso From acddd8d55aa68ed6b858aba22942e569d120b543 Mon Sep 17 00:00:00 2001 From: William Yang Date: Tue, 30 Mar 2021 11:29:39 +0200 Subject: [PATCH 04/23] mnesia send_table_batch_size as app env. --- lib/mnesia/src/mnesia_monitor.erl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/mnesia/src/mnesia_monitor.erl b/lib/mnesia/src/mnesia_monitor.erl index c4b48518f0ba..42a240835228 100644 --- a/lib/mnesia/src/mnesia_monitor.erl +++ b/lib/mnesia/src/mnesia_monitor.erl @@ -693,6 +693,7 @@ env() -> dc_dump_limit, send_compressed, max_transfer_size, + send_table_batch_size, schema ]. @@ -745,6 +746,8 @@ default_env(send_compressed) -> 0; default_env(max_transfer_size) -> 64000; +default_env(send_table_batch_size) -> + 0; default_env(schema) -> []. @@ -795,6 +798,7 @@ do_check_type(no_table_loaders, N) when is_integer(N), N > 0 -> N; do_check_type(dc_dump_limit,N) when is_number(N), N > 0 -> N; do_check_type(send_compressed, L) when is_integer(L), L >= 0, L =< 9 -> L; do_check_type(max_transfer_size, N) when is_integer(N), N > 0 -> N; +do_check_type(send_table_batch_size, L) when is_integer(L), L >= 0 -> L; do_check_type(schema, L) when is_list(L) -> L. bool(true) -> true; From 207cda8da8b118fe74c984f9e166fc365e503d17 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 31 Mar 2021 15:45:46 +0200 Subject: [PATCH 05/23] mnesia: copy table from a specified node When a new node is added to the mnesia cluster, it selects a random node to copy the table from if no 'master' node is defined (set master node is not mandatory). by setting arg: 'copy_from_node', operator is now free to choice which node to copy from. default is 'undefined', means disable. note, if the specified node is not in the candidates list, it will fallback to default behavior. --- lib/mnesia/src/mnesia_loader.erl | 14 +++++++++++++- lib/mnesia/src/mnesia_monitor.erl | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/mnesia/src/mnesia_loader.erl b/lib/mnesia/src/mnesia_loader.erl index 6f4bf36bb584..a388dda234e1 100644 --- a/lib/mnesia/src/mnesia_loader.erl +++ b/lib/mnesia/src/mnesia_loader.erl @@ -211,7 +211,19 @@ do_get_network_copy(Tab, Reason, _Ns, unknown, _Cs) -> verbose("Local table copy of ~0tp (~0p) has recently been deleted, ignored.~n", [Tab,Reason]), {not_loaded, storage_unknown}; do_get_network_copy(Tab, Reason, Ns, Storage, Cs) -> - [Node | Tail] = Ns, + [Node | Tail] = + case ?catch_val(copy_from_node) of + undefined -> Ns; + CPNode when is_atom(CPNode) -> + case lists:member(CPNode, Ns) of + true -> + [CPNode | Ns -- [CPNode]]; + false -> + Ns + end; + _ -> + Ns + end, case lists:member(Node,val({current, db_nodes})) of true -> dbg_out("Getting table ~0tp (~0p) from node ~0p: ~0tp~n", diff --git a/lib/mnesia/src/mnesia_monitor.erl b/lib/mnesia/src/mnesia_monitor.erl index 42a240835228..e082e286489d 100644 --- a/lib/mnesia/src/mnesia_monitor.erl +++ b/lib/mnesia/src/mnesia_monitor.erl @@ -691,6 +691,7 @@ env() -> pid_sort_order, no_table_loaders, dc_dump_limit, + copy_from_node, send_compressed, max_transfer_size, send_table_batch_size, @@ -742,6 +743,8 @@ default_env(no_table_loaders) -> 2; default_env(dc_dump_limit) -> 4; +default_env(copy_from_node) -> + undefined; default_env(send_compressed) -> 0; default_env(max_transfer_size) -> @@ -796,6 +799,7 @@ do_check_type(pid_sort_order, "standard") -> standard; do_check_type(pid_sort_order, _) -> false; do_check_type(no_table_loaders, N) when is_integer(N), N > 0 -> N; do_check_type(dc_dump_limit,N) when is_number(N), N > 0 -> N; +do_check_type(copy_from_node, L) when is_atom(L) -> L; do_check_type(send_compressed, L) when is_integer(L), L >= 0, L =< 9 -> L; do_check_type(max_transfer_size, N) when is_integer(N), N > 0 -> N; do_check_type(send_table_batch_size, L) when is_integer(L), L >= 0 -> L; From c0f3fcf0f16afcf761f697cea6e98cb32874d1b0 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 16 Nov 2021 01:21:47 +0100 Subject: [PATCH 06/23] chore: export gen_tcp:ipv6_probe/0 as emqx's fork identifier --- lib/kernel/src/gen_tcp.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/kernel/src/gen_tcp.erl b/lib/kernel/src/gen_tcp.erl index 794f4345714a..374b54c795bd 100644 --- a/lib/kernel/src/gen_tcp.erl +++ b/lib/kernel/src/gen_tcp.erl @@ -250,6 +250,7 @@ way, option `send_timeout` comes in handy. -export([send/2, recv/2, recv/3, unrecv/2]). -export([controlling_process/2]). -export([fdopen/2]). +-export([ipv6_probe/0]). -include("inet_int.hrl"). -include("file.hrl"). @@ -398,6 +399,8 @@ As returned by [`accept/1,2`](`accept/1`) and [`connect/3,4`](`connect/3`). %% -define(DBG(T), erlang:display({{self(), ?MODULE, ?LINE, ?FUNCTION_NAME}, T})). +-spec ipv6_probe() -> true. +ipv6_probe() -> true. %% %% Connect a socket From 96cdd37b36ce0974e24fb770fe646b832f8c67d3 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 24 Jan 2022 09:19:55 -0300 Subject: [PATCH 07/23] mnesia: Add post-commit hook Merge pull request #20 from emqx/mnesia-post-commit-hook-emqx-OTP-24.1.5 --- lib/mnesia/src/Makefile | 1 + lib/mnesia/src/mnesia.app.src | 1 + lib/mnesia/src/mnesia_hook.erl | 74 ++++++++++++++++++++++++++++++++++ lib/mnesia/src/mnesia_tm.erl | 1 + 4 files changed, 77 insertions(+) create mode 100644 lib/mnesia/src/mnesia_hook.erl diff --git a/lib/mnesia/src/Makefile b/lib/mnesia/src/Makefile index 72aa054fb326..ec540ce4e482 100644 --- a/lib/mnesia/src/Makefile +++ b/lib/mnesia/src/Makefile @@ -55,6 +55,7 @@ MODULES= \ mnesia_ext_sup \ mnesia_frag \ mnesia_frag_hash \ + mnesia_hook \ mnesia_index \ mnesia_kernel_sup \ mnesia_late_loader \ diff --git a/lib/mnesia/src/mnesia.app.src b/lib/mnesia/src/mnesia.app.src index 6ce0c68de8cd..23bc77c97701 100644 --- a/lib/mnesia/src/mnesia.app.src +++ b/lib/mnesia/src/mnesia.app.src @@ -15,6 +15,7 @@ mnesia_ext_sup, mnesia_frag, mnesia_frag_hash, + mnesia_hook, mnesia_index, mnesia_kernel_sup, mnesia_late_loader, diff --git a/lib/mnesia/src/mnesia_hook.erl b/lib/mnesia/src/mnesia_hook.erl new file mode 100644 index 000000000000..f9bada98c511 --- /dev/null +++ b/lib/mnesia/src/mnesia_hook.erl @@ -0,0 +1,74 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2021. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +-module(mnesia_hook). + +-include("mnesia.hrl"). + +-export([ + register_hook/2, + do_post_commit/2 + ]). + +-define(hook(NAME), {mnesia_hook, NAME}). + +-type post_commit_hook_data() :: + #{ node => node() + , ram_copies => list() + , disc_copies => list() + , disc_only_copies => list() + , ext => list() + , schema_ops => list() + }. + +-type post_commit_hook() :: fun((_Tid, post_commit_hook_data()) -> ok). + +-spec register_hook(post_commit, post_commit_hook()) -> ok | {error, term()}. +register_hook(post_commit, Hook) when is_function(Hook, 2) -> + persistent_term:put(?hook(post_commit), Hook); +register_hook(_, _) -> + {error, bad_type}. + +-spec do_post_commit(_Tid, #commit{}) -> ok. +do_post_commit(Tid, Commit) -> + case persistent_term:get(?hook(post_commit), undefined) of + undefined -> + ok; + Fun -> + #commit{ node = Node + , ram_copies = Ram + , disc_copies = Disc + , disc_only_copies = DiscOnly + , ext = Ext + , schema_ops = SchemaOps + } = Commit, + CommitData = #{ node => Node + , ram_copies => Ram + , disc_copies => Disc + , disc_only_copies => DiscOnly + , ext => Ext + , schema_ops => SchemaOps + }, + try Fun(Tid, CommitData) + catch EC:Err:Stack -> + mnesia_lib:error("Mnesia post_commit hook failed: ~p:~p~nStacktrace:~p~n", [EC, Err, Stack]) + end, + ok + end. diff --git a/lib/mnesia/src/mnesia_tm.erl b/lib/mnesia/src/mnesia_tm.erl index 8e5e517bceb8..ae7b89746946 100644 --- a/lib/mnesia/src/mnesia_tm.erl +++ b/lib/mnesia/src/mnesia_tm.erl @@ -1850,6 +1850,7 @@ do_commit(Tid, C, DumperMode) -> R4 = do_update(Tid, disc_only_copies, C#commit.disc_only_copies, R3), R5 = do_update_ext(Tid, C#commit.ext, R4), mnesia_subscr:report_activity(Tid), + mnesia_hook:do_post_commit(Tid, C), R5. %% This could/should be optimized From 45d7607614dc4f4dcbe878c31c7c5bc6d9653ef6 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 31 May 2022 14:09:10 -0300 Subject: [PATCH 08/23] feat: add minimal/experimental OCSP server support (TLS 1.2 and 1.3) Merge pull request #25 from thalesmg/ocsp-stapling-tmg-otp24 --- lib/ssl/src/ssl.erl | 13 +++- lib/ssl/src/ssl_handshake.erl | 32 ++++++++- lib/ssl/src/tls_dtls_server_connection.erl | 21 ++++-- lib/ssl/src/tls_handshake_1_3.erl | 37 +++++++++-- lib/ssl/src/tls_server_connection_1_3.erl | 14 +++- lib/ssl/test/make_certs.erl | 16 +++++ lib/ssl/test/openssl_stapling_SUITE.erl | 75 ++++++++++++++++++++-- lib/ssl/test/ssl_test_lib.erl | 11 ++++ 8 files changed, 200 insertions(+), 19 deletions(-) diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl index 5ab8c07ce622..040a5ad78d88 100644 --- a/lib/ssl/src/ssl.erl +++ b/lib/ssl/src/ssl.erl @@ -3705,6 +3705,7 @@ ssl_options() -> max_fragment_length, next_protocol_selector, next_protocols_advertised, stapling, + certificate_status, padding_check, partial_chain, password, @@ -3740,10 +3741,11 @@ update_options(Opts, Role, InheritedSslOpts) when is_map(InheritedSslOpts) -> Env = #{role => Role, validate_certs_or_anon_ciphers => Role == server}, process_options(UserSslOpts, InheritedSslOpts, Env). -process_options(UserSslOpts, SslOpts0, Env) -> +process_options(UserSslOpts, SslOptsIn, Env) -> %% Reverse option list so we get the last set option if set twice, %% users depend on it. UserSslOptsMap = proplists:to_map(lists:reverse(UserSslOpts)), + SslOpts0 = opt_certificate_status(UserSslOptsMap, SslOptsIn, Env), SslOpts1 = opt_protocol_versions(UserSslOptsMap, SslOpts0, Env), SslOpts2 = opt_verification(UserSslOptsMap, SslOpts1, Env), SslOpts3 = opt_certs(UserSslOptsMap, SslOpts2, Env), @@ -4212,6 +4214,15 @@ opt_stapling(UserOpts, Opts, #{role := server}) -> assert_client_only(stapling, UserOpts), Opts. +opt_certificate_status(UserOpts, Opts, #{role := _Role}) -> + {_, CertificateStatus} = get_opt(certificate_status, undefined, UserOpts, Opts), + case CertificateStatus of + undefined -> ok; + #certificate_status{} -> ok; + _Value -> option_error(certificate_status, CertificateStatus) + end, + Opts#{certificate_status => CertificateStatus}. + opt_sni(UserOpts, #{versions := _Versions} = Opts, #{role := server}) -> {_, SniHosts} = get_opt_list(sni_hosts, [], UserOpts, Opts), %% Postpone option checking until all other options are checked FIXME diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl index 9cb75753ec53..954f9f8b3e61 100644 --- a/lib/ssl/src/ssl_handshake.erl +++ b/lib/ssl/src/ssl_handshake.erl @@ -634,6 +634,9 @@ encode_handshake(#certificate_request{certificate_types = CertTypes, <> }; +encode_handshake(#certificate_status{status_type = StatusType, response = Response}, _Version) -> + Size = byte_size(Response), + {?CERTIFICATE_STATUS, <>}; encode_handshake(#server_hello_done{}, _Version) -> {?SERVER_HELLO_DONE, <<>>}; encode_handshake(#client_key_exchange{exchange_keys = ExchangeKeys}, _Version) -> @@ -837,7 +840,13 @@ encode_cert_status_req( request_extensions = ReqExtns}) -> ResponderIDListBin = encode_responderID_list(ResponderIDList), ReqExtnsBin = encode_request_extensions(ReqExtns), - <>. + <>; +encode_cert_status_req(_StatusType, #certificate_status{} = Status) -> + Version = {3, 4}, + {_, EncStatus} = encode_handshake(Status, Version), + EncStatus; +encode_cert_status_req(_StatusType, _Value) -> + <<>>. encode_responderID_list([]) -> <>; @@ -1524,7 +1533,8 @@ handle_client_hello_extensions(RecordCB, Random, ClientCipherSuites, ec_point_formats => server_ecc_extension(Version, maps:get(ec_point_formats, Exts, undefined)), use_srtp => use_srtp_ext(Opts), - max_frag_enum => ServerMaxFragEnum + max_frag_enum => ServerMaxFragEnum, + status_request => handle_status_request(Opts, Exts) }, %% If we receive an ALPN extension and have ALPN configured for this connection, @@ -1597,6 +1607,19 @@ handle_server_hello_extensions(RecordCB, Random, CipherSuite, end end. +handle_status_request(SSLOptions, ClientExtensions) -> + case {SSLOptions, ClientExtensions} of + { #{certificate_status := #certificate_status{}} + , #{status_request := #certificate_status_request{}} + } -> + #certificate_status_request{ + status_type = ?CERTIFICATE_STATUS_TYPE_OCSP, + request = <<>> + }; + _ -> + undefined + end. + select_curve(Client, Server) -> select_curve(Client, Server, false). @@ -3151,6 +3174,11 @@ decode_extensions(<> -> decode_extensions(Rest, Version, MessageType, Acc#{status_request => #certificate_status{response = ASN1OCSPResponse}}); + <> -> + decode_extensions(Rest, Version, MessageType, + Acc#{status_request => #certificate_status_request{}}); _Other -> decode_extensions(Rest, Version, MessageType, Acc) end; diff --git a/lib/ssl/src/tls_dtls_server_connection.erl b/lib/ssl/src/tls_dtls_server_connection.erl index d34eead9c8ae..b9550d69714a 100644 --- a/lib/ssl/src/tls_dtls_server_connection.erl +++ b/lib/ssl/src/tls_dtls_server_connection.erl @@ -349,12 +349,19 @@ do_server_hello(Type, #{next_protocol_negotiation := NextProtocols} = handshake_env = HsEnv, session = #session{session_id = SessId}, connection_states = ConnectionStates0, - ssl_options = #{versions := [HighestVersion|_]}} + ssl_options = #{versions := [HighestVersion|_]} = SSLOpts0} = State0) when is_atom(Type) -> %% TLS 1.3 - Section 4.1.3 %% Override server random values for TLS 1.3 downgrade protection mechanism. ConnectionStates1 = update_server_random(ConnectionStates0, Version, HighestVersion), - State1 = State0#state{connection_states = ConnectionStates1}, + SSLOpts1 = case {SSLOpts0, ServerHelloExt} of + { #{certificate_status := #certificate_status{}} + , #{status_request := #certificate_status_request{}} + } -> SSLOpts0; + _ -> SSLOpts0#{certificate_status => undefined} + end, + State1 = State0#state{connection_states = ConnectionStates1, + ssl_options = SSLOpts1}, ServerHello = ssl_handshake:server_hello(SessId, ssl:tls_version(Version), ConnectionStates1, ServerHelloExt), @@ -465,8 +472,14 @@ server_hello_done(State, Connection) -> server_certify_and_key_exchange(State0, Connection) -> State1 = certify_server(State0, Connection), - State2 = key_exchange(State1, Connection), - request_client_cert(State2, Connection). + State2 = certificate_status(State1, Connection), + State3 = key_exchange(State2, Connection), + request_client_cert(State3, Connection). + +certificate_status(#state{ssl_options = #{certificate_status := #certificate_status{} = Status}} = State, Connection) -> + Connection:queue_handshake(Status, State); +certificate_status(State, _) -> + State. certify_server(#state{handshake_env = #handshake_env{kex_algorithm = KexAlg}} = State, _) when KexAlg == dh_anon; diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl index 776e2a580872..2cf3b4d7c82c 100644 --- a/lib/ssl/src/tls_handshake_1_3.erl +++ b/lib/ssl/src/tls_handshake_1_3.erl @@ -288,10 +288,11 @@ certificate(undefined, _, _, _, client) -> {ok, #certificate_1_3{ certificate_request_context = <<>>, certificate_list = []}}; -certificate([OwnCert], CertDbHandle, CertDbRef, _CRContext, Role) -> +certificate([OwnCert], CertDbHandle, CertDbRef, CRContext, Role) -> case ssl_certificate:certificate_chain(OwnCert, CertDbHandle, CertDbRef) of {ok, _, Chain} -> - CertList = chain_to_cert_list(Chain), + CertList0 = chain_to_cert_list(Chain), + CertList = maybe_add_certificate_entry_extensions(CertList0, CRContext), %% If this message is in response to a CertificateRequest, the value of %% certificate_request_context in that message. Otherwise (in the case %%of server authentication), this field SHALL be zero length. @@ -311,8 +312,9 @@ certificate([OwnCert], CertDbHandle, CertDbRef, _CRContext, Role) -> certificate_request_context = <<>>, certificate_list = []}} end; -certificate([_,_| _] = Chain, _,_,_,_) -> - CertList = chain_to_cert_list(Chain), +certificate([_,_| _] = Chain, _,_,CRContext,_) -> + CertList0 = chain_to_cert_list(Chain), + CertList = maybe_add_certificate_entry_extensions(CertList0, CRContext), {ok, #certificate_1_3{ certificate_request_context = <<>>, certificate_list = CertList}}. @@ -1247,17 +1249,20 @@ update_start_state(State, Map) -> PeerPublicKey = maps:get(peer_public_key, Map, undefined), ALPNProtocol = maps:get(alpn, Map, undefined), Random = maps:get(random, Map), + StatusRequest = maps:get(status_request, Map, undefined), update_start_state(State, Cipher, KeyShare, SessionId, Group, SelectedSignAlg, PeerPublicKey, - ALPNProtocol, Random). + ALPNProtocol, Random, StatusRequest). %% update_start_state(#state{connection_states = ConnectionStates0, handshake_env = #handshake_env{} = HsEnv, + protocol_specific = ProtocolSpecific0, static_env = #static_env{role = Role}, connection_env = CEnv, session = Session} = State, Cipher, KeyShare, SessionId, - Group, SelectedSignAlg, PeerPublicKey, ALPNProtocol, Random) -> + Group, SelectedSignAlg, PeerPublicKey, ALPNProtocol, Random, + StatusRequest) -> #{security_parameters := SecParamsR0} = PendingRead = maps:get(pending_read, ConnectionStates0), #{security_parameters := SecParamsW0} = PendingWrite = @@ -1271,9 +1276,11 @@ update_start_state(#state{connection_states = ConnectionStates0, ConnectionStates = ConnectionStates0#{pending_read => PendingRead#{security_parameters => SecParamsR}, pending_write => PendingWrite#{security_parameters => SecParamsW}}, + ProtocolSpecific = ProtocolSpecific0#{status_request => StatusRequest}, State#state{connection_states = ConnectionStates, handshake_env = HsEnv#handshake_env{alpn = ALPNProtocol, key_share = KeyShare}, + protocol_specific = ProtocolSpecific, session = Session#session{session_id = SessionId, ecc = Group, sign_alg = SelectedSignAlg, @@ -1971,3 +1978,21 @@ plausible_missing_chain([_] = EncodedChain, undefined, SignAlg, Key, Session0) - }; plausible_missing_chain(_,Plausible,_,_,_) -> Plausible. + +maybe_add_certificate_entry_extensions( + [ServerCertEntry = #certificate_entry{} | Rest], + #{ status_request := #certificate_status_request{} = Req + , certificate_status := #certificate_status{} = Status + }) -> + [ ServerCertEntry#certificate_entry{ + extensions = + #{ status_request => + Req#certificate_status_request{ + status_type = ?CERTIFICATE_STATUS_TYPE_OCSP, + request = Status + } + } + } + | Rest]; +maybe_add_certificate_entry_extensions(CertList, _CRContext) -> + CertList. diff --git a/lib/ssl/src/tls_server_connection_1_3.erl b/lib/ssl/src/tls_server_connection_1_3.erl index 8801f00b6787..c984729c8dbb 100644 --- a/lib/ssl/src/tls_server_connection_1_3.erl +++ b/lib/ssl/src/tls_server_connection_1_3.erl @@ -477,6 +477,7 @@ do_handle_client_hello(#client_hello{cipher_suites = ClientCiphers, State2 end, + StatusRequest = maps:get(status_request, Extensions, undefined), State4 = tls_handshake_1_3:update_start_state(State3, #{cipher => Cipher, key_share => KeyShare, @@ -485,7 +486,8 @@ do_handle_client_hello(#client_hello{cipher_suites = ClientCiphers, sign_alg => SelectedSignAlg, peer_public_key => ClientPubKey, alpn => ALPNProtocol, - random => Random}), + random => Random, + status_request => StatusRequest}), %% 4.1.4. Hello Retry Request %% @@ -844,11 +846,19 @@ maybe_send_certificate_request(#state{static_env = #static_env{protocol_cb = Con maybe_send_certificate(State, PSK) when PSK =/= undefined -> {ok, State}; maybe_send_certificate(#state{session = #session{own_certificates = OwnCerts}, + protocol_specific = ProtocolSpecific, + ssl_options = SslOpts, static_env = #static_env{ protocol_cb = Connection, cert_db = CertDbHandle, cert_db_ref = CertDbRef}} = State, _) -> - case tls_handshake_1_3:certificate(OwnCerts, CertDbHandle, CertDbRef, <<>>, server) of + %% hack: apparently, CRContext is not used by the server (whatever that may be...) + StatusRequest = maps:get(status_request, ProtocolSpecific, undefined), + CertificateStatus = maps:get(certificate_status, SslOpts, undefined), + CRContext = #{ status_request => StatusRequest + , certificate_status => CertificateStatus + }, + case tls_handshake_1_3:certificate(OwnCerts, CertDbHandle, CertDbRef, CRContext, server) of {ok, Certificate} -> {ok, Connection:queue_handshake(Certificate, State)}; Error -> diff --git a/lib/ssl/test/make_certs.erl b/lib/ssl/test/make_certs.erl index 42963b77cb60..ff4d24c8c9e2 100644 --- a/lib/ssl/test/make_certs.erl +++ b/lib/ssl/test/make_certs.erl @@ -187,6 +187,22 @@ revoke(Root, CA, User, C) -> cmd(Cmd, Env), gencrl(Root, CA, C). +make_ocsp_response(Port, Root, CA, User, Issuer, C) -> + UsrCert = filename:join([Root, User, "cert.pem"]), + IssuerCert = filename:join([Root, Issuer, "cert.pem"]), + CACertFile = filename:join([Root, CA, "cert.pem"]), + OCSPResp = filename:join([Root, User, "ocsp.resp"]), + Cmd = [C#config.openssl_cmd, " ocsp" + " -CAfile ", CACertFile, + " -url ", "http://localhost:" ++ integer_to_list(Port), + " -no_nonce", + " -respout ", OCSPResp, + " -issuer ", IssuerCert, + " -cert ", UsrCert], + Env = [{"ROOTDIR", filename:absname(Root)}], + cmd(Cmd, Env), + OCSPResp. + %% Remove the certificate's entry from the database. The OCSP responder %% will consider the certificate to be unknown. remove_entry(Root, CA, User, C) -> diff --git a/lib/ssl/test/openssl_stapling_SUITE.erl b/lib/ssl/test/openssl_stapling_SUITE.erl index 1a8fdee54d07..ff7f7a0c5d47 100644 --- a/lib/ssl/test/openssl_stapling_SUITE.erl +++ b/lib/ssl/test/openssl_stapling_SUITE.erl @@ -22,6 +22,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("public_key/include/public_key.hrl"). +-include("ssl_handshake.hrl"). -include("ssl_test_lib.hrl"). %% Callback functions @@ -43,7 +44,8 @@ staple_with_nonce/0, staple_with_nonce/1, cert_status_revoked/0, cert_status_revoked/1, cert_status_undetermined/0, cert_status_undetermined/1, - staple_missing/0, staple_missing/1 + staple_missing/0, staple_missing/1, + stapling_server/0, stapling_server/1 ]). %% spawn export @@ -62,11 +64,11 @@ all() -> {group, 'dtlsv1.2'}]. groups() -> - [{'tlsv1.3', [], ocsp_tests()}, + [{'tlsv1.3', [], ocsp_tests() ++ ocsp_server_tests()}, {'tlsv1.3_issuer_nonce', [], [staple_by_issuer, staple_with_nonce]}, {no_next_update, [], [{group, 'tlsv1.3'}]}, {no_resp_certs, [], [{group, 'tlsv1.3_issuer_nonce'}]}, - {'tlsv1.2', [], ocsp_tests()}, + {'tlsv1.2', [], ocsp_tests() ++ ocsp_server_tests()}, {'dtlsv1.2', [], ocsp_tests()}]. ocsp_tests() -> @@ -85,6 +87,9 @@ negative() -> cert_status_undetermined, staple_missing]. +ocsp_server_tests() -> + [stapling_server]. + %%-------------------------------------------------------------------- init_per_suite(Config0) -> Config = [{debug, ?DEBUG}] ++ @@ -130,7 +135,8 @@ init_per_testcase_helper(Testcase, Config0) -> staple_by_trusted => "erlangCA", staple_by_designated => "b.server", staple_not_designated => "a.server", - staple_wrong_issuer => "localhost"}, + staple_wrong_issuer => "localhost", + stapling_server => "b.server"}, ResponderFolder = maps:get(Testcase, TestcaseMapping, Default), Config = start_ocsp_responder( [{responder_folder, ResponderFolder} | Config0]) ++ Config0, @@ -282,6 +288,61 @@ stapling_negative_helper(Config, CACertsPath, ServerVariant, ExpectedError) -> true = is_pid(Client), ssl_test_lib:check_client_alert(Client, ExpectedError). +%%-------------------------------------------------------------------- +stapling_server() -> + [{doc, "Verify basic OCSP stapling works (server side)"}]. +stapling_server(Config0) + when is_list(Config0) -> + PrivDir = proplists:get_value(priv_dir, Config0), + ResponderPort = proplists:get_value(responder_port, Config0), + OCSPRespPath = make_certs:make_ocsp_response(ResponderPort, PrivDir, "otpCA", + "server", "b.server", + make_certs:default_config()), + {ok, OCSPRespDer} = file:read_file(OCSPRespPath), + ServerOpts = proplists:get_value(server_opts, Config0, []), + Config = [ {server_opts, [ {sni_fun, + fun(SN) -> ocsp_sni_fun(SN, OCSPRespDer) end} + | ServerOpts]} + | Config0], + stapling_server_helper(Config, []). + +stapling_server_helper(Config, Opts) -> + Data = "ping", %% 4 bytes + %% GroupName = undefined, + %% ServerOpts = [{group, GroupName}], + ServerOpts = [], + Server = ssl_test_lib:start_server(erlang, + [{options, ServerOpts}], + Config), + Port = ssl_test_lib:inet_port(Server), + + ClientOpts = ssl_test_lib:ssl_options(Opts, Config), + Client = ssl_test_lib:start_client(openssl, + [{port, Port}, + {options, ClientOpts}, + {server_name_indication, "server"}, + {ocsp_stapling, true}, + {ocsp_nonce, false}, + {debug_openssl, false}], + Config), + true = is_pid(Client), + ct:sleep(1000), + {messages, ClientMsgs} = process_info(Client, messages), + [OCSPOutput] = [Output || + {_Port, {data, Output}} <- ClientMsgs, + case re:run(Output, "OCSP response") of + {match, _} -> true; + _ -> false + end], + {match, _} = re:run(OCSPOutput, "Response Status: successful"), + {match, _} = re:run(OCSPOutput, "Cert Status:"), + + ssl_test_lib:check_active_receive(Server, "Hello world"), + ssl_test_lib:send(Client, Data), + Data = ssl_test_lib:check_active_receive(Server, Data), + ssl_test_lib:close(Server), + ssl_test_lib:close(Client). + %%-------------------------------------------------------------------- %% Internal functions ----------------------------------------------- %%-------------------------------------------------------------------- @@ -364,3 +425,9 @@ get_free_port() -> {ok, Port} = inet:port(Listen), ok = gen_tcp:close(Listen), Port. + +ocsp_sni_fun(_Servername, OCSPRespDer) -> + [{certificate_status, #certificate_status{ + status_type = 1, + response = OCSPRespDer + }}]. diff --git a/lib/ssl/test/ssl_test_lib.erl b/lib/ssl/test/ssl_test_lib.erl index a1346381307d..a964ccb55ebc 100644 --- a/lib/ssl/test/ssl_test_lib.erl +++ b/lib/ssl/test/ssl_test_lib.erl @@ -2347,6 +2347,7 @@ start_client(openssl, Port, ClientOpts, Config) -> HostName = proplists:get_value(hostname, ClientOpts, net_adm:localhost()), SNI = openssl_sni(proplists:get_value(server_name_indication, ClientOpts, undefined)), Debug = openssl_debug_options(DOpenssl), + OCSPStatus = openssl_ocsp_status(proplists:get_value(ocsp_stapling, ClientOpts, undefined)), Exe = "openssl", Args0 = case Groups0 of @@ -2365,6 +2366,7 @@ start_client(openssl, Port, ClientOpts, Config) -> Reconnect ++ MaxFragLen ++ SessionArgs ++ + OCSPStatus ++ Debug; Group -> ["s_client", @@ -2382,6 +2384,7 @@ start_client(openssl, Port, ClientOpts, Config) -> Reconnect ++ MaxFragLen ++ SessionArgs ++ + OCSPStatus ++ Debug end, Args = maybe_force_ipv4(Args0), @@ -2525,6 +2528,14 @@ openssl_debug_options(true) -> ["-msg", "-debug"]; openssl_debug_options(false) -> []. + +openssl_ocsp_status(undefined) -> + []; +openssl_ocsp_status(true) -> + ["-status"]; +openssl_ocsp_status(false) -> + []. + %% openssl_debug_options(PrivDir, true) -> case is_keylogfile_supported() of From 42f7d7565fab5653d450877f41132eaecc369a37 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 8 Dec 2022 10:47:12 +0300 Subject: [PATCH 09/23] fix(ipv6_probe): handle local sockets properly They are legal `Address`es to connect to. --- lib/kernel/src/gen_tcp.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/kernel/src/gen_tcp.erl b/lib/kernel/src/gen_tcp.erl index 374b54c795bd..123eb19b32f0 100644 --- a/lib/kernel/src/gen_tcp.erl +++ b/lib/kernel/src/gen_tcp.erl @@ -615,6 +615,9 @@ connect_0(Address, Port, Opts, Timeout) -> Error -> Error end. +maybe_ipv6({local, _}, Opts, _TryIpv6) -> + %% unapplicable to local sockets + Opts; maybe_ipv6(Host, Opts, TryIpv6) -> case lists:member(inet, Opts) orelse lists:member(inet6, Opts) of true -> @@ -629,8 +632,8 @@ maybe_ipv6(Host, Opts, TryIpv6) -> Opts end. -maybe_ipv6_1(Host, Opts) when tuple_size(Host) =:= 4 -> Opts; -maybe_ipv6_1(Host, Opts) when tuple_size(Host) =:= 8 -> [inet6 | Opts]. +maybe_ipv6_1(Ip, Opts) when tuple_size(Ip) =:= 4 -> Opts; +maybe_ipv6_1(Ip, Opts) when tuple_size(Ip) =:= 8 -> [inet6 | Opts]. maybe_ipv6_2(Host, Opts) -> case inet:parse_address(Host) of From 45e324b12251ea575d4711acc0477af4a2170383 Mon Sep 17 00:00:00 2001 From: Serge Tupchii Date: Tue, 21 Mar 2023 18:02:50 +0200 Subject: [PATCH 10/23] feat(mnesia): implement unregister_hook/1 in mnesia_hook.erl --- lib/mnesia/src/mnesia_hook.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/mnesia/src/mnesia_hook.erl b/lib/mnesia/src/mnesia_hook.erl index f9bada98c511..e23c8a6471e1 100644 --- a/lib/mnesia/src/mnesia_hook.erl +++ b/lib/mnesia/src/mnesia_hook.erl @@ -24,6 +24,7 @@ -export([ register_hook/2, + unregister_hook/1, do_post_commit/2 ]). @@ -46,6 +47,12 @@ register_hook(post_commit, Hook) when is_function(Hook, 2) -> register_hook(_, _) -> {error, bad_type}. +-spec unregister_hook(post_commit) -> boolean() | {error, term()}. +unregister_hook(post_commit) -> + persistent_term:erase(?hook(post_commit)); +unregister_hook(_) -> + {error, bad_type}. + -spec do_post_commit(_Tid, #commit{}) -> ok. do_post_commit(Tid, Commit) -> case persistent_term:get(?hook(post_commit), undefined) of From 2c9fd27524ad1a61029d42e359917e70ea3042c9 Mon Sep 17 00:00:00 2001 From: Serge Tupchii Date: Mon, 21 Aug 2023 19:32:25 +0300 Subject: [PATCH 11/23] fix(mnesia): exclude sensitive data from mnesia_hook log message Closes: EMQX-10390 --- lib/mnesia/src/mnesia_hook.erl | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/mnesia/src/mnesia_hook.erl b/lib/mnesia/src/mnesia_hook.erl index e23c8a6471e1..dc5d4afbb704 100644 --- a/lib/mnesia/src/mnesia_hook.erl +++ b/lib/mnesia/src/mnesia_hook.erl @@ -74,8 +74,32 @@ do_post_commit(Tid, Commit) -> , schema_ops => SchemaOps }, try Fun(Tid, CommitData) - catch EC:Err:Stack -> - mnesia_lib:error("Mnesia post_commit hook failed: ~p:~p~nStacktrace:~p~n", [EC, Err, Stack]) + catch EC:Err:St -> + CommitTabs = commit_tabs(Ram, Disc, DiscOnly, Ext), + mnesia_lib:error("Mnesia post_commit hook failed: ~p:~p~nStacktrace:~p~nCommit tables:~p~n", + [EC, Err, stack_without_args(St), CommitTabs]) end, ok end. + +%% May be helpful for debugging +commit_tabs(Ram, Disc, DiscOnly, Ext) -> + Acc = tabs_from_ops(Ram, []), + Acc1 = tabs_from_ops(Disc, Acc), + Acc2 = tabs_from_ops(DiscOnly, Acc1), + lists:uniq(tabs_from_ops(Ext, Acc2)). + +tabs_from_ops([{{Tab, _K}, _Val, _Op} | T], Acc) -> + tabs_from_ops(T, [Tab | Acc]); +tabs_from_ops([_ | T], Acc) -> + tabs_from_ops(T, Acc); +tabs_from_ops([], Acc) -> + Acc. + +%% Args may contain sensitive data +stack_without_args([{M, F, Args, Info} | T]) when is_list(Args) -> + [{M, F, length(Args), Info} | stack_without_args(T)]; +stack_without_args([StItem | T] ) -> + [StItem | stack_without_args(T)]; +stack_without_args([]) -> + []. From 626ed9f6b102c9dd8c1a77dcda385a482e3bb7d2 Mon Sep 17 00:00:00 2001 From: Serge Tupchii Date: Tue, 22 Aug 2023 15:23:59 +0300 Subject: [PATCH 12/23] fix(mnesia): report mnesia_hook post_commit failures at debug level --- lib/mnesia/src/mnesia_hook.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mnesia/src/mnesia_hook.erl b/lib/mnesia/src/mnesia_hook.erl index dc5d4afbb704..cb23868db68d 100644 --- a/lib/mnesia/src/mnesia_hook.erl +++ b/lib/mnesia/src/mnesia_hook.erl @@ -76,8 +76,8 @@ do_post_commit(Tid, Commit) -> try Fun(Tid, CommitData) catch EC:Err:St -> CommitTabs = commit_tabs(Ram, Disc, DiscOnly, Ext), - mnesia_lib:error("Mnesia post_commit hook failed: ~p:~p~nStacktrace:~p~nCommit tables:~p~n", - [EC, Err, stack_without_args(St), CommitTabs]) + mnesia_lib:dbg_out("Mnesia post_commit hook failed: ~p:~p~nStacktrace:~p~nCommit tables:~p~n", + [EC, Err, stack_without_args(St), CommitTabs]) end, ok end. From 407a58416544b7d1a182bb7166a844356187edef Mon Sep 17 00:00:00 2001 From: Serge Tupchii Date: Wed, 13 Dec 2023 19:41:06 +0200 Subject: [PATCH 13/23] feat: implement `mnesia:match_delete/2` function --- lib/mnesia/src/mnesia.erl | 15 +- lib/mnesia/src/mnesia_checkpoint.erl | 9 +- lib/mnesia/src/mnesia_dumper.erl | 8 +- lib/mnesia/src/mnesia_log.erl | 7 +- lib/mnesia/src/mnesia_subscr.erl | 11 +- lib/mnesia/src/mnesia_tm.erl | 2 +- lib/mnesia/test/Makefile | 3 +- lib/mnesia/test/mnesia_SUITE.erl | 3 +- lib/mnesia/test/mnesia_match_delete_test.erl | 217 +++++++++++++++++++ 9 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 lib/mnesia/test/mnesia_match_delete_test.erl diff --git a/lib/mnesia/src/mnesia.erl b/lib/mnesia/src/mnesia.erl index 0788e57a9456..f2ccda5118ae 100644 --- a/lib/mnesia/src/mnesia.erl +++ b/lib/mnesia/src/mnesia.erl @@ -341,6 +341,7 @@ are checked, and finally, the default value is chosen. read_table_property/2, write_table_property/2, delete_table_property/2, change_table_frag/2, clear_table/1, clear_table/4, + match_delete/2, %% Table load dump_tables/1, wait_for_tables/2, force_load_table/1, @@ -4422,21 +4423,25 @@ Delete all entries in the table `Tab`. """. -spec clear_table(Tab::table()) -> t_result('ok'). clear_table(Tab) -> + match_delete(Tab, '_'). + +-spec match_delete(Tab::table(), ets:match_pattern()) -> t_result('ok'). +match_delete(Tab, Pattern) -> case get(mnesia_activity_state) of State = {Mod, Tid, _Ts} when element(1, Tid) =/= tid -> - transaction(State, fun() -> do_clear_table(Tab) end, [], infinity, Mod, sync); + transaction(State, fun() -> do_clear_table(Tab, Pattern) end, [], infinity, Mod, sync); undefined -> - transaction(undefined, fun() -> do_clear_table(Tab) end, [], infinity, ?DEFAULT_ACCESS, sync); + transaction(undefined, fun() -> do_clear_table(Tab, Pattern) end, [], infinity, ?DEFAULT_ACCESS, sync); _ -> %% Not allowed for clear_table mnesia:abort({aborted, nested_transaction}) end. -do_clear_table(Tab) -> +do_clear_table(Tab, Pattern) -> case get(mnesia_activity_state) of {?DEFAULT_ACCESS, Tid, Ts} -> - clear_table(Tid, Ts, Tab, '_'); + clear_table(Tid, Ts, Tab, Pattern); {Mod, Tid, Ts} -> - Mod:clear_table(Tid, Ts, Tab, '_'); + Mod:clear_table(Tid, Ts, Tab, Pattern); _ -> abort(no_transaction) end. diff --git a/lib/mnesia/src/mnesia_checkpoint.erl b/lib/mnesia/src/mnesia_checkpoint.erl index 57ac12b3db6f..2bec0072344e 100644 --- a/lib/mnesia/src/mnesia_checkpoint.erl +++ b/lib/mnesia/src/mnesia_checkpoint.erl @@ -31,6 +31,7 @@ tm_prepare/1, tm_retain/4, tm_retain/5, + tm_retain/6, tm_enter_pending/1, tm_enter_pending/3, tm_exit_pending/1 @@ -149,7 +150,6 @@ enter_still_pending([Tid | Tids], Tab) -> enter_still_pending([], _Tab) -> ok. - %% Looks up checkpoints for functions in mnesia_tm. tm_retain(Tid, Tab, Key, Op) -> case val({Tab, commit_work}) of @@ -158,11 +158,14 @@ tm_retain(Tid, Tab, Key, Op) -> _ -> undefined end. - + tm_retain(Tid, Tab, Key, Op, Checkpoints) -> + tm_retain(Tid, Tab, Key, Op, Checkpoints, '_'). + +tm_retain(Tid, Tab, Key, Op, Checkpoints, Obj) -> case Op of clear_table -> - OldRecs = mnesia_lib:db_match_object(Tab, '_'), + OldRecs = mnesia_lib:db_match_object(Tab, Obj), send_group_retain(OldRecs, Checkpoints, Tid, Tab, []), OldRecs; _ -> diff --git a/lib/mnesia/src/mnesia_dumper.erl b/lib/mnesia/src/mnesia_dumper.erl index 2d56bbd6fc4c..a6cf088864b8 100644 --- a/lib/mnesia/src/mnesia_dumper.erl +++ b/lib/mnesia/src/mnesia_dumper.erl @@ -405,8 +405,12 @@ dets_insert(Op,Tab,Key,Val, Storage0) -> dets_updated(Tab,Key), mnesia_lib:db_match_erase(Storage, Tab, Val); clear_table -> - dets_cleared(Tab), - ok = mnesia_lib:db_match_erase(Storage, Tab, '_') + %% Val is a match_delete pattern + case Val of + '_' -> dets_cleared(Tab); + _ -> dets_updated(Tab, Val) + end, + ok = mnesia_lib:db_match_erase(Storage, Tab, Val) end. dets_updated(Tab,Key) -> diff --git a/lib/mnesia/src/mnesia_log.erl b/lib/mnesia/src/mnesia_log.erl index e322e98f0213..ad5487cd8147 100644 --- a/lib/mnesia/src/mnesia_log.erl +++ b/lib/mnesia/src/mnesia_log.erl @@ -1025,9 +1025,14 @@ add_recs([LogH|Rest], N) LogH#log_header.log_kind == dcl_log, LogH#log_header.log_version >= "1.0" -> add_recs(Rest, N); -add_recs([{{Tab, _Key}, _Val, clear_table} | Rest], N) -> +add_recs([{{Tab, _Key}, '_', clear_table} | Rest], N) -> Size = ets:info(Tab, size), true = ets:delete_all_objects(Tab), add_recs(Rest, N+Size); +add_recs([{{Tab, _Key}, Pattern, clear_table} | Rest], N) -> + SizeBefore = ets:info(Tab, size), + true = ets:match_delete(Tab, Pattern), + SizeAfter = ets:info(Tab, size), + add_recs(Rest, N+SizeBefore-SizeAfter); add_recs([], N) -> N. diff --git a/lib/mnesia/src/mnesia_subscr.erl b/lib/mnesia/src/mnesia_subscr.erl index 4fc8c62454de..a9746812a28f 100644 --- a/lib/mnesia/src/mnesia_subscr.erl +++ b/lib/mnesia/src/mnesia_subscr.erl @@ -153,7 +153,7 @@ report_table_event(Tab, Tid, Obj, Op) -> report_table_event(Subscr, Tab, Tid, Obj, Op) -> report_table_event(Subscr, Tab, Tid, Obj, Op, undefined). -report_table_event({subscribers, S1, S2}, Tab, Tid, _Obj, clear_table, _Old) -> +report_table_event({subscribers, S1, S2}, Tab, Tid, '_' = _Obj, clear_table, _Old) -> What = {delete, {schema, Tab}, Tid}, deliver(S1, {mnesia_table_event, What}), TabDef = mnesia_schema:cs2list(?catch_val({Tab, cstruct})), @@ -164,6 +164,15 @@ report_table_event({subscribers, S1, S2}, Tab, Tid, _Obj, clear_table, _Old) -> What4 = {write, schema, {schema, Tab, TabDef}, [], Tid}, deliver(S2, {mnesia_table_event, What4}); +report_table_event({subscribers, S1, _S2}, Tab, Tid, Obj, clear_table, _Old) -> + %% Obj is a match pattern here. + %% Sending delete_object event is compatible with `mnesia_loader`, + %% that uses `db_match_erase/2` which actually removes records by pattern. + %% Extended event is omitted: it's possible to match and get `OldRecords`, + %% but the list can be quite large. + Standard = {delete_object, patch_record(Tab, Obj), Tid}, + deliver(S1, {mnesia_table_event, Standard}); + report_table_event({subscribers, Subscr, []}, Tab, Tid, Obj, Op, _Old) -> What = {Op, patch_record(Tab, Obj), Tid}, deliver(Subscr, {mnesia_table_event, What}); diff --git a/lib/mnesia/src/mnesia_tm.erl b/lib/mnesia/src/mnesia_tm.erl index ae7b89746946..a4471891f181 100644 --- a/lib/mnesia/src/mnesia_tm.erl +++ b/lib/mnesia/src/mnesia_tm.erl @@ -1973,7 +1973,7 @@ commit_del_object([H|R], Tid, Storage, Tab, K, Obj) when element(1, H) == index commit_clear([], _, _, _, _, _) -> ok; commit_clear([{checkpoints, CpList}|R], Tid, Storage, Tab, K, Obj) -> - mnesia_checkpoint:tm_retain(Tid, Tab, K, clear_table, CpList), + mnesia_checkpoint:tm_retain(Tid, Tab, K, clear_table, CpList, Obj), commit_clear(R, Tid, Storage, Tab, K, Obj); commit_clear([H|R], Tid, Storage, Tab, K, Obj) when element(1, H) == subscribers -> diff --git a/lib/mnesia/test/Makefile b/lib/mnesia/test/Makefile index adce21c9f372..6931731917d0 100644 --- a/lib/mnesia/test/Makefile +++ b/lib/mnesia/test/Makefile @@ -55,7 +55,8 @@ MODULES= \ mnesia_dbn_meters \ ext_test \ mnesia_index_plugin_test \ - mnesia_external_backend_test + mnesia_external_backend_test \ + mnesia_match_delete_test DocExamplesDir := ../doc/src/ diff --git a/lib/mnesia/test/mnesia_SUITE.erl b/lib/mnesia/test/mnesia_SUITE.erl index 39cc5fdbee87..c80bee4c5884 100644 --- a/lib/mnesia/test/mnesia_SUITE.erl +++ b/lib/mnesia/test/mnesia_SUITE.erl @@ -70,7 +70,7 @@ groups() -> [{light, [], [{group, install}, {group, nice}, {group, evil}, {group, mnesia_frag_test, light}, {group, qlc}, {group, index_plugins}, - {group, registry}, {group, config}, {group, examples}]}, + {group, registry}, {group, config}, {group, examples}, {group, match_delete}]}, {install, [], [{mnesia_install_test, all}]}, {nice, [], [{mnesia_nice_coverage_test, all}]}, {evil, [], [{mnesia_evil_coverage_test, all}]}, @@ -79,6 +79,7 @@ groups() -> {registry, [], [{mnesia_registry_test, all}]}, {config, [], [{mnesia_config_test, all}]}, {examples, [], [{mnesia_examples_test, all}]}, + {match_delete, [], [{mnesia_match_delete_test, all}]}, %% The 'medium' test suite verfies the ACID (atomicity, consistency %% isolation and durability) properties and various recovery scenarios %% These tests may take quite while to run. diff --git a/lib/mnesia/test/mnesia_match_delete_test.erl b/lib/mnesia/test/mnesia_match_delete_test.erl new file mode 100644 index 000000000000..c77a260fb0b7 --- /dev/null +++ b/lib/mnesia/test/mnesia_match_delete_test.erl @@ -0,0 +1,217 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2023. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% +-module(mnesia_match_delete_test). +-include("mnesia_test_lib.hrl"). + +-export([all/0, groups/0, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2]). + +-export([match_delete/1, + match_delete_checkpoint/1, + match_delete_subscribe/1, + match_delete_index/1, + match_delete_restart/1, + match_delete_dump_restart/1, + match_delete_frag/1]). + +all() -> + [match_delete, + match_delete_checkpoint, + match_delete_subscribe, + match_delete_index, + match_delete_restart, + match_delete_dump_restart, + match_delete_frag]. + +groups() -> + []. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +init_per_testcase(Func, Conf) -> + mnesia_test_lib:init_per_testcase(Func, Conf). + +end_per_testcase(Func, Conf) -> + mnesia_test_lib:end_per_testcase(Func, Conf). + +match_delete(suite) -> []; +match_delete(Config) when is_list(Config) -> + [Node1, Node2, Node3] = Nodes = ?acquire_nodes(3, Config), + Tab = match_delete_tab, + Def = [{ram_copies, [Node1]}, {disc_copies, [Node2]}, {disc_only_copies, [Node3]}], + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + ?match({atomic, ok}, write(Tab)), + ?match({atomic, ok}, mnesia:match_delete(Tab, {Tab, '_', bar})), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + ?verify_mnesia(Nodes, []). + +match_delete_checkpoint(suite) -> []; +match_delete_checkpoint(Config) when is_list(Config) -> + [Node1, Node2, Node3] = Nodes = ?acquire_nodes(3, Config), + Tab = match_delete_retain_tab, + Def = [{disc_copies, [Node1, Node2]}, {disc_only_copies, [Node3]}], + Checkpoint = ?FUNCTION_NAME, + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + ?match({atomic, ok}, write(Tab)), + + ?match({ok, Checkpoint, _}, mnesia:activate_checkpoint([{name, Checkpoint}, {max, [Tab]}])), + ?match({atomic, ok}, mnesia:match_delete(Tab, {Tab, '_', bar})), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + + File = "match_delete_backup.BUP", + ?match(ok, mnesia:backup_checkpoint(Checkpoint, File)), + ?match(ok, mnesia:deactivate_checkpoint(?FUNCTION_NAME)), + + ?match({atomic, [Tab]}, mnesia:restore(File, [{default_op, clear_tables}])), + ?match({atomic, [1,2,3,4,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + + ?match(ok, file:delete(File)), + ?verify_mnesia(Nodes, []). + +match_delete_subscribe(suite) -> []; +match_delete_subscribe(Config) when is_list(Config) -> + Nodes = ?acquire_nodes(3, Config), + Tab = match_delete_sub_tab, + Def = [{ram_copies, Nodes}], + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + ?match({atomic, ok}, write(Tab)), + Pattern = {Tab, '_', bar}, + ?match({ok, _}, mnesia:subscribe({table, Tab})), + ?match({atomic, ok}, mnesia:match_delete(Tab, Pattern)), + ?match_receive({mnesia_table_event, {delete_object, Pattern, _}}), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + ?verify_mnesia(Nodes, []). + +match_delete_index(suite) -> []; +match_delete_index(Config) when is_list(Config) -> + Nodes = ?acquire_nodes(3, Config), + {atomic, ok} = mnesia:create_table(match_delete_index, + [{index, [ix]}, {attributes, [key, ix, val]}, + {disc_copies, Nodes}]), + {atomic, ok} = mnesia:create_table(match_delete_index_ram, + [{index, [ix]}, {attributes, [key, ix, val]}, + {ram_copies, Nodes}]), + {atomic, ok} = mnesia:create_table(match_delete_index_do, + [{index, [ix]}, {attributes, [key, ix, val]}, + {disc_only_copies, Nodes}]), + Test = fun(Tab) -> + Rec = {Tab, 1, 4, data}, + Rec2 = {Tab, 2, 5, data}, + Rec3 = {Tab, 3, 5, data}, + Rec4 = {Tab, 4, 6, data}, + Pattern = {Tab, '_', 5, '_'}, + + {atomic, ok} = mnesia:transaction(fun() -> mnesia:write(Rec), + mnesia:write(Rec2), + mnesia:write(Rec3), + mnesia:write(Rec4) + end), + + ?match({atomic, ok}, mnesia:match_delete(Tab, Pattern)), + + ?match([Rec], mnesia:dirty_index_read(Tab, 4, ix)), + ?match([Rec4], mnesia:dirty_index_read(Tab, 6, ix)), + ?match({atomic, [Rec]}, mnesia:transaction(fun() -> mnesia:index_read(Tab, 4, ix) end)), + ?match({atomic, [Rec4]}, mnesia:transaction(fun() -> mnesia:index_read(Tab, 6, ix) end)), + + ?match([], mnesia:dirty_index_match_object(Pattern, ix)), + ?match({atomic, []}, mnesia:transaction(fun() -> mnesia:index_match_object(Pattern, ix) end)), + + ?match([Rec], mnesia:dirty_index_match_object({Tab, '_', 4, '_'}, ix)), + ?match({atomic, [Rec4]}, + mnesia:transaction(fun() -> mnesia:index_match_object({Tab, '_', 6, data}, ix) end)) + end, + [Test(Tab) || Tab <- [match_delete_index, match_delete_index_ram, match_delete_index_do]], + ?verify_mnesia(Nodes, []). + +match_delete_restart(suite) -> []; +match_delete_restart(Config) when is_list(Config) -> + Nodes = ?acquire_nodes(1, Config), + Tab = match_delete_log_tab, + Def = [{disc_copies, Nodes}], + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + ?match({atomic, ok}, write(Tab)), + Pattern = {Tab, '_', bar}, + ?match({atomic, ok}, mnesia:match_delete(Tab, Pattern)), + %% Restart Mnesia right after calling match_delete/2 to verify that + %% the table is correctly loaded + ?match([], mnesia_test_lib:stop_mnesia(Nodes)), + ?match([], mnesia_test_lib:start_mnesia(Nodes, [Tab])), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + ?verify_mnesia(Nodes, []). + +match_delete_dump_restart(suite) -> []; +match_delete_dump_restart(Config) when is_list(Config) -> + [Node1] = Nodes = ?acquire_nodes(1, Config), + Tab = match_delete_dump_tab, + Def = [{disc_copies, Nodes}], + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + ?match({atomic, ok}, write(Tab)), + Pattern = {Tab, '_', bar}, + ?match({atomic, ok}, mnesia:match_delete(Tab, Pattern)), + dumped = rpc:call(Node1, mnesia, dump_log, []), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + ?match([], mnesia_test_lib:stop_mnesia(Nodes)), + ?match([], mnesia_test_lib:start_mnesia(Nodes, [Tab])), + ?match({atomic, [1,2,5]}, ?sort(mnesia:transaction(fun() -> mnesia:all_keys(Tab) end))), + ?verify_mnesia(Nodes, []). + +match_delete_frag(suite) -> []; +match_delete_frag(Config) when is_list(Config) -> + Nodes = ?acquire_nodes(2, Config), + Tab = match_delete_frag_tab, + FragProps = [{n_fragments, 2}, {node_pool, Nodes}], + Def = [{frag_properties, FragProps}, {ram_copies, Nodes}], + ?match({atomic, ok}, mnesia:create_table(Tab, Def)), + KVs = [{1, foo}, {2, foo}, + {3, bar}, {4, bar}, + {5, baz}, {6, baz}, + {7, foo}, {8, foo}], + ?match([ok, ok | _], frag_write(Tab, KVs)), + Pattern = {Tab, '_', bar}, + %% match_delete/2 is a transaction itself + ?match({atomic, ok}, + mnesia:activity( + async_dirty, fun(P) -> mnesia:match_delete(Tab, P) end, [Pattern], mnesia_frag) + ), + Keys = mnesia:activity(transaction, fun() -> mnesia:all_keys(Tab) end, [], mnesia_frag), + ?match([1,2,5,6,7,8], ?sort(Keys)), + ?verify_mnesia(Nodes, []). + +frag_write(Tab, KVs) -> + Fun = fun(KVs1) -> [mnesia:write(Tab, {Tab, K, V}, write) || {K, V} <- KVs1] end, + mnesia:activity(transaction, Fun, [KVs], mnesia_frag). + +write(Tab) -> + mnesia:transaction( + fun() -> + mnesia:write({Tab, 1, foo}), + mnesia:write({Tab, 2, foo}), + mnesia:write({Tab, 3, bar}), + mnesia:write({Tab, 4, bar}), + mnesia:write({Tab, 5, baz}) + end). From 3b6e74848aa0ec8f0c7149db5e185e753f0fc159 Mon Sep 17 00:00:00 2001 From: zmstone Date: Wed, 19 Jun 2024 22:30:33 +0200 Subject: [PATCH 14/23] feat(ssl): cacert option suppor 'system_defaults' so the use is not forced to bloat the options with public_key:cacerts_get() --- lib/ssl/src/ssl.erl | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl index 040a5ad78d88..97bbbe18a597 100644 --- a/lib/ssl/src/ssl.erl +++ b/lib/ssl/src/ssl.erl @@ -147,6 +147,9 @@ Special Erlang node configuration for the application can be found in %% Tracing -export([handle_trace/3]). +%% EMQ fork +-export([default_cacerts/0]). + -deprecated([{prf,5,"Use export_key_materials/4 instead. " "Note that in OTP 28 the 'testing' way of calling this function will no longer be supported." }]). @@ -1495,8 +1498,11 @@ different semantics for the client and server. > a `{missing, ocsp_nonce}` logger event. """. --type client_option_cert() :: {verify, Verify ::verify_peer | verify_none} | - {cacerts, CACerts::[public_key:der_encoded()] | [public_key:combined_cert()]} | +-type client_option_cert() :: {verify, Verify :: verify_peer | verify_none} | + {cacerts, CACerts :: + system_defaults | + [public_key:der_encoded()] | + [public_key:combined_cert()]} | {cacertfile, CACertFile::file:filename()} | {server_name_indication, SNI::inet:hostname() | disable} | {customize_hostname_check, HostNameCheckOpts::list()} | @@ -1816,7 +1822,10 @@ Certificate related options for a server. """. -doc(#{title => <<"Server Options">>}). --type server_option_cert() :: {cacerts, CACerts::[public_key:der_encoded()] | [public_key:combined_cert()]} | +-type server_option_cert() :: {cacerts, CACerts:: + system_defaults | + [public_key:der_encoded()] | + [public_key:combined_cert()]} | {cacertfile, CACertFile::file:filename()} | {verify, Verify:: verify_none | verify_peer} | {fail_if_no_peer_cert, FailNoPeerCert::boolean()} | @@ -2092,6 +2101,22 @@ TLS connection keys for which information can be retrieved. %%%-------------------------------------------------------------------- %%% API %%%-------------------------------------------------------------------- + +%% This function is added in EMQ's OTP fork until the upstream provides a similar solution. +%% The application code can be implement like: +%% +%% default_cacerts() -> +%% try +%% ssl:default_cacerts() +%% catch +%% _:_ -> +%% public_key:cacerts_get() +%% end. +-spec default_cacerts() -> system_defaults. +default_cacerts() -> + system_defaults. +%%-------------------------------------------------------------------- + -doc(#{title => <<"Utility Functions">>, equiv => start(temporary), since => <<"OTP R14B">>}). @@ -4114,8 +4139,13 @@ check_key_legacy_version_dep(Versions, Key) -> opt_cacerts(UserOpts, #{verify := Verify, log_level := LogLevel, versions := Versions} = Opts, #{role := Role}) -> - {_, CaCerts} = get_opt_list(cacerts, undefined, UserOpts, Opts), - + CaCerts = case get_opt(cacerts, undefined, UserOpts, Opts) of + {_, system_defaults} -> + public_key:cacerts_get(); + _ -> + {_, CaCerts0} = get_opt_list(cacerts, undefined, UserOpts, Opts), + CaCerts0 + end, CaCertFile = case get_opt_file(cacertfile, <<>>, UserOpts, Opts) of {Where1, _FileName} when CaCerts =/= undefined -> warn_override(Where1, UserOpts, cacerts, [cacertfile], LogLevel), From c800d5042e153673b2f29b5f73bfd2896192e877 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 25 Jun 2024 15:31:56 -0300 Subject: [PATCH 15/23] feat(ct): add `flaky` test case property Albeit there's already the `{repeat_until_ok, N}` TC property, it's arguably of reduced use in a CI environment, as any failure will fail the whole suite, even if the final execution succeeds. Here we introduce a new kind of TC property: `{flaky, N}`, where `N` is a positive integer. It works similarly to `{repeat_until_ok, N}`: the TC is repeated up to `N` times until it succeeds or retries are exhausted. The TC gets the status of its last run: if it eventually succeeded, it's considered a success. Example usage: ```erlang -module(my_SUITE). all() -> %% This test case will be run up to 10 times. [{testcase, t_my_flaky_test, [{flaky, 10}]}]. t_my_flaky_test(Config) -> K = {?MODULE, ?FUNCTION_NAME}, N = persistent_term:get(K, 0), case N > 5 of true -> ok; false -> persistent_term:put(K, N + 1), error(boom) end. ``` Execution logs: ``` Testing lib.my_SUITE: Stopping test case repeat operation: {flaky,10} Testing lib.my_SUITE: TEST COMPLETE, 1 ok, 0 failed of 1 test cases ``` Note that, even though it actually ran 6 times, the final statistics are not duplicated. The produced HTML logs do contain logs from all failures, nevertheless, for debugging purposes. --- lib/common_test/src/ct_framework.erl | 2 ++ lib/common_test/src/test_server_ctrl.erl | 31 ++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index 423e01c8e0f7..7725a073d37c 100644 --- a/lib/common_test/src/ct_framework.erl +++ b/lib/common_test/src/ct_framework.erl @@ -1495,6 +1495,8 @@ report(What,Data) -> add_to_stats(user_skipped); {_,{auto_skipped,_}} -> add_to_stats(auto_skipped); + {_,{{failed,keep_going}, _}} -> + ok; {_,{SkipOrFail,_Reason}} -> add_to_stats(SkipOrFail) end; diff --git a/lib/common_test/src/test_server_ctrl.erl b/lib/common_test/src/test_server_ctrl.erl index e54fe32b8c21..24f4207bca34 100644 --- a/lib/common_test/src/test_server_ctrl.erl +++ b/lib/common_test/src/test_server_ctrl.erl @@ -2960,6 +2960,7 @@ run_test_cases_loop([{Mod,Func,Args}=Case|Cases], Config, TimetrapData, Mode0, S ok end, + maybe_put_flaky_info(Mode0), case run_test_case(undefined, Num+1, Mod, Func, Args, RunInit, TimetrapData, Mode) of %% callback to framework module failed, exit immediately @@ -3934,6 +3935,8 @@ run_test_case1(Ref, Num, Mod, Func, Args, RunInit, ok; {_,ok} -> put(test_server_ok, get(test_server_ok)+1); + {_,{failed, keep_going}} -> + ok; {_,failed} -> put(test_server_failed, get(test_server_failed)+1); {_,skip} -> @@ -4173,8 +4176,15 @@ progress(failed, CaseNum, Mod, Func, GrName, Loc, Reason, T, print(major, "=elapsed ~.6fs", [Time]), print(1, "*** FAILED ~ts ***", [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), + + StatusTag = case should_keep_going() of + true -> + {failed, keep_going}; + false -> + failed + end, test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, - {failed,Reason}}]), + {StatusTag,Reason}}]), TimeStr = io_lib:format("~.fs", [Time]), Comment = case Comment0 of @@ -4193,7 +4203,7 @@ progress(failed, CaseNum, Mod, Func, GrName, Loc, Reason, T, print(minor, "~ts", ["=== Reason: " ++ escape_chars(io_lib:format(FStr, [FormattedReason]))]), - failed; + StatusTag; progress(ok, _CaseNum, Mod, Func, GrName, _Loc, RetVal, T, Comment0, {St0,St1}) -> @@ -5835,6 +5845,10 @@ do_update_repeat_data(ok,{repeat_until_ok=RT,M,N}) -> report_repeat_testcase(M,N), report_stop_repeat_testcase(RT,{RT,N}), false; +do_update_repeat_data(ok,{flaky=RT,M,N}) -> + report_repeat_testcase(M,N), + report_stop_repeat_testcase(RT,{RT,N}), + false; do_update_repeat_data(failed,{repeat_until_fail=RT,M,N}) -> report_repeat_testcase(M,N), report_stop_repeat_testcase(RT,{RT,N}), @@ -5854,3 +5868,16 @@ report_repeat_testcase(M,forever) -> print(minor, "~n=== Repeated test case: ~w of infinity", [M]); report_repeat_testcase(M,N) -> print(minor, "~n=== Repeated test case: ~w of ~w", [M,N]). + +maybe_put_flaky_info([{_Ref, [{repeat, {flaky, N, M}}], _Time} | _]) -> + put('$ct_flaky_info', {N, M}); +maybe_put_flaky_info(_) -> + erase('$ct_flaky_info'). + +should_keep_going() -> + case get('$ct_flaky_info') of + {N, M} when N < M -> + true; + _ -> + false + end. From a9b550d624550a5e2042cabbd127ca31611f9f2a Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 26 Jun 2024 13:35:25 -0300 Subject: [PATCH 16/23] feat: print when a flaky test requires more than one run --- lib/common_test/src/test_server_ctrl.erl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/common_test/src/test_server_ctrl.erl b/lib/common_test/src/test_server_ctrl.erl index 24f4207bca34..43c6a5971f80 100644 --- a/lib/common_test/src/test_server_ctrl.erl +++ b/lib/common_test/src/test_server_ctrl.erl @@ -5828,6 +5828,7 @@ encoding(File) -> check_repeat_testcase(Case,Result,Cases, [{Ref,[{repeat,RepeatData0}],StartTime}|Mode0]) -> + maybe_report_flaky_test(Case, RepeatData0), case do_update_repeat_data(Result,RepeatData0) of false -> {Cases,Mode0}; @@ -5869,6 +5870,16 @@ report_repeat_testcase(M,forever) -> report_repeat_testcase(M,N) -> print(minor, "~n=== Repeated test case: ~w of ~w", [M,N]). +maybe_report_flaky_test({Mod, Func, _Args}, {flaky,N,_Max}) -> + case N > 1 of + true -> + print(minor, "~n=== FLAKY test case: ~w:~w", [Mod, Func]); + false -> + ok + end; +maybe_report_flaky_test(_TestCase, _RepeatInfo) -> + ok. + maybe_put_flaky_info([{_Ref, [{repeat, {flaky, N, M}}], _Time} | _]) -> put('$ct_flaky_info', {N, M}); maybe_put_flaky_info(_) -> From d3ace5ce33aa2f7381d1c463ae3a5742af285ffd Mon Sep 17 00:00:00 2001 From: zmstone Date: Mon, 19 Aug 2024 16:29:13 +0200 Subject: [PATCH 17/23] chore: hint +c false in vm.args when monotonic time stepped back --- erts/emulator/beam/erl_time_sup.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erts/emulator/beam/erl_time_sup.c b/erts/emulator/beam/erl_time_sup.c index d8e39139237c..7f6d55e48ef2 100644 --- a/erts/emulator/beam/erl_time_sup.c +++ b/erts/emulator/beam/erl_time_sup.c @@ -266,7 +266,7 @@ update_last_mtime(ErtsSchedulerData *esdp, ErtsMonotonicTime mtime) if (esdp->last_monotonic_time > mtime) { ERTS_ASSERT(esdp == erts_get_scheduler_data()); erts_exit(ERTS_ABORT_EXIT, - "Erlang monotonic time stepped backwards!\n" + "Erlang monotonic time stepped backwards! Maybe add '+c false' in vm.args\n" "Previous time: %b64d\n" "Current time: %b64d\n", esdp->last_monotonic_time, @@ -287,7 +287,7 @@ check_os_monotonic_time(ErtsSchedulerData *esdp, ErtsMonotonicTime mtime) if (esdp->last_os_monotonic_time > mtime) { ERTS_ASSERT(esdp == erts_get_scheduler_data()); erts_exit(ERTS_ABORT_EXIT, - "OS monotonic time stepped backwards!\n" + "OS monotonic time stepped backwards! Maybe add '+c false' in vm.args\n" "Previous time: %b64d\n" "Current time: %b64d\n", esdp->last_os_monotonic_time, @@ -374,7 +374,7 @@ read_corrected_time(int os_drift_corrected, ErtsSchedulerData *esdp) else { if (os_mtime < time_sup.inf.c.parmon.cdata.insts.prev.os_mtime) erts_exit(ERTS_ABORT_EXIT, - "OS monotonic time stepped backwards\n"); + "OS monotonic time stepped backwards, maybe add '+c false' in vm.args\n"); ci = time_sup.inf.c.parmon.cdata.insts.prev; } @@ -473,7 +473,7 @@ check_time_correction(void *vesdp) if (os_mtime < ci.os_mtime) erts_exit(ERTS_ABORT_EXIT, - "OS monotonic time stepped backwards\n"); + "OS monotonic time stepped backwards, maybe add '+c false' in vm.args\n"); erl_mtime = calc_corrected_erl_mtime(os_mtime, &ci, &mdiff, os_drift_corrected); @@ -901,7 +901,7 @@ finalize_corrected_time_offset(ErtsSystemTime *stimep) if (os_mtime < ci.os_mtime) erts_exit(ERTS_ABORT_EXIT, - "OS monotonic time stepped backwards\n"); + "OS monotonic time stepped backwards, maybe add '+c false' in vm.args\n"); return calc_corrected_erl_mtime(os_mtime, &ci, NULL, os_drift_corrected); From 8fc0abdfc527099c274d6dc38920394622183a78 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 24 Oct 2024 22:19:49 +0200 Subject: [PATCH 18/23] feat: add basic mqtt packet parsing support --- erts/emulator/beam/atom.names | 1 + erts/emulator/beam/erl_bif_port.c | 1 + erts/emulator/beam/packet_parser.c | 34 ++++++++++++++++++++++++++++++ erts/emulator/beam/packet_parser.h | 3 ++- erts/preloaded/src/erlang.erl | 3 ++- erts/preloaded/src/prim_inet.erl | 1 + lib/kernel/src/gen_tcp.erl | 3 ++- lib/kernel/src/inet_int.hrl | 1 + 8 files changed, 44 insertions(+), 3 deletions(-) diff --git a/erts/emulator/beam/atom.names b/erts/emulator/beam/atom.names index 0b328a6d9a5c..02d01d9caab9 100644 --- a/erts/emulator/beam/atom.names +++ b/erts/emulator/beam/atom.names @@ -465,6 +465,7 @@ atom monitors atom monotonic atom monotonic_timestamp atom more +atom mqtt atom multi_scheduling atom multiline atom nano_seconds diff --git a/erts/emulator/beam/erl_bif_port.c b/erts/emulator/beam/erl_bif_port.c index c81869564729..0fd698c58ced 100644 --- a/erts/emulator/beam/erl_bif_port.c +++ b/erts/emulator/beam/erl_bif_port.c @@ -1502,6 +1502,7 @@ BIF_RETTYPE decode_packet_3(BIF_ALIST_3) case am_httph: type = TCP_PB_HTTPH; break; case am_http_bin: type = TCP_PB_HTTP_BIN; break; case am_httph_bin: type = TCP_PB_HTTPH_BIN; break; + case am_mqtt: type = TCP_PB_MQTT; break; case am_ssl_tls: type = TCP_PB_SSL_TLS; break; default: BIF_P->fvalue = am_badopt; diff --git a/erts/emulator/beam/packet_parser.c b/erts/emulator/beam/packet_parser.c index a349c3ff8446..f33a6669c310 100644 --- a/erts/emulator/beam/packet_parser.c +++ b/erts/emulator/beam/packet_parser.c @@ -471,6 +471,40 @@ int packet_get_length(enum PacketParseType htype, plen = get_int16(&ptr[3]); } goto remain; + + case TCP_PB_MQTT: { + /* Byte 1: MQTT Control Packet fixed header + * Bytes 2-2/3/4/5: Remaining Length (variable byte integer) + */ + byte vb, ptype; + hlen = 2; + plen = 0; + if (n < 1) goto more; + /* Bits 4-8: Packet type */ + ptype = ptr[0] >> 4; + /* ERROR: Type 0 is reserved, forbidden */ + if (ptype == 0) + goto error; + while (hlen <= 1 + 4) { + if (hlen > n) goto more; + vb = ptr[hlen - 1] & 0x7F; + plen |= vb << (7 * (hlen - 2)); + if (ptr[hlen - 1] & 0x80) { + hlen = hlen + 1; + } + else { + /* NOTE: Tolerate minumum-number-of-bytes rule violation + * [MQTT-1.5.5-1] + */ + goto packet; + } + } + /* ERROR: variable byte integer >4 bytes long */ + goto error; + packet: + /* No special parsing for now */ + goto remain; + } default: DEBUGF((" => case error\r\n")); diff --git a/erts/emulator/beam/packet_parser.h b/erts/emulator/beam/packet_parser.h index 633e3794c5d4..432a2728700d 100644 --- a/erts/emulator/beam/packet_parser.h +++ b/erts/emulator/beam/packet_parser.h @@ -44,7 +44,8 @@ enum PacketParseType { TCP_PB_HTTPH = 11, TCP_PB_SSL_TLS = 12, TCP_PB_HTTP_BIN = 13, - TCP_PB_HTTPH_BIN = 14 + TCP_PB_HTTPH_BIN = 14, + TCP_PB_MQTT = 15 }; typedef struct http_atom { diff --git a/erts/preloaded/src/erlang.erl b/erts/preloaded/src/erlang.erl index df90b942eadb..2b75febd5332 100644 --- a/erts/preloaded/src/erlang.erl +++ b/erts/preloaded/src/erlang.erl @@ -1791,7 +1791,8 @@ Examples: {more, Length} | {error, Reason} when Type :: 'raw' | 0 | 1 | 2 | 4 | 'asn1' | 'cdr' | 'sunrm' | 'fcgi' - | 'tpkt' | 'line' | 'http' | 'http_bin' | 'httph' | 'httph_bin', + | 'tpkt' | 'line' | 'http' | 'http_bin' | 'httph' | 'httph_bin' + | 'mqtt', Bin :: binary(), Options :: [Opt], Opt :: {packet_size, non_neg_integer()} diff --git a/erts/preloaded/src/prim_inet.erl b/erts/preloaded/src/prim_inet.erl index f3d1f5928e95..503230ecf205 100644 --- a/erts/preloaded/src/prim_inet.erl +++ b/erts/preloaded/src/prim_inet.erl @@ -1738,6 +1738,7 @@ type_opt_1(packet) -> {httph,?TCP_PB_HTTPH}, {http_bin, ?TCP_PB_HTTP_BIN}, {httph_bin,?TCP_PB_HTTPH_BIN}, + {mqtt, ?TCP_PB_MQTT}, {ssl, ?TCP_PB_SSL_TLS}, % obsolete {ssl_tls, ?TCP_PB_SSL_TLS}]}; type_opt_1(line_delimiter) -> int; diff --git a/lib/kernel/src/gen_tcp.erl b/lib/kernel/src/gen_tcp.erl index 123eb19b32f0..dd9753ee0549 100644 --- a/lib/kernel/src/gen_tcp.erl +++ b/lib/kernel/src/gen_tcp.erl @@ -278,7 +278,8 @@ way, option `send_timeout` comes in handy. {nodelay, boolean()} | {packet, 0 | 1 | 2 | 4 | raw | sunrm | asn1 | - cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin } | + cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin | + mqtt } | {packet_size, non_neg_integer()} | {priority, non_neg_integer()} | {raw, diff --git a/lib/kernel/src/inet_int.hrl b/lib/kernel/src/inet_int.hrl index beab45fd19c5..0511c01b4beb 100644 --- a/lib/kernel/src/inet_int.hrl +++ b/lib/kernel/src/inet_int.hrl @@ -214,6 +214,7 @@ -define(TCP_PB_SSL_TLS, 12). -define(TCP_PB_HTTP_BIN,13). -define(TCP_PB_HTTPH_BIN,14). +-define(TCP_PB_MQTT, 15). %% getstat, INET_REQ_GETSTAT From 1b82dbb558b912de1461c77fe76945207fc63fbd Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 27 Nov 2024 15:44:06 +0100 Subject: [PATCH 19/23] test: add testcases for `mqtt` packet decoding --- erts/emulator/test/decode_packet_SUITE.erl | 34 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/erts/emulator/test/decode_packet_SUITE.erl b/erts/emulator/test/decode_packet_SUITE.erl index ef8a32cfe6e2..992e187e207b 100644 --- a/erts/emulator/test/decode_packet_SUITE.erl +++ b/erts/emulator/test/decode_packet_SUITE.erl @@ -26,15 +26,15 @@ -export([all/0, suite/0,groups/0, init_per_testcase/2,end_per_testcase/2, - basic/1, ipv6/1, packet_size/1, neg/1, http/1, line/1, ssl/1, otp_8536/1, - otp_9389/1, otp_9389_line/1]). + basic/1, ipv6/1, packet_size/1, neg/1, http/1, line/1, mqtt/1, ssl/1, + otp_8536/1, otp_9389/1, otp_9389_line/1]). suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap, {minutes, 1}}]. all() -> - [basic, packet_size, neg, http, line, ssl, otp_8536, + [basic, packet_size, neg, http, line, mqtt, ssl, otp_8536, otp_9389, otp_9389_line, ipv6]. groups() -> @@ -62,7 +62,7 @@ basic(Config) when is_list(Config) -> {more, undefined} = decode_pkt(2,<<0>>), {more, undefined} = decode_pkt(4,<<0,0,0>>), - Types = [1,2,4,asn1,sunrm,cdr,fcgi,tpkt,ssl_tls], + Types = [1,2,4,asn1,sunrm,cdr,fcgi,tpkt,mqtt,ssl_tls], %% Run tests for different header types and bit offsets. @@ -185,6 +185,11 @@ pack(tpkt,Bin) -> Size = byte_size(Bin) + 4, Res = <>, {Res, Res}; +pack(mqtt,Bin) -> + Type = 3, % PUBLISH + Size = pack_mqtt_vbi(byte_size(Bin)), + Res = <>, + {Res, Res}; pack(ssl_tls,Bin) -> Content = case (rand:uniform(256) - 1) of C when C<128 -> C; @@ -208,6 +213,11 @@ pack_ssl(Content, Major, Minor, Body) -> end, {Res, {ssl_tls,[],C,{Major,Minor}, Data}}. +pack_mqtt_vbi(N) when N =< 2#01111111 -> + <<0:1, N:7>>; +pack_mqtt_vbi(N) -> + <<1:1, (N rem 2#10000000):7, (pack_mqtt_vbi(N div 2#10000000))/binary>>. + ipv6(Config) when is_list(Config) -> %% Test with port Packet = <<"GET http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:4000/echo_components HTTP/1.1\r\nhost: orange\r\n\r\n">>, @@ -242,7 +252,7 @@ packet_size(Config) when is_list(Config) -> ok end end, - lists:foreach(F, [{T,D} || T<-[1,2,4,asn1,sunrm,cdr,fcgi,tpkt,ssl_tls], + lists:foreach(F, [{T,D} || T<-[1,2,4,asn1,sunrm,cdr,fcgi,tpkt,mqtt,ssl_tls], D<-lists:seq(0, byte_size(Packet)*2)]), %% Test OTP-8102, "negative" 4-byte sizes. @@ -303,6 +313,20 @@ neg(Config) when is_list(Config) -> ok. +mqtt(_Config) -> + Type = 1, % CONNECT + {more, undefined} = decode_pkt(mqtt,<<>>), + {error, invalid} = decode_pkt(mqtt,<<0>>), + {more, undefined} = decode_pkt(mqtt,<>), + {more, 2 + 10} = decode_pkt(mqtt,<>), + {more, undefined} = decode_pkt(mqtt,<>), + {more, 3 + 138} = decode_pkt(mqtt,<>), + {more, 5 + 13*128*128*128 + 12*128*128 + 11*128 + 10} = + decode_pkt(mqtt,<>), + {error, invalid} = + decode_pkt(mqtt,<>). + + http(Config) when is_list(Config) -> <<"foo">> = http_do(http_request("foo")), <<" bar">> = http_do(http_request(" bar")), From d3383d4104baf8ffd69a7f9455dd14db69551588 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 27 Nov 2024 15:51:01 +0100 Subject: [PATCH 20/23] feat: consider `mqtt` valid packet type in `gen_tcp_socket` --- lib/kernel/src/gen_tcp_socket.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/kernel/src/gen_tcp_socket.erl b/lib/kernel/src/gen_tcp_socket.erl index cea9740bcbd6..74f496135268 100644 --- a/lib/kernel/src/gen_tcp_socket.erl +++ b/lib/kernel/src/gen_tcp_socket.erl @@ -1314,7 +1314,8 @@ nopush_or_cork() -> %% -type packet_option_value() :: %% 0 | 1 | 2 | 4 | raw | sunrm | asn1 | -%% cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin. +%% cdr | fcgi | line | tpkt | mqtt | +%% http | httph | http_bin | httph_bin. -compile({inline, [is_packet_option_value/1]}). is_packet_option_value(Value) -> @@ -1327,6 +1328,7 @@ is_packet_option_value(Value) -> fcgi -> true; line -> true; tpkt -> true; + mqtt -> true; http -> true; httph -> true; http_bin -> true; @@ -2507,6 +2509,7 @@ packet_header_length(PacketType) -> sunrm -> 4; fcgi -> 8; tpkt -> 4; + mqtt -> 2; ssl -> 5; ssl_tls -> 5; asn1 -> 2; From f5cc6fc6d0d970de9394ad34611ea1a7ab3aa11e Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 27 Nov 2024 15:51:36 +0100 Subject: [PATCH 21/23] docs: mention `mqtt` as valid packet type in module docs --- erts/preloaded/src/erlang.erl | 4 +++- lib/kernel/src/inet.erl | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/erts/preloaded/src/erlang.erl b/erts/preloaded/src/erlang.erl index 2b75febd5332..5e5432dab5c7 100644 --- a/erts/preloaded/src/erlang.erl +++ b/erts/preloaded/src/erlang.erl @@ -1723,7 +1723,7 @@ returned. latin-1 newline character. The delimiter byte is included in the returned packet unless the line was truncated according to option `line_length`. -- **`asn1 | cdr | sunrm | fcgi | tpkt`** - The header is _not_ stripped off. +- **`asn1 | cdr | sunrm | fcgi | tpkt | mqtt`** - The header is _not_ stripped off. The meanings of the packet types are as follows: @@ -1737,6 +1737,8 @@ returned. - **`tpkt` \- TPKT format \[RFC1006]** + - **`mqtt` \- MQTT packet \[mqtt-v5.0\] / \[mqtt-v3.1.1\] + - **`http | httph | http_bin | httph_bin`** - The Hypertext Transfer Protocol. The packets are returned with the format according to `HttpPacket` described earlier. A packet is either a request, a response, a header, or an end of diff --git a/lib/kernel/src/inet.erl b/lib/kernel/src/inet.erl index ba1d303c1f83..14d0e0715eb7 100644 --- a/lib/kernel/src/inet.erl +++ b/lib/kernel/src/inet.erl @@ -1257,12 +1257,12 @@ The following options are available: The 4-byte header is limited to 2Gb. - - **`asn1 | cdr | sunrm | fcgi | tpkt | line`** - These packet types only have - effect on receiving. When sending a packet, it is the responsibility of the - application to supply a correct header. On receiving, however, one message - is sent to the controlling process for each complete packet received, and, - similarly, each call to `gen_tcp:recv/2,3` returns one complete packet. The - header is _not_ stripped off. + - **`asn1 | cdr | sunrm | fcgi | tpkt | mqtt | line`** - These packet types only + have effect on receiving. When sending a packet, it is the responsibility + of the application to supply a correct header. On receiving, however, one + message is sent to the controlling process for each complete packet received, + and, similarly, each call to `gen_tcp:recv/2,3` returns one complete packet. + The header is _not_ stripped off. The meanings of the packet types are as follows: @@ -1271,6 +1271,7 @@ The following options are available: - `cdr` - CORBA (GIOP 1.1) - `fcgi` - Fast CGI - `tpkt` - TPKT format \[RFC1006] + - `mqtt` - MQTT packet \[mqtt-v5.0\] / \[mqtt-v3.1.1\] - `line` - Line mode, a packet is a line-terminated with newline, lines longer than the receive buffer are truncated From b0224ea3506e664822592295771aea6e53efb58b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 16 Dec 2024 18:23:31 +0100 Subject: [PATCH 22/23] chore: update preloaded modules --- erts/preloaded/ebin/erlang.beam | Bin 39168 -> 39168 bytes erts/preloaded/ebin/prim_inet.beam | Bin 42400 -> 42404 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/erts/preloaded/ebin/erlang.beam b/erts/preloaded/ebin/erlang.beam index 8a212178bb0516fb45b5b4de2e5929acdf27f804..28b05da1f04df1302ed8f9922895b64880611c1e 100644 GIT binary patch delta 2423 zcmWNRX_OS@0Y!zFV|7<|S9eucci$SBW|^+)ndz?jK9EH~KnRgNVP-U-&J18gAuvFc zH9!w22qIh94RFFHyT~daf(c+8F$l+KBrIXefrc0X(E){X!q4~4efQ_9-O;diN5gp? z8r0A4m}&6i{9%)G_v`xBmDLTc`(53Px|ww=>Q?3}^Hup#`QPVX&ri#5%fFY;=da~& z=5L{7P_ihUQM#a%p_HTaM|l>d66JZ67f}YI3`eO!c?IQFl<_DNP^O?vMVWyz6J;*S zJe0*KOHekUY(~i)Mmd6V0p%h-4PO{vYkY0+4a7GH-+Fu-@NL8Q9==cU9m978-&K4! z@!i6A7vGQg4g693kK%8HzZ3o^@pr@D9e*wUDfk!QUxSXv5HkqfJB0O-Eadwghbr+FG=IX#3IX&^|>wjdlj@ zHrn@u8xbxf+?;R=!W{^AB;1{F55nbyD+pH;9zf(FA`OU4CGr}P)kM}1IYi_mB7Y}x zoX8m>XNg=Tk|**5k$Q9;-N>Ok=xKC;F43Pt?}lE5UX4BkeJJ{9^fBlY(I=rFL_dW7 zDf%(=Yv|X}e?q^9p<#qEiZD_bZ817vJb}>%qc=t+MioXiMlX!^7*jB&Vl2Q|h_Mf2 zKRM$t#wQpjFiv88g>eSs6h5yn^9bfq%&#Bxr!dcBUckJ9c@^_p z%o~{Xn0JUhM63a^M~FGZ(!{zD`vtMS#L9@(5F0^k0oFb>AUTQD0o!DiSAyWl8%3@6|uT!;TcJ>0?i8P>yCL97r~ z0t<3hQ>W$S0s~=W>tU*{WU=7Er!5WJ-4r?~n9IUli>#*L% z+J?0kYaiA*tn*lxDM(V#n1T)zbflmM1wARKq@ap|F%*pD(LFr+K81x8=8`0bldK^* zo#Y!NH;~*&ayQ96B#)8&jHZv#v?Wd3(X>59FuOl8uq=Nwyc+XURTCb|Be7WT%sTgX}7@ ztH~ZAdz9=2vKMiExPIJbxJ9@wu7}$Kw@Ix_jBCSxaV-s<6gqk@WQzDxOZ^x;oir481Lt} z-{IcIi{kn5RJ{zc^&aO;dRFAiZ=>x z1)>m`g?tq`8aWO*9yt*? z30aGrf_!jg(~&cgZz5+S=OE`J|A<_KT#Q_Xd>gp}xe~dGYPlM@4*3ppBXSdREAn0B zcH|D^ZsZ>1KIDGnLF6IiUy+BAe?xwPJcj%Xc>;M7`48lm$WzFFA<{9 delta 2423 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 UR*w#Z43D#Yc53Hom$b$I0q`|YXaE2J delta 2160 zcmXAkc{JAf0*7Bj-=)Hk`lJ$FN!l#YW@$r1mTZygs+lCqC5n3Gjg%I2?G$Mf6&0$n z&sY=6v`sNtQmUUqsnBA&&bj}5KIc6DJmc+(C*#NegO#v5rZ5ADF<-^8Fe&+G=k)TTm`ufk`HneBnsp>NDN3C z$R&^*kSidkLDE4|K;l52fDD0*hK+#yBLQgxc?n8DDd=cx4@f`AXOL&0V?bp=z>NY7A-$Y6)ru>ICWv>IS+IbQ9c~=JqvmfG#fMr z^fG8JXddV_&;rmx&@#|+(BDBTLGOSzfZhdt1o{~CDd;oMHqZ{xu2Ew*=xfk7pgo}P zK!?B>m=f4Du<2kLU^Btyfa!qggBgG=0$U7b4rT$i4$KzJ3CtPH9c&ZW7O;PV?F91% z3jo^(CI$-u3kKT@777*wwg)T_Y&Y0`um}m*L9kRt#1ORt{DLRtr`Sb_c8h>>gM%*nO~vV2{E60&4~90P6(n2I~du z1N#6r0QM2=GbC&fY#8h-xHPy7_!w|ma5?a?;N!r@gUf?Y1fL8(9eftJHnJn#kJ zy5RcYhTulvi@}$GF9SCNUkSbn+#K8r+#1{l+!1^u_-61e;2z*x!MA~XfvfHW_W=(8 z4+IYa-wiGX4+9Su-WT@DHl38{HkXP-QWDYVh(u#j|NkP9%qX|^3UVA}M@6FX<3rMG zRo2G8SZ$a5a*f}RUiAb;C8ZTjGI9~UW;K5t;#wgGEZ08njnA56@6^LGXH=PX^3d?# z{5$pk>#Er@@WA+Vap%GGB=Pp{^(z!Y)y7nQzF5^?Z=*L~VwPvMJhaI5@Sdi`_=fqP z+*F;jCtfRF(W)iWy2v}xV7+CVVT|MI%|6bRx${mY@>|BILd6v|f#(7X*SOEv>h@0U zI)a@qCG%fi|~X-#YA?QOvmY$s0|GIKXw+I}g+%x~DZ z&_SI$SGLa5shg4`Ax6Qei8oqY&lfp9%k90;)fKCKYW80{<<^|7Z*cMyyWg35*|^E~ zaZh)g)?Up;ItpU_Kk8hsJc&w&@|iw`Z|$N}#B05mtV6Yqa)IDuJhR>I>Qt%9*vNLX zEChsJUvi<={AN?fkLs<>%VSK=mIoP8x5nk&vSTy@{-E@tGrF;ZSL=TQ>x@u-ja1wO7>>h-!&#(BaI81kKD`9J$XD@byjOhS8EZC zG>rc>Sx?Hf_CQ(Yi~P>kjOtkT>r+-s=}w=t&bRg4q*V1+54P<~Q(Y}-+fnZl7P>pS zS@GL1QN3wflFAb*{5mU_y^c8L)?xh9u&bxuz|heN`<}pK$T~Igu=r|7UD^A^2?bwE zRBeCgUsjFG_E&DPmA{p7P2b&Qp7{JN^}HYH^-pg8XS;pQ_lo}7^5v$E0gkKI>ez9I zT>ptFcKO)@r?p-Dx7GWnm2QSFo@y(VS;%zB`%Z*(RE0`VVP}Edi*`QmaV83tT291_{zeATj ztRv3UT4jDOvss(v=9;?#;E57_B_g z8S8Rk!&|08tQ9TSWqGU!`~B(1WuIR8XU83E`+6`=(y9=1tfSYwCUoE2pXFC-O;J63 zHs*V%T7i9ZMetIScZMz}@|BA-_rEB~8y9sX(sNiP`bVu=)wW2B0QuF%Nq#<}DC6p! zu@O>>{H@x*{Hb!GgMYy z+-PmigoruXr^dYwFnSblcFN4Q%+d~TWZyWAKh8yu7_7r?F@=$)^C4sKefYPal4!Q(mq2?lkAacC7(uKDUB4` z8g=v)p7SZT-5WB$`Gtm(>EfFoRCA`yxOL{^U`C8uuxx4VJ?);p_O|ol2dwS-v*-WF Pe7B1=6NwVMvzGi1PpCj) From 83b4238d679793f51022f6fe42cf6d7cfe46c3b6 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 28 Nov 2024 13:30:11 +0100 Subject: [PATCH 23/23] chore: bump OTP_VERSION to prepare patch release --- OTP_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OTP_VERSION b/OTP_VERSION index 40682b3dccc1..32d02d897176 100644 --- a/OTP_VERSION +++ b/OTP_VERSION @@ -1 +1 @@ -27.2 +27.2-1