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..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,8 +115,9 @@ 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 + // 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 e84666144c9379..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,8 +481,9 @@ 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 + // 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) @@ -1000,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 46d899426459e8..5f924485e734e2 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,46 @@ await Assert.ThrowsAsync(async () => await DisposeZipArchive(async, archive); } + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + 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(); + + 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)Array.MaxLength + 1L); + + if (async) + { + await Assert.ThrowsAsync(() => entry.OpenAsync()); + } + else + { + Assert.Throws(() => entry.Open()); + } + + await DisposeZipArchive(async, archive); + } + [Theory] [MemberData(nameof(Get_Booleans_Data))] public static async Task UnseekableVeryLargeArchive_DataDescriptor_Read_Zip64(bool async)