Fix decompression bomb and crash bugs (4 security fixes)

Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/7dbc048c-0fb5-46be-bf9a-b5b0b3c3f8bf

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-01 17:40:35 +00:00
committed by GitHub
parent 1e7369a6ee
commit f7b8225422
9 changed files with 123 additions and 37 deletions

View File

@@ -151,7 +151,17 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
}
else
{
headerBytes = reader.ReadUInt32();
try
{
headerBytes = reader.ReadUInt32();
}
catch (EndOfStreamException ex)
{
throw new InvalidFormatException(
"Unexpected end of stream while reading ZIP archive",
ex
);
}
}
_lastEntryHeader = null;

View File

@@ -706,6 +706,10 @@ internal partial class CBZip2InputStream : Stream
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);
@@ -788,6 +792,10 @@ internal partial class CBZip2InputStream : Stream
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);
@@ -862,6 +870,10 @@ internal partial class CBZip2InputStream : Stream
BlockOverrun();
}
if (nextSym - 1 < 0 || nextSym - 1 >= yy.Length)
{
throw new InvalidFormatException("BZip2: symbol out of range");
}
tmp = yy[nextSym - 1];
unzftab[seqToUnseq[tmp]]++;
ll8[last] = seqToUnseq[tmp];
@@ -898,6 +910,10 @@ internal partial class CBZip2InputStream : Stream
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);

View File

@@ -244,6 +244,10 @@ internal sealed class HuffmanTree
overflowBits--;
} while (overflowBits != 0);
if (index < 0 || index >= array.Length)
{
throw new InvalidFormatException("Deflate64: invalid Huffman data");
}
array[index] = (short)ch;
}
}

View File

@@ -26,6 +26,7 @@ public partial class ReduceStream
{
if (inByteCount == compressedSize)
{
_inputExhausted = true;
return EOF;
}
@@ -35,6 +36,7 @@ public partial class ReduceStream
.ConfigureAwait(false);
if (bytesRead == 0)
{
_inputExhausted = true;
return EOF;
}
@@ -117,6 +119,13 @@ public partial class ReduceStream
{
if (length == 0)
{
if (_inputExhausted && bitBufferCount <= 0)
{
throw new InvalidFormatException(
"ReduceStream: compressed data exhausted before uncompressed size reached"
);
}
byte nextByte = await GetNextByteAsync(cancellationToken).ConfigureAwait(false);
if (nextByte != RunLengthCode)
{

View File

@@ -115,16 +115,24 @@ public partial class ReduceStream : Stream
private int bitBufferCount;
private ulong bitBuffer;
private bool _inputExhausted;
private int NEXTBYTE()
{
if (inByteCount == compressedSize)
{
_inputExhausted = true;
return EOF;
}
inByteCount++;
return inStream.ReadByte();
int b = inStream.ReadByte();
if (b < 0)
{
_inputExhausted = true;
return EOF;
}
return b;
}
private void READBITS(int nbits, out byte zdest)
@@ -208,6 +216,13 @@ public partial class ReduceStream : Stream
{
if (length == 0)
{
if (_inputExhausted && bitBufferCount <= 0)
{
throw new InvalidFormatException(
"ReduceStream: compressed data exhausted before uncompressed size reached"
);
}
byte nextByte = GetNextByte();
if (nextByte != RunLengthCode)
{

View File

@@ -33,29 +33,13 @@ internal partial class ShrinkStream : Stream
return;
}
// Read all compressed data asynchronously
var src = new byte[_compressedSize];
int bytesRead = 0;
int totalBytesRead = 0;
while (totalBytesRead < (int)_compressedSize)
{
bytesRead = await inStream
.ReadAsync(
src,
totalBytesRead,
(int)_compressedSize - totalBytesRead,
cancellationToken
)
.ConfigureAwait(false);
if (bytesRead == 0)
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading compressed data"
);
}
totalBytesRead += bytesRead;
}
// Read actual compressed data from stream rather than pre-allocating based on the
// declared compressed size, which may be crafted to cause an OutOfMemoryException.
// The stream is already bounded by ReadOnlySubStream in ZipFilePart.
using var srcMs = new MemoryStream();
await inStream.CopyToAsync(srcMs, cancellationToken).ConfigureAwait(false);
var src = srcMs.ToArray();
var srcLen = src.Length;
// Decompress synchronously (CPU-bound operation)
var srcUsed = 0;
@@ -63,7 +47,7 @@ internal partial class ShrinkStream : Stream
HwUnshrink.Unshrink(
src,
(int)_compressedSize,
srcLen,
out srcUsed,
_byteOut,
(int)_uncompressedSize,

View File

@@ -8,7 +8,6 @@ internal partial class ShrinkStream : Stream
{
private Stream inStream;
private ulong _compressedSize;
private long _uncompressedSize;
private byte[] _byteOut;
private long _outBytesCount;
@@ -24,7 +23,6 @@ internal partial class ShrinkStream : Stream
{
inStream = stream;
_compressedSize = (ulong)compressedSize;
_uncompressedSize = uncompressedSize;
_byteOut = new byte[_uncompressedSize];
_outBytesCount = 0L;
@@ -55,14 +53,19 @@ internal partial class ShrinkStream : Stream
{
if (!_decompressed)
{
var src = new byte[_compressedSize];
inStream.ReadExact(src, 0, (int)_compressedSize);
// Read actual compressed data from stream rather than pre-allocating based on the
// declared compressed size, which may be crafted to cause an OutOfMemoryException.
// The stream is already bounded by ReadOnlySubStream in ZipFilePart.
using var srcMs = new MemoryStream();
inStream.CopyTo(srcMs);
var src = srcMs.ToArray();
var srcLen = src.Length;
var srcUsed = 0;
var dstUsed = 0;
HwUnshrink.Unshrink(
src,
(int)_compressedSize,
srcLen,
out srcUsed,
_byteOut,
(int)_uncompressedSize,

View File

@@ -268,9 +268,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.5, )",
"resolved": "10.0.5",
"contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA=="
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -442,9 +442,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.25, )",
"resolved": "8.0.25",
"contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg=="
"requested": "[8.0.22, )",
"resolved": "8.0.22",
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",

View File

@@ -130,5 +130,50 @@ public class MalformedInputTests
"504b03040a0200000e001c0068646c6c6f2e7478745554ac507578000000000000000000000000000000000000000000e80300000000000068030a0000000000147f040020303a360600002e7478745554090003a8c8b6696045ac69f5780b0006ff1d000908180000e8030000000000a4810000109a9a9a8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b9a0000000000000000000000e80300000000000068030a0000009a9a9a504b03440a6fcb486c6c6f2e74ffff"
);
}
[Fact]
public void Reduce_DecompressionBomb_Method2_ThrowsLibraryException()
{
// 31-byte ZIP using Reduce method 2 with declared uncompressed size far exceeding the
// actual compressed data - the decompressor must not generate unbounded output.
VerifyMalformedInputThrowsLibraryException(
"504b03040a000000020000000200f7ff0500f7ff05ff200600180700000000"
);
}
[Fact]
public void Deflate64_HuffmanTree_IndexOutOfRange_ThrowsLibraryException()
{
// 105-byte ZIP using Deflate64 with invalid Huffman code lengths causing IOOB in CreateTable
VerifyMalformedInputThrowsLibraryException(
"504b03040a00005409000088c8b669757800009ac8b66975783606000000640028b52ffd047fff"
+ "02009a888888888820313735303600303132002030007573746172202000757001307230819b75"
+ "72756e7475410a000c2000391eeb061ffe391eeb068f0c0a000c20"
);
}
[Fact]
public void BZip2_GetAndMoveToFrontDecode_IndexOutOfRange_ThrowsLibraryException()
{
// 93-byte BZip2 stream triggering IOOB deeper in GetAndMoveToFrontDecode
VerifyMalformedInputThrowsLibraryException(
"425a6839314159265359c1c080e2000001410000100244a00100808b640006000775780b2ef2ed"
+ "0001393beb06060606060606060606f9050605060606060f0654090003ffffff7f003403"
+ "0a0002001f8b7fff0000000000e98b8b3931"
);
}
[Fact]
public void Zip_ShrinkOOM_CraftedCompressedSize_ThrowsLibraryException()
{
// 122-byte ZIP with Shrink compression and compressed size set to 0x7FFFFFFF (2 GB).
// The library must not attempt to allocate a 2 GB buffer based on the untrusted header.
VerifyMalformedInputThrowsLibraryException(
"504b03040a0000000100147f6f5c20303a36ffffff7f0600000009001c0068656c6c6f2e747874"
+ "5554090003a8c8b6696045ac6975780b01e8303a36060000000600000009001800000001004f2a"
+ "2a2a2a0c2000395d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d000004e8303a360600000006000000"
+ "0900180000"
);
}
}
#endif