Fix multiple decompressor crashes on malformed input (IOOB, DivByZero, NullRef)

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/3037a2f7-f243-4261-802f-e8c83b4d6722
This commit is contained in:
copilot-swe-agent[bot]
2026-03-23 09:55:55 +00:00
parent f608957c7a
commit f038652d3b
15 changed files with 366 additions and 18 deletions

View File

@@ -73,6 +73,10 @@ public partial class ArcLzwStream : Stream
if (useCrunched)
{
if (input.Length == 0)
{
throw new InvalidFormatException("ArcLzwStream: compressed data is empty");
}
if (input[0] != BITS)
{
throw new InvalidFormatException($"File packed with {input[0]}, expected {BITS}.");
@@ -129,6 +133,10 @@ public partial class ArcLzwStream : Stream
while (code >= 256)
{
if (code >= suffix.Length)
{
throw new InvalidFormatException("ArcLzwStream: code out of range");
}
stack.Push(suffix[code]);
code = prefix[code];
}

View File

@@ -237,6 +237,10 @@ internal partial class CBZip2InputStream
/* Now the selectors */
nGroups = await BsRAsync(3, cancellationToken).ConfigureAwait(false);
if (nGroups < 2 || nGroups > BZip2Constants.N_GROUPS)
{
throw new InvalidFormatException("BZip2: invalid number of Huffman trees");
}
nSelectors = await BsRAsync(15, cancellationToken).ConfigureAwait(false);
for (i = 0; i < nSelectors; i++)
{
@@ -244,6 +248,10 @@ internal partial class CBZip2InputStream
while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1)
{
j++;
if (j >= nGroups)
{
throw new InvalidFormatException("BZip2: invalid selector MTF value");
}
}
if (i < BZip2Constants.MAX_SELECTORS)
{
@@ -266,6 +274,10 @@ internal partial class CBZip2InputStream
for (i = 0; i < nSelectors; i++)
{
v = selectorMtf[i];
if (v >= nGroups)
{
throw new InvalidFormatException("BZip2: selector MTF value out of range");
}
tmp = pos[v];
while (v > 0)
{
@@ -374,6 +386,10 @@ internal partial class CBZip2InputStream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -405,7 +421,14 @@ internal partial class CBZip2InputStream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
while (true)
@@ -448,6 +471,10 @@ internal partial class CBZip2InputStream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -479,7 +506,14 @@ internal partial class CBZip2InputStream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
} while (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB);
@@ -550,6 +584,10 @@ internal partial class CBZip2InputStream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -581,7 +619,14 @@ internal partial class CBZip2InputStream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
}
}
@@ -605,10 +650,18 @@ internal partial class CBZip2InputStream
for (i = 0; i <= last; i++)
{
ch = ll8[i];
if (cftab[ch] < 0 || cftab[ch] >= tt.Length)
{
throw new InvalidFormatException("BZip2: block data out of bounds");
}
tt[cftab[ch]] = i;
cftab[ch]++;
}
if (origPtr < 0 || origPtr >= tt.Length)
{
throw new InvalidFormatException("BZip2: origPtr out of bounds");
}
tPos = tt[origPtr];
count = 0;
@@ -806,6 +859,10 @@ internal partial class CBZip2InputStream
int v;
while (bsLive < n)
{
if (bsStream is null)
{
CompressedStreamEOF();
}
int zzi;
int thech = '\0';
var b = ArrayPool<byte>.Shared.Rent(1);
@@ -858,7 +915,10 @@ internal partial class CBZip2InputStream
cbZip2InputStream.ll8 = null;
cbZip2InputStream.tt = null;
cbZip2InputStream.BsSetStream(zStream);
await cbZip2InputStream.InitializeAsync(true, cancellationToken).ConfigureAwait(false);
if (!await cbZip2InputStream.InitializeAsync(true, cancellationToken).ConfigureAwait(false))
{
throw new InvalidFormatException("Not a valid BZip2 stream");
}
await cbZip2InputStream.InitBlockAsync(cancellationToken).ConfigureAwait(false);
await cbZip2InputStream.SetupBlockAsync(cancellationToken).ConfigureAwait(false);
return cbZip2InputStream;

View File

@@ -179,7 +179,10 @@ internal partial class CBZip2InputStream : Stream
cbZip2InputStream.ll8 = null;
cbZip2InputStream.tt = null;
cbZip2InputStream.BsSetStream(zStream);
cbZip2InputStream.Initialize(true);
if (!cbZip2InputStream.Initialize(true))
{
throw new InvalidFormatException("Not a valid BZip2 stream");
}
cbZip2InputStream.InitBlock();
cbZip2InputStream.SetupBlock();
return cbZip2InputStream;
@@ -403,6 +406,10 @@ internal partial class CBZip2InputStream : Stream
int v;
while (bsLive < n)
{
if (bsStream is null)
{
CompressedStreamEOF();
}
int zzi;
int thech = '\0';
try
@@ -477,6 +484,10 @@ internal partial class CBZip2InputStream : Stream
}
for (i = 0; i < alphaSize; i++)
{
if (length[i] >= BZip2Constants.MAX_CODE_LEN - 1)
{
throw new InvalidFormatException("BZip2: invalid Huffman code length");
}
basev[length[i] + 1]++;
}
@@ -553,6 +564,10 @@ internal partial class CBZip2InputStream : Stream
/* Now the selectors */
nGroups = BsR(3);
if (nGroups < 2 || nGroups > BZip2Constants.N_GROUPS)
{
throw new InvalidFormatException("BZip2: invalid number of Huffman trees");
}
nSelectors = BsR(15);
for (i = 0; i < nSelectors; i++)
{
@@ -560,6 +575,10 @@ internal partial class CBZip2InputStream : Stream
while (BsR(1) == 1)
{
j++;
if (j >= nGroups)
{
throw new InvalidFormatException("BZip2: invalid selector MTF value");
}
}
if (i < BZip2Constants.MAX_SELECTORS)
{
@@ -582,6 +601,10 @@ internal partial class CBZip2InputStream : Stream
for (i = 0; i < nSelectors; i++)
{
v = selectorMtf[i];
if (v >= nGroups)
{
throw new InvalidFormatException("BZip2: selector MTF value out of range");
}
tmp = pos[v];
while (v > 0)
{
@@ -689,6 +712,10 @@ internal partial class CBZip2InputStream : Stream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -717,7 +744,14 @@ internal partial class CBZip2InputStream : Stream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
while (true)
@@ -760,6 +794,10 @@ internal partial class CBZip2InputStream : Stream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -788,7 +826,14 @@ internal partial class CBZip2InputStream : Stream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
} while (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB);
@@ -859,6 +904,10 @@ internal partial class CBZip2InputStream : Stream
while (zvec > limit[zt][zn])
{
zn++;
if (zn >= BZip2Constants.MAX_CODE_LEN)
{
throw new InvalidFormatException("BZip2: Huffman code too long");
}
{
{
while (bsLive < 1)
@@ -883,7 +932,14 @@ internal partial class CBZip2InputStream : Stream
}
zvec = (zvec << 1) | zj;
}
nextSym = perm[zt][zvec - basev[zt][zn]];
{
int permIdx = zvec - basev[zt][zn];
if (permIdx < 0 || permIdx >= perm[zt].Length)
{
throw new InvalidFormatException("BZip2: invalid Huffman symbol");
}
nextSym = perm[zt][permIdx];
}
}
}
}
@@ -907,10 +963,18 @@ internal partial class CBZip2InputStream : Stream
for (i = 0; i <= last; i++)
{
ch = ll8[i];
if (cftab[ch] < 0 || cftab[ch] >= tt.Length)
{
throw new InvalidFormatException("BZip2: block data out of bounds");
}
tt[cftab[ch]] = i;
cftab[ch]++;
}
if (origPtr < 0 || origPtr >= tt.Length)
{
throw new InvalidFormatException("BZip2: origPtr out of bounds");
}
tPos = tt[origPtr];
count = 0;

View File

@@ -208,6 +208,10 @@ internal sealed class HuffmanTree
do
{
if (index < 0 || index >= array.Length)
{
throw new InvalidFormatException("Deflate64: invalid Huffman data");
}
var value = array[index];
if (value == 0)

View File

@@ -20,7 +20,10 @@ public partial class ExplodeStream
)
{
var ex = new ExplodeStream(inStr, compressedSize, uncompressedSize, generalPurposeBitFlag);
await ex.explode_SetTables_async(cancellationToken).ConfigureAwait(false);
if (await ex.explode_SetTables_async(cancellationToken).ConfigureAwait(false) != 0)
{
throw new InvalidFormatException("ExplodeStream: invalid Huffman table data");
}
ex.explode_var_init();
return ex;
}

View File

@@ -61,7 +61,10 @@ public partial class ExplodeStream : Stream
)
{
var ex = new ExplodeStream(inStr, compressedSize, uncompressedSize, generalPurposeBitFlag);
ex.explode_SetTables();
if (ex.explode_SetTables() != 0)
{
throw new InvalidFormatException("ExplodeStream: invalid Huffman table data");
}
ex.explode_var_init();
return ex;
}

View File

@@ -5,6 +5,7 @@ using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Compressors.LZMA.LZ;
@@ -25,6 +26,10 @@ internal partial class OutWindow : IDisposable
public void Create(int windowSize)
{
if (windowSize <= 0)
{
throw new InvalidFormatException($"LZMA: invalid dictionary size {windowSize}");
}
if (_windowSize != windowSize)
{
if (_buffer is not null)

View File

@@ -70,7 +70,15 @@ public partial class LzwStream
{
if (!headerParsed)
{
await ParseHeaderAsync(cancellationToken).ConfigureAwait(false);
try
{
await ParseHeaderAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
eof = true;
throw;
}
}
if (eof)
@@ -348,6 +356,17 @@ public partial class LzwStream
);
}
if (maxBits < LzwConstants.INIT_BITS)
{
throw new InvalidFormatException(
"Stream compressed with "
+ maxBits
+ " bits, but minimum supported is "
+ LzwConstants.INIT_BITS
+ " bits."
);
}
if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0)
{
throw new ArchiveException("Unsupported bits set in the header.");

View File

@@ -129,7 +129,15 @@ public partial class LzwStream : Stream
{
if (!headerParsed)
{
ParseHeader();
try
{
ParseHeader();
}
catch
{
eof = true;
throw;
}
}
if (eof)
@@ -421,6 +429,17 @@ public partial class LzwStream : Stream
);
}
if (maxBits < LzwConstants.INIT_BITS)
{
throw new InvalidFormatException(
"Stream compressed with "
+ maxBits
+ " bits, but minimum supported is "
+ LzwConstants.INIT_BITS
+ " bits."
);
}
if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0)
{
throw new ArchiveException("Unsupported bits set in the header.");

View File

@@ -4,6 +4,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
// This is a port of Dmitry Shkarin's PPMd Variant I Revision 1.
// Ported by Michael Bone (mjbone03@yahoo.com.au).
@@ -253,6 +254,10 @@ internal partial class Model
_coder.RangeDecoderInitialize(source);
StartModel(properties.ModelOrder, properties.RestorationMethod);
_minimumContext = _maximumContext;
if (_minimumContext == PpmContext.ZERO)
{
throw new InvalidFormatException("PPMd: model context not initialized");
}
_numberStatistics = _minimumContext.NumberStatistics;
return _coder;
}
@@ -268,6 +273,10 @@ internal partial class Model
await _coder.RangeDecoderInitializeAsync(source, cancellationToken).ConfigureAwait(false);
StartModel(properties.ModelOrder, properties.RestorationMethod);
_minimumContext = _maximumContext;
if (_minimumContext == PpmContext.ZERO)
{
throw new InvalidFormatException("PPMd: model context not initialized");
}
_numberStatistics = _minimumContext.NumberStatistics;
return _coder;
}
@@ -429,13 +438,16 @@ internal partial class Model
if (modelOrder < 2)
{
_orderFall = _modelOrder;
for (
var context = _maximumContext;
context.Suffix != PpmContext.ZERO;
context = context.Suffix
)
if (_maximumContext != PpmContext.ZERO)
{
_orderFall--;
for (
var context = _maximumContext;
context.Suffix != PpmContext.ZERO;
context = context.Suffix
)
{
_orderFall--;
}
}
return;
}

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Reduce;
@@ -96,6 +97,10 @@ public partial class ReduceStream
cancellationToken
)
.ConfigureAwait(false);
if (nextByteIndex >= nextByteTable[outByte].Length)
{
throw new InvalidFormatException("ReduceStream: next byte table index out of range");
}
outByte = nextByteTable[outByte][nextByteIndex];
return outByte;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SharpCompress.Common;
namespace SharpCompress.Compressors.Reduce;
@@ -192,6 +193,10 @@ public partial class ReduceStream : Stream
return outByte;
}
READBITS(bitCountTable[nextByteTable[outByte].Length], out byte nextByteIndex);
if (nextByteIndex >= nextByteTable[outByte].Length)
{
throw new InvalidFormatException("ReduceStream: next byte table index out of range");
}
outByte = nextByteTable[outByte][nextByteIndex];
return outByte;
}

View File

@@ -99,6 +99,10 @@ public partial class SqueezeStream
huffmanDecoded.WriteByte((byte)i);
i = 0;
}
else if (i >= numnodes)
{
throw new InvalidFormatException("SqueezeStream: invalid Huffman tree node index");
}
}
huffmanDecoded.Position = 0;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SharpCompress.Common;
using SharpCompress.Compressors.RLE90;
namespace SharpCompress.Compressors.Squeezed;
@@ -93,6 +94,10 @@ public partial class SqueezeStream : Stream
huffmanDecoded.WriteByte((byte)i);
i = 0;
}
else if (i >= numnodes)
{
throw new InvalidFormatException("SqueezeStream: invalid Huffman tree node index");
}
}
huffmanDecoded.Position = 0;

View File

@@ -0,0 +1,132 @@
using System;
using System.IO;
using AwesomeAssertions;
using SharpCompress.Common;
using SharpCompress.Readers;
using Xunit;
namespace SharpCompress.Test;
/// <summary>
/// Tests that malformed compressed input is handled gracefully, throwing library exceptions
/// rather than unhandled IndexOutOfRangeException, DivideByZeroException, or NullReferenceException.
/// </summary>
public class MalformedInputTests
{
private static void AssertHandledGracefully(string hex)
{
var data = Convert.FromHexString(hex);
using var ms = new MemoryStream(data);
var buf = new byte[4096];
Action act = () =>
{
using var reader = ReaderFactory.OpenReader(ms);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using var entryStream = reader.OpenEntryStream();
while (entryStream.Read(buf, 0, buf.Length) > 0) { }
}
}
};
act.Should()
.Throw<Exception>()
.And.Should()
.BeAssignableTo<SharpCompressException>(
"malformed input should throw a library exception, not a raw system exception"
);
}
[Fact]
public void LzwStream_DivideByZero_ThrowsLibraryException()
{
// LZW stream with invalid header that would cause DivideByZero on subsequent reads
AssertHandledGracefully(
"1f9d1a362f20000000130003edd1310a8030f1605ca2b26245c47b97e6d615e29400000000130003edd1310a8030f1605c606060606060606060606060606060606060606060606060007f60606060280000"
);
}
[Fact]
public void LzwStream_IndexOutOfRange_ThrowsLibraryException()
{
// LZW stream with maxBits < INIT_BITS causing table size mismatch
AssertHandledGracefully(
"1f9d0836e1553ac4e1ce9ea227000000000000001070b4058faf051127c54144f8bfe54192e141bab6efe8032c41cd64004aef53da4acc8077a5b26245c47b97e6d615e29400000000000003edd1310a8030f1e2ee66ff535d800000000b00000000"
);
}
[Fact]
public void BZip2_NullRef_InBsR_ThrowsLibraryException()
{
// BZip2 stream with invalid block size causing null bsStream access
AssertHandledGracefully(
"425a6857575757575768575757575757fff2fff27c007159425a6857ff0f21007159c1e2d5e2"
);
}
[Fact]
public void BZip2_IndexOutOfRange_InGetAndMoveToFrontDecode_ThrowsLibraryException()
{
// BZip2 with malformed Huffman tables causing code-too-long or bad perm index
AssertHandledGracefully(
"425a6839314159265359c1c080e2000001410000100244a000305a6839314159265359c1c080e2000001410000100244a00030cd00c3cd00c34629971772c080e2"
);
}
[Fact]
public void SqueezeStream_IndexOutOfRange_ThrowsLibraryException()
{
// Squeezed ARC stream with malformed Huffman tree node indices
AssertHandledGracefully(
"1a041a425a081a0000090000606839425a081730765cbb311042265300040000090000606839425a081730765cbb31104226530053"
);
}
[Fact]
public void ArcLzwStream_IndexOutOfRange_ThrowsLibraryException()
{
// ARC LZW stream with empty or malformed compressed data
AssertHandledGracefully(
"1a081a1931081a00000000f9ffffff00000000ddff000000000000000000000000000012006068394200000080c431b37fff531042d9ff"
);
}
[Fact]
public void ExplodeStream_IndexOutOfRange_ThrowsLibraryException()
{
// ZIP entry using Implode/Explode with invalid Huffman tables
AssertHandledGracefully(
"504b03040a000000060000ff676767676767676767676767676700000000683a36060000676767676767676767676700000000000000000000000000000000000000000000000000000000630000000000800000000000002e7478745554090003a8c8b6696045ac6975780b000104e803000004e803000068656c6c6f0a504b01021e030a0000000000147f6f5c20303a3639314159265359c1c080e2000001410000100244a00030cd00c346299717786975870b000104e8030000780b000104e803000004e8030000504b050600000000010000e74f004040490000000064"
);
}
[Fact]
public void Deflate64_IndexOutOfRange_ThrowsLibraryException()
{
// ZIP entry using Deflate64 with invalid Huffman data
AssertHandledGracefully(
"504b03040a00009709001c0068656c6c6f2e807874555409000000000000147f6f5c20303a36060000ff0600000009425a6839314159265359595959595959a481000000000000000000007478925554050001c601003dffff000000000000001e000000001e00000000000000000000e1490000000000"
);
}
[Fact]
public void PPMd_NullRef_ThrowsLibraryException()
{
// ZIP entry using PPMd with malformed properties triggering uninitialized model access
AssertHandledGracefully(
"504b03040000007462001c905c206600fa80ffffffffff1f8b0a00000000000003edd1310a80cf0c00090010000b000000e000000000030000002e000000686515e294362f763ac439d493d62a3671081e05c14114b4058faf051127c54144f8bfe541ace141bab6ef643c2ce2000001410000100244a00040cd41bdc76c4aef3977a5b25645c47b97e6d615e294362f763ac439d493d62a367108f1e2ee66ff535efa7f3015e2943601003ac439d493d62a3671081e05c14114b4058faf3a0003edd1310a80cf8597e6d60500140409"
);
}
[Fact]
public void LZMA_NullRef_ThrowsLibraryException()
{
// ZIP entry using LZMA with invalid dictionary size (0) causing null window buffer access
AssertHandledGracefully(
"504b03040a0200000e001c0068646c6c6f2e7478745554ac507578000000000000000000000000000000000000000000e80300000000000068030a0000000000147f040020303a360600002e7478745554090003a8c8b6696045ac69f5780b0006ff1d000908180000e8030000000000a4810000109a9a9a8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b9a0000000000000000000000e80300000000000068030a0000009a9a9a504b03440a6fcb486c6c6f2e74ffff"
);
}
}