Skip to content

Conversation

@michael-redpanda
Copy link

This commit makes the following changes to add better concurrency protection around the output path:

  • No longer treats the BIO methods as friend functions
  • Adds an _output_in_progress flag

It was originally understood that data being written to the output BIO would only occur while the TLS session's _out_sem semaphore was held. Assertions ensuring there were no concurrent output operations occuring provided guarantees that concurrent writes would not occur. Anytime data was written to the output BIO of the TLS session, it was followed by a call to wait_for_output() which would resolve only after the write completed.

However, we have come to learn that data may be written outside of the protections afforded by the _out_sem semaphore during the get() method. This would, rarely, result in assertions firing during put().

This additionally could cause, in rare circumstances, a nullptr dereference as this extraneous write would overwrite the packet of a write in progress.

The change to use the _output_in_progress flag ensures that the output operation has fully completed before attempting another write to the socket. Additionally, removing the friend relationship between the BIO methods and the TLS session ensures that future developers use the public functions of the session rather than directly modifying the members of the class.

@michael-redpanda michael-redpanda self-assigned this Nov 19, 2025
Copilot AI review requested due to automatic review settings November 19, 2025 20:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds concurrency protections around the TLS session's output operations to prevent rare race conditions during writes. The changes address a scenario where data could be written to the output BIO during get() operations (e.g., during renegotiation) outside the protection of the _out_sem semaphore, potentially causing assertion failures or nullptr dereferences.

Key Changes:

  • Introduced _output_in_progress flag to track when output operations are actively being processed
  • Removed friend declarations for BIO methods to enforce proper encapsulation
  • Added public accessor methods (output_pending(), output_available(), assign_output_pending(), etc.) for controlled access to output state

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@michael-redpanda michael-redpanda force-pushed the better-write-concurrency-protection branch from 5099fbf to a2a5da3 Compare November 19, 2025 20:51
Copy link

@rockwotj rockwotj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this makes sense. I wish there was a simpler way to encode this but I'm struggling to come up with something.

PS it'd be nice to coroutinize some of this code now.

@michael-redpanda michael-redpanda force-pushed the better-write-concurrency-protection branch from a2a5da3 to 33cf225 Compare November 24, 2025 21:20
@michael-redpanda
Copy link
Author

Force push:

  • Addressed bot comments and removed commented out code

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

rockwotj
rockwotj previously approved these changes Nov 24, 2025
@michael-redpanda
Copy link
Author

Force push:

  • Set output_in_progress to be false when output_pending is assigned an exception future

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/net/ossl.cc Outdated
Comment on lines 1671 to 1672
_output_in_progress = false;
});
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Directly setting _output_in_progress = false bypasses the encapsulation provided by assign_output_error(). Consider using a dedicated method like clear_output_in_progress() to maintain consistency with other output state management functions and improve maintainability.

Copilot uses AI. Check for mistakes.
semaphore _in_sem;
semaphore _out_sem;
tls_options _options;

Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _output_in_progress flag is a critical concurrency control mechanism but lacks documentation. Add a comment explaining its purpose: tracking when output operations are actively being processed, distinct from when futures are pending, to prevent concurrent writes during renegotiation.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
@michael-redpanda
Copy link
Author

All failing the DNS unit test:

18/84 Test #18: Seastar.unit.dns ..............................***Failed    3.88 sec
[0/1] cd /home/runner/work/seastar/seastar/build/release/tests/unit && /usr/local/bin/cmake -E env ASAN_OPTIONS=disable_coredump=0:abort_on_error=1:detect_stack_use_after_return=1 UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1 BOOST_TEST_CATCH_SYSTEM_ERRORS=no /home/runner/work/seastar/seastar/build/release/tests/unit/dns_test -- -c 2
Running 9 test cases...
INFO  2025-11-24 21:25:38,304 seastar - Reactor backend: linux-aio
INFO  2025-11-24 21:25:38,304 seastar - Perf-based stall detector creation failed (EACCESS), try setting /proc/sys/kernel/perf_event_paranoid to 1 or less to enable kernel backtraces: falling back to posix timer.
INFO  2025-11-24 21:25:38,304 cpu_profiler - Perf-based cpu profiler creation failed (EACCESS), try setting /proc/sys/kernel/perf_event_paranoid to 1 or less to enable kernel backtraces: falling back to posix timer.
INFO  2025-11-24 21:25:38,305 [shard 0:main] seastar - IO queue was unable to find a suitable maximum request length, the search was cut-off early at: 16MB
INFO  2025-11-24 21:25:38,305 [shard 0:main] seastar - IO queue was unable to find a suitable maximum request length, the search was cut-off early at: 16MB
random-seed=1820110842
unknown location(0): fatal error: in "test_resolve_udp": std::system_error: 172.67.217.70: Not found
/home/runner/work/seastar/seastar/src/testing/seastar_test.cc(43): last checkpoint
unknown location(0): fatal error: in "test_resolve_tcp": std::system_error: 172.67.217.70: Not found
/home/runner/work/seastar/seastar/src/testing/seastar_test.cc(43): last checkpoint

*** 2 failures are detected in the test module "Master Test Suite"

happening in both OpenSSL & GnuTLS so probably not what I did....

src/net/ossl.cc Outdated
SEASTAR_ASSERT(!_output_in_progress);
SEASTAR_ASSERT(_output_pending.available());
_output_in_progress = true;
_output_pending = std::move(f);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just tag the reset for output in progress here

_output_in_progress = true;
_output_pending = std::move(f).finally([this]{this->_output_in_progress = false;});

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good thought... that won't conflict with error handling in wait_for_output right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move the error handling to here too?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really only need wait_for_output to side channel the future from the synchronous BIO_* stuff in ossl right? Feels like consolidating everythere here is better.

tls_log.debug("{} bio_write_ex: system error occurred: {}", *session, e.what());
ERR_raise_data(ERR_LIB_SYS, e.code().value(), e.what());
session->_output_pending = make_exception_future<>(std::current_exception());
session->assign_output_error(std::current_exception());

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?

Copy link
Author

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

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"

Copy link
Author

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_exception is triggered and that emplaces the error into _error
  • Any future calls to put() check _error and if that is set, returns _error

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?

// _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();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should theoretically be sufficient to just check _output_in_progress, ya?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... I guess I'm just overdoing it a little bit

@michael-redpanda michael-redpanda force-pushed the better-write-concurrency-protection branch from c30d237 to 027a751 Compare November 25, 2025 16:21
@michael-redpanda
Copy link
Author

DNS tests will be fixed here: #246

auto n = std::min(dlen, session->_input.size());
memcpy(data, session->_input.get(), n);
session->_input.trim_front(n);
auto n = std::min(dlen, session->input().size());

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)

This commit makes the following changes to add better concurrency
protection around the output path:

* No longer treats the BIO methods as friend functions
* Adds an _output_in_progress flag

It was originally understood that data being written to the output BIO
would only occur while the TLS session's _out_sem semaphore was held.
Assertions ensuring there were no concurrent output operations occuring
provided guarantees that concurrent writes would not occur.  Anytime
data was written to the output BIO of the TLS session, it was followed
by a call to wait_for_output() which would resolve only after the write
completed.

However, we have come to learn that data may be written outside of the
protections afforded by the _out_sem semaphore during the get() method.
This would, rarely, result in assertions firing during put().

This additionally could cause, in rare circumstances, a nullptr
dereference as this extraneous write would overwrite the packet of a
write in progress.

The change to use the _output_in_progress flag ensures that the output
operation has fully completed before attempting another write to the
socket.  Additionally, removing the friend relationship between the BIO
methods and the TLS session ensures that future developers use the
public functions of the session rather than directly modifying the
members of the class.

Signed-off-by: Michael Boquard <[email protected]>
@michael-redpanda michael-redpanda force-pushed the better-write-concurrency-protection branch from 027a751 to b374848 Compare November 25, 2025 21:35
@michael-redpanda
Copy link
Author

Force push:

  • Rebased off of v25.3.x

Copy link

@rockwotj rockwotj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status here? Do we feel good about this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants