Skip to content

Commit 5a73a9b

Browse files
generatedunixname1383054420177565meta-codesync[bot]
authored andcommitted
Fix out-of-bounds read in Lz4Immutable::decompress / IovecCursor::peekInto (T267530293)
Reviewed By: alikhtarov Differential Revision: D114025354 fbshipit-source-id: 14fc6b1e29ec0343f00a9f4c77b1d897c68fa842
1 parent afdb05a commit 5a73a9b

3 files changed

Lines changed: 56 additions & 3 deletions

File tree

mcrouter/lib/IovecCursor.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,27 @@ void IovecCursor::seek(size_t pos) {
6060
}
6161

6262
void IovecCursor::peekInto(uint8_t* dest, size_t size) const {
63+
// Defense-in-depth: callers are contractually required to guarantee that
64+
// "size" bytes are available, but a precondition violation must degrade to a
65+
// short read rather than dereferencing iov_ past its end (out-of-bounds
66+
// read). Compliant callers never trip these guards.
67+
size_t i = iovIndex_;
68+
if (FOLLY_UNLIKELY(i >= iovLength_)) {
69+
return;
70+
}
6371
const uint8_t* cur =
64-
reinterpret_cast<uint8_t*>(iov_[iovIndex_].iov_base) + curBufPos_;
72+
reinterpret_cast<uint8_t*>(iov_[i].iov_base) + curBufPos_;
6573
size_t curLen = curBufLen_;
66-
size_t i = iovIndex_;
6774

6875
while (size > 0) {
6976
size_t toCopy = std::min(size, curLen);
7077
std::memcpy(dest, cur, toCopy);
7178
dest += toCopy;
7279
size -= toCopy;
7380
if (size > 0) {
74-
++i;
81+
if (FOLLY_UNLIKELY(++i >= iovLength_)) {
82+
return;
83+
}
7584
cur = reinterpret_cast<uint8_t*>(iov_[i].iov_base);
7685
curLen = iov_[i].iov_len;
7786
}

mcrouter/lib/Lz4Immutable.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,22 +509,40 @@ std::unique_ptr<folly::IOBuf> Lz4Immutable::decompress(
509509
IovecCursor source(iov, iovcnt);
510510
IovecCursor match = dicCursor;
511511

512+
// The compressed stream is untrusted (it can originate from a remote
513+
// backend). IovecCursor pushes bounds-checking onto its caller, so every read
514+
// below must be guarded against the end of the source: an encoded length that
515+
// exceeds the bytes actually present would otherwise drive the cursor past
516+
// the source iovec and read out of bounds.
517+
const auto sourceBytesLeft = [&source] {
518+
return source.totalLength() - source.tell();
519+
};
520+
512521
// Main loop
513522
while (true) {
514523
// LZ4 token
524+
if (FOLLY_UNLIKELY(sourceBytesLeft() < sizeof(uint8_t))) {
525+
return nullptr;
526+
}
515527
size_t token = source.read<uint8_t>();
516528

517529
// Get literal length
518530
size_t literalLength = token >> kMlBits;
519531
if (literalLength == kRunMask) {
520532
size_t s;
521533
do {
534+
if (FOLLY_UNLIKELY(sourceBytesLeft() < sizeof(uint8_t))) {
535+
return nullptr;
536+
}
522537
s = source.read<uint8_t>();
523538
literalLength += s;
524539
} while (FOLLY_LIKELY(s == 255));
525540
}
526541

527542
// Copy literals
543+
if (FOLLY_UNLIKELY(literalLength > sourceBytesLeft())) {
544+
return nullptr;
545+
}
528546
uint8_t* cpy = output + literalLength;
529547
if (cpy > outputLimit - kCopyLength) {
530548
if (cpy != outputLimit) {
@@ -538,6 +556,9 @@ std::unique_ptr<folly::IOBuf> Lz4Immutable::decompress(
538556
output = cpy;
539557

540558
// Get match offset
559+
if (FOLLY_UNLIKELY(sourceBytesLeft() < sizeof(uint16_t))) {
560+
return nullptr;
561+
}
541562
uint16_t offset = peekLE(source);
542563
source.advance(2);
543564
size_t outputProgress = static_cast<size_t>(output - outputStart);
@@ -555,6 +576,9 @@ std::unique_ptr<folly::IOBuf> Lz4Immutable::decompress(
555576
if (matchLength == kMlMask) {
556577
size_t s;
557578
do {
579+
if (FOLLY_UNLIKELY(sourceBytesLeft() < sizeof(uint8_t))) {
580+
return nullptr;
581+
}
558582
s = source.read<uint8_t>();
559583
matchLength += s;
560584
} while (s == 255);

mcrouter/lib/test/Lz4ImmutableTest.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,26 @@ TEST(Lz4Immutable, error_checks) {
240240
EXPECT_THROW(compressor.compress(&iov, 1), std::invalid_argument);
241241
}
242242

243+
// Regression test for T267530293: a malformed compressed stream whose token
244+
// declares more literal bytes than the source actually contains used to drive
245+
// IovecCursor past the end of the source iovec (out-of-bounds read). A correct
246+
// decompressor must reject the stream and return nullptr instead of reading
247+
// past the source. Under ASAN this over-read would otherwise be a detectable
248+
// heap-buffer-overflow.
249+
TEST(Lz4Immutable, decompress_truncated_source) {
250+
auto dictionary = getAsciiDictionary();
251+
Lz4Immutable compressor(dictionary->clone());
252+
253+
// Single LZ4 token byte: literal length 5, match length 0, but zero literal
254+
// bytes follow. A large declared output size keeps the output-bounds check
255+
// from firing first, so the literal copy is reached with an exhausted source.
256+
const uint8_t token = 0x50;
257+
iovec iov = {const_cast<uint8_t*>(&token), sizeof(token)};
258+
259+
EXPECT_EQ(
260+
compressor.decompress(&iov, 1, /* uncompressedSize */ 4096), nullptr);
261+
}
262+
243263
TEST(Lz4Immutable, largeData_ascii) {
244264
auto dictionary = getAsciiDictionary();
245265
Lz4Immutable compressor(dictionary->clone());

0 commit comments

Comments
 (0)