diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index bbb10afbcf342a..9c121844d89619 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -212,6 +212,9 @@ Entries larger than 4GB are not supported in Update mode. + + Entries with uncompressed data larger than 2GB are not supported in Update mode. + End of Central Directory record could not be found. 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..353b1a71e7631d 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,14 @@ 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 + // MemoryStream is backed by a single byte[] and cannot grow beyond Array.MaxLength. + // Validate up front before attempting the (int) cast. + if ((ulong)_uncompressedSize > (ulong)Array.MaxLength) + { + _currentlyOpenForWrite = false; + throw new InvalidDataException(SR.EntryUncompressedSizeTooLargeForUpdateMode); + } + _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..755e88abf67550 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,14 @@ 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 + // MemoryStream is backed by a single byte[] and cannot grow beyond Array.MaxLength. + // Validate up front before attempting the (int) cast. + if ((ulong)_uncompressedSize > (ulong)Array.MaxLength) + { + _currentlyOpenForWrite = false; + throw new InvalidDataException(SR.EntryUncompressedSizeTooLargeForUpdateMode); + } + _storedUncompressedData = new MemoryStream((int)_uncompressedSize); 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..25aa227a0dd63f 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,47 @@ 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 + // when _uncompressedSize exceeds int.MaxValue. + 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 + 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)