Skip to content

Commit 5763bb5

Browse files
committed
ssl: Buffer unsent encrypted data on send timeout
When using gen_tcp with {inet_backend, socket} and send_timeout, gen_tcp:send may return {error, {timeout, RestData}} with the unsent encrypted data. Previously this fell through to the generic error handler which killed the connection. Buffer the RestData in a new #rest{} record in the tls_sender state and retry sending it together with new data on the next ssl:send call. Reply {error, timeout} to the caller to simulate {inet_backend, inet} behavior. When alerts, post-handshake data or renegotiation need to send while a #rest{} buffer exists, attempt to flush the buffer first. If the flush succeeds, proceed normally. If it times out again, postpone the event and re-buffer the remaining data. Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
1 parent ab68539 commit 5763bb5

2 files changed

Lines changed: 181 additions & 13 deletions

File tree

lib/ssl/src/tls_sender.erl

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@
9191
low = undefined
9292
}).
9393

94+
%% Buffer for unsent encrypted data returned by gen_tcp:send
95+
%% as {error, {timeout, RestData}} when using {inet_backend, socket}
96+
-record(rest,
97+
{
98+
q_rev = [] %% Remaining encrypted data (iodata)
99+
}).
100+
94101
-define(IS_ASYNC(Tag), Tag =:= select; Tag =:= completion).
95102

96103
%%%===================================================================
@@ -285,24 +292,52 @@ connection({call, From}, {post_handshake_data, HSData}, #data{buff = Buff} = Sta
285292
case Buff of
286293
undefined ->
287294
send_post_handshake_data(HSData, From, connection, StateData, [{reply, From, ok}]);
288-
Async ->
289-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
295+
#async{} = Async ->
296+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
297+
#rest{} ->
298+
case flush_rest_buffer(StateData) of
299+
{ok, #data{buff = undefined} = StateData1} ->
300+
send_post_handshake_data(HSData, From, connection, StateData1, [{reply, From, ok}]);
301+
{ok, StateData1} ->
302+
{keep_state, StateData1, [postpone]};
303+
{error, Reason, StateData1} ->
304+
death_row_shutdown({error, Reason}, StateData1)
305+
end
290306
end;
291307
connection({call, From}, {ack_alert, #alert{} = Alert}, #data{buff = Buff} = StateData0) ->
292308
case Buff of
293309
undefined ->
294310
StateData = send_tls_alert(Alert, StateData0),
295311
{next_state, connection, StateData, [{reply,From,ok}]};
296-
Async ->
297-
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]}
312+
#async{} = Async ->
313+
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]};
314+
#rest{} ->
315+
case flush_rest_buffer(StateData0) of
316+
{ok, #data{buff = undefined} = StateData1} ->
317+
StateData = send_tls_alert(Alert, StateData1),
318+
{next_state, connection, StateData, [{reply,From,ok}]};
319+
{ok, StateData1} ->
320+
{keep_state, StateData1, [postpone]};
321+
{error, Reason, StateData1} ->
322+
death_row_shutdown({error, Reason}, StateData1)
323+
end
298324
end;
299325
connection({call, From}, renegotiate,
300326
#data{connection_states = #{current_write := Write}, buff = Buff} = StateData) ->
301327
case Buff of
302328
undefined ->
303329
{next_state, handshake, StateData, [{reply, From, {ok, Write}}]};
304-
Async ->
305-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
330+
#async{} = Async ->
331+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
332+
#rest{} ->
333+
case flush_rest_buffer(StateData) of
334+
{ok, #data{buff = undefined} = StateData1} ->
335+
{next_state, handshake, StateData1, [{reply, From, {ok, Write}}]};
336+
{ok, StateData1} ->
337+
{keep_state, StateData1, [postpone]};
338+
{error, Reason, StateData1} ->
339+
death_row_shutdown({error, Reason}, StateData1)
340+
end
306341
end;
307342
connection({call, From}, downgrade, #data{connection_states =
308343
#{current_write := Write}} = StateData) ->
@@ -329,17 +364,36 @@ connection(internal, {post_handshake_data, From, HSData}, #data{buff = Buff} = S
329364
case Buff of
330365
undefined ->
331366
send_post_handshake_data(HSData, From, connection, StateData, []);
332-
Async ->
333-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
367+
#async{} = Async ->
368+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
369+
#rest{} ->
370+
case flush_rest_buffer(StateData) of
371+
{ok, #data{buff = undefined} = StateData1} ->
372+
send_post_handshake_data(HSData, From, connection, StateData1, []);
373+
{ok, StateData1} ->
374+
{keep_state, StateData1, [postpone]};
375+
{error, Reason, StateData1} ->
376+
death_row_shutdown({error, Reason}, StateData1)
377+
end
334378
end;
335379

336380
connection(cast, #alert{} = Alert, #data{buff = Buff} = StateData0) ->
337381
case Buff of
338382
undefined ->
339383
StateData = send_tls_alert(Alert, StateData0),
340384
{next_state, connection, StateData};
341-
Async ->
342-
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]}
385+
#async{} = Async ->
386+
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]};
387+
#rest{} ->
388+
case flush_rest_buffer(StateData0) of
389+
{ok, #data{buff = undefined} = StateData1} ->
390+
StateData = send_tls_alert(Alert, StateData1),
391+
{next_state, connection, StateData};
392+
{ok, StateData1} ->
393+
{keep_state, StateData1, [postpone]};
394+
{error, Reason, StateData1} ->
395+
death_row_shutdown({error, Reason}, StateData1)
396+
end
343397
end;
344398
connection(cast, {new_write, WritesState, Version, MaxFragLen},
345399
#data{connection_states = ConnectionStates0, env = Env} = StateData) ->
@@ -589,6 +643,12 @@ send_or_buffer(Transport, Socket, Msgs, From, #data{buff = undefined} = StateDat
589643
ok ->
590644
send_reply(From, ok),
591645
{ok, StateData0};
646+
{error, {timeout, RestData}} ->
647+
%% gen_tcp:send with {inet_backend, socket} returns unsent
648+
%% encrypted data on timeout. Buffer it for retry on next send.
649+
%% Reply {error, timeout} to simulate {inet_backend, inet} behavior.
650+
send_reply(From, {error, timeout}),
651+
{ok, StateData0#data{buff = #rest{q_rev = RestData}}};
592652
{error, timeout} = Error ->
593653
%% This clause is to retain some backwards compatibility with
594654
%% inet-driver behavior for gen_tcp:send timeout. That
@@ -626,7 +686,25 @@ send_or_buffer(Transport, Socket, Msgs, From, #data{buff = undefined} = StateDat
626686
{block, StateData0#data{buff = Async#async{reply_to = From}}}
627687
end
628688
end;
629-
%% Buffer exists, push more data to buffer
689+
%% Rest buffer exists, flush buffered data together with new data.
690+
%% Transport is gen_tcp, no async select/completion results.
691+
send_or_buffer(Transport, Socket, Msgs, From,
692+
#data{buff = #rest{q_rev = BuffData}} = StateData0) ->
693+
case tls_socket:send(Transport, Socket, [BuffData | Msgs]) of
694+
ok ->
695+
send_reply(From, ok),
696+
{ok, StateData0#data{buff = undefined}};
697+
{error, {timeout, RestData}} ->
698+
send_reply(From, {error, timeout}),
699+
{ok, StateData0#data{buff = #rest{q_rev = RestData}}};
700+
{error, timeout} = Error ->
701+
send_reply(From, Error),
702+
{ok, StateData0#data{buff = undefined}};
703+
{error, _Err} = Error ->
704+
send_reply(From, Error),
705+
Error
706+
end;
707+
%% Async buffer exists, push more data to buffer
630708
send_or_buffer(_Transport, _Socket, Msgs, From, #data{buff = Async0} = StateData) ->
631709
#async{high = High, size = Sz0, q_rev = Q} = Async0,
632710
Sz = Sz0 + iolist_size(Msgs),
@@ -639,6 +717,22 @@ send_or_buffer(_Transport, _Socket, Msgs, From, #data{buff = Async0} = StateData
639717
{block, StateData#data{buff = Async#async{reply_to = From}}}
640718
end.
641719

720+
%% Try to flush the #rest{} buffer. Returns {ok, #data{}} on success
721+
%% or timeout, {error, Reason, #data{}} on hard send failure.
722+
flush_rest_buffer(#data{env = #env{socket = Socket,
723+
transport_cb = Transport},
724+
buff = #rest{q_rev = BuffData}} = StateData) ->
725+
case tls_socket:send(Transport, Socket, BuffData) of
726+
ok ->
727+
{ok, StateData#data{buff = undefined}};
728+
{error, {timeout, RestData}} ->
729+
{ok, StateData#data{buff = #rest{q_rev = RestData}}};
730+
{error, timeout} ->
731+
{ok, StateData#data{buff = undefined}};
732+
{error, Reason} ->
733+
{error, Reason, StateData#data{buff = undefined}}
734+
end.
735+
642736
do_async_send(_Transport, _Socket, _Handle, _Nextstate, {error, Err} = Error,
643737
#data{buff = #async{reply_to = From}} = StateData) ->
644738
send_reply(From, Error),

lib/ssl/test/ssl_api_SUITE.erl

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
select_sha1_cert/1,
6464
inet_backend_option_order/0,
6565
inet_backend_option_order/1,
66+
send_timeout_buffering/0,
67+
send_timeout_buffering/1,
6668
root_any_sign/0,
6769
root_any_sign/1,
6870
connection_information/0,
@@ -230,6 +232,8 @@
230232
suite_check/2,
231233
ecdsa_cert_check/1,
232234
check_peercert/2,
235+
send_timeout_sink/1,
236+
send_timeout_fill/1,
233237
%%TODO Keep?
234238
run_error_server/1,
235239
run_client_error/1
@@ -267,9 +271,11 @@ groups() ->
267271
{'tlsv1.1', [parallel], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3() ++ pre_1_2()},
268272
{'tlsv1', [parallel], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3() ++ pre_1_2() ++
269273
beast_mitigation_test()},
270-
{'dtlsv1.2', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server] ++
274+
{'dtlsv1.2', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server,
275+
send_timeout_buffering] ++
271276
handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3()},
272-
{'dtlsv1', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server] ++
277+
{'dtlsv1', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server,
278+
send_timeout_buffering] ++
273279
handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3() ++ pre_1_2()},
274280
{transport_socket, [parallel], gen_api_tests() -- [ssl_not_started, dh_params]}
275281
].
@@ -309,6 +315,7 @@ gen_api_tests() ->
309315
peercert_with_client_cert,
310316
select_sha1_cert,
311317
inet_backend_option_order,
318+
send_timeout_buffering,
312319
connection_information,
313320
secret_connection_info,
314321
keylog_connection_info,
@@ -677,6 +684,73 @@ inet_backend_option_order(Config) when is_list(Config) ->
677684
ssl_test_lib:close(Server),
678685
ssl_test_lib:close(Client).
679686

687+
%%--------------------------------------------------------------------
688+
send_timeout_buffering() ->
689+
[{doc,"Test that ssl buffers unsent encrypted data on send timeout "
690+
"when using {inet_backend, socket} and retries on next send"}].
691+
send_timeout_buffering(Config) when is_list(Config) ->
692+
ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
693+
ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config),
694+
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
695+
Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
696+
{from, self()},
697+
{mfa, {?MODULE, send_timeout_sink, []}},
698+
{options, [{inet_backend, socket},
699+
{active, false}
700+
| ServerOpts]}]),
701+
Port = ssl_test_lib:inet_port(Server),
702+
Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port},
703+
{host, Hostname},
704+
{from, self()},
705+
{mfa, {?MODULE, send_timeout_fill, []}},
706+
{options, [{inet_backend, socket},
707+
{active, false},
708+
{sndbuf, 4096},
709+
{send_timeout, 1}
710+
| ClientOpts]}]),
711+
712+
ssl_test_lib:check_result(Server, ok, Client, ok),
713+
714+
ssl_test_lib:close(Server),
715+
ssl_test_lib:close(Client).
716+
717+
send_timeout_sink(Socket) ->
718+
%% Server side: register, wait for signal, drain, signal back
719+
register(send_timeout_sink_server, self()),
720+
receive start_recv -> ok end,
721+
send_timeout_recv_loop(Socket),
722+
send_timeout_sink_client ! drained,
723+
ok.
724+
725+
send_timeout_recv_loop(Socket) ->
726+
case ssl:recv(Socket, 0, 1000) of
727+
{ok, _} -> send_timeout_recv_loop(Socket);
728+
{error, timeout} -> ok;
729+
{error, closed} -> ok
730+
end.
731+
732+
send_timeout_fill(Socket) ->
733+
%% Client side: fill buffer, signal server, wait for drain, send again
734+
register(send_timeout_sink_client, self()),
735+
Data = <<0:(1024*8)>>,
736+
send_timeout_fill_loop(Socket, Data, 0).
737+
738+
send_timeout_fill_loop(Socket, Data, N) ->
739+
case ssl:send(Socket, Data) of
740+
ok ->
741+
send_timeout_fill_loop(Socket, Data, N + 1);
742+
{error, timeout} when N > 0 ->
743+
%% Buffer filled. Signal server to start draining.
744+
send_timeout_sink_server ! start_recv,
745+
%% Wait for server to finish draining.
746+
receive drained -> ok end,
747+
%% Verify connection is still usable.
748+
ok = ssl:send(Socket, <<"still alive">>),
749+
ok;
750+
{error, _} = Error ->
751+
Error
752+
end.
753+
680754
%%--------------------------------------------------------------------
681755
connection_information() ->
682756
[{doc,"Test the API function ssl:connection_information/1"}].

0 commit comments

Comments
 (0)