diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs index c89fb923..24b28922 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -201,6 +201,11 @@ public partial class LzmaStream } } + if (_availableBytes < 0) + { + throw new DataErrorException(); + } + var toProcess = count - total; if (toProcess > _availableBytes) { @@ -224,6 +229,10 @@ public partial class LzmaStream } var read = _outWindow.Read(buffer, offset, toProcess); + if (read == 0 && _availableBytes > 0) + { + throw new DataErrorException(); + } total += read; offset += read; _position += read; @@ -317,6 +326,11 @@ public partial class LzmaStream } } + if (_availableBytes < 0) + { + throw new DataErrorException(); + } + var toProcess = count - total; if (toProcess > _availableBytes) { @@ -340,6 +354,10 @@ public partial class LzmaStream } var read = _outWindow.Read(buffer, offset, toProcess); + if (read == 0 && _availableBytes > 0) + { + throw new DataErrorException(); + } total += read; offset += read; _position += read; diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index f45b33a3..e4454771 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -266,6 +266,11 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable } } + if (_availableBytes < 0) + { + throw new DataErrorException(); + } + var toProcess = count - total; if (toProcess > _availableBytes) { @@ -283,6 +288,11 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable } var read = _outWindow.Read(buffer, offset, toProcess); + // Unknown-size raw LZMA can end without output; otherwise no progress is corruption. + if (read == 0 && _availableBytes > 0) + { + throw new DataErrorException(); + } total += read; offset += read; _position += read; @@ -416,10 +426,21 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable return value; } + private int ReadChunkHeaderByte() + { + var value = _inputStream!.ReadByte(); + // EOF is not an LZMA2 end marker and must never enter chunk-size arithmetic. + if (value < 0) + { + throw new DataErrorException(); + } + _inputPosition++; + return value; + } + private void DecodeChunkHeader() { - var control = _inputStream!.ReadByte(); - _inputPosition++; + var control = ReadChunkHeaderByte(); if (control == 0x00) { @@ -448,17 +469,14 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable _uncompressedChunk = false; _availableBytes = (control & 0x1F) << 16; - _availableBytes += (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1; - _inputPosition += 2; + _availableBytes += (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1; - _rangeDecoderLimit = (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1; - _inputPosition += 2; + _rangeDecoderLimit = (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1; if (control >= 0xC0) { _needProps = false; - Properties[0] = (byte)_inputStream.ReadByte(); - _inputPosition++; + Properties[0] = (byte)ReadChunkHeaderByte(); _decoder = new Decoder(); _decoder.SetDecoderProperties(Properties); @@ -482,8 +500,7 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable else { _uncompressedChunk = true; - _availableBytes = (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1; - _inputPosition += 2; + _availableBytes = (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1; } } diff --git a/tests/SharpCompress.Test/Streams/Lzma2TruncationTests.cs b/tests/SharpCompress.Test/Streams/Lzma2TruncationTests.cs new file mode 100644 index 00000000..4fa16d41 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/Lzma2TruncationTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.Xz; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class Lzma2TruncationTests +{ + private const string TruncatedXzHex = + "FD377A585A0000016922DE360200210100000000372797D6010011616C7068610A616C7068610A616C7068610A"; + private const string MutatedSevenZipBase64 = + "N3q8ryccAATrtqxIDgAAAAAAAABaAAAAAAAAAKyqq98BAApoZWxsbyB3b3JsZAEEBgABCQ4ABwsBAAEhIQEADAwACAoBhRFKDQAABQEZDAAAAAAAAAAAAAAAABETAHQAZQBzAHQALgB0AHgAdAAAABkAFAoBAI6gjYxMV90BFQYBACAAAAAAAA=="; + + public static IEnumerable PartialHeaders() + { + // Start with a complete dictionary-reset chunk so a missing control byte + // cannot be rejected merely because the dictionary still needs resetting. + byte[] prefix = [0x01, 0x00, 0x00, 0x58]; + yield return [prefix]; + byte[][] headers = + [ + [0x01, 0x00, 0x00], + [0x02, 0x00, 0x00], + [0xE0, 0x00, 0x00, 0x00, 0x04, 0x00], + ]; + foreach (var header in headers) + { + for (var length = 1; length < header.Length; length++) + { + var data = new byte[prefix.Length + length]; + prefix.CopyTo(data, 0); + Array.Copy(header, 0, data, prefix.Length, length); + yield return [data]; + } + } + } + + [Theory] + [MemberData(nameof(PartialHeaders))] + public void Lzma2_Rejects_Partial_Header(byte[] data) + { + using var input = new MemoryStream(data); + using var decoder = LzmaStream.Create([0x00], input); + Assert.ThrowsAny(() => decoder.CopyTo(Stream.Null)); + } + + [Theory] + [MemberData(nameof(PartialHeaders))] + public void Lzma2_ReadByte_Rejects_Partial_Header(byte[] data) + { + using var input = new MemoryStream(data); + using var decoder = LzmaStream.Create([0x00], input); + Assert.Equal(0x58, decoder.ReadByte()); + Assert.ThrowsAny(() => decoder.ReadByte()); + } + + [Theory] + [MemberData(nameof(PartialHeaders))] + public async Task Lzma2_Async_Rejects_Partial_Header(byte[] data) + { + using var input = new MemoryStream(data); + await using var decoder = await LzmaStream.CreateAsync([0x00], input, -1, -1); + var buffer = new byte[64]; + await Assert.ThrowsAnyAsync(async () => + await decoder.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None) + ); +#if !LEGACY_DOTNET + // Use a fresh decoder because parser state is undefined after a failed read. + using var memoryInput = new MemoryStream(data); + await using var memoryDecoder = await LzmaStream.CreateAsync([0x00], memoryInput, -1, -1); + await Assert.ThrowsAnyAsync(async () => + await memoryDecoder.ReadAsync(buffer.AsMemory(), CancellationToken.None) + ); +#endif + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Xz_Rejects_Missing_Lzma2_End_Marker(bool useAsync) + { + var data = new byte[TruncatedXzHex.Length / 2]; + for (var i = 0; i < data.Length; i++) + { + data[i] = Convert.ToByte(TruncatedXzHex.Substring(i * 2, 2), 16); + } + using var input = new MemoryStream(data); + await using var decoder = new XZStream(input); + if (useAsync) + { + await Assert.ThrowsAnyAsync(() => + decoder.CopyToAsync(Stream.Null, 81920, CancellationToken.None) + ); + } + else + { + Assert.ThrowsAny(() => decoder.CopyTo(Stream.Null)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SevenZip_Rejects_Missing_Lzma2_End_Marker(bool useAsync) + { + using var input = new MemoryStream(Convert.FromBase64String(MutatedSevenZipBase64)); + if (useAsync) + { + await using var archive = await ArchiveFactory.OpenAsyncArchive(input); + await foreach (var entry in archive.EntriesAsync) + { + await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await Assert.ThrowsAnyAsync(() => + stream.CopyToAsync(Stream.Null, 81920, CancellationToken.None) + ); + } + } + else + { + using var archive = ArchiveFactory.OpenArchive(input); + foreach (var entry in archive.Entries) + { + using var stream = entry.OpenEntryStream(); + Assert.ThrowsAny(() => stream.CopyTo(Stream.Null)); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Lzma2_Accepts_End_Marker_After_Uncompressed_Chunks(bool useAsync) + { + byte[] data = [0x01, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x59, 0x00]; + using var input = new MemoryStream(data); + using var output = new MemoryStream(); + if (useAsync) + { + await using var decoder = await LzmaStream.CreateAsync([0x00], input, data.Length, 2); + await decoder.CopyToAsync(output, 1, CancellationToken.None); + } + else + { + using var decoder = LzmaStream.Create([0x00], input, data.Length, 2); + decoder.CopyTo(output, 1); + } + Assert.Equal(new byte[] { 0x58, 0x59 }, output.ToArray()); + } +}