aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRory Byrne <[email protected]>2015-04-16 18:01:23 +0100
committerRory Byrne <[email protected]>2015-06-09 21:51:29 +0100
commiteed21b4fd99c8ab71dcefd3802104186af1cb6b0 (patch)
treef4191a382894c67ee002c2087749a251e5316b05
parent0098eed847516cc6760d961421c83527816e35ae (diff)
downloadotp-eed21b4fd99c8ab71dcefd3802104186af1cb6b0.tar.gz
otp-eed21b4fd99c8ab71dcefd3802104186af1cb6b0.tar.bz2
otp-eed21b4fd99c8ab71dcefd3802104186af1cb6b0.zip
Fix socket option {linger, {true, 0}} to abort TCP connections
Up until now, if {linger, {true, 0}} is set on the socket and there is data in the port driver queue, the connection is not aborted until the port queue is empty and close() is called on the underlying file descriptor. This bug allows an idle TCP client to prevent a server from terminating the connection and freeing resources. This patch fixes the problem by discarding the port queue if the socket is closed when {linger, {true, 0}} is set.
-rw-r--r--erts/emulator/drivers/common/inet_drv.c10
-rw-r--r--erts/preloaded/src/prim_inet.erl13
-rw-r--r--lib/kernel/test/gen_tcp_misc_SUITE.erl37
3 files changed, 54 insertions, 6 deletions
diff --git a/erts/emulator/drivers/common/inet_drv.c b/erts/emulator/drivers/common/inet_drv.c
index a8c58a8149..80cb705896 100644
--- a/erts/emulator/drivers/common/inet_drv.c
+++ b/erts/emulator/drivers/common/inet_drv.c
@@ -838,6 +838,7 @@ static int my_strncasecmp(const char *s1, const char *s2, size_t n)
#define TCP_ADDF_SHOW_ECONNRESET 64 /* Tell user about incoming RST */
#define TCP_ADDF_DELAYED_ECONNRESET 128 /* An ECONNRESET error occured on send or shutdown */
#define TCP_ADDF_SHUTDOWN_WR_DONE 256 /* A shutdown(sock, SHUT_WR) or SHUT_RDWR was made */
+#define TCP_ADDF_LINGER_ZERO 512 /* Discard driver queue on port close */
/* *_REQ_* replies */
#define INET_REP_ERROR 0
@@ -6318,6 +6319,13 @@ static int inet_set_opts(inet_descriptor* desc, char* ptr, int len)
arg_sz = sizeof(li_val);
DEBUGF(("inet_set_opts(%ld): s=%d, SO_LINGER=%d,%d",
(long)desc->port, desc->s, li_val.l_onoff,li_val.l_linger));
+ if (desc->sprotocol == IPPROTO_TCP) {
+ tcp_descriptor* tdesc = (tcp_descriptor*) desc;
+ if (li_val.l_onoff && li_val.l_linger == 0)
+ tdesc->tcp_add_flags |= TCP_ADDF_LINGER_ZERO;
+ else
+ tdesc->tcp_add_flags &= ~TCP_ADDF_LINGER_ZERO;
+ }
break;
case INET_OPT_PRIORITY:
@@ -9699,6 +9707,8 @@ static void tcp_inet_flush(ErlDrvData e)
/* Discard send queue to avoid hanging port (OTP-7615) */
tcp_clear_output(desc);
}
+ if (desc->tcp_add_flags & TCP_ADDF_LINGER_ZERO)
+ tcp_clear_output(desc);
}
static void tcp_inet_process_exit(ErlDrvData e, ErlDrvMonitor *monitorp)
diff --git a/erts/preloaded/src/prim_inet.erl b/erts/preloaded/src/prim_inet.erl
index fb0f67aa8a..5e0b38aa68 100644
--- a/erts/preloaded/src/prim_inet.erl
+++ b/erts/preloaded/src/prim_inet.erl
@@ -146,11 +146,16 @@ shutdown_1(S, How) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
close(S) when is_port(S) ->
- case subscribe(S, [subs_empty_out_q]) of
- {ok, [{subs_empty_out_q,N}]} when N > 0 ->
- close_pend_loop(S, N); %% wait for pending output to be sent
+ case getopt(S, linger) of
+ {ok,{true,0}} ->
+ close_port(S);
_ ->
- close_port(S)
+ case subscribe(S, [subs_empty_out_q]) of
+ {ok, [{subs_empty_out_q,N}]} when N > 0 ->
+ close_pend_loop(S, N); %% wait for pending output to be sent
+ _ ->
+ close_port(S)
+ end
end.
close_pend_loop(S, N) ->
diff --git a/lib/kernel/test/gen_tcp_misc_SUITE.erl b/lib/kernel/test/gen_tcp_misc_SUITE.erl
index ddd9d356ee..83bfd1f69d 100644
--- a/lib/kernel/test/gen_tcp_misc_SUITE.erl
+++ b/lib/kernel/test/gen_tcp_misc_SUITE.erl
@@ -35,7 +35,7 @@
show_econnreset_passive/1, econnreset_after_sync_send/1,
econnreset_after_async_send_active/1,
econnreset_after_async_send_active_once/1,
- econnreset_after_async_send_passive/1,
+ econnreset_after_async_send_passive/1, linger_zero/1,
default_options/1, http_bad_packet/1,
busy_send/1, busy_disconnect_passive/1, busy_disconnect_active/1,
fill_sendq/1, partial_recv_and_close/1,
@@ -101,7 +101,7 @@ all() ->
show_econnreset_passive, econnreset_after_sync_send,
econnreset_after_async_send_active,
econnreset_after_async_send_active_once,
- econnreset_after_async_send_passive,
+ econnreset_after_async_send_passive, linger_zero,
default_options, http_bad_packet, busy_send,
busy_disconnect_passive, busy_disconnect_active,
fill_sendq, partial_recv_and_close,
@@ -1358,6 +1358,39 @@ econnreset_after_async_send_passive(Config) when is_list(Config) ->
ok = ?t:sleep(20),
{error, econnreset} = gen_tcp:recv(Client1, 0).
+%%
+%% Test {linger {true, 0}} aborts a connection
+%%
+
+linger_zero(Config) when is_list(Config) ->
+ %% All the econnreset tests will prove that {linger, {true, 0}} aborts
+ %% a connection when the driver queue is empty. We will test here
+ %% that it also works when the driver queue is not empty.
+ {OS, _} = os:type(),
+ {ok, L} = gen_tcp:listen(0, [{active, false},
+ {recbuf, 4096},
+ {show_econnreset, true}]),
+ {ok, Port} = inet:port(L),
+ {ok, Client} = gen_tcp:connect(localhost, Port,
+ [{active, false},
+ {sndbuf, 4096}]),
+ {ok, S} = gen_tcp:accept(L),
+ ok = gen_tcp:close(L),
+ PayloadSize = 1024 * 1024,
+ Payload = lists:duplicate(PayloadSize, $.),
+ ok = gen_tcp:send(Client, Payload),
+ case erlang:port_info(Client, queue_size) of
+ {queue_size, N} when N > 0 -> ok;
+ {queue_size, 0} when OS =:= win32 -> ok;
+ {queue_size, 0} = T -> ?t:fail(T)
+ end,
+ ok = inet:setopts(Client, [{linger, {true, 0}}]),
+ ok = gen_tcp:close(Client),
+ ok = ?t:sleep(1),
+ undefined = erlang:port_info(Client, connected),
+ {error, econnreset} = gen_tcp:recv(S, PayloadSize).
+
+
%% Thanks to Luke Gorrie. Tests for a very specific problem with
%% corrupt data. The testcase will be killed by the timetrap timeout
%% if the bug is present.