Race condition in TLS distribution
Magnus Henoch <[email protected]> Mon, 19 Oct 2015 19:04:57 +0100
| Newsgroups | gmane.comp.lang.erlang.bugs |
|---|---|
| Message-ID | <[email protected]> |
Hi all,
I'm trying to use Erlang distribution over TLS ("-proto_dist
inet_tls"), and I've stumbled upon an interesting race condition.
The kernel supervisor starts the distribution subsystem before it
starts the code server. Therefore, it's possible for another node
to establish a connection to the distribution port while the code
server is not yet running. (Apologies for not providing a recipe
for reproducing this; I could work on that if that would be
useful.)
In that case, the TLS distribution module eventually calls
ssl:ssl_accept/2 on the connection socket. This in turn will
eventually call crypto:supports/0. That's when I got this error:
{error_logger,{{2015,10,19},{15,1,22}},
supervisor_report,
[{supervisor,{local,ssl_dist_sup}},
{errorContext,child_terminated},
{reason,{undef,[{crypto,supports,[],[]},
{tls_record,supported_protocol_versions,1,[{file,"tls_record.erl"},{line,322}]},
{tls_record,supported_protocol_versions,0,
[{file,"tls_record.erl"},{line,257}]},
{ssl,handle_options,1,[{file,"ssl.erl"},{line,617}]},
{ssl,ssl_accept,3,[{file,"ssl.erl"},{line,228}]},
{ssl_tls_dist_proxy,accept_loop,4,[{file,"ssl_tls_dist_proxy.erl"},{line,152}]}]}},
{offender,[{pid,<0.22.0>},
{name,ssl_tls_dist_proxy},
{mfargs,{ssl_tls_dist_proxy,start_link,[]}},
{restart_type,permanent},
{shutdown,4000},
{child_type,worker}]}]}
(though it was formatted as one long line, using the kernel's
primitive error reporter.)
Why is that function undefined, you ask. That's because the
crypto module has an on_load function, which calls code:priv_dir/1
to figure out where the NIF library is. Since the code server
isn't running yet, code:priv_dir/1 raises an exception, and as I
just learnt from reading the documentation, if an on_load function
raises an exception (or returns anything but 'ok'), the module is
unloaded - and thus we get an 'undef' error.
(This will make the ssl_tls_dist_proxy process terminate. Its
supervisor will restart it, but that doesn't help: it has lost its
listening socket, and net_kernel won't ask it to open another one,
rendering the node "alive" but unable to receive connections for
distribution - but that's a separate issue.)
I came up with the attached patch, which waits for the code server
to start before proceeding, and that fixes the problem for me.
What do you think about it? Might there be a better way to solve
this?
Regards,
Magnus
_______________________________________________
erlang-bugs mailing list
[email protected]
http://erlang.org/mailman/listinfo/erlang-bugs
wait-for-code-server.patch
(text/x-patch, 2 KB)
diff --git a/lib/ssl/src/ssl_tls_dist_proxy.erl b/lib/ssl/src/ssl_tls_dist_proxy.erl
index a22af6b..cc4c410 100644
--- a/lib/ssl/src/ssl_tls_dist_proxy.erl
+++ b/lib/ssl/src/ssl_tls_dist_proxy.erl
@@ -149,6 +149,7 @@ accept_loop(Proxy, world = Type, Listen, Extra) ->
case gen_tcp:accept(Listen) of
{ok, Socket} ->
Opts = get_ssl_options(server),
+ wait_for_code_server(),
case ssl:ssl_accept(Socket, Opts) of
{ok, SslSocket} ->
PairHandler =
@@ -165,6 +166,35 @@ accept_loop(Proxy, world = Type, Listen, Extra) ->
end,
accept_loop(Proxy, Type, Listen, Extra).
+wait_for_code_server() ->
+ %% This is an ugly hack. Upgrading a socket to TLS requires the
+ %% crypto module to be loaded. Loading the crypto module triggers
+ %% its on_load function, which calls code:priv_dir/1 to find the
+ %% directory where its NIF library is. However, distribution is
+ %% started earlier than the code server, so the code server is not
+ %% necessarily started yet, and code:priv_dir/1 might fail because
+ %% of that, if we receive an incoming connection on the
+ %% distribution port early enough.
+ %%
+ %% If the on_load function of a module fails, the module is
+ %% unloaded, and the function call that triggered loading it fails
+ %% with 'undef', which is rather confusing.
+ %%
+ %% Thus, the ssl_tls_dist_proxy process will terminate, and be
+ %% restarted by ssl_dist_sup. However, it won't have any memory
+ %% of being asked by net_kernel to listen for incoming
+ %% connections. Hence, the node will believe that it's open for
+ %% distribution, but it actually isn't.
+ %%
+ %% So let's avoid that by waiting for the code server to start.
+ case whereis(code_server) of
+ undefined ->
+ timer:sleep(10),
+ wait_for_code_server();
+ Pid when is_pid(Pid) ->
+ ok
+ end.
+
try_connect(Port) ->
case gen_tcp:connect({127,0,0,1}, Port, [{active, false}, {packet,?PPRE}]) of
R = {ok, _S} ->