Reject Deflate back-references beyond initialized history

This commit is contained in:
Adam Hathcock
2026-11-04 17:03:41 +00:00
parent ef3a923211
commit 9a04b51c0b
2 changed files with 342 additions and 8 deletions

View File

@@ -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;
}

View File

@@ -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<ZlibException>(() =>
decoder.CopyToAsync(output, 1024, CancellationToken.None)
);
}
else
{
exception = Assert.Throws<ZlibException>(() => 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<ZlibException>(() => 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<ZlibException>(() => 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<ZlibException>(() => 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<ZlibException>(() => 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;
}
}