mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Validate 7z header counts before allocation
Bound nested metadata counts and coder properties by their backing data, validate file and stream consistency, and buffer decoded headers incrementally instead of reserving attacker-declared output sizes. Add synchronous and asynchronous allocation regressions for GHSA-vwpg-q8vc-g6fg, including external metadata and valid bit-packed empty entries.
This commit is contained in:
@@ -480,4 +480,6 @@ ARC LZW compressed data is buffered incrementally from the bytes actually presen
|
||||
|
||||
ZIP Shrink output buffering grows with decoded output rather than the declared uncompressed size. Invalid codes and output-size mismatches are rejected.
|
||||
|
||||
7z header counts and coder property lengths are validated against their backing metadata before allocation. File counts are checked against parsed substreams and empty-stream bitmaps, including external metadata. Encoded headers and additional metadata are buffered incrementally as the decoder produces bytes, rather than preallocating the declared unpacked size.
|
||||
|
||||
LZMA/LZMA2 dictionary storage starts small and grows with decoded history, up to the supported dictionary size. These safeguards avoid allocations based solely on forged sizes; they do not impose an overall decompressed-output or process-memory limit.
|
||||
|
||||
@@ -193,6 +193,7 @@ internal sealed partial class ArchiveReader
|
||||
var packIndex = 0;
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var unpackSize = GetMetadataSize(folder);
|
||||
var oldDataStartPos = dataStartPos;
|
||||
var myPackSizes = new long[folder._packStreams.Count];
|
||||
for (var i = 0; i < myPackSizes.Length; i++)
|
||||
@@ -202,7 +203,7 @@ internal sealed partial class ArchiveReader
|
||||
dataStartPos += packSize;
|
||||
}
|
||||
|
||||
var outStream = await DecoderStreamHelper
|
||||
using var outStream = await DecoderStreamHelper
|
||||
.CreateDecoderStreamAsync(
|
||||
_stream,
|
||||
oldDataStartPos,
|
||||
@@ -213,15 +214,29 @@ internal sealed partial class ArchiveReader
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var unpackSize = checked((int)folder.GetUnpackSize());
|
||||
var data = new byte[unpackSize];
|
||||
await outStream
|
||||
.ReadExactAsync(data, 0, data.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (outStream.ReadByte() >= 0)
|
||||
// Grow only as the decoder actually produces metadata, never
|
||||
// from the untrusted unpack-size declaration alone.
|
||||
using var decoded = new PooledMemoryStream();
|
||||
var buffer = new byte[8192];
|
||||
for (var remaining = unpackSize; remaining > 0; )
|
||||
{
|
||||
var count = Math.Min(buffer.Length, remaining);
|
||||
await outStream
|
||||
.ReadExactAsync(buffer, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await decoded
|
||||
.WriteAsync(buffer, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
remaining -= count;
|
||||
}
|
||||
if (
|
||||
await outStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false)
|
||||
!= 0
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Decoded stream is longer than expected.");
|
||||
}
|
||||
var data = decoded.ToArray();
|
||||
dataVector.Add(data);
|
||||
|
||||
if (folder.UnpackCrcDefined)
|
||||
@@ -319,6 +334,7 @@ internal sealed partial class ArchiveReader
|
||||
}
|
||||
|
||||
var numFiles = ReadNum();
|
||||
ValidateFileCount(numFiles, unpackSizes.Count);
|
||||
db._files = new List<CFileItem>(numFiles);
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
|
||||
@@ -92,6 +92,7 @@ internal partial class ArchiveReader
|
||||
|
||||
private BitVector ReadBitVector(int length)
|
||||
{
|
||||
_currentReader.RequireBytes(((long)length + 7) / 8);
|
||||
var bits = new BitVector(length);
|
||||
|
||||
byte data = 0;
|
||||
@@ -202,9 +203,16 @@ internal partial class ArchiveReader
|
||||
try
|
||||
{
|
||||
var numCoders = ReadNum();
|
||||
// CFolder.CheckStructure supports at most 32 coders/bind pairs, but runs
|
||||
// after parsing. Enforce those limits before allocating their lists.
|
||||
if (numCoders == 0 || numCoders > CFolder.MaxCoders)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid number of 7z coders.");
|
||||
}
|
||||
_currentReader.RequireBytes(numCoders);
|
||||
folder._coders = new List<CCoderInfo>(numCoders);
|
||||
var numInStreams = 0;
|
||||
var numOutStreams = 0;
|
||||
long numInStreamsTotal = 0;
|
||||
long numOutStreamsTotal = 0;
|
||||
for (var i = 0; i < numCoders; i++)
|
||||
{
|
||||
try
|
||||
@@ -241,6 +249,7 @@ internal partial class ArchiveReader
|
||||
if ((mainByte & 0x20) != 0)
|
||||
{
|
||||
var propsSize = ReadNum();
|
||||
_currentReader.RequireBytes(propsSize);
|
||||
coder._props = new byte[propsSize];
|
||||
ReadBytes(coder._props, 0, propsSize);
|
||||
}
|
||||
@@ -250,30 +259,48 @@ internal partial class ArchiveReader
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
numInStreams += coder._numInStreams;
|
||||
numOutStreams += coder._numOutStreams;
|
||||
if (coder._numInStreams == 0 || coder._numOutStreams == 0)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z coder stream count.");
|
||||
}
|
||||
numInStreamsTotal += coder._numInStreams;
|
||||
numOutStreamsTotal += coder._numOutStreams;
|
||||
}
|
||||
finally { }
|
||||
}
|
||||
|
||||
if (
|
||||
numOutStreamsTotal - 1 > CFolder.MaxBindPairs
|
||||
|| numInStreamsTotal > int.MaxValue
|
||||
|| numInStreamsTotal < numOutStreamsTotal
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z folder stream counts.");
|
||||
}
|
||||
var numInStreams = (int)numInStreamsTotal;
|
||||
var numOutStreams = (int)numOutStreamsTotal;
|
||||
var numBindPairs = numOutStreams - 1;
|
||||
_currentReader.RequireBytes(2L * numBindPairs);
|
||||
folder._bindPairs = new List<CBindPair>(numBindPairs);
|
||||
for (var i = 0; i < numBindPairs; i++)
|
||||
{
|
||||
var bp = new CBindPair();
|
||||
bp._inIndex = ReadNum();
|
||||
bp._outIndex = ReadNum();
|
||||
if (
|
||||
bp._inIndex >= numInStreams
|
||||
|| bp._outIndex >= numOutStreams
|
||||
|| folder.FindBindPairForInStream(bp._inIndex) >= 0
|
||||
|| folder.FindBindPairForOutStream(bp._outIndex) >= 0
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z bind pair.");
|
||||
}
|
||||
folder._bindPairs.Add(bp);
|
||||
}
|
||||
|
||||
if (numInStreams < numBindPairs)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var numPackStreams = numInStreams - numBindPairs;
|
||||
|
||||
//folder.PackStreams.Reserve(numPackStreams);
|
||||
if (numPackStreams == 1)
|
||||
{
|
||||
for (var i = 0; i < numInStreams; i++)
|
||||
@@ -292,9 +319,19 @@ internal partial class ArchiveReader
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentReader.RequireBytes(numPackStreams);
|
||||
var packStreams = new HashSet<int>();
|
||||
for (var i = 0; i < numPackStreams; i++)
|
||||
{
|
||||
var num = ReadNum();
|
||||
if (
|
||||
num >= numInStreams
|
||||
|| folder.FindBindPairForInStream(num) >= 0
|
||||
|| !packStreams.Add(num)
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z packed stream index.");
|
||||
}
|
||||
folder._packStreams.Add(num);
|
||||
}
|
||||
}
|
||||
@@ -304,7 +341,23 @@ internal partial class ArchiveReader
|
||||
|
||||
private List<uint?> ReadHashDigests(int count)
|
||||
{
|
||||
// An all-defined vector has no bitmap, but must have a CRC per item.
|
||||
// Check it before ReadOptionalBitVector allocates the implicit bitmap.
|
||||
var reader = _currentReader.Clone();
|
||||
if (reader.ReadByte() != 0)
|
||||
{
|
||||
reader.RequireBytes(4L * count);
|
||||
}
|
||||
var defined = ReadOptionalBitVector(count);
|
||||
long numDefined = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (defined[i])
|
||||
{
|
||||
numDefined++;
|
||||
}
|
||||
}
|
||||
_currentReader.RequireBytes(4 * numDefined);
|
||||
var digests = new List<uint?>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -336,6 +389,7 @@ internal partial class ArchiveReader
|
||||
var numPackStreams = ReadNum();
|
||||
|
||||
WaitAttribute(BlockType.Size);
|
||||
_currentReader.RequireBytes(numPackStreams);
|
||||
packSizes = new List<long>(numPackStreams);
|
||||
for (var i = 0; i < numPackStreams; i++)
|
||||
{
|
||||
@@ -382,8 +436,9 @@ internal partial class ArchiveReader
|
||||
{
|
||||
streamSwitch.Set(this, dataVector ?? []);
|
||||
|
||||
//folders.Clear();
|
||||
//folders.Reserve(numFolders);
|
||||
// Folder definitions may live in an external metadata stream.
|
||||
// Validate against the selected stream, not the outer header.
|
||||
_currentReader.RequireBytes(numFolders);
|
||||
folders = new List<CFolder>(numFolders);
|
||||
var index = 0;
|
||||
for (var i = 0; i < numFolders; i++)
|
||||
@@ -405,6 +460,10 @@ internal partial class ArchiveReader
|
||||
var size = checked((long)ReadNumber());
|
||||
folder._unpackSizes.Add(size);
|
||||
}
|
||||
if (!folder.CheckStructure())
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z folder structure.");
|
||||
}
|
||||
}
|
||||
|
||||
for (; ; )
|
||||
@@ -476,6 +535,21 @@ internal partial class ArchiveReader
|
||||
}
|
||||
}
|
||||
|
||||
// Every substream except the last in each folder needs an explicit
|
||||
// size. Reject impossible counts before loops or digest allocations.
|
||||
long numExplicitSizes = 0;
|
||||
long numStreamsTotal = 0;
|
||||
foreach (var count in numUnpackStreamsInFolders)
|
||||
{
|
||||
numExplicitSizes += Math.Max(0, count - 1);
|
||||
numStreamsTotal += count;
|
||||
}
|
||||
if (numStreamsTotal > int.MaxValue || (numExplicitSizes != 0 && type != BlockType.Size))
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z substream count.");
|
||||
}
|
||||
_currentReader.RequireBytes(numExplicitSizes);
|
||||
|
||||
unpackSizes = new List<long>(folders.Count);
|
||||
for (var i = 0; i < numUnpackStreamsInFolders.Count; i++)
|
||||
{
|
||||
@@ -492,6 +566,10 @@ internal partial class ArchiveReader
|
||||
if (type == BlockType.Size)
|
||||
{
|
||||
var size = checked((long)ReadNumber());
|
||||
if (size > folders[i].GetUnpackSize() - sum)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z substream size.");
|
||||
}
|
||||
unpackSizes.Add(size);
|
||||
sum += size;
|
||||
}
|
||||
@@ -620,6 +698,67 @@ internal partial class ArchiveReader
|
||||
finally { }
|
||||
}
|
||||
|
||||
private static int GetMetadataSize(CFolder folder)
|
||||
{
|
||||
var size = folder.GetUnpackSize();
|
||||
if (size < 0 || size > int.MaxValue)
|
||||
{
|
||||
throw new InvalidFormatException("7z metadata is too large.");
|
||||
}
|
||||
return (int)size;
|
||||
}
|
||||
|
||||
private void ValidateFileCount(int numFiles, int numStreams)
|
||||
{
|
||||
// Nonempty files correspond to already parsed substreams. Empty files
|
||||
// require one bit each in kEmptyStream, not one byte per file. Preflight
|
||||
// that property before allocating CFileItem objects or implicit vectors.
|
||||
var reader = _currentReader.Clone();
|
||||
var numEmptyStreams = 0;
|
||||
var hasEmptyStreams = false;
|
||||
for (; ; )
|
||||
{
|
||||
var type = reader.ReadNumber();
|
||||
if (type == (ulong)BlockType.End)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var size = reader.ReadNumber();
|
||||
if (size > (ulong)reader.Remaining)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
var start = reader.Offset;
|
||||
if (type == (ulong)BlockType.EmptyStream)
|
||||
{
|
||||
var bitmapSize = ((long)numFiles + 7) / 8;
|
||||
if (hasEmptyStreams || size < (ulong)bitmapSize)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z empty stream bitmap.");
|
||||
}
|
||||
hasEmptyStreams = true;
|
||||
byte bits = 0;
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
if ((i & 7) == 0)
|
||||
{
|
||||
bits = reader.ReadByte();
|
||||
}
|
||||
if ((bits & 0x80) != 0)
|
||||
{
|
||||
numEmptyStreams++;
|
||||
}
|
||||
bits <<= 1;
|
||||
}
|
||||
}
|
||||
reader.SkipData((long)size - (reader.Offset - start));
|
||||
}
|
||||
if ((long)numStreams + numEmptyStreams != numFiles)
|
||||
{
|
||||
throw new InvalidFormatException("7z file count does not match the stream count.");
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> ReadAndDecodePackedStreams(long baseOffset, IPasswordProvider pass)
|
||||
{
|
||||
try
|
||||
@@ -641,6 +780,7 @@ internal partial class ArchiveReader
|
||||
var packIndex = 0;
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var unpackSize = GetMetadataSize(folder);
|
||||
var oldDataStartPos = dataStartPos;
|
||||
var myPackSizes = new long[folder._packStreams.Count];
|
||||
for (var i = 0; i < myPackSizes.Length; i++)
|
||||
@@ -650,7 +790,7 @@ internal partial class ArchiveReader
|
||||
dataStartPos += packSize;
|
||||
}
|
||||
|
||||
var outStream = DecoderStreamHelper.CreateDecoderStream(
|
||||
using var outStream = DecoderStreamHelper.CreateDecoderStream(
|
||||
_stream,
|
||||
oldDataStartPos,
|
||||
myPackSizes,
|
||||
@@ -658,13 +798,22 @@ internal partial class ArchiveReader
|
||||
pass
|
||||
);
|
||||
|
||||
var unpackSize = checked((int)folder.GetUnpackSize());
|
||||
var data = new byte[unpackSize];
|
||||
outStream.ReadExact(data, 0, data.Length);
|
||||
// Do not reserve the declared unpack size: malformed compressed
|
||||
// metadata can advertise GiB of output without producing a byte.
|
||||
using var decoded = new PooledMemoryStream();
|
||||
var buffer = new byte[8192];
|
||||
for (var remaining = unpackSize; remaining > 0; )
|
||||
{
|
||||
var count = Math.Min(buffer.Length, remaining);
|
||||
outStream.ReadExact(buffer, 0, count);
|
||||
decoded.Write(buffer, 0, count);
|
||||
remaining -= count;
|
||||
}
|
||||
if (outStream.ReadByte() >= 0)
|
||||
{
|
||||
throw new InvalidFormatException("Decoded stream is longer than expected.");
|
||||
}
|
||||
var data = decoded.ToArray();
|
||||
dataVector.Add(data);
|
||||
|
||||
if (folder.UnpackCrcDefined)
|
||||
@@ -756,6 +905,7 @@ internal partial class ArchiveReader
|
||||
}
|
||||
|
||||
var numFiles = ReadNum();
|
||||
ValidateFileCount(numFiles, unpackSizes.Count);
|
||||
db._files = new List<CFileItem>(numFiles);
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace SharpCompress.Common.SevenZip;
|
||||
|
||||
internal class CFolder
|
||||
{
|
||||
internal const int MaxCoders = 32;
|
||||
internal const int MaxBindPairs = 32;
|
||||
|
||||
internal List<CCoderInfo> _coders = new();
|
||||
internal List<CBindPair> _bindPairs = new();
|
||||
internal List<int> _packStreams = new();
|
||||
@@ -98,11 +101,9 @@ internal class CFolder
|
||||
|
||||
public bool CheckStructure()
|
||||
{
|
||||
const int kNumCodersMax = 32; // don't change it
|
||||
const int kMaskSize = 32; // it must be >= kNumCodersMax
|
||||
const int kNumBindsMax = 32;
|
||||
const int kMaskSize = 32; // it must be >= MaxCoders
|
||||
|
||||
if (_coders.Count > kNumCodersMax || _bindPairs.Count > kNumBindsMax)
|
||||
if (_coders.Count > MaxCoders || _bindPairs.Count > MaxBindPairs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ internal class DataReader
|
||||
|
||||
public int Offset { get; private set; }
|
||||
|
||||
public int Remaining => _ending - Offset;
|
||||
|
||||
public DataReader Clone() => new(_buffer, Offset, Remaining);
|
||||
|
||||
public void RequireBytes(long count)
|
||||
{
|
||||
if (count < 0 || count > Remaining)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
if (Offset >= _ending)
|
||||
@@ -57,10 +69,7 @@ internal class DataReader
|
||||
|
||||
public void ReadBytes(byte[] buffer, int offset, int length)
|
||||
{
|
||||
if (length > _ending - Offset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
RequireBytes(length);
|
||||
|
||||
while (length-- > 0)
|
||||
{
|
||||
@@ -70,10 +79,7 @@ internal class DataReader
|
||||
|
||||
public void SkipData(long size)
|
||||
{
|
||||
if (size > _ending - Offset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
RequireBytes(size);
|
||||
|
||||
Offset += (int)size;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.SevenZip;
|
||||
|
||||
[CollectionDefinition(nameof(SevenZipHeaderAllocationTests), DisableParallelization = true)]
|
||||
public class SevenZipHeaderAllocationCollection { }
|
||||
|
||||
[Collection(nameof(SevenZipHeaderAllocationTests))]
|
||||
public class SevenZipHeaderAllocationTests
|
||||
{
|
||||
public static IEnumerable<object[]> MalformedHeaders()
|
||||
{
|
||||
var count = Number(100_000);
|
||||
yield return ["folders", Join([1, 4, 7, 11], count, [0])];
|
||||
yield return ["files", Join([1, 5], count, [0])];
|
||||
yield return ["coders", Join([1, 4, 7, 11, 1, 0], count)];
|
||||
yield return ["properties", Join([1, 4, 7, 11, 1, 0, 1, 0x21, 0x21], count)];
|
||||
yield return ["pack streams", Join([1, 4, 6, 0], count, [9, 0])];
|
||||
yield return ["input streams", Join([1, 4, 7, 11, 1, 0, 1, 0x11, 0], count, [1])];
|
||||
yield return ["output streams", Join([1, 4, 7, 11, 1, 0, 1, 0x11, 0, 1], count)];
|
||||
yield return ["zero coders", new byte[] { 1, 4, 7, 11, 1, 0, 0 }];
|
||||
yield return ["zero input streams", new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 0, 1 }];
|
||||
yield return ["zero output streams", new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 1, 0 }];
|
||||
yield return ["truncated empty bitmap", Join([1, 5], count, [14, 1, 0x80, 0])];
|
||||
// One valid Copy folder followed by forged per-folder substream counts.
|
||||
byte[] folder = [1, 4, 7, 11, 1, 0, 1, 1, 0, 12, 0, 0, 8, 13];
|
||||
yield return ["substream CRCs", Join(folder, count, [10, 1])];
|
||||
yield return ["substream sizes", Join(folder, count, [9, 0])];
|
||||
yield return ["implicit substream CRCs", Join(folder, count, [0])];
|
||||
yield return ["substream size exceeds folder", Join(folder, [2, 9, 1, 0])];
|
||||
yield return ["invalid bind index", new byte[] { 1, 4, 7, 11, 1, 0, 2, 1, 0, 1, 0, 2, 0 }];
|
||||
yield return
|
||||
[
|
||||
"invalid pack index",
|
||||
new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 2, 1, 0, 2 },
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"duplicate pack index",
|
||||
new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 2, 1, 0, 0 },
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"oversized decoded metadata",
|
||||
MetadataHeader(true, (ulong)int.MaxValue + 1, 0, [1, 0]),
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"overflowing inputs",
|
||||
Join(
|
||||
[1, 4, 7, 11, 1, 0, 2, 0x11, 0],
|
||||
Number(int.MaxValue),
|
||||
[1, 0x11, 0],
|
||||
Number(int.MaxValue),
|
||||
[1]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedHeaders))]
|
||||
public void SevenZipArchive_InvalidHeaderCounts_RejectBeforeAllocation(
|
||||
string name,
|
||||
byte[] header
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(header);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
var exception = Record.Exception(() => archive.Entries.ToList());
|
||||
Assert.True(
|
||||
exception is InvalidFormatException or IncompleteArchiveException,
|
||||
$"{name}: {exception}"
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"{name}: allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedHeaders))]
|
||||
public async Task SevenZipArchive_InvalidHeaderCounts_AsyncRejectBeforeAllocation(
|
||||
string name,
|
||||
byte[] header
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(header);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
var exception = await Record.ExceptionAsync(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
Assert.True(
|
||||
exception is InvalidFormatException or IncompleteArchiveException,
|
||||
$"{name}: {exception}"
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"{name}: allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SevenZipArchive_TruncatedDecodedMetadata_DoesNotReserveUnpackSize(
|
||||
bool encodedHeader
|
||||
)
|
||||
{
|
||||
// Copy produces only the bytes present, despite advertising 16 MiB.
|
||||
var header = MetadataHeader(encodedHeader, 16 * 1024 * 1024, 1, [1, 0]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_TruncatedDecodedMetadata_AsyncRejects(bool encodedHeader)
|
||||
{
|
||||
var header = MetadataHeader(encodedHeader, 16 * 1024 * 1024, 1, [1, 0]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ReportedLzma2MetadataPayload_Rejects(bool async)
|
||||
{
|
||||
// The report puts the first packed byte at the start of the next header.
|
||||
var header = MetadataHeader(false, 16 * 1024 * 1024, 1, [0x21, 0x21, 1, 0x18]);
|
||||
using var stream = CreateArchive(header);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
Assert.Throws<DataErrorException>(() => archive.Entries.ToList());
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_BitPackedEmptyFiles_AreValid(bool async)
|
||||
{
|
||||
// Nine nameless directories need only two bitmap bytes. A byte-per-file
|
||||
// bound on the remaining header would incorrectly reject this archive.
|
||||
using var stream = CreateArchive([1, 5, 9, 14, 2, 0xFF, 0x80, 0, 0]);
|
||||
if (async)
|
||||
{
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
stream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
var count = 0;
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
Assert.True(entry.IsDirectory);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(9, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Equal(9, archive.Entries.Count());
|
||||
Assert.All(archive.Entries, entry => Assert.True(entry.IsDirectory));
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ExternalFolderCount_UsesExternalBuffer(bool async)
|
||||
{
|
||||
// Decode one byte of additional metadata, then reference it as the
|
||||
// folder-definition buffer. The inline header has ample padding.
|
||||
var header = Join(MetadataHeader(false, 1, 1, [1, 0]), [4, 7, 11, 2, 1, 0], new byte[128]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ExternalFolderDefinition_IsValid(bool async)
|
||||
{
|
||||
// Additional metadata contains a Copy folder. Its unpack size remains
|
||||
// inline; the file's packed byte follows the external folder definition.
|
||||
var header = Join(
|
||||
MetadataHeader(false, 3, 3, [1, 0]),
|
||||
[4, 6, 3, 1, 9, 1, 0, 7, 11, 1, 1, 0, 12, 1, 0, 8, 0, 0, 5, 1, 0, 0]
|
||||
);
|
||||
using var stream = CreateArchive(header, [1, 1, 0, 42]);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
var count = 0;
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
var data = new byte[1];
|
||||
Assert.Equal(1, await entryStream.ReadAsync(data, 0, 1, CancellationToken.None));
|
||||
Assert.Equal(42, data[0]);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
using var entryStream = archive.Entries.Single().OpenEntryStream();
|
||||
Assert.Equal(42, entryStream.ReadByte());
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_DecodedMetadataSpanningBuffers_IsValid(bool async)
|
||||
{
|
||||
// An unknown archive property forces decoding across several buffers.
|
||||
var decoded = Join([1, 2, 26], Number(20_000), new byte[20_000], [0, 0]);
|
||||
var header = MetadataHeader(true, (ulong)decoded.Length, (ulong)decoded.Length, [1, 0]);
|
||||
using var stream = CreateArchive(header, decoded);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
Assert.Fail("Expected an empty archive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Empty(archive.Entries);
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
private static byte[] MetadataHeader(
|
||||
bool encoded,
|
||||
ulong unpackSize,
|
||||
ulong packSize,
|
||||
byte[] coder
|
||||
) =>
|
||||
Join(
|
||||
encoded ? [23] : [1, 3],
|
||||
[6, 0, 1, 9],
|
||||
Number(packSize),
|
||||
[0, 7, 11, 1, 0, 1],
|
||||
coder,
|
||||
[12],
|
||||
Number(unpackSize),
|
||||
[0, 0]
|
||||
);
|
||||
|
||||
private static byte[] Number(ulong value)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
stream.WriteEncodedUInt64(value);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] Join(params byte[][] parts) => parts.SelectMany(part => part).ToArray();
|
||||
|
||||
private static MemoryStream CreateArchive(byte[] header, byte[]? packedData = null)
|
||||
{
|
||||
packedData ??= [];
|
||||
var crc = Crc32Stream.Compute(
|
||||
Crc32Stream.DEFAULT_POLYNOMIAL,
|
||||
Crc32Stream.DEFAULT_SEED,
|
||||
header
|
||||
);
|
||||
var stream = new MemoryStream();
|
||||
SevenZipSignatureHeaderWriter.WriteFinal(
|
||||
stream,
|
||||
(ulong)packedData.Length,
|
||||
(ulong)header.Length,
|
||||
crc
|
||||
);
|
||||
stream.Write(packedData, 0, packedData.Length);
|
||||
stream.Write(header, 0, header.Length);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user