1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
https://bugs.gentoo.org/977642
From 33d66d22d8bf9c86af307307004ed43850face5f Mon Sep 17 00:00:00 2001
From: Harin Vadodaria <harin.vadodaria@oracle.com>
Date: Mon, 1 Jun 2026 10:15:59 +0200
Subject: [PATCH 1/2] Bug#39116965: MySQL Out-of-Bounds Read
Description:
Connection attribute parsing read a length-encoded size field before
checking that the complete field was present in the packet.
Fix:
A size check was added before reading the field. Debug coverage was
added for short length-encoded attribute packets during
pre-authentication.
Change-Id: Ia90e387a219556bb040a26853946c022f73de730
--- a/sql/auth/sql_authentication.cc
+++ b/sql/auth/sql_authentication.cc
@@ -2262,9 +2262,20 @@ static bool read_client_connect_attrs(THD *thd, char **ptr,
size_t length, length_length;
char *ptr_save;
- /* not enough bytes to hold the length */
+ /* Need one byte to determine the length-encoded field size. */
if (*max_bytes_available < 1) return true;
+ uchar *pos = (uchar *)*ptr;
+ DBUG_EXECUTE_IF("connect_attrs_too_short_3", *pos = 252;
+ *max_bytes_available = 2;);
+ DBUG_EXECUTE_IF("connect_attrs_too_short_4", *pos = 253;
+ *max_bytes_available = 3;);
+ DBUG_EXECUTE_IF("connect_attrs_too_short_9", *pos = 254;
+ *max_bytes_available = 8;);
+
+ const size_t required_length = (size_t)net_field_length_size(pos);
+ if (*max_bytes_available < required_length) return true;
+
/* read the length */
ptr_save = *ptr;
length = static_cast<size_t>(net_field_length_ll((uchar **)ptr));
--
2.54.0
From 2747a03d7a522a4f880df5c978f2e5c1db17b119 Mon Sep 17 00:00:00 2001
From: Lukasz Kotula <lukasz.kotula@oracle.com>
Date: Fri, 17 Apr 2026 09:12:57 +0200
Subject: [PATCH 2/2] Bug#39204635 - Unauthenticated repeated X Protocol TLS
upgrade crashes MySQL Router
Description
For X Protocol routing with `client_ssl_mode=PREFERRED` and
`server_ssl_mode=AS_CLIENT`, a client can send
`CON_CAPABILITIES_SET(tls=true)` after TLS is already active.
Router accepted this second TLS capability request and re-entered
the TLS upgrade path. During `tls_accept_finalize()`, the server-side
TLS channel could already be initialized, which led to a `logic_error`
and connection termination.
Fix
===
Reject repeated TLS capability upgrades when client-side TLS is already
active. For a second `tls=true` capability request, Router now returns
error 5001 (`Capability prepare failed for 'tls'`) and continues normal
session handling.
Change-Id: Ifaaa93eeec1019160c7a6db50daa7e1af8f383c5
--- a/router/src/routing/src/x_connection.cc
+++ b/router/src/routing/src/x_connection.cc
@@ -1471,6 +1471,7 @@ void MysqlRoutingXConnection::client_cap_set() {
if (switch_to_tls) {
bool continue_with_tls{false};
+
switch (source_ssl_mode()) {
case SslMode::kDisabled: {
continue_with_tls = false;
@@ -1525,6 +1526,11 @@ void MysqlRoutingXConnection::client_cap_set() {
discard_current_msg(src_channel, src_protocol);
std::vector<uint8_t> out_buf;
+ // The client tries to activate TLS when it is already enabled.
+ if (continue_with_tls && src_channel->ssl()) {
+ continue_with_tls = false;
+ }
+
if (!continue_with_tls) {
#ifdef DEBUG_IO
std::cerr << __LINE__ << ": "
--- a/router/tests/component/test_routing.cc
+++ b/router/tests/component/test_routing.cc
@@ -122,6 +122,23 @@ static xcl::XError make_x_connection(XProtocolSession &session,
password.c_str(), "");
}
+static xcl::XError make_x_raw_connection(
+ XProtocolSession &session, const std::string &host, const uint16_t port,
+ int64_t connect_timeout = 10000 /*10s*/) {
+ session = xcl::create_session();
+ xcl::XError err = setup_x_session(session, connect_timeout, "PREFERRED");
+ if (err) return err;
+
+ return session->get_protocol().get_connection().connect(
+ host, port, xcl::Internet_protocol::Any);
+}
+
+static std::string format_xerror(const xcl::XError &e) {
+ if (!e) return "no-error";
+
+ return "(code:" + std::to_string(e.error()) + ", message:" + e.what() + ")";
+}
+
TEST_F(RouterRoutingTest, RoutingOk) {
const auto server_port = port_pool_.get_next_available();
const auto router_port = port_pool_.get_next_available();
@@ -1882,6 +1899,78 @@ static size_t xproto_frame_encode(const T &msg, uint8_t msg_type,
return msg.SerializeToCodedStream(&codecouts);
}
+/**
+ * @test Verify that repeated TLS activation requests are properly rejected
+ * and do not crash the Router.
+ */
+TEST_F(RouterRoutingTest, XProtocolRepeatedTlsUpgrade) {
+ const auto server_classic_port = port_pool_.get_next_available();
+ const auto server_x_port = port_pool_.get_next_available();
+ const auto router_x_rw_port = port_pool_.get_next_available();
+
+ const std::string json_stmts = get_data_dir().join("bootstrap_gr.js").str();
+
+ launch_mysql_server_mock(json_stmts, server_classic_port, EXIT_SUCCESS, false,
+ /*http_port*/ 0, server_x_port, /*module_prefix*/ "",
+ /*bind_address*/ "127.0.0.1",
+ /*wait_for_notify_ready*/ std::chrono::seconds(30),
+ /*enable_ssl*/ true);
+
+ const std::string routing_x_section = get_static_routing_section(
+ "x", router_x_rw_port, "", {server_x_port}, "x");
+
+ TempDirectory conf_dir("conf");
+ const std::string ssl_conf =
+ "client_ssl_mode=PREFERRED\n"
+ "server_ssl_mode=AS_CLIENT\n"
+ "client_ssl_key=" SSL_TEST_DATA_DIR
+ "/server-key-sha512.pem\n"
+ "client_ssl_cert=" SSL_TEST_DATA_DIR "/server-cert-sha512.pem";
+ std::string conf_file =
+ create_config_file(conf_dir.name(), routing_x_section, nullptr,
+ "mysqlrouter.conf", ssl_conf, true);
+
+ launch_router({"-c", conf_file});
+
+ Mysqlx::Connection::CapabilitiesSet switch_tls_msg;
+ auto *cap = switch_tls_msg.mutable_capabilities()->add_capabilities();
+ cap->set_name("tls");
+ auto *cap_value = cap->mutable_value();
+ cap_value->set_type(Mysqlx::Datatypes::Any_Type::Any_Type_SCALAR);
+ auto *cap_scalar = cap_value->mutable_scalar();
+ cap_scalar->set_type(Mysqlx::Datatypes::Scalar_Type::Scalar_Type_V_BOOL);
+ cap_scalar->set_v_bool(true);
+
+ XProtocolSession x_session;
+ const auto x_connect_error =
+ make_x_raw_connection(x_session, "127.0.0.1", router_x_rw_port);
+ ASSERT_FALSE(x_connect_error) << format_xerror(x_connect_error);
+
+ const auto x_cap_set_error =
+ x_session.get()->get_protocol().execute_set_capability(switch_tls_msg);
+ ASSERT_FALSE(x_cap_set_error) << format_xerror(x_cap_set_error);
+
+ const auto x_tls_error =
+ x_session.get()->get_protocol().get_connection().activate_tls();
+
+ ASSERT_FALSE(x_tls_error) << format_xerror(x_tls_error);
+
+ const auto x_cap_set2_error =
+ x_session.get()->get_protocol().execute_set_capability(switch_tls_msg);
+ ASSERT_EQ(x_cap_set2_error.error(), 5001) << format_xerror(x_cap_set2_error);
+
+ const auto x_login_error =
+ x_session.get()->get_protocol().execute_authenticate("root", "fake-pass",
+ "", "SHA256_MEMORY");
+ ASSERT_FALSE(x_login_error) << format_xerror(x_login_error);
+
+ // Router should still accept a fresh X Protocol connection after the loop.
+ XProtocolSession x_session2;
+ const auto res = make_x_connection(x_session2, "127.0.0.1", router_x_rw_port,
+ "root", "fake-pass");
+ EXPECT_THAT(res.error(), ::testing::AnyOf(0, 3159));
+}
+
/**
* @test Check that if the x protocol client sends CONCLOSE message the Router
* replies with OK{bye!} message.
--
2.54.0
|