Skip to content

Commit f112115

Browse files
Pierre-Luc Gagnéclaude
andcommitted
fix: varchar_type now owns its value to prevent use-after-free
Changed varchar_type internal storage from std::string_view to std::string so values survive after the source buffer is destroyed (e.g. MySQL result sets). Updated integration tests to use const reference iteration and added ownership unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4040ff2 commit f112115

4 files changed

Lines changed: 62 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

99
## [Unreleased]
1010

11+
### Fixed
12+
13+
- `varchar_type` now owns its value (`std::string` storage instead of `std::string_view`), preventing use-after-free when values outlive the source buffer (e.g. query results).
14+
1115
---
1216

1317
## [4.6.2] – 2026-03-30

lib/include/ds_mysql/sql_varchar.hpp

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#pragma once
22

3-
#include <algorithm>
4-
#include <array>
53
#include <cstddef>
64
#include <expected>
75
#include <string>
@@ -27,12 +25,12 @@ class varchar_type {
2725

2826
// Compile-time constructor for string literals — capacity validated at compile time.
2927
template <std::size_t M>
30-
constexpr varchar_type(char const (&str)[M]) noexcept : storage_(str, M - 1) {
28+
constexpr varchar_type(char const (&str)[M]) : storage_(str, M - 1) {
3129
static_assert(M - 1 <= capacity, "string literal exceeds varchar_type capacity");
3230
}
3331

3432
// Factory for runtime strings — validates capacity, returns std::expected.
35-
[[nodiscard]] static std::expected<varchar_type, varchar_error> create(std::string_view value) noexcept {
33+
[[nodiscard]] static std::expected<varchar_type, varchar_error> create(std::string_view value) {
3634
if (value.size() > capacity) {
3735
return std::unexpected(varchar_error::too_long);
3836
}
@@ -43,48 +41,48 @@ class varchar_type {
4341
return "VARCHAR(" + std::to_string(capacity) + ")";
4442
}
4543

46-
[[nodiscard]] constexpr std::size_t size() const noexcept {
44+
[[nodiscard]] std::size_t size() const noexcept {
4745
return storage_.size();
4846
}
4947

5048
[[nodiscard]] static consteval std::size_t max_size() noexcept {
5149
return capacity;
5250
}
5351

54-
[[nodiscard]] constexpr std::string_view view() const noexcept {
55-
return {storage_.data(), storage_.size()};
52+
[[nodiscard]] std::string_view view() const noexcept {
53+
return storage_;
5654
}
5755

58-
[[nodiscard]] constexpr char const* c_str() const noexcept {
59-
return storage_.data();
56+
[[nodiscard]] char const* c_str() const noexcept {
57+
return storage_.c_str();
6058
}
6159

62-
[[nodiscard]] constexpr bool empty() const noexcept {
60+
[[nodiscard]] bool empty() const noexcept {
6361
return storage_.empty();
6462
}
6563

66-
constexpr operator std::string_view() const noexcept {
64+
operator std::string_view() const noexcept {
6765
return view();
6866
}
6967

70-
[[nodiscard]] constexpr bool operator==(varchar_type const& other) const noexcept {
68+
[[nodiscard]] bool operator==(varchar_type const& other) const noexcept {
7169
return view() == other.view();
7270
}
7371

74-
[[nodiscard]] constexpr bool operator==(std::string_view other) const noexcept {
72+
[[nodiscard]] bool operator==(std::string_view other) const noexcept {
7573
return view() == other;
7674
}
7775

7876
template <std::size_t M>
79-
[[nodiscard]] constexpr bool operator==(char const (&other)[M]) const noexcept {
77+
[[nodiscard]] bool operator==(char const (&other)[M]) const noexcept {
8078
return view() == std::string_view{other, M - 1};
8179
}
8280

8381
private:
84-
varchar_type(detail::varchar_unchecked_tag, std::string_view value) noexcept : storage_(value) {
82+
varchar_type(detail::varchar_unchecked_tag, std::string_view value) : storage_(value) {
8583
}
8684

87-
std::string_view storage_;
85+
std::string storage_;
8886
};
8987

9088
template <typename T>

tests/integration/mysql/test_query_builder.cpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ suite<"INSERT Integration"> insert_integration_suite = [] {
260260

261261
expect(db->execute(drop_table(trade{}).if_exists().then().create_table(trade{})).has_value());
262262

263-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
263+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
264264
trade row;
265265
row.code_ = code;
266266
row.type_ = "Stock";
@@ -327,7 +327,7 @@ suite<"SELECT Integration"> select_integration_suite = [] {
327327
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
328328
expect(db->execute(delete_from(trade{})).has_value());
329329

330-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
330+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
331331
trade row;
332332
row.code_ = code;
333333
row.type_ = "Stock";
@@ -400,7 +400,7 @@ suite<"SELECT Integration"> select_integration_suite = [] {
400400

401401
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
402402
expect(db->execute(delete_from(trade{})).has_value());
403-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}}) {
403+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}}) {
404404
trade row;
405405
row.code_ = code;
406406
row.type_ = "Stock";
@@ -456,8 +456,8 @@ suite<"SELECT Integration"> select_integration_suite = [] {
456456

457457
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
458458
expect(db->execute(delete_from(trade{})).has_value());
459-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"},
460-
varchar_type<32>{"AMZN"}, varchar_type<32>{"TSLA"}}) {
459+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"},
460+
varchar_type<32>{"AMZN"}, varchar_type<32>{"TSLA"}}) {
461461
trade row;
462462
row.code_ = code;
463463
row.type_ = "Stock";
@@ -556,7 +556,7 @@ suite<"DELETE Integration"> delete_integration_suite = [] {
556556

557557
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
558558
expect(db->execute(delete_from(trade{})).has_value());
559-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
559+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
560560
trade row;
561561
row.code_ = code;
562562
row.type_ = "Stock";
@@ -614,7 +614,7 @@ suite<"WHERE Operator Syntax Integration"> where_operator_integration_suite = []
614614

615615
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
616616
expect(db->execute(delete_from(trade{})).has_value());
617-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
617+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
618618
trade row;
619619
row.code_ = code;
620620
row.type_ = "Stock";
@@ -639,7 +639,7 @@ suite<"WHERE Operator Syntax Integration"> where_operator_integration_suite = []
639639

640640
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
641641
expect(db->execute(delete_from(trade{})).has_value());
642-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
642+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
643643
trade row;
644644
row.code_ = code;
645645
row.type_ = "Stock";
@@ -665,7 +665,7 @@ suite<"WHERE Operator Syntax Integration"> where_operator_integration_suite = []
665665

666666
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
667667
expect(db->execute(delete_from(trade{})).has_value());
668-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
668+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
669669
trade row;
670670
row.code_ = code;
671671
row.type_ = "Stock";
@@ -694,7 +694,7 @@ suite<"WHERE Operator Syntax Integration"> where_operator_integration_suite = []
694694

695695
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
696696
expect(db->execute(delete_from(trade{})).has_value());
697-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
697+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
698698
trade row;
699699
row.code_ = code;
700700
row.type_ = "Stock";
@@ -722,7 +722,7 @@ suite<"WHERE Operator Syntax Integration"> where_operator_integration_suite = []
722722

723723
expect(db->execute(create_table(trade{}).if_not_exists()).has_value());
724724
expect(db->execute(delete_from(trade{})).has_value());
725-
for (auto const code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
725+
for (auto const& code : {varchar_type<32>{"AAPL"}, varchar_type<32>{"GOOGL"}, varchar_type<32>{"MSFT"}}) {
726726
trade row;
727727
row.code_ = code;
728728
row.type_ = "Stock";

tests/unit/test_field_types.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,39 @@ suite<"varchar_type"> varchar_type_suite = [] {
154154
expect(std::string_view{f.c_str()} == "hi"sv);
155155
};
156156

157+
"varchar_type owns its value — copy is independent"_test = [] {
158+
varchar_type<32> a{"AAPL"};
159+
varchar_type<32> b = a;
160+
a = varchar_type<32>{"MSFT"};
161+
expect(b == "AAPL"sv);
162+
};
163+
164+
"varchar_type owns its value — move leaves valid state"_test = [] {
165+
varchar_type<32> a{"AAPL"};
166+
varchar_type<32> b = std::move(a);
167+
expect(b == "AAPL"sv);
168+
};
169+
170+
"varchar_type owns its value — survives source string destruction"_test = [] {
171+
auto make = [] {
172+
std::string src = "hello world";
173+
return varchar_type<32>::create(src);
174+
};
175+
auto result = make();
176+
expect(fatal(result.has_value()));
177+
expect(result->view() == "hello world"sv);
178+
};
179+
180+
"varchar_type create — value survives via copy assignment"_test = [] {
181+
varchar_type<64> v;
182+
{
183+
auto tmp = varchar_type<64>::create("temporary");
184+
expect(fatal(tmp.has_value()));
185+
v = *tmp;
186+
}
187+
expect(v.view() == "temporary"sv);
188+
};
189+
157190
// Compile-time trait checks
158191
static_assert(is_varchar_type_v<varchar_type<32>>);
159192
static_assert(is_varchar_type_v<varchar_type<255>>);

0 commit comments

Comments
 (0)