From c4028139fda176b2208d7a9df325265fb15da628 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 27 Oct 2026 15:02:07 +0000 Subject: [PATCH] Report invalid LZMA2 chunk properties as a data error An LZMA2 chunk header properties byte of 225 or more cannot occur in valid data, but it reached Decoder.SetDecoderProperties and surfaced as InvalidParamException. EncryptedDecoderStream only maps DataErrorException to CryptographicException, so a wrong 7z password occasionally leaked the internal codec exception, making SevenZipWriter_Encrypted_WrongPasswordFails flaky. Co-Authored-By: Claude Opus 5.5 --- .../Compressors/LZMA/LzmaStream.Async.cs | 5 +--- .../Compressors/LZMA/LzmaStream.cs | 21 ++++++++++---- .../Streams/LzmaDecoderSafetyTests.cs | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs index c5e8b4ec..ec427519 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -146,11 +146,8 @@ public partial class LzmaStream await _inputStream! .ReadExactAsync(headerBuffer, 0, 1, cancellationToken) .ConfigureAwait(false); - Properties[0] = headerBuffer[0]; _inputPosition++; - - _decoder ??= new Decoder(); - _decoder.SetDecoderProperties(Properties); + SetChunkProperties(headerBuffer[0]); } else if (_needProps) { diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 615ea7d6..833037bc 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -547,6 +547,21 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable return value; } + private void SetChunkProperties(byte properties) + { + // A properties byte encodes (pb * 5 + lp) * 9 + lc with pb <= 4; a larger value is + // corrupt stream data, not an invalid caller argument. + if (properties >= 9 * 5 * 5) + { + throw new DataErrorException(); + } + Properties[0] = properties; + + // Properties initialization resets every probability and match-state field. + _decoder ??= new Decoder(); + _decoder.SetDecoderProperties(Properties); + } + private void DecodeChunkHeader() { var control = ReadChunkHeaderByte(); @@ -585,11 +600,7 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable if (control >= 0xC0) { _needProps = false; - Properties[0] = (byte)ReadChunkHeaderByte(); - - // Properties initialization resets every probability and match-state field. - _decoder ??= new Decoder(); - _decoder.SetDecoderProperties(Properties); + SetChunkProperties((byte)ReadChunkHeaderByte()); } else if (_needProps) { diff --git a/tests/SharpCompress.Test/Streams/LzmaDecoderSafetyTests.cs b/tests/SharpCompress.Test/Streams/LzmaDecoderSafetyTests.cs index fcc55cef..0f157ed1 100644 --- a/tests/SharpCompress.Test/Streams/LzmaDecoderSafetyTests.cs +++ b/tests/SharpCompress.Test/Streams/LzmaDecoderSafetyTests.cs @@ -78,6 +78,34 @@ public class LzmaDecoderSafetyTests }); } + // LZMA2 chunk with dictionary and properties reset whose properties byte encodes pb > 4. + private static readonly byte[] InvalidChunkProperties = { 0xE0, 0, 0, 0, 4, 0xFF, 0, 0, 0, 0 }; + + [Fact] + public void Lzma2InvalidChunkPropertiesThrowsDataError() + { + using var input = new MemoryStream(InvalidChunkProperties); + using var stream = LzmaStream.Create(new byte[] { 0 }, input, leaveOpen: true); + Assert.Throws(() => stream.ReadByte()); + } + + [Fact] + public async Task AsyncLzma2InvalidChunkPropertiesThrowsDataError() + { + using var input = new MemoryStream(InvalidChunkProperties); + using var stream = await LzmaStream.CreateAsync( + new byte[] { 0 }, + input, + -1, + -1, + leaveOpen: true + ); + var buffer = new byte[16]; + await Assert.ThrowsAsync(() => + stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None) + ); + } + #if !LEGACY_DOTNET [Theory] [InlineData(false)]