Reject stalled BZip2 decoder states after corruption

This commit is contained in:
Adam Hathcock
2026-11-09 11:16:14 +00:00
parent 54432058de
commit fa3dff056a
5 changed files with 217 additions and 14 deletions

View File

@@ -21,26 +21,24 @@ internal partial class CBZip2InputStream
var retChar = currentChar;
switch (currentState)
{
case START_BLOCK_STATE:
break;
case RAND_PART_A_STATE:
break;
case RAND_PART_B_STATE:
await SetupRandPartBAsync(cancellationToken).ConfigureAwait(false);
break;
case RAND_PART_C_STATE:
await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false);
break;
case NO_RAND_PART_A_STATE:
break;
case NO_RAND_PART_B_STATE:
await SetupNoRandPartBAsync(cancellationToken).ConfigureAwait(false);
break;
case NO_RAND_PART_C_STATE:
await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false);
break;
case START_BLOCK_STATE:
case RAND_PART_A_STATE:
case NO_RAND_PART_A_STATE:
default:
break;
// Match ReadByte(): retries after a decoding error must not generate endless output.
throw new InvalidFormatException("BZip2: invalid decoder state");
}
return retChar;
}

View File

@@ -263,26 +263,25 @@ internal partial class CBZip2InputStream : Stream
var retChar = currentChar;
switch (currentState)
{
case START_BLOCK_STATE:
break;
case RAND_PART_A_STATE:
break;
case RAND_PART_B_STATE:
SetupRandPartB();
break;
case RAND_PART_C_STATE:
SetupRandPartC();
break;
case NO_RAND_PART_A_STATE:
break;
case NO_RAND_PART_B_STATE:
SetupNoRandPartB();
break;
case NO_RAND_PART_C_STATE:
SetupNoRandPartC();
break;
case START_BLOCK_STATE:
case RAND_PART_A_STATE:
case NO_RAND_PART_A_STATE:
default:
break;
// These transitional states can remain after a decoding error. A retry (including
// entry disposal) must fail rather than return the same byte indefinitely.
throw new InvalidFormatException("BZip2: invalid decoder state");
}
return retChar;
}

View File

@@ -2,7 +2,9 @@ using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Test.Mocks;
using Xunit;
@@ -11,6 +13,41 @@ namespace SharpCompress.Test.BZip2;
public class BZip2StreamAsyncTests
{
[Fact]
public async Task BZip2Stream_Throws_On_Read_After_Corrupt_Checksum_Async()
{
using var compressed = new MemoryStream();
await using (
var writer = await BZip2Stream.CreateAsync(
new AsyncOnlyStream(compressed),
SharpCompress.Compressors.CompressionMode.Compress,
false,
leaveOpen: true,
cancellationToken: CancellationToken.None
)
)
{
var data = Encoding.ASCII.GetBytes("BZip2 checksum validation test data.");
await writer.WriteAsync(data, 0, data.Length, CancellationToken.None);
}
var bytes = compressed.ToArray();
bytes[^5] ^= 1;
await using var stream = await BZip2Stream.CreateAsync(
new AsyncOnlyStream(new MemoryStream(bytes)),
SharpCompress.Compressors.CompressionMode.Decompress,
false,
cancellationToken: CancellationToken.None
);
await Assert.ThrowsAsync<ArchiveOperationException>(() =>
stream.CopyToAsync(Stream.Null, 81920, CancellationToken.None)
);
await Assert.ThrowsAsync<InvalidFormatException>(() =>
stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)
);
}
private byte[] CreateTestData(int size)
{
var data = new byte[size];

View File

@@ -24,6 +24,8 @@ public class BZip2StreamTests
using var output = new MemoryStream();
Assert.Throws<ArchiveOperationException>(() => stream.CopyTo(output));
Assert.Throws<InvalidFormatException>(() => stream.ReadByte());
Assert.Throws<InvalidFormatException>(() => stream.Read(new byte[1], 0, 1));
}
// A stream that ends exactly where a block header is expected (no stream footer) is the shape a caller

View File

@@ -0,0 +1,167 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipBZip2CorruptionTests : TestBase
{
[Theory]
[InlineData(240, 0x8d, 0x5a, false)]
[InlineData(11318, 0x35, 0xa8, false)]
[InlineData(41884, 0x12, 0x89, false)]
[InlineData(240, 0x8d, 0x5a, true)]
[InlineData(11318, 0x35, 0xa8, true)]
[InlineData(41884, 0x12, 0x89, true)]
public void Zip_BZip2_Reader_Throws_On_Corruption(
int offset,
byte original,
byte replacement,
bool skipEntry
)
{
using var stream = new ForwardOnlyStream(
CreateCorruptArchive(offset, original, replacement)
);
using var reader = ReaderFactory.OpenReader(stream, ReaderOptions.ForExternalStream);
var exception = Assert.ThrowsAny<SharpCompressException>(() =>
{
while (reader.MoveToNextEntry())
{
// Disposal must also terminate when a failed read leaves the decoder between states.
using var entry = reader.OpenEntryStream();
if (!skipEntry)
{
entry.CopyTo(Stream.Null);
}
}
});
Assert.StartsWith("BZip2", exception.Message);
if (!skipEntry)
{
Assert.IsType<InvalidFormatException>(exception);
}
}
[Theory]
[InlineData(240, 0x8d, 0x5a, false)]
[InlineData(11318, 0x35, 0xa8, false)]
[InlineData(41884, 0x12, 0x89, false)]
[InlineData(240, 0x8d, 0x5a, true)]
[InlineData(11318, 0x35, 0xa8, true)]
[InlineData(41884, 0x12, 0x89, true)]
public async Task Zip_BZip2_Reader_Throws_On_Corruption_Async(
int offset,
byte original,
byte replacement,
bool skipEntry
)
{
using var stream = new AsyncOnlyStream(
new ForwardOnlyStream(CreateCorruptArchive(offset, original, replacement))
);
await using var reader = await ReaderFactory.OpenAsyncReader(
stream,
ReaderOptions.ForExternalStream,
cancellationToken: CancellationToken.None
);
var exception = await Assert.ThrowsAnyAsync<SharpCompressException>(async () =>
{
while (await reader.MoveToNextEntryAsync(CancellationToken.None))
{
#if LEGACY_DOTNET
using var entry = await reader.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var entry = await reader.OpenEntryStreamAsync(CancellationToken.None);
#endif
if (!skipEntry)
{
await entry.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
}
}
});
Assert.StartsWith("BZip2", exception.Message);
if (!skipEntry)
{
Assert.IsType<InvalidFormatException>(exception);
}
}
[Theory]
[InlineData(240, 0x8d, 0x5a)]
[InlineData(11318, 0x35, 0xa8)]
[InlineData(41884, 0x12, 0x89)]
public void Zip_BZip2_Archive_Throws_On_Corruption(int offset, byte original, byte replacement)
{
using var stream = CreateCorruptArchive(offset, original, replacement);
using var archive = ArchiveFactory.OpenArchive(stream, ReaderOptions.ForExternalStream);
Assert.ThrowsAny<SharpCompressException>(() =>
{
foreach (var entry in archive.Entries)
{
if (!entry.IsDirectory)
{
using var entryStream = entry.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
}
});
}
[Theory]
[InlineData(240, 0x8d, 0x5a)]
[InlineData(11318, 0x35, 0xa8)]
[InlineData(41884, 0x12, 0x89)]
public async Task Zip_BZip2_Archive_Throws_On_Corruption_Async(
int offset,
byte original,
byte replacement
)
{
using var stream = new AsyncOnlyStream(CreateCorruptArchive(offset, original, replacement));
await using var archive = await ArchiveFactory.OpenAsyncArchive(
stream,
ReaderOptions.ForExternalStream,
cancellationToken: CancellationToken.None
);
await Assert.ThrowsAnyAsync<SharpCompressException>(async () =>
{
await foreach (var entry in archive.EntriesAsync)
{
if (!entry.IsDirectory)
{
#if LEGACY_DOTNET
using var entryStream = await entry.OpenEntryStreamAsync(
CancellationToken.None
);
#else
await using var entryStream = await entry.OpenEntryStreamAsync(
CancellationToken.None
);
#endif
await entryStream.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
}
}
});
}
private static MemoryStream CreateCorruptArchive(int offset, byte original, byte replacement)
{
// Mutate the existing fixture in memory so each regression differs by exactly one byte.
var bytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.dd.zip"));
Assert.Equal(original, bytes[offset]);
bytes[offset] = replacement;
return new MemoryStream(bytes);
}
}