From d4d9ebf1343ae968efbdd151840c5646fc77c1ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:48:42 +0000 Subject: [PATCH 1/2] Fix ZipArchiveEntry.Open AORE when uncompressed size > int.MaxValue (#127834) Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d2fb03b5-3b19-489d-9856-bb2c42a4d7e4 Co-authored-by: alinpahontu2912 <56953855+alinpahontu2912@users.noreply.github.com> --- .../IO/Compression/ZipArchiveEntry.Async.cs | 9 +++-- .../System/IO/Compression/ZipArchiveEntry.cs | 9 +++-- .../zip_InvalidParametersAndStrangeFiles.cs | 39 +++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs index a393ea76491e0f..99e0b226758f3b 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs @@ -115,9 +115,12 @@ private async Task GetUncompressedDataAsync(CancellationToken canc { // this means we have never opened it before - // if _uncompressedSize > int.MaxValue, it's still okay, because MemoryStream will just - // grow as data is copied into it - _storedUncompressedData = new MemoryStream((int)_uncompressedSize); + // The initial capacity is only a hint to pre-size the backing buffer. If + // _uncompressedSize exceeds the maximum capacity accepted by MemoryStream + // (Array.MaxLength), clamp it: MemoryStream will throw on a negative value + // produced by an unchecked cast and on values greater than Array.MaxLength. + // MemoryStream will grow as data is copied into it, up to Array.MaxLength. + _storedUncompressedData = new MemoryStream((int)Math.Min(_uncompressedSize, Array.MaxLength)); if (_originallyInArchive) { diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index e84666144c9379..78e8017b66167f 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -481,9 +481,12 @@ private MemoryStream GetUncompressedData() { // this means we have never opened it before - // if _uncompressedSize > int.MaxValue, it's still okay, because MemoryStream will just - // grow as data is copied into it - _storedUncompressedData = new MemoryStream((int)_uncompressedSize); + // The initial capacity is only a hint to pre-size the backing buffer. If + // _uncompressedSize exceeds the maximum capacity accepted by MemoryStream + // (Array.MaxLength), clamp it: MemoryStream will throw on a negative value + // produced by an unchecked cast and on values greater than Array.MaxLength. + // MemoryStream will grow as data is copied into it, up to Array.MaxLength. + _storedUncompressedData = new MemoryStream((int)Math.Min(_uncompressedSize, Array.MaxLength)); if (_originallyInArchive) { 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 46d899426459e8..23e36852446bd6 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -408,6 +408,45 @@ await Assert.ThrowsAsync(async () => await DisposeZipArchive(async, archive); } + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + public static async Task ZipArchiveEntry_OpenInUpdateMode_UncompressedSizeGreaterThanIntMaxValue_DoesNotThrow(bool async) + { + // Regression test for https://github.com/dotnet/runtime/issues/127834 + // When _uncompressedSize > int.MaxValue (e.g. for a Zip64 entry whose declared + // uncompressed size exceeds int.MaxValue), GetUncompressedData() passed an + // unchecked (int) cast to the MemoryStream constructor. That cast wrapped to a + // negative number, causing ArgumentOutOfRangeException to be thrown from the + // MemoryStream constructor before any data was loaded. The capacity argument is + // only a pre-sizing hint and must be clamped to a valid MemoryStream capacity. + byte[] payload = [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF]; + MemoryStream stream = new MemoryStream(); + + ZipArchive archive = await CreateZipArchive(async, stream, ZipArchiveMode.Create, leaveOpen: true); + ZipArchiveEntry entry = archive.CreateEntry("entry.bin", CompressionLevel.NoCompression); + Stream entryStream = await OpenEntryStream(async, entry); + await entryStream.WriteAsync(payload); + await DisposeStream(async, entryStream); + await DisposeZipArchive(async, archive); + + stream.Position = 0; + archive = await CreateZipArchive(async, stream, ZipArchiveMode.Update, leaveOpen: true); + entry = archive.GetEntry("entry.bin"); + + FieldInfo uncompressedSizeField = typeof(ZipArchiveEntry).GetField("_uncompressedSize", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(uncompressedSizeField); + uncompressedSizeField.SetValue(entry, (long)int.MaxValue + 100L); + + // Previously this threw ArgumentOutOfRangeException from the MemoryStream + // constructor inside GetUncompressedData because (int)_uncompressedSize wrapped + // to a negative value. With the fix it must succeed (the actual stored bytes are + // small, so loading completes normally). + Stream openStream = await OpenEntryStream(async, entry); + await DisposeStream(async, openStream); + + await DisposeZipArchive(async, archive); + } + [Theory] [MemberData(nameof(Get_Booleans_Data))] public static async Task UnseekableVeryLargeArchive_DataDescriptor_Read_Zip64(bool async) From ff62bbb4e90946edcd98257abd190d73dd11e1aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 13:05:59 +0000 Subject: [PATCH 2/2] Validate _uncompressedSize up front in IsOpenableFinalVerifications Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0fbb8331-f2e4-4bd0-acb1-a58139224285 Co-authored-by: alinpahontu2912 <56953855+alinpahontu2912@users.noreply.github.com> --- .../IO/Compression/ZipArchiveEntry.Async.cs | 10 +++--- .../System/IO/Compression/ZipArchiveEntry.cs | 22 ++++++++---- .../zip_InvalidParametersAndStrangeFiles.cs | 35 ++++++++++--------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs index 99e0b226758f3b..3f5dd12bd83cca 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs @@ -115,12 +115,10 @@ private async Task GetUncompressedDataAsync(CancellationToken canc { // this means we have never opened it before - // The initial capacity is only a hint to pre-size the backing buffer. If - // _uncompressedSize exceeds the maximum capacity accepted by MemoryStream - // (Array.MaxLength), clamp it: MemoryStream will throw on a negative value - // produced by an unchecked cast and on values greater than Array.MaxLength. - // MemoryStream will grow as data is copied into it, up to Array.MaxLength. - _storedUncompressedData = new MemoryStream((int)Math.Min(_uncompressedSize, Array.MaxLength)); + // OpenInUpdateModeAsync validates that _uncompressedSize fits in [0, Array.MaxLength] + // via ThrowIfNotOpenableAsync before reaching this code, so the (int) cast is safe + // and the capacity hint is bounded by MemoryStream's maximum. + _storedUncompressedData = new MemoryStream((int)_uncompressedSize); if (_originallyInArchive) { diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index 78e8017b66167f..4b3695dc2161d6 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -481,12 +481,10 @@ private MemoryStream GetUncompressedData() { // this means we have never opened it before - // The initial capacity is only a hint to pre-size the backing buffer. If - // _uncompressedSize exceeds the maximum capacity accepted by MemoryStream - // (Array.MaxLength), clamp it: MemoryStream will throw on a negative value - // produced by an unchecked cast and on values greater than Array.MaxLength. - // MemoryStream will grow as data is copied into it, up to Array.MaxLength. - _storedUncompressedData = new MemoryStream((int)Math.Min(_uncompressedSize, Array.MaxLength)); + // OpenInUpdateMode validates that _uncompressedSize fits in [0, Array.MaxLength] + // via ThrowIfNotOpenable before reaching this code, so the (int) cast is safe + // and the capacity hint is bounded by MemoryStream's maximum. + _storedUncompressedData = new MemoryStream((int)_uncompressedSize); if (_originallyInArchive) { @@ -1003,6 +1001,18 @@ private bool IsOpenableFinalVerifications(bool needToLoadIntoMemory, long offset return false; } } + + // The uncompressed data is loaded into a MemoryStream, which is backed by a + // single byte[] and therefore cannot grow beyond Array.MaxLength. Reject + // up front rather than failing later from the MemoryStream constructor with + // a misleading argument-out-of-range exception (caused by the unchecked + // (int) cast in GetUncompressedData wrapping a long > int.MaxValue to a + // negative value). + if ((ulong)_uncompressedSize > (ulong)Array.MaxLength) + { + message = SR.EntryTooLarge; + return false; + } } return true; 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 23e36852446bd6..5f924485e734e2 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -410,15 +410,14 @@ await Assert.ThrowsAsync(async () => [Theory] [MemberData(nameof(Get_Booleans_Data))] - public static async Task ZipArchiveEntry_OpenInUpdateMode_UncompressedSizeGreaterThanIntMaxValue_DoesNotThrow(bool async) - { - // Regression test for https://github.com/dotnet/runtime/issues/127834 - // When _uncompressedSize > int.MaxValue (e.g. for a Zip64 entry whose declared - // uncompressed size exceeds int.MaxValue), GetUncompressedData() passed an - // unchecked (int) cast to the MemoryStream constructor. That cast wrapped to a - // negative number, causing ArgumentOutOfRangeException to be thrown from the - // MemoryStream constructor before any data was loaded. The capacity argument is - // only a pre-sizing hint and must be clamped to a valid MemoryStream capacity. + public static async Task ZipArchiveEntry_OpenInUpdateMode_UncompressedSizeGreaterThanArrayMaxLength_ThrowsInvalidData(bool async) + { + // When _uncompressedSize > Array.MaxLength, the entry's uncompressed payload + // cannot be loaded into a MemoryStream (which is backed by a single byte[] and + // therefore bounded by Array.MaxLength). The entry must be rejected up front + // with a descriptive InvalidDataException when opened in Update mode, rather + // than failing later from the MemoryStream constructor with a misleading + // argument-out-of-range exception caused by the (int) cast wrapping negative. byte[] payload = [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF]; MemoryStream stream = new MemoryStream(); @@ -435,14 +434,16 @@ public static async Task ZipArchiveEntry_OpenInUpdateMode_UncompressedSizeGreate FieldInfo uncompressedSizeField = typeof(ZipArchiveEntry).GetField("_uncompressedSize", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(uncompressedSizeField); - uncompressedSizeField.SetValue(entry, (long)int.MaxValue + 100L); - - // Previously this threw ArgumentOutOfRangeException from the MemoryStream - // constructor inside GetUncompressedData because (int)_uncompressedSize wrapped - // to a negative value. With the fix it must succeed (the actual stored bytes are - // small, so loading completes normally). - Stream openStream = await OpenEntryStream(async, entry); - await DisposeStream(async, openStream); + uncompressedSizeField.SetValue(entry, (long)Array.MaxLength + 1L); + + if (async) + { + await Assert.ThrowsAsync(() => entry.OpenAsync()); + } + else + { + Assert.Throws(() => entry.Open()); + } await DisposeZipArchive(async, archive); }