#!/usr/bin/env escript %% -*- erlang -*- %%! -smp enable -sname test +S 32:32 %% inet_race_test - program to demonstrate a race condition in %% gen_tcp:controlling_process function. %% %% Written by Santeri Paavolainen 2016, released as Public Domain. %% %% This program demonstrates a race condition that can occur when %% gen_tcp:controlling_process is used without explicit barrier %% synchronization between parent and child process when network %% connections are receive in parent, and child is spawned to handle %% the new connection. %% %% IMPORTANT!! If there is no race condition port control handoff then %% the server_socket_check should NEVER see any packets on the client %% socket C. If this was true, then server_socket_check would %% **NEVER** print out anything. Unfortunately, it does: %% %% $ ./inet_race_test %% #3073 PARENT <0.2.0> Received: ping. %% #5925 PARENT <0.2.0> Received: ping. %% #5940 PARENT <0.2.0> Received: ping. %% #7272 PARENT <0.2.0> Received: ping. %% #7866 PARENT <0.2.0> Received: ping. %% %% The race condition happens inside inet:tcp_controlling_process/2 %% because the combination of message queue sync (inet:tcp_sync_input) %% and controlling process handoff (erlang:port_connect) **is not %% atomic**. It is possible that the child will change socket to %% {active, once} **and** the I/O schedule will put the incoming TCP %% packet to parent's message queue between these two calls. main(Args) -> Host = "localhost", Port = 12345, spawn(fun() -> connector_loop(Host, Port) end), {ok,S} = gen_tcp:listen(Port, [{active, false}, {packet, line}]), case Args of ["-safe"] -> io:format("Starting safe server loop~n"), server_loop_2(S); _ -> io:format("Starting server loop with race condition~n"), server_loop(S) end. %% connector_loop - this will just connect to the given host/port and %% send a single TCP packet, then close the connection, ad infinitum. connector_loop(Host, Port) -> Message = "ping.\n", case gen_tcp:connect(Host, Port, [{active, false}]) of {ok, S} -> %io:format("CONN ~w Connected: ~w~n", [self(), S]), gen_tcp:send(S, Message), %io:format("CONN ~w Sent: ~ts", [self(), Message]), gen_tcp:close(S); _ -> ok end, connector_loop(Host, Port). %% server_socket_check - given a list of client sockets will check if %% any of them has input for the **current** process, and if so, print %% out. Returns a list of open client sockets. server_socket_check([], _) -> []; server_socket_check([S|Rest], Iteration) -> receive {tcp,S,Data} -> io:format("#~w PARENT ~w Received: ~ts", [Iteration, self(), Data]), server_socket_check([S] ++ Rest, Iteration); {tcp_closed,S} -> % io:format("PARENT ~w Socket ~w closed~n", [self(), S]), server_socket_check(Rest, Iteration) after 0 -> [S] ++ server_socket_check(Rest, Iteration) end. %% server_loop - main server loop, accepts connections, spawns them to %% child, assigns socket to child process and checks on status of all %% open client sockets it has accepted. server_loop(S, Sockets, Iteration) -> {ok,C} = gen_tcp:accept(S), Pid = spawn(fun () -> child_loop(C) end), gen_tcp:controlling_process(C, Pid), NewSockets = server_socket_check([C] ++ Sockets, Iteration), server_loop(S, NewSockets, Iteration + 1). server_loop(S) -> server_loop(S, [], 0). %% server_loop_2 - a version of the server loop with barrier %% synchronization wrt/ controlling process change -- this does not %% have the race condition. server_loop_2(S, Sockets, Iteration) -> {ok,C} = gen_tcp:accept(S), %% child_loop is not called *until* process control has been %% handed over. Pid = spawn(fun () -> receive Socket -> child_loop(Socket) end end), gen_tcp:controlling_process(C, Pid), Pid ! C, NewSockets = server_socket_check([C] ++ Sockets, Iteration), server_loop_2(S, NewSockets, Iteration + 1). server_loop_2(S) -> server_loop_2(S, [], 0). %% child_loop - not really a loop, will just set to receive one %% packet, then closes the socket. child_loop(C) -> inet:setopts(C, [{active, once}]), receive {tcp,_,_} -> % io:format("CHILD ~w Received: ~ts", [self(), Data]), gen_tcp:close(C); _ -> % io:format("CHILD ~w Error or socket closed, closing.~n", [self()]), gen_tcp:close(C) end.