Skip to content

Commit 1dd74ea

Browse files
rzikmCopilot
andauthored
Fix reading Zip64 end of central directory locator (#118239)
* Fix reading Zip64 end of central directory locator * Update src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs Co-authored-by: Copilot <[email protected]> * Fix build * Add test * Update src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs * Handle another offset out of bounds case. * Improve ZipArchiveFuzzer * Add another test case * Add a comment --------- Co-authored-by: Copilot <[email protected]>
1 parent d84ca95 commit 1dd74ea

File tree

7 files changed

+132
-53
lines changed

7 files changed

+132
-53
lines changed

src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipArchiveFuzzer.cs

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,73 @@ internal sealed class ZipArchiveFuzzer : IFuzzer
1515

1616
public void FuzzTarget(ReadOnlySpan<byte> bytes)
1717
{
18-
1918
if (bytes.IsEmpty)
2019
{
2120
return;
2221
}
2322

24-
try
25-
{
26-
using var stream = new MemoryStream(bytes.ToArray());
27-
28-
Task sync_test = TestArchive(stream, async: false);
29-
Task async_test = TestArchive(stream, async: true);
30-
31-
Task.WaitAll(sync_test, async_test);
32-
}
33-
catch (Exception) { }
23+
TestArchive(CopyToRentedArray(bytes), bytes.Length, async: false).GetAwaiter().GetResult();
24+
TestArchive(CopyToRentedArray(bytes), bytes.Length, async: true).GetAwaiter().GetResult();
3425
}
3526

36-
private async Task TestArchive(Stream stream, bool async)
27+
private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
3728
{
38-
stream.Position = 0;
39-
40-
ZipArchive archive;
41-
42-
if (async)
29+
byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
30+
try
4331
{
44-
archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
32+
bytes.CopyTo(buffer);
33+
return buffer;
4534
}
46-
else
35+
catch
4736
{
48-
archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
37+
ArrayPool<byte>.Shared.Return(buffer);
38+
throw;
4939
}
40+
}
5041

51-
foreach (var entry in archive.Entries)
42+
private async Task TestArchive(byte[] buffer, int length, bool async)
43+
{
44+
try
5245
{
53-
// Access entry properties to simulate usage
54-
_ = entry.FullName;
55-
_ = entry.Length;
56-
_ = entry.Comment;
57-
_ = entry.LastWriteTime;
58-
_ = entry.Crc32;
59-
}
46+
using var stream = new MemoryStream(buffer, 0, length);
6047

61-
if (async)
48+
ZipArchive archive;
49+
50+
if (async)
51+
{
52+
archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
53+
}
54+
else
55+
{
56+
archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
57+
}
58+
59+
foreach (var entry in archive.Entries)
60+
{
61+
// Access entry properties to simulate usage
62+
_ = entry.FullName;
63+
_ = entry.Length;
64+
_ = entry.Comment;
65+
_ = entry.LastWriteTime;
66+
_ = entry.Crc32;
67+
}
68+
69+
if (async)
70+
{
71+
await archive.DisposeAsync();
72+
}
73+
else
74+
{
75+
archive.Dispose();
76+
}
77+
}
78+
catch (InvalidDataException)
6279
{
63-
await archive.DisposeAsync();
80+
// ignore, this exception is expected to be thrown for invalid/corrupted archives.
6481
}
65-
else
82+
finally
6683
{
67-
archive.Dispose();
84+
ArrayPool<byte>.Shared.Return(buffer);
6885
}
6986
}
7087
}

src/libraries/System.IO.Compression/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,7 @@
293293
<data name="PlatformNotSupported_Compression" xml:space="preserve">
294294
<value>System.IO.Compression is not supported on this platform.</value>
295295
</data>
296+
<data name="InvalidOffsetToZip64EOCD" xml:space="preserve">
297+
<value>Invalid offset to the Zip64 End of Central Directory record.</value>
298+
</data>
296299
</root>

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,12 @@ private async Task ReadEndOfCentralDirectoryAsync(CancellationToken cancellation
247247
cancellationToken).ConfigureAwait(false))
248248
throw new InvalidDataException(SR.EOCDNotFound);
249249

250+
long eocdStart = _archiveStream.Position;
251+
250252
// read the EOCD
251253
ZipEndOfCentralDirectoryBlock eocd = await ZipEndOfCentralDirectoryBlock.ReadBlockAsync(_archiveStream, cancellationToken).ConfigureAwait(false);
252254

253-
ReadEndOfCentralDirectoryInnerWork(eocd, out long eocdStart);
255+
ReadEndOfCentralDirectoryInnerWork(eocd);
254256

255257
await TryReadZip64EndOfCentralDirectoryAsync(eocd, eocdStart, cancellationToken).ConfigureAwait(false);
256258

@@ -284,6 +286,12 @@ private async ValueTask TryReadZip64EndOfCentralDirectoryAsync(ZipEndOfCentralDi
284286
{
285287
// Read Zip64 End of Central Directory Locator
286288

289+
// Check if there's enough space before the EOCD to look for the Zip64 EOCDL
290+
if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
291+
{
292+
throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
293+
}
294+
287295
// This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature would be located
288296
_archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin);
289297

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -587,10 +587,8 @@ private void ReadCentralDirectory()
587587
}
588588
}
589589

590-
private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd, out long eocdStart)
590+
private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd)
591591
{
592-
eocdStart = _archiveStream.Position;
593-
594592
if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory)
595593
throw new InvalidDataException(SR.SplitSpanned);
596594

@@ -624,10 +622,12 @@ private void ReadEndOfCentralDirectory()
624622
ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength + ZipEndOfCentralDirectoryBlock.FieldLengths.Signature))
625623
throw new InvalidDataException(SR.EOCDNotFound);
626624

625+
long eocdStart = _archiveStream.Position;
626+
627627
// read the EOCD
628628
ZipEndOfCentralDirectoryBlock eocd = ZipEndOfCentralDirectoryBlock.ReadBlock(_archiveStream);
629629

630-
ReadEndOfCentralDirectoryInnerWork(eocd, out long eocdStart);
630+
ReadEndOfCentralDirectoryInnerWork(eocd);
631631

632632
TryReadZip64EndOfCentralDirectory(eocd, eocdStart);
633633

@@ -653,6 +653,9 @@ private void TryReadZip64EndOfCentralDirectoryInnerInitialWork(Zip64EndOfCentral
653653

654654
long zip64EOCDOffset = (long)locator.OffsetOfZip64EOCD;
655655

656+
if (zip64EOCDOffset < 0 || zip64EOCDOffset > _archiveStream.Length)
657+
throw new InvalidDataException(SR.InvalidOffsetToZip64EOCD);
658+
656659
_archiveStream.Seek(zip64EOCDOffset, SeekOrigin.Begin);
657660
}
658661

@@ -686,6 +689,12 @@ private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eoc
686689
{
687690
// Read Zip64 End of Central Directory Locator
688691

692+
// Check if there's enough space before the EOCD to look for the Zip64 EOCDL
693+
if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
694+
{
695+
throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
696+
}
697+
689698
// This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature would be located
690699
_archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin);
691700

@@ -701,7 +710,6 @@ private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eoc
701710

702711
// Read Zip64 End of Central Directory Record
703712
Zip64EndOfCentralDirectoryRecord record = Zip64EndOfCentralDirectoryRecord.TryReadBlock(_archiveStream);
704-
705713
TryReadZip64EndOfCentralDirectoryInnerFinalWork(record);
706714
}
707715
}

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,18 @@ internal static async Task<bool> SeekBackwardsToSignatureAsync(Stream stream, Re
5353
bool signatureFound = false;
5454

5555
int totalBytesRead = 0;
56-
int duplicateBytesRead = 0;
5756

58-
while (!signatureFound && !outOfBytes && totalBytesRead <= maxBytesToRead)
57+
while (!signatureFound && !outOfBytes && totalBytesRead < maxBytesToRead)
5958
{
60-
int bytesRead = await SeekBackwardsAndReadAsync(stream, bufferMemory, signatureToFind.Length, cancellationToken).ConfigureAwait(false);
59+
int overlap = totalBytesRead == 0 ? 0 : signatureToFind.Length;
60+
61+
if (maxBytesToRead - totalBytesRead + overlap < bufferMemory.Length)
62+
{
63+
// If we have less than a full buffer left to read, we adjust the buffer size.
64+
bufferMemory = bufferMemory.Slice(0, maxBytesToRead - totalBytesRead + overlap);
65+
}
66+
67+
int bytesRead = await SeekBackwardsAndReadAsync(stream, bufferMemory, overlap, cancellationToken).ConfigureAwait(false);
6168

6269
outOfBytes = bytesRead < bufferMemory.Length;
6370
if (bytesRead < bufferMemory.Length)
@@ -68,15 +75,13 @@ internal static async Task<bool> SeekBackwardsToSignatureAsync(Stream stream, Re
6875
bufferPointer = bufferMemory.Span.LastIndexOf(signatureToFind.Span);
6976
Debug.Assert(bufferPointer < bufferMemory.Length);
7077

71-
totalBytesRead += (bufferMemory.Length - duplicateBytesRead);
78+
totalBytesRead += bytesRead - overlap;
7279

7380
if (bufferPointer != -1)
7481
{
7582
signatureFound = true;
7683
break;
7784
}
78-
79-
duplicateBytesRead = signatureToFind.Length;
8085
}
8186

8287
if (!signatureFound)

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,18 @@ internal static bool SeekBackwardsToSignature(Stream stream, ReadOnlySpan<byte>
124124
bool signatureFound = false;
125125

126126
int totalBytesRead = 0;
127-
int duplicateBytesRead = 0;
128127

129-
while (!signatureFound && !outOfBytes && totalBytesRead <= maxBytesToRead)
128+
while (!signatureFound && !outOfBytes && totalBytesRead < maxBytesToRead)
130129
{
131-
int bytesRead = SeekBackwardsAndRead(stream, bufferSpan, signatureToFind.Length);
130+
int overlap = totalBytesRead == 0 ? 0 : signatureToFind.Length;
131+
132+
if (maxBytesToRead - totalBytesRead + overlap < bufferSpan.Length)
133+
{
134+
// If we have less than a full buffer left to read, we adjust the buffer size.
135+
bufferSpan = bufferSpan.Slice(0, maxBytesToRead - totalBytesRead + overlap);
136+
}
137+
138+
int bytesRead = SeekBackwardsAndRead(stream, bufferSpan, overlap);
132139

133140
outOfBytes = bytesRead < bufferSpan.Length;
134141
if (bytesRead < bufferSpan.Length)
@@ -139,15 +146,13 @@ internal static bool SeekBackwardsToSignature(Stream stream, ReadOnlySpan<byte>
139146
bufferPointer = bufferSpan.LastIndexOf(signatureToFind);
140147
Debug.Assert(bufferPointer < bufferSpan.Length);
141148

142-
totalBytesRead += (bufferSpan.Length - duplicateBytesRead);
149+
totalBytesRead += bytesRead - overlap;
143150

144151
if (bufferPointer != -1)
145152
{
146153
signatureFound = true;
147154
break;
148155
}
149-
150-
duplicateBytesRead = signatureToFind.Length;
151156
}
152157

153158
if (!signatureFound)

src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public static async Task LargeArchive_DataDescriptor_Read_NonZip64_FileLengthGre
195195
Stream source = await OpenEntryStream(async, e);
196196
byte[] buffer = new byte[s_bufferSize];
197197
int read = await source.ReadAsync(buffer, 0, buffer.Length); // We don't want to inflate this large archive entirely
198-
// just making sure it read successfully
198+
// just making sure it read successfully
199199
Assert.Equal(s_bufferSize, read);
200200
foreach (byte b in buffer)
201201
{
@@ -564,7 +564,7 @@ public static async Task UpdateZipArchive_AddFileTo_ZipWithCorruptedFile(bool as
564564
byte[] buffer2 = new byte[1024];
565565
file.Seek(0, SeekOrigin.Begin);
566566

567-
while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0 )
567+
while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0)
568568
{
569569
await file.ReadAsync(buffer2, 0, buffer2.Length);
570570
Assert.Equal(buffer1, buffer2);
@@ -1418,7 +1418,7 @@ public async Task ZipArchive_InvalidExtraFieldData_Async(byte validVersionToExtr
14181418
// for first.bin would normally be skipped (because it hasn't changed) but it needs to be rewritten
14191419
// because the central directory headers will be rewritten with a valid value and the local file header
14201420
// needs to match.
1421-
await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null))
1421+
await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null))
14221422
{
14231423
ZipArchiveEntry newEntry = updatedArchive.CreateEntry("second.bin", CompressionLevel.NoCompression);
14241424

@@ -1659,6 +1659,39 @@ public static async Task NoSyncCallsWhenUsingAsync()
16591659
}
16601660
}
16611661

1662+
[Theory]
1663+
[MemberData(nameof(Get_Booleans_Data))]
1664+
public async Task ReadArchive_FrontTruncatedFile_Throws(bool async)
1665+
{
1666+
for (int i = 1; i < s_slightlyIncorrectZip64.Length - 1; i++)
1667+
{
1668+
await Assert.ThrowsAsync<InvalidDataException>(
1669+
// The archive is truncated, so it should throw an exception
1670+
() => CreateZipArchive(async, new MemoryStream(s_slightlyIncorrectZip64, i, s_slightlyIncorrectZip64.Length - i), ZipArchiveMode.Read));
1671+
}
1672+
}
1673+
1674+
[Theory]
1675+
[MemberData(nameof(Get_Booleans_Data))]
1676+
public async Task ReadArchive_Zip64EocdLocatorInvalidOffset_Throws(bool async)
1677+
{
1678+
byte[] data = s_slightlyIncorrectZip64.ToArray();
1679+
1680+
foreach (long offset in new[] { -1, int.MaxValue - 1 })
1681+
{
1682+
// Find Zip64 EOCD locator record
1683+
int eocdlOffset = data.AsSpan().LastIndexOf(new byte[] { 0x50, 0x4b, 0x06, 0x07 });
1684+
Assert.True(eocdlOffset >= 0, "Zip64 EOCD locator not found in test data");
1685+
1686+
// skip 4B signature and 4B index of disk, then overwrite the 8B offset to start of central directory
1687+
BinaryPrimitives.WriteInt64LittleEndian(data.AsSpan(eocdlOffset + 8, 8), offset);
1688+
1689+
await Assert.ThrowsAsync<InvalidDataException>(
1690+
// The archive is truncated, so it should throw an exception
1691+
() => CreateZipArchive(async, new MemoryStream(data), ZipArchiveMode.Read));
1692+
}
1693+
}
1694+
16621695
// Generates a copy of s_invalidExtraFieldData with a variable number of bytes as extra field data.
16631696
private static byte[] GenerateInvalidExtraFieldData(byte modifiedVersionToExtract, ushort extraFieldDataLength,
16641697
out int lhVersionToExtractOffset,

0 commit comments

Comments
 (0)