File 1435-kernel-Fix-deadlock-caused-by-net_kernel-setopts-new.patch of Package erlang

From d0ea376d289bb38cca79db112bfd165bafbe196f Mon Sep 17 00:00:00 2001
From: Rickard Green <rickard@erlang.org>
Date: Thu, 11 Aug 2022 23:09:42 +0200
Subject: [PATCH] [kernel] Fix deadlock caused by net_kernel:setopts(new, _)

Both pending connection owners and net_kernel could do synchronous
requests towards eachother when calling net_kernel:setopts(new, _) which
could end up in a deadlock. This is solved by making asynchronous requests
from net_kernel instead of synchronous requests.
---
 lib/kernel/src/net_kernel.erl              | 145 ++++++++++++++-------
 lib/kernel/test/erl_distribution_SUITE.erl |  42 +++++-
 2 files changed, 139 insertions(+), 48 deletions(-)

diff --git a/lib/kernel/src/net_kernel.erl b/lib/kernel/src/net_kernel.erl
index 8dfae3a505..fcb969e0af 100644
--- a/lib/kernel/src/net_kernel.erl
+++ b/lib/kernel/src/net_kernel.erl
@@ -109,7 +109,8 @@
 	  listen,       %% list of  #listen
 	  allowed,       %% list of allowed nodes in a restricted system
 	  verbose = 0,   %% level of verboseness
-	  publish_on_nodes = undefined
+	  publish_on_nodes = undefined,
+          req_map = #{} %% Map for outstanding async requests
 	 }).
 
 -record(listen, {
@@ -676,36 +677,13 @@ handle_call({new_ticktime,_T,_TP},
     async_reply({reply, {ongoing_change_to, T}, State}, From);
 
 handle_call({setopts, new, Opts}, From, State) ->
-    Ret = setopts_new(Opts, State),
-    async_reply({reply, Ret, State}, From);
+    setopts_new(Opts, From, State);
 
 handle_call({setopts, Node, Opts}, From, State) ->
-    Return =
-	case ets:lookup(sys_dist, Node) of
-	    [Conn] when Conn#connection.state =:= up ->
-		case call_owner(Conn#connection.owner, {setopts, Opts}) of
-		    {ok, Ret} -> Ret;
-		    _ -> {error, noconnection}
-		end;
-
-	    _ ->
-		{error, noconnection}
-    end,
-    async_reply({reply, Return, State}, From);
+    opts_node(setopts, Node, Opts, From, State);
 
 handle_call({getopts, Node, Opts}, From, State) ->
-    Return =
-	case ets:lookup(sys_dist, Node) of
-	    [Conn] when Conn#connection.state =:= up ->
-		case call_owner(Conn#connection.owner, {getopts, Opts}) of
-		    {ok, Ret} -> Ret;
-		    _ -> {error, noconnection}
-		end;
-
-	    _ ->
-		{error, noconnection}
-    end,
-    async_reply({reply, Return, State}, From);
+    opts_node(getopts, Node, Opts, From, State);
 
 handle_call(_Msg, _From, State) ->
     {noreply, State}.
@@ -716,6 +694,14 @@ handle_info({SetupPid, {is_pending, Node
     {noreply, State};
 
 %%
+%% Responses to asynchronous requests we've made...
+%%
+handle_info({ReqId, Reply}, S) ->
+    handle_async_response(reply, ReqId, Reply, S);
+handle_info({'DOWN', ReqId, process, _Pid, Reason}, S) ->
+    handle_async_response(down, ReqId, Reason, S);
+
+%%
 %% Handle different types of process terminations.
 %%
 handle_info({'EXIT', From, Reason}, State) when is_pid(From) ->
@@ -1624,17 +1610,38 @@ async_gen_server_reply(From, Msg) ->
             ok
     end.
 
-call_owner(Owner, Msg) ->
-    Mref = monitor(process, Owner),
-    Owner ! {self(), Mref, Msg},
-    receive
-	{Mref, Reply} ->
-	    erlang:demonitor(Mref, [flush]),
-	    {ok, Reply};
-	{'DOWN', Mref, _, _, _} ->
-	    error
+handle_async_response(ResponseType, ReqId, Result, #state{req_map = ReqMap0} = S0) ->
+    case maps:take(ReqId, ReqMap0) of
+        error -> {noreply, S0};
+        {{SetGetOpts, From}, ReqMap1} when SetGetOpts == setopts;
+                                           SetGetOpts == getopts ->
+            ResponseType =:= down orelse erlang:demonitor(ReqId, [flush]),
+            Reply = case ResponseType of
+                        reply -> Result;
+                        down -> {error, noconnection}
+                    end,
+            S1 = S0#state{req_map = ReqMap1},
+            async_reply({reply, Reply, S1}, From);
+        {{setopts_new, Op}, ReqMap1} ->
+            ResponseType =:= down orelse erlang:demonitor(ReqId, [flush]),
+            case maps:get(Op, ReqMap1) of
+                {setopts_new, From, 1} ->
+                    %% Last response for this operation...
+                    ReqMap2 = maps:remove(Op, ReqMap1),
+                    S1 = S0#state{req_map = ReqMap2},
+                    async_reply({reply, ok, S1}, From);
+                {setopts_new, From, N} ->
+                    ReqMap2 = ReqMap1#{Op => {setopts_new, From, N-1}},
+                    S1 = S0#state{req_map = ReqMap2},
+                    {noreply, S1}
+            end
     end.
 
+send_owner_request(ReqOpMap, Label, Owner, Msg) ->
+    ReqId = monitor(process, Owner),
+    Owner ! {self(), ReqId, Msg},
+    ReqOpMap#{ReqId => Label}.
+
 -spec setopts(Node, Options) -> ok | {error, Reason} | ignored when
       Node :: node() | new,
       Options :: [inet:socket_setopt()],
@@ -2089,15 +2100,16 @@ call_owner(Owner, Msg) ->
 setopts(Node, Opts) when is_atom(Node), is_list(Opts) ->
     request({setopts, Node, Opts}).
 
-setopts_new(Opts, State) ->
+setopts_new(Opts, From, State) ->
     %% First try setopts on listening socket(s)
     %% Bail out on failure.
     %% If successful, we are pretty sure Opts are ok
     %% and we continue with config params and pending connections.
     case setopts_on_listen(Opts, State#state.listen) of
 	ok ->
-	    setopts_new_1(Opts);
-	Fail -> Fail
+	    setopts_new_1(Opts, From, State);
+	Fail ->
+            async_reply({reply, Fail, State}, From)
     end.
 
 setopts_on_listen(_, []) -> ok;
@@ -2110,7 +2122,7 @@ setopts_on_listen(Opts, [#listen {listen = LSocket, module = Mod} | T]) ->
 	error:undef -> {error, enotsup}
     end.
 
-setopts_new_1(Opts) ->
+setopts_new_1(Opts, From, #state{req_map = ReqMap0} = State) ->
     ConnectOpts = case application:get_env(kernel, inet_dist_connect_options) of
 		      {ok, CO} -> CO;
 		      _ -> []
@@ -2133,13 +2145,36 @@ setopts_new_1(Opts) ->
     PendingConns = ets:select(sys_dist, [{'_',
 					  [{'=/=',{element,#connection.state,'$_'},up}],
 					  ['$_']}]),
-    lists:foreach(fun(#connection{state = pending, owner = Owner}) ->
-			  call_owner(Owner, {setopts, Opts});
-		     (#connection{state = up_pending, pending_owner = Owner}) ->
-			  call_owner(Owner, {setopts, Opts});
-		     (_) -> ignore
-		  end, PendingConns),
-    ok.
+
+    Op = make_ref(),
+    SendReq = fun (ReqMap, N, Owner) ->
+                      {send_owner_request(ReqMap, {setopts_new, Op},
+                                          Owner,
+                                          {setopts, Opts}),
+                       N+1}
+              end,
+    {ReqMap1, NoReqs} = lists:foldl(fun(#connection{state = pending,
+                                                    owner = Owner},
+                                        {ReqMap, N}) ->
+                                            SendReq(ReqMap, N, Owner);
+                                       (#connection{state = up_pending,
+                                                    pending_owner = Owner},
+                                        {ReqMap, N}) ->
+                                            SendReq(ReqMap, N, Owner);
+                                       (_, Acc) ->
+                                            Acc
+                                    end,
+                                    {ReqMap0, 0},
+                                    PendingConns),
+    if NoReqs == 0 ->
+            async_reply({reply, ok, State}, From);
+       true ->
+            %% Reply made later from handle_async_response() when
+            %% we've got responses from all owners that we've
+            %% made requests to...
+            ReqMap2 = ReqMap1#{Op => {setopts_new, From, NoReqs}},
+            {noreply, State#state{req_map = ReqMap2}}
+    end.
 
 merge_opts([], B) ->
     B;
@@ -2158,3 +2193,19 @@ merge_opts([H|T], B0) ->
 getopts(Node, Opts) when is_atom(Node), is_list(Opts) ->
     request({getopts, Node, Opts}).
 
+opts_node(Op, Node, Opts, From, #state{req_map = ReqMap0} = S0) ->
+    case ets:lookup(sys_dist, Node) of
+        [Conn] when Conn#connection.state =:= up ->
+            ReqMap1 = send_owner_request(ReqMap0,
+                                         {Op, From},
+                                         Conn#connection.owner,
+                                         {Op, Opts}),
+            %% Reply made later from handle_async_response() when
+            %% we get a response from the owner that we made the
+            %% request to...
+            S1 = S0#state{req_map = ReqMap1},
+            {noreply, S1};
+        _ ->
+            async_reply({reply, {error, noconnection}, S0}, From)
+    end.
+
diff --git a/lib/kernel/test/erl_distribution_SUITE.erl b/lib/kernel/test/erl_distribution_SUITE.erl
index 1c7b067375..8a5547e3e9 100644
--- a/lib/kernel/test/erl_distribution_SUITE.erl
+++ b/lib/kernel/test/erl_distribution_SUITE.erl
@@ -57,6 +57,7 @@
 	 tick_serv_test/2, tick_serv_test1/1,
 	 run_remote_test/1,
 	 setopts_do/2,
+         setopts_deadlock_test/2,
 	 keep_conn/1, time_ping/1]).
 
 -export([init_per_testcase/2, end_per_testcase/2]).
@@ -573,7 +574,7 @@ setopts(Config) when is_list(Config) ->
 
 setopts(Config) when is_list(Config) ->
     register(setopts_regname, self()),
-    [N1,N2,N3,N4] = get_nodenames(4, setopts),
+    [N1,N2,N3,N4,N5] = get_nodenames(5, setopts),
 
     {_N1F,Port1} = start_node_unconnected(N1, ?MODULE, run_remote_test,
 					["setopts_do", atom_to_list(node()), "1", "ping"]),
@@ -598,6 +599,32 @@ setopts(Config) when is_list(Config) ->
     wait_and_connect(LSock, N4F, Port4),
     0 = wait_for_port_exit(Port4),
 
+    %% net_kernel:setopts(new, _) used to be able to produce a deadlock
+    %% in net_kernel. GH-6129/OTP-18198
+    {N5F,Port5} = start_node_unconnected(DCfg, N5, ?MODULE, run_remote_test,
+					["setopts_deadlock_test", atom_to_list(node()),
+					 integer_to_list(LTcpPort)]),
+    wait_and_connect(LSock, N5F, Port5),
+    repeat(fun () ->
+                   receive after 10 -> ok end,
+                   erlang:disconnect_node(N5F),
+                   WD = spawn_link(fun () ->
+                                           receive after 2000 -> ok end,
+                                           exit({net_kernel_probably_deadlocked, N5F})
+                                   end),
+                   pong = net_adm:ping(N5F),
+                   unlink(WD),
+                   exit(WD, kill),
+                   false = is_process_alive(WD)
+           end,
+           200),
+    try
+        erpc:call(N5F, erlang, halt, [])
+    catch
+        error:{erpc,noconnection} -> ok
+    end,
+    0 = wait_for_port_exit(Port5),
+
     ok.
 
 wait_and_connect(LSock, NodeName, NodePort) ->
@@ -686,6 +713,19 @@ setopts_do(TestNode, [OptNr, ConnectData]) ->
 
     ok.
 
+setopts_deadlock_test(_TestNode, [TcpPort]) ->
+    {ok, Sock} = gen_tcp:connect("localhost", list_to_integer(TcpPort),
+                                 [{active,false},{packet,2}]),
+    ok = gen_tcp:send(Sock, "Connect please"),
+    {ok, "Connect done"} = gen_tcp:recv(Sock, 0),
+    gen_tcp:close(Sock),
+    setopts_new_loop().
+    
+setopts_new_loop() ->
+    ok = net_kernel:setopts(new, [{nodelay, true}]),
+    receive after 10 -> ok end,
+    setopts_new_loop().
+    
 opt_from_nr("1") -> {nodelay, true};
 opt_from_nr("2") -> {nodelay, false}.
 
-- 
2.35.3

openSUSE Build Service is sponsored by