-
Notifications
You must be signed in to change notification settings - Fork 21
tls/ossl: Add concurrency protections around output #245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v25.3.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1007,6 +1007,7 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| , _in_sem(1) | ||||||||||||||
| , _out_sem(1) | ||||||||||||||
| , _options(std::move(options)) | ||||||||||||||
| , _output_in_progress(false) | ||||||||||||||
| , _output_pending(make_ready_future<>()) | ||||||||||||||
| , _ctx(make_ssl_context(t)) | ||||||||||||||
| , _ssl([this]() { | ||||||||||||||
|
|
@@ -1065,7 +1066,7 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| : session(t, std::move(creds), net::get_impl::get(std::move(sock)), options) {} | ||||||||||||||
|
|
||||||||||||||
| ~session() { | ||||||||||||||
| SEASTAR_ASSERT(_output_pending.available()); | ||||||||||||||
| SEASTAR_ASSERT(output_available()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const char * get_type_string() const { | ||||||||||||||
|
|
@@ -1166,7 +1167,16 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| // any unprocessed part of the packet is returned. | ||||||||||||||
| future<> do_put(net::packet p) { | ||||||||||||||
| tls_log.trace("{} do_put", *this); | ||||||||||||||
| SEASTAR_ASSERT(_output_pending.available()); | ||||||||||||||
|
|
||||||||||||||
| // the put path is protected from concurrent calls by the _out_sem semaphore, | ||||||||||||||
| // however it is possible via the read (get()) path that a renegotiation may | ||||||||||||||
| // occur, putting an outstanding write on the output path. Originally, | ||||||||||||||
| // there was an assert to ensure that _output_pending was available. This assert | ||||||||||||||
| // would correctly serve the purpose that no writes occurred outside of the protection | ||||||||||||||
| // of the _out_sem. However, since writes from OpenSSL can occur outside of this semaphore, | ||||||||||||||
| // we will attempt to perform a write even if there is an outstanding write pending. | ||||||||||||||
| // This will result in SSL_write_ex failing with SSL_WANT_WRITE as the error code. | ||||||||||||||
| // This will force a wait_for_output to occur. | ||||||||||||||
| return do_with(std::move(p), | ||||||||||||||
| [this](net::packet& p) { | ||||||||||||||
| // This do_until runs until either a renegotiation occurs or the packet is empty | ||||||||||||||
|
|
@@ -1655,7 +1665,8 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| // handshake aqcuire, because in worst case, we get here while a reader is attempting | ||||||||||||||
| // re-handshake. | ||||||||||||||
| return with_semaphore(_in_sem, 1, [this] { | ||||||||||||||
| return with_semaphore(_out_sem, 1, [] { }); | ||||||||||||||
| return with_semaphore(_out_sem, 1, [] { | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
| }).handle_exception([me = shared_from_this()](std::exception_ptr){ | ||||||||||||||
| }).discard_result()); | ||||||||||||||
|
|
@@ -1788,6 +1799,40 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| return _remote_address; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| future<>& output_pending() { | ||||||||||||||
| return _output_pending; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| bool output_available() { | ||||||||||||||
| // the wait_for_output method exchanges _output_pending for a ready | ||||||||||||||
| // future and then awaits for the output to complete. Checking that | ||||||||||||||
| // _output_pending is not enough to ensure that the output has completed. | ||||||||||||||
| // The purpose of the output_in_progress flag is to await that that | ||||||||||||||
| // future has resolved before permitting another write to be enqueued. | ||||||||||||||
| return !_output_in_progress && _output_pending.available(); | ||||||||||||||
joe-redpanda marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should theoretically be sufficient to just check _output_in_progress, ya?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah... I guess I'm just overdoing it a little bit |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| void assign_output_pending(future<> f) { | ||||||||||||||
| SEASTAR_ASSERT(!_output_in_progress); | ||||||||||||||
| SEASTAR_ASSERT(_output_pending.available()); | ||||||||||||||
| _output_in_progress = true; | ||||||||||||||
| _output_pending = std::move(f).finally([this]{_output_in_progress = false;}); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| void assign_output_error(std::exception_ptr ep) { | ||||||||||||||
michael-redpanda marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| SEASTAR_ASSERT(_output_pending.available()); | ||||||||||||||
| _output_in_progress = false; | ||||||||||||||
| _output_pending = make_exception_future<>(ep); | ||||||||||||||
joe-redpanda marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| data_sink& out() { | ||||||||||||||
| return _out; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| buf_type& input() { | ||||||||||||||
| return _input; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private: | ||||||||||||||
| std::vector<subject_alt_name> do_get_alt_name_information(const x509_ptr &peer_cert, | ||||||||||||||
| const std::unordered_set<subject_alt_name_type> &types) const { | ||||||||||||||
|
|
@@ -2152,6 +2197,7 @@ class session : public enable_shared_from_this<session>, public session_impl { | |||||||||||||
| semaphore _out_sem; | ||||||||||||||
| tls_options _options; | ||||||||||||||
|
|
||||||||||||||
|
||||||||||||||
| // Tracks when output operations are actively being processed. | |
| // This is distinct from when futures are pending (_output_pending). | |
| // Used as a concurrency control mechanism to prevent concurrent writes | |
| // during renegotiation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q:
so if i'm reading this correctly the source of errors is our put onto the network layer
which just gets stored in the future as an exception future,
and then all subsequent calls to bio_write_ex will just continually grab, rethrow, and re-store the exception back into the future?
Who is the ultimate upstream error handler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caller to put() will handle the error. These are considered fatal so then the session should be destructed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does get_exception extract the exception?
If thats the case we could have
put -> bio_write -> future emplaced -> exception -> put detects and throws
followed by an out of band write or another put
Wondering if we should check that the future remains errored for all subsequent callers as a signal of "this session is borked"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what happens is
- PUT
- BIO write (error occurs and error is put into
_output_pending wait_for_output()gets called and attemps to resolve_output_pending- Within
wait_for_ouptut()handle_exceptionis triggered and that emplaces the error into_error - Any future calls to
put()check_errorand if that is set, returns_error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PUT
BIO write -> error stored in _output_pending
wait_for_output -> moves the exception out?, regardless throws
read fiber performs an out of band write, will it see the exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
the usages of input are either const (get size/empty)
or an extraction of a chunk.
the ref handing function breaks encapsulation, maybe just two member functions for get_input_size and extract_input(max_size)
Uh oh!
There was an error while loading. Please reload this page.