From 83b44786ba9559a19ae101e179a2f3e4042613f5 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:06:15 -0600 Subject: [PATCH 1/6] tls: openssl: preserve syscall error details Signed-off-by: Eduardo Silva --- src/tls/openssl.c | 72 +++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 53e3574ea8a..19ed6c1cce0 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1442,10 +1442,34 @@ static const char *tls_session_alpn_get(void *session_) return backend_session->alpn; } +static void tls_log_syscall_error(int io_ret, int saved_errno) +{ + unsigned long err_code; + char err_buf[256]; + + err_code = ERR_get_error(); + + if (err_code != 0) { + ERR_error_string_n(err_code, err_buf, sizeof(err_buf) - 1); + flb_error("[tls] error: %s", err_buf); + } + else if (saved_errno != 0) { + flb_error("[tls] syscall error: %s", strerror(saved_errno)); + } + else if (io_ret == 0) { + flb_error("[tls] error: unexpected EOF"); + } + else { + flb_error("[tls] syscall error without error code"); + } +} + static int tls_net_read(struct flb_tls_session *session, void *buf, size_t len) { int ret; + int ssl_ret; + int saved_errno; unsigned long err_code; char err_buf[256]; struct tls_context *ctx; @@ -1465,40 +1489,32 @@ static int tls_net_read(struct flb_tls_session *session, ERR_clear_error(); + errno = 0; ret = SSL_read(backend_session->ssl, buf, len); + saved_errno = errno; if (ret <= 0) { - ret = SSL_get_error(backend_session->ssl, ret); + ssl_ret = SSL_get_error(backend_session->ssl, ret); - if (ret == SSL_ERROR_WANT_READ) { + if (ssl_ret == SSL_ERROR_WANT_READ) { ret = FLB_TLS_WANT_READ; } - else if (ret == SSL_ERROR_WANT_WRITE) { + else if (ssl_ret == SSL_ERROR_WANT_WRITE) { ret = FLB_TLS_WANT_WRITE; } - else if (ret == SSL_ERROR_SYSCALL) { - flb_errno(); - - err_code = ERR_get_error(); - - if (err_code != 0) { - ERR_error_string_n(err_code, err_buf, sizeof(err_buf)-1); - flb_error("[tls] syscall error: %s", err_buf); - } - else { - flb_error("[tls] syscall error: %s", strerror(errno)); - } + else if (ssl_ret == SSL_ERROR_SYSCALL) { + tls_log_syscall_error(ret, saved_errno); /* According to the documentation these are non-recoverable * errors so we don't need to screen them before saving them * to the net_error field. */ - session->connection->net_error = errno; + session->connection->net_error = saved_errno; ret = -1; } - else if (ret < 0) { + else if (ssl_ret < 0) { err_code = ERR_get_error(); if (err_code != 0) { @@ -1506,7 +1522,7 @@ static int tls_net_read(struct flb_tls_session *session, flb_error("[tls] error: %s", err_buf); } else { - flb_error("[tls] error: %s", strerror(errno)); + flb_error("[tls] error: %s", strerror(saved_errno)); } } else { @@ -1523,6 +1539,7 @@ static int tls_net_write(struct flb_tls_session *session, { int ret; int ssl_ret; + int saved_errno; unsigned long err_code; char err_buf[256]; size_t total = 0; @@ -1542,9 +1559,11 @@ static int tls_net_write(struct flb_tls_session *session, ERR_clear_error(); + errno = 0; ret = SSL_write(backend_session->ssl, (unsigned char *) data + total, len - total); + saved_errno = errno; if (ret <= 0) { ssl_ret = SSL_get_error(backend_session->ssl, ret); @@ -1556,27 +1575,14 @@ static int tls_net_write(struct flb_tls_session *session, ret = FLB_TLS_WANT_READ; } else if (ssl_ret == SSL_ERROR_SYSCALL) { - err_code = ERR_get_error(); - - if (err_code == 0) { - if (ret == 0) { - flb_debug("[tls] connection closed"); - } - else { - flb_error("[tls] syscall error: %s", strerror(errno)); - } - } - else { - ERR_error_string_n(err_code, err_buf, sizeof(err_buf) - 1); - flb_error("[tls] syscall error: %s", err_buf); - } + tls_log_syscall_error(ret, saved_errno); /* According to the documentation these are non-recoverable * errors so we don't need to screen them before saving them * to the net_error field. */ - session->connection->net_error = errno; + session->connection->net_error = saved_errno; ret = -1; } From eb7337ca55e350bc9600645aed74258dd3af1a13 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:06:15 -0600 Subject: [PATCH 2/6] tests: integration: cover TLS syscall error reporting Signed-off-by: Eduardo Silva --- .../in_forward/tests/test_in_forward_001.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py index 0f5ef1db9e3..257fd9cba58 100644 --- a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py +++ b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py @@ -5,6 +5,7 @@ import shutil import socket import ssl +import struct import subprocess import sys import tempfile @@ -648,6 +649,53 @@ def _send_tls_payload(port, payload, cafile): tls_sock.sendall(payload) +def _create_tls_memory_bio_client(port, cafile): + context = ssl.create_default_context(cafile=cafile) + incoming = ssl.MemoryBIO() + outgoing = ssl.MemoryBIO() + tls = context.wrap_bio(incoming, outgoing, server_hostname="localhost") + raw_sock = socket.create_connection(("127.0.0.1", port), timeout=5) + + while True: + try: + tls.do_handshake() + break + except ssl.SSLWantReadError: + encrypted = outgoing.read() + if encrypted: + raw_sock.sendall(encrypted) + + encrypted = raw_sock.recv(65536) + assert encrypted + incoming.write(encrypted) + except ssl.SSLWantWriteError: + encrypted = outgoing.read() + if encrypted: + raw_sock.sendall(encrypted) + + encrypted = outgoing.read() + if encrypted: + raw_sock.sendall(encrypted) + + return raw_sock, tls, outgoing + + +def _reset_tls_connection(port, cafile): + raw_sock, tls, outgoing = _create_tls_memory_bio_client(port, cafile) + + tls.write(b"\x91") + wire_payload = outgoing.read() + raw_sock.sendall(wire_payload[:-1]) + time.sleep(1) + + raw_sock.setsockopt( + socket.SOL_SOCKET, + socket.SO_LINGER, + struct.pack("ii", 1, 0), + ) + raw_sock.close() + + def _recv_msgpack_value(sock): sock.settimeout(5) data = sock.recv(4096) @@ -1027,6 +1075,35 @@ def test_in_forward_tls_message_mode(): assert records[0]["message"] == "tls-message" +def test_in_forward_tls_syscall_error_preserves_errno(): + service = Service("in_forward_tls.yaml") + service.start() + + try: + _reset_tls_connection( + service.flb_listener_port, + service.tls_crt_file, + ) + log_text = service.wait_for_log_contains("[tls] syscall error:", timeout=10) + + payload = _message_mode_payload( + TEST_TAG, + {"message": "after-tls-reset"}, + ) + _send_tls_payload(service.flb_listener_port, payload, service.tls_crt_file) + records = service.wait_for_record_count(1, timeout=10) + finally: + service.stop() + + assert "Connection reset by peer" in log_text + assert "Inappropriate ioctl for device" not in log_text + assert not any( + "openssl.c:" in line and "errno=" in line + for line in log_text.splitlines() + ) + assert records[0]["message"] == "after-tls-reset" + + def test_in_forward_tls_idle_connection_timeout_churn(monkeypatch): """ Regression test for issue #12025. From ac99e2942e62474079faa33407b5c4f28b5ff893 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:28:41 -0600 Subject: [PATCH 3/6] tls: openssl: handle fatal I/O errors Signed-off-by: Eduardo Silva --- src/tls/openssl.c | 52 ++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 19ed6c1cce0..58ce307e560 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1442,7 +1442,7 @@ static const char *tls_session_alpn_get(void *session_) return backend_session->alpn; } -static void tls_log_syscall_error(int io_ret, int saved_errno) +static void tls_log_io_error(int ssl_error, int saved_errno) { unsigned long err_code; char err_buf[256]; @@ -1453,14 +1453,14 @@ static void tls_log_syscall_error(int io_ret, int saved_errno) ERR_error_string_n(err_code, err_buf, sizeof(err_buf) - 1); flb_error("[tls] error: %s", err_buf); } - else if (saved_errno != 0) { + else if (ssl_error == SSL_ERROR_SYSCALL && saved_errno != 0) { flb_error("[tls] syscall error: %s", strerror(saved_errno)); } - else if (io_ret == 0) { + else if (ssl_error == SSL_ERROR_SYSCALL) { flb_error("[tls] error: unexpected EOF"); } else { - flb_error("[tls] syscall error without error code"); + flb_error("[tls] unknown error (ssl_error=%d)", ssl_error); } } @@ -1470,8 +1470,6 @@ static int tls_net_read(struct flb_tls_session *session, int ret; int ssl_ret; int saved_errno; - unsigned long err_code; - char err_buf[256]; struct tls_context *ctx; struct tls_session *backend_session; @@ -1503,29 +1501,26 @@ static int tls_net_read(struct flb_tls_session *session, ret = FLB_TLS_WANT_WRITE; } else if (ssl_ret == SSL_ERROR_SYSCALL) { - tls_log_syscall_error(ret, saved_errno); + tls_log_io_error(ssl_ret, saved_errno); /* According to the documentation these are non-recoverable * errors so we don't need to screen them before saving them * to the net_error field. */ - session->connection->net_error = saved_errno; - - ret = -1; - } - else if (ssl_ret < 0) { - err_code = ERR_get_error(); - - if (err_code != 0) { - ERR_error_string_n(err_code, err_buf, sizeof(err_buf)-1); - flb_error("[tls] error: %s", err_buf); + if (saved_errno != 0) { + session->connection->net_error = saved_errno; } else { - flb_error("[tls] error: %s", strerror(saved_errno)); + session->connection->net_error = ECONNRESET; } + + ret = -1; } else { + tls_log_io_error(ssl_ret, saved_errno); + session->connection->net_error = ECONNRESET; + ret = -1; } } @@ -1540,8 +1535,6 @@ static int tls_net_write(struct flb_tls_session *session, int ret; int ssl_ret; int saved_errno; - unsigned long err_code; - char err_buf[256]; size_t total = 0; struct tls_context *ctx; struct tls_session *backend_session; @@ -1575,26 +1568,25 @@ static int tls_net_write(struct flb_tls_session *session, ret = FLB_TLS_WANT_READ; } else if (ssl_ret == SSL_ERROR_SYSCALL) { - tls_log_syscall_error(ret, saved_errno); + tls_log_io_error(ssl_ret, saved_errno); /* According to the documentation these are non-recoverable * errors so we don't need to screen them before saving them * to the net_error field. */ - session->connection->net_error = saved_errno; + if (saved_errno != 0) { + session->connection->net_error = saved_errno; + } + else { + session->connection->net_error = ECONNRESET; + } ret = -1; } else { - err_code = ERR_get_error(); - if (err_code == 0) { - flb_error("[tls] unknown error"); - } - else { - ERR_error_string_n(err_code, err_buf, sizeof(err_buf) - 1); - flb_error("[tls] error: %s", err_buf); - } + tls_log_io_error(ssl_ret, saved_errno); + session->connection->net_error = ECONNRESET; ret = -1; } From c783a2a0c41985f909127c73c9a08a94dabcb2e5 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:28:41 -0600 Subject: [PATCH 4/6] tests: integration: cover TLS protocol errors Signed-off-by: Eduardo Silva --- .../in_forward/tests/test_in_forward_001.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py index 257fd9cba58..2e7207406c5 100644 --- a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py +++ b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py @@ -696,6 +696,17 @@ def _reset_tls_connection(port, cafile): raw_sock.close() +def _send_corrupted_tls_record(port, cafile): + raw_sock, tls, outgoing = _create_tls_memory_bio_client(port, cafile) + + tls.write(b"\x91") + wire_payload = bytearray(outgoing.read()) + wire_payload[-1] ^= 1 + raw_sock.sendall(wire_payload) + + return raw_sock + + def _recv_msgpack_value(sock): sock.settimeout(5) data = sock.recv(4096) @@ -1104,6 +1115,36 @@ def test_in_forward_tls_syscall_error_preserves_errno(): assert records[0]["message"] == "after-tls-reset" +def test_in_forward_tls_protocol_error_is_reported(): + service = Service("in_forward_tls.yaml") + protocol_sock = None + service.start() + + try: + protocol_sock = _send_corrupted_tls_record( + service.flb_listener_port, + service.tls_crt_file, + ) + log_text = service.wait_for_log_contains("[tls] error:", timeout=10) + protocol_sock.close() + protocol_sock = None + + payload = _message_mode_payload( + TEST_TAG, + {"message": "after-tls-protocol-error"}, + ) + _send_tls_payload(service.flb_listener_port, payload, service.tls_crt_file) + records = service.wait_for_record_count(1, timeout=10) + finally: + if protocol_sock is not None: + protocol_sock.close() + service.stop() + + assert "bad record mac" in log_text.lower() + assert "unknown error" not in log_text + assert records[0]["message"] == "after-tls-protocol-error" + + def test_in_forward_tls_idle_connection_timeout_churn(monkeypatch): """ Regression test for issue #12025. From 722ecf3e969e19b45e26af25c8bd6120d39acdcd Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:39:00 -0600 Subject: [PATCH 5/6] tls: openssl: preserve clean shutdown behavior Signed-off-by: Eduardo Silva --- src/tls/openssl.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 58ce307e560..75f3a0dabb0 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1500,6 +1500,9 @@ static int tls_net_read(struct flb_tls_session *session, else if (ssl_ret == SSL_ERROR_WANT_WRITE) { ret = FLB_TLS_WANT_WRITE; } + else if (ssl_ret == SSL_ERROR_ZERO_RETURN) { + ret = -1; + } else if (ssl_ret == SSL_ERROR_SYSCALL) { tls_log_io_error(ssl_ret, saved_errno); @@ -1567,6 +1570,9 @@ static int tls_net_write(struct flb_tls_session *session, else if (ssl_ret == SSL_ERROR_WANT_READ) { ret = FLB_TLS_WANT_READ; } + else if (ssl_ret == SSL_ERROR_ZERO_RETURN) { + ret = -1; + } else if (ssl_ret == SSL_ERROR_SYSCALL) { tls_log_io_error(ssl_ret, saved_errno); From e4b494f8af43d12beee4bdc6f88580e161037404 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 30 Jul 2026 17:39:00 -0600 Subject: [PATCH 6/6] tests: integration: cover TLS clean shutdown Signed-off-by: Eduardo Silva --- .../in_forward/tests/test_in_forward_001.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py index 2e7207406c5..37ff24e360f 100644 --- a/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py +++ b/tests/integration/scenarios/in_forward/tests/test_in_forward_001.py @@ -707,6 +707,20 @@ def _send_corrupted_tls_record(port, cafile): return raw_sock +def _send_tls_close_notify(port, cafile): + raw_sock, tls, outgoing = _create_tls_memory_bio_client(port, cafile) + + try: + tls.unwrap() + except ssl.SSLWantReadError: + pass + + wire_payload = outgoing.read() + assert wire_payload + raw_sock.sendall(wire_payload) + raw_sock.close() + + def _recv_msgpack_value(sock): sock.settimeout(5) data = sock.recv(4096) @@ -1145,6 +1159,31 @@ def test_in_forward_tls_protocol_error_is_reported(): assert records[0]["message"] == "after-tls-protocol-error" +def test_in_forward_tls_close_notify_is_not_an_error(): + service = Service("in_forward_tls.yaml") + service.start() + + try: + _send_tls_close_notify( + service.flb_listener_port, + service.tls_crt_file, + ) + time.sleep(1) + + payload = _message_mode_payload( + TEST_TAG, + {"message": "after-tls-close-notify"}, + ) + _send_tls_payload(service.flb_listener_port, payload, service.tls_crt_file) + records = service.wait_for_record_count(1, timeout=10) + log_text = read_file(service.service.flb.log_file) + finally: + service.stop() + + assert "[tls] unknown error (ssl_error=6)" not in log_text + assert records[0]["message"] == "after-tls-close-notify" + + def test_in_forward_tls_idle_connection_timeout_churn(monkeypatch): """ Regression test for issue #12025.