From 9a04b51c0bc4555d4caf779c8b5f20ff32f8f8a1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 4 Nov 2026 17:03:41 +0000 Subject: [PATCH] Reject Deflate back-references beyond initialized history --- .../Compressors/Deflate/Inflate.cs | 58 +++- .../Streams/DeflateHistoryTests.cs | 292 ++++++++++++++++++ 2 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 tests/SharpCompress.Test/Streams/DeflateHistoryTests.cs diff --git a/src/SharpCompress/Compressors/Deflate/Inflate.cs b/src/SharpCompress/Compressors/Deflate/Inflate.cs index 7c972ef7..0afc6aa4 100644 --- a/src/SharpCompress/Compressors/Deflate/Inflate.cs +++ b/src/SharpCompress/Compressors/Deflate/Inflate.cs @@ -109,6 +109,9 @@ internal sealed class InflateBlocks internal object checkfn; // check function internal InflateCodes codes = new(); // if CODES, current state internal int end; // one byte after sliding window + + // Track initialized history separately from the circular read/write pointers. + internal int historyBytes; internal int[] hufts; // single malloc for tree space internal int index; // index into blens (or border) internal InfTree inftree = new(); @@ -140,6 +143,7 @@ internal sealed class InflateBlocks bitk = 0; bitb = 0; readAt = writeAt = 0; + historyBytes = 0; if (checkfn != null) { @@ -148,6 +152,8 @@ internal sealed class InflateBlocks return oldCheck; } + internal void AddHistory(int count) => historyBytes = Math.Min(end, historyBytes + count); + internal int Process(int r) { int t; // temporary storage @@ -344,6 +350,7 @@ internal sealed class InflateBlocks t = m; } _codec.InputBuffer.AsSpan(p, t).CopyTo(window.Memory.Span.Slice(q)); + AddHistory(t); p += t; n -= t; q += t; @@ -731,6 +738,7 @@ internal sealed class InflateBlocks { d.AsSpan(start, n).CopyTo(window.Memory.Span.Slice(0, n)); readAt = writeAt = n; + historyBytes = n; } // Returns true if inflate is currently at the end of a block generated @@ -1181,15 +1189,29 @@ internal sealed class InflateCodes b >>= j; k -= j; + if (dist > blocks.historyBytes) + { + mode = BADCODE; + z.Message = "invalid distance too far back"; + r = ZlibConstants.Z_DATA_ERROR; + + blocks.bitb = b; + blocks.bitk = k; + z.AvailableBytesIn = n; + z.TotalBytesIn += p - z.NextIn; + z.NextIn = p; + blocks.writeAt = q; + return blocks.Flush(r); + } + mode = COPY; goto case COPY; case COPY: // o: copying bytes in window, waiting for space f = q - dist; - while (f < 0) + if (f < 0) { - // modulo window size-"while" instead - f += blocks.end; // of "if" handles invalid distances + f += blocks.end; } while (len != 0) { @@ -1227,6 +1249,7 @@ internal sealed class InflateCodes } blocks.window.Memory.Span[q++] = blocks.window.Memory.Span[f++]; + blocks.AddHistory(1); m--; if (f == blocks.end) @@ -1273,6 +1296,7 @@ internal sealed class InflateCodes r = ZlibConstants.Z_OK; blocks.window.Memory.Span[q++] = (byte)lit; + blocks.AddHistory(1); m--; mode = START; @@ -1410,6 +1434,7 @@ internal sealed class InflateCodes k -= (tp[tp_index_t_3 + 1]); s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2]; + s.AddHistory(1); m--; continue; } @@ -1465,7 +1490,27 @@ internal sealed class InflateCodes b >>= e; k -= e; + if (d > s.historyBytes) + { + z.Message = "invalid distance too far back"; + + c = z.AvailableBytesIn - n; + c = (k >> 3) < c ? k >> 3 : c; + n += c; + p -= c; + k -= c << 3; + + s.bitb = b; + s.bitk = k; + z.AvailableBytesIn = n; + z.TotalBytesIn += p - z.NextIn; + z.NextIn = p; + s.writeAt = q; + return ZlibConstants.Z_DATA_ERROR; + } + // do the copy + s.AddHistory(c); m -= c; if (q >= d) { @@ -1490,11 +1535,7 @@ internal sealed class InflateCodes else { // else offset after destination - r = q - d; - do - { - r += s.end; // force pointer in window - } while (r < 0); // covers invalid distances + r = q - d + s.end; e = s.end - r; if (c > e) { @@ -1577,6 +1618,7 @@ internal sealed class InflateCodes b >>= (tp[tp_index_t_3 + 1]); k -= (tp[tp_index_t_3 + 1]); s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2]; + s.AddHistory(1); m--; break; } diff --git a/tests/SharpCompress.Test/Streams/DeflateHistoryTests.cs b/tests/SharpCompress.Test/Streams/DeflateHistoryTests.cs new file mode 100644 index 00000000..2ebe90c8 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/DeflateHistoryTests.cs @@ -0,0 +1,292 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class DeflateHistoryTests +{ + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public async Task Deflate_Rejects_Distance_Beyond_Output_History( + bool useAsync, + bool useFastPath, + bool emptyHistory + ) + { + // Fixed Huffman blocks: either a match before any literal, or 'A' then length 3/distance 4. + byte[] compressed = emptyHistory ? [0x03, 0x02, 0x00] : [0x73, 0x04, 0x62, 0x27, 0x00]; + using var input = new MemoryStream(WithReadAhead(compressed, useFastPath)); + using var decoder = new DeflateStream(input, CompressionMode.Decompress); + using var output = new MemoryStream(); + + ZlibException exception; + if (useAsync) + { + exception = await Assert.ThrowsAsync(() => + decoder.CopyToAsync(output, 1024, CancellationToken.None) + ); + } + else + { + exception = Assert.Throws(() => decoder.CopyTo(output)); + } + + Assert.Contains("invalid distance too far back", exception.Message); + Assert.Empty(output.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Deflate_Invalid_Distance_Does_Not_Copy_Uninitialized_History(bool useFastPath) + { + var codec = new ZlibCodec(); + codec.InitializeInflate(false); + try + { + // Seed this decoder's window directly, without relying on shared-pool reuse. + codec.istate.blocks.window.Memory.Span.Fill(0xa5); + SetInput(codec, WithReadAhead([0x73, 0x04, 0x62, 0x27, 0x00], useFastPath)); + codec.OutputBuffer = new byte[16]; + codec.AvailableBytesOut = codec.OutputBuffer.Length; + + Assert.Throws(() => codec.Inflate(FlushType.None)); + + Assert.Equal(1, codec.NextOut); + Assert.Equal((byte)'A', codec.OutputBuffer[0]); + Assert.Equal(new byte[15], codec.OutputBuffer.AsSpan(1).ToArray()); + } + finally + { + codec.EndInflate(); + } + } + + [Theory] + [InlineData(false, 3)] + [InlineData(false, 4)] + [InlineData(true, 3)] + [InlineData(true, 4)] + public void Deflate_Preset_Dictionary_Limits_Available_History( + bool useFastPath, + int dictionarySize + ) + { + var codec = new ZlibCodec(); + codec.InitializeInflate(false); + try + { + byte[] dictionary = [(byte)'A', (byte)'B', (byte)'C', (byte)'D']; + codec.istate.blocks.SetDictionary(dictionary, 0, dictionarySize); + // Length 3/distance 4, with no preceding literal. + SetInput(codec, WithReadAhead([0x03, 0x62, 0x00], useFastPath)); + codec.OutputBuffer = new byte[16]; + codec.AvailableBytesOut = codec.OutputBuffer.Length; + + if (dictionarySize < 4) + { + var exception = Assert.Throws(() => codec.Inflate(FlushType.None)); + Assert.Contains("invalid distance too far back", exception.Message); + Assert.Equal(0, codec.NextOut); + } + else + { + Assert.Equal(ZlibConstants.Z_STREAM_END, codec.Inflate(FlushType.None)); + Assert.Equal(3, codec.NextOut); + Assert.Equal( + dictionary.AsSpan(0, 3).ToArray(), + codec.OutputBuffer.AsSpan(0, 3).ToArray() + ); + } + } + finally + { + codec.EndInflate(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Deflate_Reset_Discards_Previous_History(bool useFastPath) + { + var codec = new ZlibCodec(); + codec.InitializeInflate(false); + try + { + codec.istate.blocks.SetDictionary([1, 2, 3, 4], 0, 4); + codec.istate.Reset(); + SetInput(codec, WithReadAhead([0x03, 0x62, 0x00], useFastPath)); + codec.OutputBuffer = new byte[16]; + codec.AvailableBytesOut = codec.OutputBuffer.Length; + + var exception = Assert.Throws(() => codec.Inflate(FlushType.None)); + Assert.Contains("invalid distance too far back", exception.Message); + Assert.Equal(0, codec.NextOut); + } + finally + { + codec.EndInflate(); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Deflate_Rejects_Distance_Beyond_Window_Size(bool useFastPath) + { + var codec = new ZlibCodec(); + codec.InitializeInflate(9, false); + try + { + // Produce more than the 512-byte window, then request length 3/distance 513. + using var compressed = new MemoryStream(); + WriteStoredBlock(compressed, new byte[513]); + var match = WithReadAhead([0x03, 0x26, 0x00, 0x00], useFastPath); + compressed.Write(match, 0, match.Length); + SetInput(codec, compressed.ToArray()); + codec.OutputBuffer = new byte[1024]; + codec.AvailableBytesOut = codec.OutputBuffer.Length; + + var exception = Assert.Throws(() => codec.Inflate(FlushType.None)); + Assert.Contains("invalid distance too far back", exception.Message); + Assert.Equal(513, codec.NextOut); + } + finally + { + codec.EndInflate(); + } + } + + [Theory] + [InlineData(false, 3)] + [InlineData(true, 3)] + [InlineData(false, 32768)] + [InlineData(true, 32768)] + [InlineData(false, 32769)] + [InlineData(true, 32769)] + public async Task Deflate_History_Survives_Stored_Blocks_And_Window_Wrap( + bool useAsync, + int historySize + ) + { + var history = new byte[historySize]; + for (var i = 0; i < history.Length; i++) + { + history[i] = (byte)(i % 251); + } + + // Follow a non-final stored block with a fixed block referencing the available history. + var distance = Math.Min(historySize, 32768); + byte[] match = distance == 3 ? [0x03, 0x22, 0x00] : [0x03, 0xde, 0xff, 0x0f, 0x00]; + using var compressed = new MemoryStream(); + WriteStoredBlock(compressed, history); + var tail = WithReadAhead(match, true); + compressed.Write(tail, 0, tail.Length); + compressed.Position = 0; + + using var decoder = new DeflateStream(compressed, CompressionMode.Decompress); + using var output = new MemoryStream(); + if (useAsync) + { + await decoder.CopyToAsync(output, 137, CancellationToken.None); + } + else + { + decoder.CopyTo(output, 137); + } + + var expected = new byte[history.Length + 3]; + history.CopyTo(expected, 0); + Array.Copy(history, history.Length - distance, expected, history.Length, 3); + Assert.Equal(expected, output.ToArray()); + } + + [Theory] + [InlineData(false, 1)] + [InlineData(true, 1)] + [InlineData(false, 137)] + [InlineData(true, 137)] + public async Task Deflate_History_Survives_Overlapping_And_Partial_Matches( + bool useAsync, + int outputBufferSize + ) + { + var expected = new byte[70000]; + for (var i = 0; i < expected.Length; i++) + { + expected[i] = (byte)(i % 7); + } + using var compressed = new MemoryStream(); + using ( + var encoder = new System.IO.Compression.DeflateStream( + compressed, + System.IO.Compression.CompressionMode.Compress, + leaveOpen: true + ) + ) + { + encoder.Write(expected, 0, expected.Length); + } + compressed.Position = 0; + + using var decoder = new DeflateStream(compressed, CompressionMode.Decompress); + using var output = new MemoryStream(); + var buffer = new byte[outputBufferSize]; + int read; + while ( + ( + read = useAsync + ? await decoder.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None) + : decoder.Read(buffer, 0, buffer.Length) + ) != 0 + ) + { + output.Write(buffer, 0, read); + } + + Assert.Equal(expected, output.ToArray()); + } + + private static byte[] WithReadAhead(byte[] compressed, bool useFastPath) + { + if (!useFastPath) + { + return compressed; + } + + // InflateFast requires at least ten input bytes; bytes after the final block are read-ahead. + var padded = new byte[compressed.Length + 16]; + compressed.CopyTo(padded, 0); + return padded; + } + + private static void WriteStoredBlock(Stream compressed, byte[] history) + { + compressed.WriteByte(0); + compressed.WriteByte((byte)history.Length); + compressed.WriteByte((byte)(history.Length >> 8)); + compressed.WriteByte((byte)~history.Length); + compressed.WriteByte((byte)(~history.Length >> 8)); + compressed.Write(history, 0, history.Length); + } + + private static void SetInput(ZlibCodec codec, byte[] compressed) + { + codec.InputBuffer = compressed; + codec.NextIn = 0; + codec.AvailableBytesIn = compressed.Length; + } +}