diff --git a/src/shared/file_send.c b/src/shared/file_send.c index 9f43724..70f0e95 100644 --- a/src/shared/file_send.c +++ b/src/shared/file_send.c @@ -124,8 +124,12 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta } /* sendfile cannot encrypt TLS records. Keep the framing identical but - route encrypted transfers through the deadline-aware IO layer. */ - if (io_get_ssl() != NULL) { + route encrypted transfers through the deadline-aware IO layer. Resolve + the transport from the bound session, not the thread-local io_ssl: a + worker thread running a TLS transfer has its SSL only on the session it + bound, so io_get_ssl() would be NULL there and the raw sendfile() path + would be taken on an encrypted socket. */ + if (protocol_current_ssl() != NULL) { unsigned char buffer[64 * 1024]; unsigned long long remaining = file_size; bool ok = true; diff --git a/src/shared/protocol.c b/src/shared/protocol.c index b6f8280..eba3a61 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -270,6 +270,17 @@ SSL* io_get_ssl(void) { return io_ssl; } +SSL* protocol_current_ssl(void) { + /* The bound session is the authoritative transport for a worker thread: it + * was explicitly handed to protocol_session_bind() and carries its own SSL, + * whereas io_ssl is thread-local and NULL in a thread that never performed + * the handshake. With no session bound (the fd-shim path), fall back to the + * legacy thread-local SSL. */ + if (bound_session) + return bound_session->ssl; + return io_ssl; +} + unsigned long long protocol_bytes_written(void) { return atomic_load(&io_bytes_written); } diff --git a/src/shared/protocol.h b/src/shared/protocol.h index 0bb0b42..ff4ec0d 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -216,6 +216,15 @@ void io_set_bwlimit(unsigned long long bytes_per_sec); unsigned long long io_get_bwlimit(void); void io_set_ssl(SSL* ssl); SSL* io_get_ssl(void); +/* SSL object of the transport in effect on this thread: the currently bound + * session's SSL when a session is bound, otherwise the legacy thread-local + * io_ssl. NULL for a plaintext transport. Unlike io_get_ssl(), this resolves + * worker threads that bound a TLS session via protocol_session_set_ssl()/ + * protocol_session_bind() but never called io_set_ssl() themselves (C11 + * _Thread_local state is not inherited by a new thread). Callers that must + * choose a TLS-only code path (e.g. file_send.c's sendfile fallback) must use + * this instead of io_get_ssl(). */ +SSL* protocol_current_ssl(void); /* Process-wide wire byte counters. protocol_send_n_data/protocol_receive_n_data * update them; the zero-copy sendfile path reports through diff --git a/tests/integration/test_tls.py b/tests/integration/test_tls.py index 1e5c029..cdc3a24 100644 --- a/tests/integration/test_tls.py +++ b/tests/integration/test_tls.py @@ -146,7 +146,6 @@ class TestTLSBasic: assert not missing, f"Missing files: {missing}" assert not mismatches, f"Mismatched files: {mismatches}" - @pytest.mark.xfail(reason="TLS multithreading has architectural limitations with per-thread SSL context") def test_tls_with_multithreading(self, certs): """TLS + multithreading.""" clean_dir(DEST_DIR)