Skip to content

Commit 0b7c637

Browse files
committed
fix: reply_builder should properly serialize bulk strings endings. (#4441)
Due to a corner-case bug, reply builder could add \0\0 to the end of bulk strings, instead of \r\n. The bug slipped our tests because redis-py parser most probably does not validate the ending as long as everything else is consistent. This PR: 1. Adds a test that catches the bug 2. Adds a debug check that verifies the destination pointer is consistent with the iovec being used. 3. Fixes the bug. Fixes #4424
1 parent be467dc commit 0b7c637

File tree

3 files changed

+34
-14
lines changed

3 files changed

+34
-14
lines changed

src/facade/reply_builder.cc

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,20 @@ template <typename... Ts> void SinkReplyBuilder::WritePieces(Ts&&... pieces) {
108108
if (size_t required = (piece_size(pieces) + ...); buffer_.AppendLen() <= required)
109109
Flush(required);
110110

111+
auto iovec_end = [](const iovec& v) { return reinterpret_cast<char*>(v.iov_base) + v.iov_len; };
112+
111113
// Ensure last iovec points to buffer segment
112114
char* dest = reinterpret_cast<char*>(buffer_.AppendBuffer().data());
113-
if (vecs_.empty() || ((char*)vecs_.back().iov_base) + vecs_.back().iov_len != dest)
114-
NextVec({dest, 0});
115+
if (vecs_.empty()) {
116+
vecs_.push_back(iovec{dest, 0});
117+
} else if (iovec_end(vecs_.back()) != dest) {
118+
if (vecs_.size() >= IOV_MAX - 2)
119+
Flush();
120+
dest = reinterpret_cast<char*>(buffer_.AppendBuffer().data());
121+
vecs_.push_back(iovec{dest, 0});
122+
}
115123

116-
dest = reinterpret_cast<char*>(buffer_.AppendBuffer().data());
124+
DCHECK(iovec_end(vecs_.back()) == dest);
117125
char* ptr = dest;
118126
([&]() { ptr = write_piece(pieces, ptr); }(), ...);
119127

@@ -124,7 +132,9 @@ template <typename... Ts> void SinkReplyBuilder::WritePieces(Ts&&... pieces) {
124132
}
125133

126134
void SinkReplyBuilder::WriteRef(std::string_view str) {
127-
NextVec(str);
135+
if (vecs_.size() >= IOV_MAX - 2)
136+
Flush();
137+
vecs_.push_back(iovec{const_cast<char*>(str.data()), str.size()});
128138
total_size_ += str.size();
129139
}
130140

@@ -183,7 +193,7 @@ void SinkReplyBuilder::FinishScope() {
183193
if (ref_bytes > buffer_.AppendLen())
184194
return Flush(ref_bytes);
185195

186-
// Copy all extenral references to buffer to safely keep batching
196+
// Copy all external references to buffer to safely keep batching
187197
for (size_t i = guaranteed_pieces_; i < vecs_.size(); i++) {
188198
auto ib = buffer_.InputBuffer();
189199
if (vecs_[i].iov_base >= ib.data() && vecs_[i].iov_base <= ib.data() + ib.size())
@@ -198,12 +208,6 @@ void SinkReplyBuilder::FinishScope() {
198208
guaranteed_pieces_ = vecs_.size(); // all vecs are pieces
199209
}
200210

201-
void SinkReplyBuilder::NextVec(std::string_view str) {
202-
if (vecs_.size() >= IOV_MAX - 2)
203-
Flush();
204-
vecs_.push_back(iovec{const_cast<char*>(str.data()), str.size()});
205-
}
206-
207211
MCReplyBuilder::MCReplyBuilder(::io::Sink* sink) : SinkReplyBuilder(sink), noreply_(false) {
208212
}
209213

@@ -285,6 +289,7 @@ void RedisReplyBuilderBase::SendBulkString(std::string_view str) {
285289
if (str.size() <= kMaxInlineSize)
286290
return WritePieces(kLengthPrefix, uint32_t(str.size()), kCRLF, str, kCRLF);
287291

292+
DVLOG(1) << "SendBulk " << str.size();
288293
WritePieces(kLengthPrefix, uint32_t(str.size()), kCRLF);
289294
WriteRef(str);
290295
WritePieces(kCRLF);

src/facade/reply_builder.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ class SinkReplyBuilder {
129129
void WritePieces(Ts&&... pieces); // Copy pieces into buffer and reference buffer
130130
void WriteRef(std::string_view str); // Add iovec bypassing buffer
131131

132-
void FinishScope(); // Called when scope ends
133-
void NextVec(std::string_view str);
134-
132+
void FinishScope(); // Called when scope ends to flush buffer if needed
135133
void Send();
136134

137135
protected:

src/facade/reply_builder_test.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,23 @@ TEST_F(RedisReplyBuilderTest, Issue3449) {
896896
EXPECT_EQ(10000, parse_result.args.size());
897897
}
898898

899+
TEST_F(RedisReplyBuilderTest, Issue4424) {
900+
vector<string> records;
901+
for (unsigned i = 0; i < 800; ++i) {
902+
records.push_back(string(100, 'a'));
903+
}
904+
905+
for (unsigned j = 0; j < 2; ++j) {
906+
builder_->SendBulkStrArr(records);
907+
ASSERT_TRUE(NoErrors());
908+
ParsingResults parse_result = Parse();
909+
ASSERT_FALSE(parse_result.IsError()) << int(parse_result.result);
910+
ASSERT_TRUE(parse_result.Verify(SinkSize()));
911+
EXPECT_EQ(800, parse_result.args.size());
912+
sink_.Clear();
913+
}
914+
}
915+
899916
static void BM_FormatDouble(benchmark::State& state) {
900917
vector<double> values;
901918
char buf[64];

0 commit comments

Comments
 (0)