From c3e990fcfc3c50a0d0c85ce62f70bc2c7515b4e7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Oct 2025 13:47:34 +1000 Subject: [PATCH 1/2] Fix #2959 --- .../Tiff/Compression/TiffBaseDecompressor.cs | 16 ++-- .../Formats/Tiff/TiffDecoderCore.cs | 95 +++++++++++++++++-- 2 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index 03cd639ad3..86b5c19d21 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -28,20 +28,20 @@ protected TiffBaseDecompressor(MemoryAllocator memoryAllocator, int width, int b /// Decompresses image data into the supplied buffer. /// /// The to read image data from. - /// The strip offset of stream. - /// The number of bytes to read from the input stream. + /// The data offset within the stream. + /// The number of bytes to read from the input stream. /// The height of the strip. /// The output buffer for uncompressed data. /// The token to monitor cancellation. - public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + public void Decompress(BufferedReadStream stream, ulong offset, ulong count, int stripHeight, Span buffer, CancellationToken cancellationToken) { - DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset)); - DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount)); + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)long.MaxValue, nameof(offset)); + DebugGuard.MustBeLessThanOrEqualTo(count, (ulong)int.MaxValue, nameof(count)); - stream.Seek((long)stripOffset, SeekOrigin.Begin); - this.Decompress(stream, (int)stripByteCount, stripHeight, buffer, cancellationToken); + stream.Seek((long)offset, SeekOrigin.Begin); + this.Decompress(stream, (int)count, stripHeight, buffer, cancellationToken); - if ((long)stripOffset + (long)stripByteCount < stream.Position) + if ((long)offset + (long)count < stream.Position) { TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index fbff352970..1dab2547b7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; @@ -441,8 +442,14 @@ private void DecodeStripsPlanar( { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); - stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize); } using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); @@ -507,15 +514,83 @@ private void DecodeStripsChunky( rowsPerStrip = height; } - int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); int bitsPerPixel = this.BitsPerPixel; - using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); - Span stripBufferSpan = stripBuffer.GetSpan(); - Buffer2D pixels = frame.PixelBuffer; - using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); + Buffer2D pixels = frame.PixelBuffer; + + // There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue. + // We can read them, but we cannot allocate a buffer that large to hold the uncompressed data. + // In this scenario we fall back to reading and decoding one row at a time. + // + // The NoneTiffCompression decompressor can be used to read individual rows since we have + // a guarantee that each row required the same number of bytes. + if (decompressor is NoneTiffCompression none && uncompressedStripSize > int.MaxValue) + { + ulong bytesPerRowU = this.CalculateStripBufferSize(width, 1); + + // This should never happen, but we check just to be sure. + if (bytesPerRowU > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + int bytesPerRow = (int)bytesPerRowU; + using IMemoryOwner rowBufferOwner = this.memoryAllocator.Allocate(bytesPerRow, AllocationOptions.Clean); + Span rowBuffer = rowBufferOwner.GetSpan(); + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 + ? rowsPerStrip + : height % rowsPerStrip; + + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > height) + { + break; + } + + ulong baseOffset = stripOffsets[stripIndex]; + ulong available = stripByteCounts[stripIndex]; + ulong required = (ulong)bytesPerRow * (ulong)stripHeight; + if (available < required) + { + TiffThrowHelper.ThrowImageFormatException("StripByteCounts is smaller than required for uncompressed data."); + } + + for (int r = 0; r < stripHeight; r++) + { + cancellationToken.ThrowIfCancellationRequested(); + + ulong rowOffset = baseOffset + ((ulong)r * (ulong)bytesPerRow); + + // Use the NoneTiffCompression decompressor to read exactly one row. + none.Decompress( + this.inputStream, + rowOffset, + (ulong)bytesPerRow, + 1, + rowBuffer, + cancellationToken); + + colorDecoder.Decode(rowBuffer, pixels, 0, top + r, width, 1); + } + } + + return; + } + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate((int)uncompressedStripSize, AllocationOptions.Clean); + Span stripBufferSpan = stripBuffer.GetSpan(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { @@ -808,7 +883,7 @@ private IMemoryOwner ConvertNumbers(Array array, out Span span) /// The height for the desired pixel buffer. /// The index of the plane for planar image configuration (or zero for chunky). /// The size (in bytes) of the required pixel buffer. - private int CalculateStripBufferSize(int width, int height, int plane = -1) + private ulong CalculateStripBufferSize(int width, int height, int plane = -1) { DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); @@ -841,8 +916,8 @@ private int CalculateStripBufferSize(int width, int height, int plane = -1) } } - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - return bytesPerRow * height; + ulong bytesPerRow = (((ulong)width * (ulong)bitsPerPixel) + 7) / 8; + return bytesPerRow * (ulong)height; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 5a9f42d1ddd5567d531766e926d5ad122790225d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 24 Oct 2025 14:14:13 +1000 Subject: [PATCH 2/2] Break, don't throw --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 1dab2547b7..8a4a27946f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -559,7 +559,7 @@ private void DecodeStripsChunky( ulong required = (ulong)bytesPerRow * (ulong)stripHeight; if (available < required) { - TiffThrowHelper.ThrowImageFormatException("StripByteCounts is smaller than required for uncompressed data."); + break; } for (int r = 0; r < stripHeight; r++)