Reject streams with literal spans of 2^32 bytes.

Without this change such spans are interpreted as 0-length (due to overflow before promotion).

Golang implementation (on 64-bit platforms) does not have such problem.

Drive-by: clarify in format description that uncompressed length varint should not be longer than 5 bytes; this is current C++ decoder behavior.
PiperOrigin-RevId: 947660809
diff --git a/format_description.txt b/format_description.txt
index 20db66c..1386d11 100644
--- a/format_description.txt
+++ b/format_description.txt
@@ -22,7 +22,8 @@
 where the lower 7 bits are data and the upper bit is set iff there are
 more bytes to be read. In other words, an uncompressed length of 64 would
 be stored as 0x40, and an uncompressed length of 2097150 (0x1FFFFE)
-would be stored as 0xFE 0xFF 0x7F.
+would be stored as 0xFE 0xFF 0x7F. Given the maximum, 5 bytes are sufficient for
+any value.
 
 
 2. The compressed stream itself
diff --git a/snappy.cc b/snappy.cc
index 05ffca2..37d3c0b 100644
--- a/snappy.cc
+++ b/snappy.cc
@@ -1687,9 +1687,13 @@
         if (SNAPPY_PREDICT_FALSE(literal_length >= 61)) {
           // Long literal.
           const size_t literal_length_length = literal_length - 60;
+          // NOTE: literal_length might be equal 2^32 (i.e. ExtractLowBytes
+          // returns 0xFFFFFFFF); this is implicitly invalid stream (since
+          // uncompressed length is capped with 0xFFFFFFFF); for performance we
+          // do not check for this case here.
           literal_length =
               ExtractLowBytes(LittleEndian::Load32(ip), literal_length_length) +
-              1;
+              size_t{1};
           ip += literal_length_length;
         }
 
diff --git a/snappy_unittest.cc b/snappy_unittest.cc
index 923a0ec..2d04200 100644
--- a/snappy_unittest.cc
+++ b/snappy_unittest.cc
@@ -790,6 +790,61 @@
   EXPECT_FALSE(snappy::IsValidCompressedBuffer(compressed, 4));
 }
 
+// A 4-byte extended literal length of 0xffffffff decodes as length =
+// 0xffffffff + 1.  In correct (64-bit or overflow-checked) arithmetic this is
+// 4294967296, which exceeds the source buffer and must be rejected.  In
+// vulnerable 32-bit unsigned arithmetic the +1 wraps to 0, causing the
+// decoder to skip the literal entirely and continue, silently producing
+// wrong output that happens to match the preamble length (65536).
+//
+// Payload structure:
+//   Bytes 0-2:     Varint 0x80 0x80 0x04 → expected length 65536
+//   Bytes 3-4:     Literal tag 0x00 + data 0x44 → 1-byte literal
+//   Bytes 5-784:   260× copy2 (0xfe 0x01 0x00) → 64 bytes from offset 1
+//   Bytes 785-786: Literal tag 0x00 + data 0x46 → 1-byte literal
+//   Bytes 787:     0xfc → literal tag with 4-byte length prefix (m=63)
+//   Bytes 788-791: 0xff 0xff 0xff 0xff → length-1 = 0xffffffff → +1 wraps
+//   Bytes 792+:    763× copy2 + 1× copy2(len=62) to fill remaining output
+TEST(Snappy, LiteralLengthU32Overflow) {
+  std::string compressed;
+  // Varint: expected output length 65536
+  compressed.push_back('\x80');
+  compressed.push_back('\x80');
+  compressed.push_back('\x04');
+  // 1-byte literal (0x44)
+  AppendLiteral(&compressed, "D");
+  // 260 copy2 elements: each copies 64 bytes from offset 1
+  // Output after this section: 1 + 260*64 = 16641
+  for (int i = 0; i < 260; i++) {
+    AppendCopy(&compressed, 1, 64);
+  }
+  // 1-byte literal (0x46)
+  AppendLiteral(&compressed, "F");
+  // Output so far (if wrapping): 16642
+
+  // Poison literal: tag 0xfc (m=63 → 4-byte extended length), length bytes
+  // 0xffffffff.  Correct length = 0xffffffff + 1 = 4294967296.
+  // Wrapping length = 0.
+  compressed.push_back('\xfc');  // literal tag, 4 extra length bytes
+  compressed.push_back('\xff');
+  compressed.push_back('\xff');
+  compressed.push_back('\xff');
+  compressed.push_back('\xff');
+
+  // Remaining copies to fill output to 65536 if the literal is skipped:
+  // 65536 - 16642 = 48894 = 763*64 + 62
+  for (int i = 0; i < 763; i++) {
+    AppendCopy(&compressed, 1, 64);
+  }
+  AppendCopy(&compressed, 1, 62);
+
+  std::string uncompressed;
+  EXPECT_FALSE(snappy::Uncompress(compressed.data(), compressed.size(),
+                                  &uncompressed));
+  EXPECT_FALSE(snappy::IsValidCompressedBuffer(compressed.data(),
+                                               compressed.size()));
+}
+
 int TestFindMatchLength(const char* s1, const char *s2, unsigned length) {
   uint64_t data;
   std::pair<size_t, bool> p =