Skip to content

Commit 33660e2

Browse files
authored
Starting operations while engaged in multi-function ops is no longer UB
Attempting to start an operation involving server communication after a multi-function operation has been started and before all its associated packets have been read now fails with client_errc::engaged_in_multi_function, instead of causing undefined behavior. Attempting to start a read_some_rows or read_resultset_head operation with no in-progress multi-function operation now fails with client_errc::not_engaged_in_multi_function. Attempting to start an operation that requires an established session before a successful connect now fails with client_errc::not_connected. Added the three client_errc enum values mentioned above. Renamed detail::connection_status (connection_pool) to node_status. Added detail::connection_status (connection_state_data). Removed redundant spotcheck tests. close #448 close #450
1 parent 3868d8a commit 33660e2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1713
-698
lines changed

doc/qbk/04_overview.qbk

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,11 @@ table in batches, which can be the case in an ETL process:
287287

288288
[overview_multifn]
289289

290-
[warning
290+
[note
291291
Once you start a multi-function operation with [refmemunq any_connection async_start_execution],
292292
the server immediately sends all rows to the client. [*You must read all rows] before engaging in further operations.
293-
Otherwise, you will encounter packet mismatches, which can lead to bugs and vulnerabilities!
293+
If you try to initiate an unrelated operation before reading all rows, you will get a
294+
`client_errc::engaged_in_multi_function` error.
294295
]
295296

296297
Multi-function operations are powerful but complex. Only use them when there is a strong reason to do so.

include/boost/mysql/client_errc.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,25 @@ enum class client_errc : int
144144
* that a single connection does not run two asynchronous operations in parallel.
145145
*/
146146
operation_in_progress,
147+
148+
/**
149+
* \brief The requested operation requires an established session.
150+
* Call `async_connect` before invoking other operations.
151+
*/
152+
not_connected,
153+
154+
/**
155+
* \brief The connection is currently engaged in a multi-function operation.
156+
* Finish the current operation by calling `async_read_some_rows` and `async_read_resultset_head`
157+
* before starting any other operation.
158+
*/
159+
engaged_in_multi_function,
160+
161+
/**
162+
* \brief The operation requires the connection to be engaged in a multi-function operation.
163+
* Use `async_start_execution` to start one.
164+
*/
165+
not_engaged_in_multi_function,
147166
};
148167

149168
BOOST_MYSQL_DECL

include/boost/mysql/detail/connection_impl.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,9 @@ class connection_impl
580580
const pipeline_request& req,
581581
std::vector<stage_response>& response
582582
);
583+
584+
// Exposed for testing
585+
connection_state& get_state() { return *st_; }
583586
};
584587

585588
// To use some completion tokens, like deferred, in C++11, the old macros

include/boost/mysql/impl/connection_impl.ipp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <boost/mysql/detail/connection_impl.hpp>
1818

1919
#include <boost/mysql/impl/internal/sansio/connection_state.hpp>
20+
#include <boost/mysql/impl/internal/sansio/connection_state_data.hpp>
2021

2122
#include <boost/throw_exception.hpp>
2223

@@ -94,7 +95,7 @@ boost::system::result<boost::mysql::character_set> boost::mysql::detail::connect
9495
boost::optional<std::uint32_t> boost::mysql::detail::connection_impl::connection_id() const
9596
{
9697
const auto& data = st_->data();
97-
if (!data.is_connected)
98+
if (data.status == connection_status::not_connected)
9899
return {};
99100
else
100101
return data.connection_id;

include/boost/mysql/impl/error_categories.ipp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ inline const char* error_to_string(client_errc error)
8989
case client_errc::operation_in_progress:
9090
return "Another operation is currently in progress for this connection. Make sure that a single "
9191
"connection does not run two asynchronous operations in parallel.";
92-
92+
case client_errc::not_connected:
93+
return "The requested operation requires an established session. Call async_connect before invoking "
94+
"other operations.";
95+
case client_errc::engaged_in_multi_function:
96+
return "The connection is currently engaged in a multi-function operation."
97+
"Finish the current operation by calling async_read_some_rows and async_read_resultset_head "
98+
"before "
99+
"starting any other operation.";
100+
case client_errc::not_engaged_in_multi_function:
101+
return "The operation requires the connection to be engaged in a multi-function operation. "
102+
"Use async_start_execution to start one.";
93103
default: return "<unknown MySQL client error>";
94104
}
95105
}

include/boost/mysql/impl/internal/connection_pool/sansio_connection_node.hpp

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace mysql {
2222
namespace detail {
2323

2424
// The status the connection is in
25-
enum class connection_status
25+
enum class node_status
2626
{
2727
// Connection task hasn't initiated yet.
2828
// This status doesn't count as pending. This facilitates tracking pending connections.
@@ -51,7 +51,7 @@ enum class connection_status
5151
};
5252

5353
// The next I/O action the connection should take. There's
54-
// no 1-1 mapping to connection_status
54+
// no 1-1 mapping to node_status
5555
enum class next_connection_action
5656
{
5757
// Do nothing, exit the loop
@@ -92,37 +92,37 @@ enum class collection_state
9292
template <class Derived>
9393
class sansio_connection_node
9494
{
95-
connection_status status_;
95+
node_status status_;
9696

97-
inline bool is_pending(connection_status status) noexcept
97+
inline bool is_pending(node_status status) noexcept
9898
{
99-
return status != connection_status::initial && status != connection_status::idle &&
100-
status != connection_status::in_use && status != connection_status::terminated;
99+
return status != node_status::initial && status != node_status::idle &&
100+
status != node_status::in_use && status != node_status::terminated;
101101
}
102102

103-
inline static next_connection_action status_to_action(connection_status status) noexcept
103+
inline static next_connection_action status_to_action(node_status status) noexcept
104104
{
105105
switch (status)
106106
{
107-
case connection_status::connect_in_progress: return next_connection_action::connect;
108-
case connection_status::sleep_connect_failed_in_progress:
107+
case node_status::connect_in_progress: return next_connection_action::connect;
108+
case node_status::sleep_connect_failed_in_progress:
109109
return next_connection_action::sleep_connect_failed;
110-
case connection_status::ping_in_progress: return next_connection_action::ping;
111-
case connection_status::reset_in_progress: return next_connection_action::reset;
112-
case connection_status::idle:
113-
case connection_status::in_use: return next_connection_action::idle_wait;
110+
case node_status::ping_in_progress: return next_connection_action::ping;
111+
case node_status::reset_in_progress: return next_connection_action::reset;
112+
case node_status::idle:
113+
case node_status::in_use: return next_connection_action::idle_wait;
114114
default: return next_connection_action::none;
115115
}
116116
}
117117

118-
next_connection_action set_status(connection_status new_status)
118+
next_connection_action set_status(node_status new_status)
119119
{
120120
auto& derived = static_cast<Derived&>(*this);
121121

122122
// Notify we're entering/leaving the idle status
123-
if (new_status == connection_status::idle && status_ != connection_status::idle)
123+
if (new_status == node_status::idle && status_ != node_status::idle)
124124
derived.entering_idle();
125-
else if (new_status != connection_status::idle && status_ == connection_status::idle)
125+
else if (new_status != node_status::idle && status_ == node_status::idle)
126126
derived.exiting_idle();
127127

128128
// Notify we're entering/leaving a pending status
@@ -138,64 +138,63 @@ class sansio_connection_node
138138
}
139139

140140
public:
141-
sansio_connection_node(connection_status initial_status = connection_status::initial) noexcept
141+
sansio_connection_node(node_status initial_status = node_status::initial) noexcept
142142
: status_(initial_status)
143143
{
144144
}
145145

146146
void mark_as_in_use() noexcept
147147
{
148-
BOOST_ASSERT(status_ == connection_status::idle);
149-
set_status(connection_status::in_use);
148+
BOOST_ASSERT(status_ == node_status::idle);
149+
set_status(node_status::in_use);
150150
}
151151

152-
void cancel() { set_status(connection_status::terminated); }
152+
void cancel() { set_status(node_status::terminated); }
153153

154154
next_connection_action resume(error_code ec, collection_state col_st)
155155
{
156156
switch (status_)
157157
{
158-
case connection_status::initial: return set_status(connection_status::connect_in_progress);
159-
case connection_status::connect_in_progress:
160-
return ec ? set_status(connection_status::sleep_connect_failed_in_progress)
161-
: set_status(connection_status::idle);
162-
case connection_status::sleep_connect_failed_in_progress:
163-
return set_status(connection_status::connect_in_progress);
164-
case connection_status::idle:
158+
case node_status::initial: return set_status(node_status::connect_in_progress);
159+
case node_status::connect_in_progress:
160+
return ec ? set_status(node_status::sleep_connect_failed_in_progress)
161+
: set_status(node_status::idle);
162+
case node_status::sleep_connect_failed_in_progress:
163+
return set_status(node_status::connect_in_progress);
164+
case node_status::idle:
165165
// The wait finished with no interruptions, and the connection
166166
// is still idle. Time to ping.
167-
return set_status(connection_status::ping_in_progress);
168-
case connection_status::in_use:
167+
return set_status(node_status::ping_in_progress);
168+
case node_status::in_use:
169169
// If col_st != none, the user has notified us to collect the connection.
170170
// This happens after they return the connection to the pool.
171171
// Update status and continue
172172
if (col_st == collection_state::needs_collect)
173173
{
174174
// No reset needed, we're idle
175-
return set_status(connection_status::idle);
175+
return set_status(node_status::idle);
176176
}
177177
else if (col_st == collection_state::needs_collect_with_reset)
178178
{
179-
return set_status(connection_status::reset_in_progress);
179+
return set_status(node_status::reset_in_progress);
180180
}
181181
else
182182
{
183183
// The user is still using the connection (it's taking long, but can happen).
184184
// Idle wait again until they return the connection.
185185
return next_connection_action::idle_wait;
186186
}
187-
case connection_status::ping_in_progress:
188-
case connection_status::reset_in_progress:
187+
case node_status::ping_in_progress:
188+
case node_status::reset_in_progress:
189189
// Reconnect if there was an error. Otherwise, we're idle
190-
return ec ? set_status(connection_status::connect_in_progress)
191-
: set_status(connection_status::idle);
192-
case connection_status::terminated:
190+
return ec ? set_status(node_status::connect_in_progress) : set_status(node_status::idle);
191+
case node_status::terminated:
193192
default: return next_connection_action::none;
194193
}
195194
}
196195

197196
// Exposed for testing
198-
connection_status status() const noexcept { return status_; }
197+
node_status status() const noexcept { return status_; }
199198
};
200199

201200
// Composes a diagnostics object containing info about the last connect error.

include/boost/mysql/impl/internal/sansio/close_connection.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ class close_connection_algo
3838
{
3939
case 0:
4040

41-
// If we're not connected, we're done
42-
if (!st.is_connected)
41+
// If we're not connected, we're done.
42+
// If we're in a multi-function operation, it's safe to proceed.
43+
if (st.status == connection_status::not_connected)
4344
return next_action();
4445

4546
// Attempt quit

include/boost/mysql/impl/internal/sansio/connect.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class connect_algo
4242
switch (resume_point_)
4343
{
4444
case 0:
45+
// Handshake and connect wipe out state, so no state checks are performed.
4546

4647
// Physical connect
4748
BOOST_MYSQL_YIELD(resume_point_, 1, next_action::connect(server_address_))

include/boost/mysql/impl/internal/sansio/connection_state_data.hpp

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
#define BOOST_MYSQL_IMPL_INTERNAL_SANSIO_CONNECTION_STATE_DATA_HPP
1010

1111
#include <boost/mysql/character_set.hpp>
12+
#include <boost/mysql/client_errc.hpp>
1213
#include <boost/mysql/diagnostics.hpp>
14+
#include <boost/mysql/error_code.hpp>
1315
#include <boost/mysql/field_view.hpp>
1416
#include <boost/mysql/metadata_mode.hpp>
1517

@@ -21,6 +23,8 @@
2123
#include <boost/mysql/impl/internal/protocol/serialization.hpp>
2224
#include <boost/mysql/impl/internal/sansio/message_reader.hpp>
2325

26+
#include <boost/assert.hpp>
27+
2428
#include <array>
2529
#include <cstddef>
2630
#include <cstdint>
@@ -30,15 +34,27 @@ namespace boost {
3034
namespace mysql {
3135
namespace detail {
3236

37+
enum class connection_status
38+
{
39+
// Never connected or closed
40+
not_connected,
41+
42+
// Connected and ready for a command
43+
ready,
44+
45+
// In the middle of a multi-function operation
46+
engaged_in_multi_function,
47+
};
48+
3349
struct connection_state_data
3450
{
51+
// Are we connected? In the middle of a multi-function operation?
52+
connection_status status{connection_status::not_connected};
53+
3554
// Are we currently executing an operation?
3655
// Prevent the user from running concurrent operations
3756
bool op_in_progress{false};
3857

39-
// Is the connection actually connected? Set by handshake
40-
bool is_connected{false};
41-
4258
// Are we talking to MySQL or MariaDB?
4359
db_flavor flavor{db_flavor::mysql};
4460

@@ -92,7 +108,7 @@ struct connection_state_data
92108

93109
void reset()
94110
{
95-
is_connected = false;
111+
status = connection_status::not_connected;
96112
flavor = db_flavor::mysql;
97113
current_capabilities = capabilities();
98114
// Metadata mode does not get reset on handshake
@@ -128,6 +144,27 @@ struct connection_state_data
128144
seqnum = res.seqnum;
129145
return next_action::write({write_buffer, false});
130146
}
147+
148+
// Helpers to implement connection status in algorithms
149+
error_code check_status_ready() const
150+
{
151+
switch (status)
152+
{
153+
case connection_status::not_connected: return client_errc::not_connected;
154+
case connection_status::engaged_in_multi_function: return client_errc::engaged_in_multi_function;
155+
default: BOOST_ASSERT(status == connection_status::ready); return error_code();
156+
}
157+
}
158+
159+
error_code check_status_multi_function() const
160+
{
161+
switch (status)
162+
{
163+
case connection_status::not_connected: return client_errc::not_connected;
164+
case connection_status::ready: return client_errc::not_engaged_in_multi_function;
165+
default: BOOST_ASSERT(status == connection_status::engaged_in_multi_function); return error_code();
166+
}
167+
}
131168
};
132169

133170
} // namespace detail

include/boost/mysql/impl/internal/sansio/execute.hpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ class read_execute_response_algo
3232
read_some_rows_algo read_some_rows_st_;
3333

3434
public:
35+
// We pass false because they are subordinate algos. This suppresses state checks.
36+
// This is always a subordinate algo, so it never performs state checks.
3537
read_execute_response_algo(execution_processor* proc) noexcept
36-
: read_head_st_({proc}), read_some_rows_st_({proc, output_ref()})
38+
: read_head_st_({proc}, false), read_some_rows_st_({proc, output_ref()}, false)
3739
{
3840
}
3941

@@ -81,8 +83,10 @@ class execute_algo
8183
execution_processor& processor() { return read_response_st_.processor(); }
8284

8385
public:
86+
// We pass false to the start execution algo's constructor because it's a subordinate algo.
87+
// This disables state checks.
8488
execute_algo(execute_algo_params params) noexcept
85-
: start_execution_st_({params.req, params.proc}), read_response_st_(params.proc)
89+
: start_execution_st_({params.req, params.proc}, false), read_response_st_(params.proc)
8690
{
8791
}
8892

@@ -94,6 +98,11 @@ class execute_algo
9498
{
9599
case 0:
96100

101+
// Check status
102+
ec = st.check_status_ready();
103+
if (ec)
104+
return ec;
105+
97106
// Send request and read the first response
98107
while (!(act = start_execution_st_.resume(st, diag, ec)).is_done())
99108
BOOST_MYSQL_YIELD(resume_point_, 1, act)

0 commit comments

Comments
 (0)