Repository: dotnet/runtime (2025-07-31T12:31:33Z)

Issue:
[ZipArchiveFuzzer] Assertion failed `zip64eocdLocatorProper && zip64EOCDLocator != null`
Fuzzer was added in https://github.com/dotnet/runtime/pull/114421

```
Process terminated.    
Assertion failed.    
zip64eocdLocatorProper && zip64EOCDLocator != null    
   at System.IO.Compression.Zip64EndOfCentralDirectoryLocator.TryReadBlock(Stream stream)    
   at System.IO.Compression.ZipArchive.TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eocd, Int64 eocdStart)    
   at System.IO.Compression.ZipArchive.ReadEndOfCentralDirectory()    
   at System.IO.Compression.ZipArchive..ctor(Stream stream, ZipArchiveMode mode, Boolean leaveOpen, Encoding entryNameEncoding)    
   at DotnetFuzzing.Fuzzers.ZipArchiveFuzzer.TestArchive(Stream stream, Boolean async) in D:\a\_work\1\s\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\ZipArchiveFuzzer.cs:line 48    
   at DotnetFuzzing.Fuzzers.ZipArchiveFuzzer.TestArchive(Stream stream, Boolean async)    
   at DotnetFuzzing.Fuzzers.ZipArchiveFuzzer.FuzzTarget(ReadOnlySpan`1 bytes) in D:\a\_work\1\s\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\ZipArchiveFuzzer.cs:line 28    
   at DotnetFuzzing.Program.<>c__DisplayClass1_0.<RunFuzzer>b__0(ReadOnlySpan`1 bytes) in D:\a\_work\1\s\src\libraries\Fuzzing\DotnetFuzzing\Program.cs:line 91    
   at SharpFuzz.Fuzzer.LibFuzzer.Run(ReadOnlySpanAction action, Boolean ignoreExceptions)    
   at SharpFuzz.Fuzzer.LibFuzzer.Run(ReadOnlySpanAction action)    
   at DotnetFuzzing.Program.RunFuzzer(IFuzzer fuzzer, String inputFiles) in D:\a\_work\1\s\src\libraries\Fuzzing\DotnetFuzzing\Program.cs:line 86    
   at DotnetFuzzing.Program.Main(String[] args) in D:\a\_work\1\s\src\libraries\Fuzzing\DotnetFuzzing\Program.cs:line 67    
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)    
   at DotnetFuzzing.Program.Main(String[] args)    
   at DotnetFuzzing.Program.<Main>(String[] args)    
```

Crash input Base64: `UFBLBQYAAAAA//////9YaQRhAAAAAAAAAFBLBgcAAEsFBv//BQ==`

You can repro it locally with `.\deployment\ZipArchiveFuzzer\local-run.bat .\crash-input.bin` from `src\libraries\Fuzzing\DotnetFuzzing` after [building it](https://github.com/dotnet/runtime/blob/main/src/libraries/Fuzzing/README.md).


Discussion:
Tagging subscribers to this area: @dotnet/area-system-io-compression
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->



Patch:
diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipArchiveFuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipArchiveFuzzer.cs
index f8119f5fabc494..865016f2ca82be 100644
--- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipArchiveFuzzer.cs
+++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ZipArchiveFuzzer.cs
@@ -15,56 +15,73 @@ internal sealed class ZipArchiveFuzzer : IFuzzer
 
     public void FuzzTarget(ReadOnlySpan<byte> bytes)
     {
-
         if (bytes.IsEmpty)
         {
             return;
         }
 
-        try
-        {
-            using var stream = new MemoryStream(bytes.ToArray());
-
-            Task sync_test = TestArchive(stream, async: false);
-            Task async_test = TestArchive(stream, async: true);
-
-            Task.WaitAll(sync_test, async_test);
-        }
-        catch (Exception) { }
+        TestArchive(CopyToRentedArray(bytes), bytes.Length, async: false).GetAwaiter().GetResult();
+        TestArchive(CopyToRentedArray(bytes), bytes.Length, async: true).GetAwaiter().GetResult();
     }
 
-    private async Task TestArchive(Stream stream, bool async)
+    private byte[] CopyToRentedArray(ReadOnlySpan<byte> bytes)
     {
-        stream.Position = 0;
-
-        ZipArchive archive;
-
-        if (async)
+        byte[] buffer = ArrayPool<byte>.Shared.Rent(bytes.Length);
+        try
         {
-            archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
+            bytes.CopyTo(buffer);
+            return buffer;
         }
-        else
+        catch
         {
-            archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
+            ArrayPool<byte>.Shared.Return(buffer);
+            throw;
         }
+    }
 
-        foreach (var entry in archive.Entries)
+    private async Task TestArchive(byte[] buffer, int length, bool async)
+    {
+        try
         {
-            // Access entry properties to simulate usage
-            _ = entry.FullName;
-            _ = entry.Length;
-            _ = entry.Comment;
-            _ = entry.LastWriteTime;
-            _ = entry.Crc32;
-        }
+            using var stream = new MemoryStream(buffer, 0, length);
 
-        if (async)
+            ZipArchive archive;
+
+            if (async)
+            {
+                archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
+            }
+            else
+            {
+                archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null);
+            }
+
+            foreach (var entry in archive.Entries)
+            {
+                // Access entry properties to simulate usage
+                _ = entry.FullName;
+                _ = entry.Length;
+                _ = entry.Comment;
+                _ = entry.LastWriteTime;
+                _ = entry.Crc32;
+            }
+
+            if (async)
+            {
+                await archive.DisposeAsync();
+            }
+            else
+            {
+                archive.Dispose();
+            }
+        }
+        catch (InvalidDataException)
         {
-            await archive.DisposeAsync();
+            // ignore, this exception is expected to be thrown for invalid/corrupted archives.
         }
-        else
+        finally
         {
-            archive.Dispose();
+            ArrayPool<byte>.Shared.Return(buffer);
         }
     }
 }
diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx
index 86e181e97ff13b..e6235c419f5f12 100644
--- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx
+++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx
@@ -293,4 +293,7 @@
   <data name="PlatformNotSupported_Compression" xml:space="preserve">
     <value>System.IO.Compression is not supported on this platform.</value>
   </data>
+  <data name="InvalidOffsetToZip64EOCD" xml:space="preserve">
+    <value>Invalid offset to the Zip64 End of Central Directory record.</value>
+  </data>
 </root>
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
index db0bdd7b1e63b0..19b025473e39b6 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
@@ -247,10 +247,12 @@ private async Task ReadEndOfCentralDirectoryAsync(CancellationToken cancellation
                     cancellationToken).ConfigureAwait(false))
                 throw new InvalidDataException(SR.EOCDNotFound);
 
+            long eocdStart = _archiveStream.Position;
+
             // read the EOCD
             ZipEndOfCentralDirectoryBlock eocd = await ZipEndOfCentralDirectoryBlock.ReadBlockAsync(_archiveStream, cancellationToken).ConfigureAwait(false);
 
-            ReadEndOfCentralDirectoryInnerWork(eocd, out long eocdStart);
+            ReadEndOfCentralDirectoryInnerWork(eocd);
 
             await TryReadZip64EndOfCentralDirectoryAsync(eocd, eocdStart, cancellationToken).ConfigureAwait(false);
 
@@ -284,6 +286,12 @@ private async ValueTask TryReadZip64EndOfCentralDirectoryAsync(ZipEndOfCentralDi
         {
             // Read Zip64 End of Central Directory Locator
 
+            // Check if there's enough space before the EOCD to look for the Zip64 EOCDL
+            if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
+            {
+                throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
+            }
+
             // This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature would be located
             _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin);
 
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
index 900f40c8ccfcb2..6e1e36fd63b8eb 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
@@ -587,10 +587,8 @@ private void ReadCentralDirectory()
             }
         }
 
-        private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd, out long eocdStart)
+        private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd)
         {
-            eocdStart = _archiveStream.Position;
-
             if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory)
                 throw new InvalidDataException(SR.SplitSpanned);
 
@@ -624,10 +622,12 @@ private void ReadEndOfCentralDirectory()
                         ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength + ZipEndOfCentralDirectoryBlock.FieldLengths.Signature))
                     throw new InvalidDataException(SR.EOCDNotFound);
 
+                long eocdStart = _archiveStream.Position;
+
                 // read the EOCD
                 ZipEndOfCentralDirectoryBlock eocd = ZipEndOfCentralDirectoryBlock.ReadBlock(_archiveStream);
 
-                ReadEndOfCentralDirectoryInnerWork(eocd, out long eocdStart);
+                ReadEndOfCentralDirectoryInnerWork(eocd);
 
                 TryReadZip64EndOfCentralDirectory(eocd, eocdStart);
 
@@ -653,6 +653,9 @@ private void TryReadZip64EndOfCentralDirectoryInnerInitialWork(Zip64EndOfCentral
 
             long zip64EOCDOffset = (long)locator.OffsetOfZip64EOCD;
 
+            if (zip64EOCDOffset < 0 || zip64EOCDOffset > _archiveStream.Length)
+                throw new InvalidDataException(SR.InvalidOffsetToZip64EOCD);
+
             _archiveStream.Seek(zip64EOCDOffset, SeekOrigin.Begin);
         }
 
@@ -686,6 +689,12 @@ private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eoc
             {
                 // Read Zip64 End of Central Directory Locator
 
+                // Check if there's enough space before the EOCD to look for the Zip64 EOCDL
+                if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
+                {
+                    throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
+                }
+
                 // This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature would be located
                 _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin);
 
@@ -701,7 +710,6 @@ private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eoc
 
                     // Read Zip64 End of Central Directory Record
                     Zip64EndOfCentralDirectoryRecord record = Zip64EndOfCentralDirectoryRecord.TryReadBlock(_archiveStream);
-
                     TryReadZip64EndOfCentralDirectoryInnerFinalWork(record);
                 }
             }
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs
index 04b65b6fda6186..8d24a06479c30f 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.Async.cs
@@ -53,11 +53,18 @@ internal static async Task<bool> SeekBackwardsToSignatureAsync(Stream stream, Re
             bool signatureFound = false;
 
             int totalBytesRead = 0;
-            int duplicateBytesRead = 0;
 
-            while (!signatureFound && !outOfBytes && totalBytesRead <= maxBytesToRead)
+            while (!signatureFound && !outOfBytes && totalBytesRead < maxBytesToRead)
             {
-                int bytesRead = await SeekBackwardsAndReadAsync(stream, bufferMemory, signatureToFind.Length, cancellationToken).ConfigureAwait(false);
+                int overlap = totalBytesRead == 0 ? 0 : signatureToFind.Length;
+
+                if (maxBytesToRead - totalBytesRead + overlap < bufferMemory.Length)
+                {
+                    // If we have less than a full buffer left to read, we adjust the buffer size.
+                    bufferMemory = bufferMemory.Slice(0, maxBytesToRead - totalBytesRead + overlap);
+                }
+
+                int bytesRead = await SeekBackwardsAndReadAsync(stream, bufferMemory, overlap, cancellationToken).ConfigureAwait(false);
 
                 outOfBytes = bytesRead < bufferMemory.Length;
                 if (bytesRead < bufferMemory.Length)
@@ -68,15 +75,13 @@ internal static async Task<bool> SeekBackwardsToSignatureAsync(Stream stream, Re
                 bufferPointer = bufferMemory.Span.LastIndexOf(signatureToFind.Span);
                 Debug.Assert(bufferPointer < bufferMemory.Length);
 
-                totalBytesRead += (bufferMemory.Length - duplicateBytesRead);
+                totalBytesRead += bytesRead - overlap;
 
                 if (bufferPointer != -1)
                 {
                     signatureFound = true;
                     break;
                 }
-
-                duplicateBytesRead = signatureToFind.Length;
             }
 
             if (!signatureFound)
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs
index 09ff508270bf4d..2f36b57c89741f 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs
@@ -124,11 +124,18 @@ internal static bool SeekBackwardsToSignature(Stream stream, ReadOnlySpan<byte>
             bool signatureFound = false;
 
             int totalBytesRead = 0;
-            int duplicateBytesRead = 0;
 
-            while (!signatureFound && !outOfBytes && totalBytesRead <= maxBytesToRead)
+            while (!signatureFound && !outOfBytes && totalBytesRead < maxBytesToRead)
             {
-                int bytesRead = SeekBackwardsAndRead(stream, bufferSpan, signatureToFind.Length);
+                int overlap = totalBytesRead == 0 ? 0 : signatureToFind.Length;
+
+                if (maxBytesToRead - totalBytesRead + overlap < bufferSpan.Length)
+                {
+                    // If we have less than a full buffer left to read, we adjust the buffer size.
+                    bufferSpan = bufferSpan.Slice(0, maxBytesToRead - totalBytesRead + overlap);
+                }
+
+                int bytesRead = SeekBackwardsAndRead(stream, bufferSpan, overlap);
 
                 outOfBytes = bytesRead < bufferSpan.Length;
                 if (bytesRead < bufferSpan.Length)
@@ -139,15 +146,13 @@ internal static bool SeekBackwardsToSignature(Stream stream, ReadOnlySpan<byte>
                 bufferPointer = bufferSpan.LastIndexOf(signatureToFind);
                 Debug.Assert(bufferPointer < bufferSpan.Length);
 
-                totalBytesRead += (bufferSpan.Length - duplicateBytesRead);
+                totalBytesRead += bytesRead - overlap;
 
                 if (bufferPointer != -1)
                 {
                     signatureFound = true;
                     break;
                 }
-
-                duplicateBytesRead = signatureToFind.Length;
             }
 
             if (!signatureFound)


Test patch:
diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs
index 215d8f6249aa92..3d1439f75458c3 100644
--- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs
+++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs
@@ -195,7 +195,7 @@ public static async Task LargeArchive_DataDescriptor_Read_NonZip64_FileLengthGre
             Stream source = await OpenEntryStream(async, e);
             byte[] buffer = new byte[s_bufferSize];
             int read = await source.ReadAsync(buffer, 0, buffer.Length);   // We don't want to inflate this large archive entirely
-                                                                // just making sure it read successfully
+                                                                           // just making sure it read successfully
             Assert.Equal(s_bufferSize, read);
             foreach (byte b in buffer)
             {
@@ -564,7 +564,7 @@ public static async Task UpdateZipArchive_AddFileTo_ZipWithCorruptedFile(bool as
             byte[] buffer2 = new byte[1024];
             file.Seek(0, SeekOrigin.Begin);
 
-            while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0 )
+            while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0)
             {
                 await file.ReadAsync(buffer2, 0, buffer2.Length);
                 Assert.Equal(buffer1, buffer2);
@@ -1418,7 +1418,7 @@ public async Task ZipArchive_InvalidExtraFieldData_Async(byte validVersionToExtr
             // for first.bin would normally be skipped (because it hasn't changed) but it needs to be rewritten
             // because the central directory headers will be rewritten with a valid value and the local file header
             // needs to match.
-            await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding:  null))
+            await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null))
             {
                 ZipArchiveEntry newEntry = updatedArchive.CreateEntry("second.bin", CompressionLevel.NoCompression);
 
@@ -1659,6 +1659,39 @@ public static async Task NoSyncCallsWhenUsingAsync()
             }
         }
 
+        [Theory]
+        [MemberData(nameof(Get_Booleans_Data))]
+        public async Task ReadArchive_FrontTruncatedFile_Throws(bool async)
+        {
+            for (int i = 1; i < s_slightlyIncorrectZip64.Length - 1; i++)
+            {
+                await Assert.ThrowsAsync<InvalidDataException>(
+                    // The archive is truncated, so it should throw an exception
+                    () => CreateZipArchive(async, new MemoryStream(s_slightlyIncorrectZip64, i, s_slightlyIncorrectZip64.Length - i), ZipArchiveMode.Read));
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(Get_Booleans_Data))]
+        public async Task ReadArchive_Zip64EocdLocatorInvalidOffset_Throws(bool async)
+        {
+            byte[] data = s_slightlyIncorrectZip64.ToArray();
+
+            foreach (long offset in new[] { -1, int.MaxValue - 1 })
+            {
+                // Find Zip64 EOCD locator record
+                int eocdlOffset = data.AsSpan().LastIndexOf(new byte[] { 0x50, 0x4b, 0x06, 0x07 });
+                Assert.True(eocdlOffset >= 0, "Zip64 EOCD locator not found in test data");
+
+                // skip 4B signature and 4B index of disk, then overwrite the 8B offset to start of central directory
+                BinaryPrimitives.WriteInt64LittleEndian(data.AsSpan(eocdlOffset + 8, 8), offset);
+
+                await Assert.ThrowsAsync<InvalidDataException>(
+                    // The archive is truncated, so it should throw an exception
+                    () => CreateZipArchive(async, new MemoryStream(data), ZipArchiveMode.Read));
+            }
+        }
+
         // Generates a copy of s_invalidExtraFieldData with a variable number of bytes as extra field data.
         private static byte[] GenerateInvalidExtraFieldData(byte modifiedVersionToExtract, ushort extraFieldDataLength,
             out int lhVersionToExtractOffset,