diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..5f9f694351 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +# GitHub Copilot Instructions + +Read and follow [AGENTS.md](../AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Prefer existing local patterns and repository configuration whenever generated code or suggestions are accepted. diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2e3c8a20a8..4c3836654f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,41 +19,17 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - - os: ubuntu-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: macos-26-intel - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: windows-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: buildjet-4vcpu-ubuntu-2204-arm - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: ubuntu-latest + - os: ubuntu-22.04-arm framework: net6.0 sdk: 6.0.x runtime: -x64 codecov: false - - os: macos-26-intel + - os: ubuntu-22.04 framework: net6.0 sdk: 6.0.x runtime: -x64 codecov: false - - os: windows-latest + - os: windows-2022 framework: net6.0 sdk: 6.0.x runtime: -x64 @@ -61,7 +37,7 @@ jobs: exclude: - isARM: false options: - os: buildjet-4vcpu-ubuntu-2204-arm + os: ubuntu-22.04-arm runs-on: ${{matrix.options.os}} @@ -72,18 +48,6 @@ jobs: sudo apt-get update sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - - name: Install libgdi+, which is required for tests running on macos - if: ${{ contains(matrix.options.os, 'macos-26') }} - run: | - brew update - brew install mono-libgdiplus - # Create symlinks to make libgdiplus discoverable - sudo mkdir -p /usr/local/lib - sudo ln -sf $(brew --prefix)/lib/libgdiplus.dylib /usr/local/lib/libgdiplus.dylib - # Verify installation - ls -la $(brew --prefix)/lib/libgdiplus* || echo "libgdiplus not found in brew prefix" - ls -la /usr/local/lib/libgdiplus* || echo "libgdiplus not found in /usr/local/lib" - - name: Git Config shell: bash run: | @@ -101,7 +65,7 @@ jobs: run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - name: Git Setup LFS Cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: lfs-cache with: path: .git/lfs @@ -114,7 +78,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget @@ -126,50 +90,32 @@ jobs: with: dotnet-version: | 8.0.x - 7.0.x 6.0.x - name: DotNet Build - if: ${{ matrix.options.sdk-preview != true }} shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" env: SIXLABORS_TESTING: True - - name: DotNet Build Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING_PREVIEW: True - - name: DotNet Test - if: ${{ matrix.options.sdk-preview != true }} shell: pwsh run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" env: SIXLABORS_TESTING: True XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - name: DotNet Test Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING_PREVIEW: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - name: Export Failed Output uses: actions/upload-artifact@v4 if: failure() with: - name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + name: actual_output_${{ matrix.options.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip path: tests/Images/ActualOutput/ Publish: needs: [Build] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: (github.event_name == 'push') @@ -190,7 +136,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index de95bf01b2..122d5a7665 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: options: - - os: ubuntu-latest + - os: ubuntu-22.04 framework: net6.0 runtime: -x64 codecov: true @@ -39,7 +39,7 @@ jobs: run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - name: Git Setup LFS Cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: lfs-cache with: path: .git/lfs @@ -52,7 +52,7 @@ jobs: uses: NuGet/setup-nuget@v1 - name: NuGet Setup Cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: nuget-cache with: path: ~/.nuget @@ -64,7 +64,6 @@ jobs: with: dotnet-version: | 8.0.x - 7.0.x 6.0.x - name: DotNet Build @@ -81,14 +80,15 @@ jobs: XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - name: Export Failed Output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip path: tests/Images/ActualOutput/ - name: Codecov Update - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') with: flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..38cc8eae99 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Six Labors AI Coding Guidelines + +These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree. + +## Working Practices + +- Inspect only the source and supporting files needed for the task before proposing or making changes. Do not infer current behavior when the source is available. +- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn. +- Trace the existing execution path before changing it. Extend and optimize the existing implementation within the requested behavior and affected path. +- Before implementing an operation, find and inspect the existing APIs that provide it. Use or extend those APIs instead of writing equivalent logic elsewhere. This includes color conversion, pixel conversion, memory management, and vectorized operations. If an API lacks required behavior, improve its owning implementation rather than bypassing it with a separate implementation. +- Follow existing naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative. +- Use simple technical English in all communication, documentation, and comments. Use short sentences, active voice, and existing code terminology. +- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details. +- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested. +- Build .NET projects in Release configuration unless explicitly instructed otherwise. + +## Performance + +- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths. +- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data. +- Reuse existing memory ownership, pooling, span, vectorization, and parallelization mechanisms. +- Use a SIMD-first design for suitable data processing code. Build on existing vectorized operations, dispatch mechanisms, and scalar fallbacks. Keep hot loops simple, hoist invariant work, and preserve memory locality. +- Preserve correctness and maintainability during optimization. Distinguish source-based reasoning from measured results. Claim a speedup only with measurements. Use existing benchmarks first. Add or update cases only when they measure relevant behavior that existing benchmarks miss. +- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes. + +## C# Conventions + +- Follow local patterns when they comply with the explicit rules. +- Do not use `record` or `record struct` types. +- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states. +- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or independently complex logic. +- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace. +- Document every added method, constructor, and property, regardless of visibility. Update documentation when a member's behavior or contract changes. Keep public API documentation limited to observable behavior. Use private and internal documentation to explain the contract and intent. +- Add inline comments that explain why complex code works. Explain algorithms, formulas, invariants, memory ownership, compatibility behavior, and performance decisions where they apply. +- For SIMD, explain lane layouts, operations, alignment, remainders, and scalar equivalence so an unfamiliar maintainer can follow the code. + +## Verification + +- Add or update tests only when they prove a required behavior or expose a real defect. Derive expectations from established contracts, explicit requirements, or defect evidence. Use source to identify contracts and integration points, not to copy implementation results into expectations. Prefer extending existing tests. Avoid redundant cases and assertions that merely repeat the implementation. +- Never weaken, skip, or bypass a valid test to make it pass. Fix the production defect or the genuine test defect. Replace or remove a test only with evidence that its expectation is invalid or its coverage is redundant. +- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts merely to silence a failure. Investigate mismatches. Update expected results only for an established requirement change or an incorrect reference result proved by independent contract or defect evidence. +- Test the actual behavior through the existing execution path. Synthetic inputs must exercise an established requirement or reproduce a real defect. Do not invent requirements or substitute internal structure and helper calls for assertions about required behavior. Run the narrowest relevant formatting, test, and Release build commands. +- Report what changed, the verification performed, and any remaining risks or unverified assumptions. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..5f08a449a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude Code Instructions + +Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `CLAUDE.md` found below the files being changed. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000000..b621d2652e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,3 @@ +# Gemini CLI Instructions + +Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `GEMINI.md` found below the files being changed. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..2e060f6287 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,103 @@ +# Security Policy + +## Supported Versions + +Six Labors provides security updates for the latest major version of each library. + +Each major version remains eligible for security updates for 12 months after the first stable release of the next major version of that library. +After this period, that major version is end-of-life and does not receive security updates. + +Users must install the latest available patch or minor release within a supported major version to receive security fixes. +Older releases within that major version are not maintained separately. + +| Version | Supported | +| ------- | --------- | +| Latest major version | Yes | +| Superseded major version within 12 months of the next major's first stable release | Yes | +| Superseded major version after that 12-month period | No | + +Security updates may require a license key under the existing license terms. This does not change the license terms. + +This policy covers security fixes only. It does not include feature backports or extend support for underlying .NET versions. + +Security fixes, if any, are provided at Six Labors' discretion. + +This policy does not create any obligation to provide support, maintenance services, SLAs, custom fixes, hosted services, managed services, operational monitoring, professional services, consulting, or certification of customer products. + +## Reporting a Vulnerability + +Please report suspected security vulnerabilities using GitHub private vulnerability reporting for the relevant Six Labors repository, where available. + +If GitHub private vulnerability reporting is not available for a repository, please report suspected security vulnerabilities by contacting Six Labors through the contact details published on the Six Labors website. + +Do not report security vulnerabilities through public GitHub issues. + +When reporting a vulnerability, please include as much relevant information as possible: + +* affected package and version +* target framework and runtime +* operating system +* input file or minimal reproduction, if safe to share +* expected and actual behavior +* potential security impact +* whether you believe the issue is being actively exploited + +Six Labors may review reported vulnerabilities and determine whether they are security issues affecting a supported version. + +A report may be declined or closed without action if, in Six Labors' opinion, it: + +* is not reproducible +* does not affect a supported version +* affects only an unsupported or end-of-life version +* is not a security vulnerability +* depends on unsafe, unsupported, or unintended use +* depends on a vulnerable application, environment, dependency, configuration, or deployment outside the Six Labors library itself +* lacks sufficient information for assessment +* is duplicative +* has already been fixed +* is otherwise outside the scope of this policy + +If a vulnerability is accepted, Six Labors may handle it through GitHub Security Advisories and, where appropriate, CVE assignment. + +Six Labors does not guarantee any response time, fix time, release date, advisory publication date, CVE assignment, workaround, mitigation, or particular outcome for any report. + +## Scope + +This policy applies only to security vulnerabilities in Six Labors libraries themselves. + +This policy does not apply to: + +* customer applications +* customer products +* customer deployments +* customer infrastructure +* customer data +* third-party services +* unsupported versions +* end-of-life versions +* forks or modified versions +* usage outside the documented or intended behavior of the relevant library + +Organizations using Six Labors libraries are responsible for assessing, securing, testing, monitoring, updating, and maintaining their own applications, products, deployments, infrastructure, and supply chains. + +## Cyber Resilience Act + +Six Labors libraries are general-purpose software libraries. + +They are not cybersecurity products, identity or access management systems, password managers, operating systems, browsers, firewalls, network management tools, SIEM tools, hypervisors, container runtimes, or other Cyber Resilience Act important or critical product classes. + +If a Six Labors library is treated as a product with digital elements under the Cyber Resilience Act, Six Labors assesses it as an ordinary software component. + +Organizations incorporating Six Labors libraries into products made available on the EU market are responsible for assessing and meeting their own regulatory obligations for those products, including any obligations under the Cyber Resilience Act. + +Six Labors does not provide support, maintenance services, SLAs, managed services, hosted services, operational monitoring, custom fixes, professional services, consulting, or certification of customer products. + +Security vulnerabilities in supported Six Labors libraries are handled through the GitHub Security Advisory process for the relevant repository, where appropriate. + +From 11 September 2026, if Six Labors becomes aware of credible active exploitation of a vulnerability in a supported Six Labors library, or a severe security incident affecting a supported Six Labors library, Six Labors may report the matter through the applicable Cyber Resilience Act reporting mechanism where legally required. + +## No Warranty + +Six Labors libraries are provided in accordance with their applicable license terms. + +Nothing in this policy creates any warranty, representation, guarantee, support obligation, maintenance obligation, service commitment, regulatory certification, or assumption of responsibility for any customer product, customer deployment, customer compliance obligation, or third-party system. diff --git a/shared-infrastructure b/shared-infrastructure index 353b9afe32..af0e25f405 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19 +Subproject commit af0e25f405788d8c7a64c7ca19e00ae2a2e48ffa diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 553a7c2e89..a96eaeff58 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -27,7 +27,17 @@ internal static class ColorNumerics /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels) - => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + { + float luminance = Vector4.Dot(vector, Bt709); + if (float.IsNaN(luminance)) + { + return 0; + } + + // Floating-point pixel formats can contain values outside their normalized range. + luminance = Math.Clamp(luminance, 0F, 1F); + return (int)MathF.Round(luminance * (luminanceLevels - 1)); + } /// /// Gets the luminance from the rgb components using the formula diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index b223bda4f5..5b214fed72 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1337,12 +1337,22 @@ private void ReadInfoHeader(BufferedReadStream stream) // > 108 bytes infoHeaderType = BmpInfoHeaderType.WinVersion5; this.infoHeader = BmpInfoHeader.ParseV5(buffer); - if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + if (!this.Options.SkipMetadata && this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) { // Read color profile. long streamPosition = stream.Position; + long profileStart = infoHeaderStart + this.infoHeader.ProfileData; + + // Validate the external offset and size before allocating. Subtraction avoids + // overflowing an end offset calculated from the declared profile size. + if (this.infoHeader.ProfileData < 0 || this.infoHeader.ProfileSize <= 0 || + profileStart > stream.Length || this.infoHeader.ProfileSize > stream.Length - profileStart) + { + BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); + } + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; - stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + stream.Position = profileStart; stream.Read(iccProfileData); this.metadata.IccProfile = new IccProfile(iccProfileData); stream.Position = streamPosition; @@ -1436,6 +1446,11 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b this.infoHeader.Height = -this.infoHeader.Height; } + if (this.infoHeader.Width <= 0 || this.infoHeader.Height <= 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); + } + int bytesPerColorMapEntry = 4; int colorMapSizeBytes = -1; if (this.infoHeader.ClrUsed == 0) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index d9f6ae3ac4..df900057d3 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -313,6 +313,10 @@ private void ReadLogicalScreenDescriptor(BufferedReadStream stream) } this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer.Span); + if (this.logicalScreenDescriptor.Width == 0 || this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); + } } /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f6dad651b7..1d8f4ff8c3 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -181,11 +181,6 @@ protected override Image Decode(BufferedReadStream stream, Cance switch (chunk.Type) { case PngChunkType.Header: - if (!Equals(this.header, default(PngHeader))) - { - PngThrowHelper.ThrowInvalidHeader(); - } - this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.AnimationControl: @@ -614,7 +609,7 @@ private void InitializeImage(ImageMetadata metadata, FrameControl frameC frameMetadata.FromChunk(in frameControl); this.bytesPerPixel = this.CalculateBytesPerPixel(); - this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; + this.bytesPerScanline = CalculateScanlineLength(this.header.Width, this.header.BitDepth, this.bytesPerPixel) + 1; this.bytesPerSample = 1; if (this.header.BitDepth >= 8) { @@ -714,21 +709,29 @@ private int CalculateBytesPerPixel() /// Calculates the scanline length. /// /// The width of the row. + /// The number of bits per sample. + /// The number of bytes per pixel. /// /// The representing the length. /// - private int CalculateScanlineLength(int width) + internal static int CalculateScanlineLength(int width, int bitDepth, int bytesPerPixel) { - int mod = this.header.BitDepth == 16 ? 16 : 8; - int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; + int mod = bitDepth == 16 ? 16 : 8; + long scanlineLength = (long)width * bitDepth * bytesPerPixel; - int amount = scanlineLength % mod; + long amount = scanlineLength % mod; if (amount != 0) { scanlineLength += mod - amount; } - return scanlineLength / mod; + scanlineLength /= mod; + if (scanlineLength >= int.MaxValue) + { + PngThrowHelper.ThrowInvalidImageContentException("PNG scanline length exceeds the supported maximum."); + } + + return (int)scanlineLength; } /// @@ -800,7 +803,7 @@ private void DecodePixelData( while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); - int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1; + int bytesPerFrameScanline = CalculateScanlineLength((int)frameControl.Width, this.header.BitDepth, this.bytesPerPixel) + 1; Span scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline]; Span prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline]; @@ -903,7 +906,7 @@ private void DecodeInterlacedPixelData( continue; } - int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; + int bytesPerInterlaceScanline = CalculateScanlineLength(numColumns, this.header.BitDepth, this.bytesPerPixel) + 1; while (currentRow < endRow) { @@ -1334,6 +1337,11 @@ private FrameControl ReadFrameControlChunk(ReadOnlySpan data) /// The containing data. private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) { + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } + this.header = PngHeader.Parse(data); this.header.Validate(); diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 8dc70e1d9a..d62ae743f5 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Png; internal static class PngThrowHelper { + /// + /// Throws an exception for invalid PNG content. + /// + /// The reason the content is invalid. + [DoesNotReturn] + public static void ThrowInvalidImageContentException(string errorMessage) + => throw new InvalidImageContentException(errorMessage); + [DoesNotReturn] public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index e2dd919d29..631b536673 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -72,9 +72,9 @@ protected override Image Decode(BufferedReadStream stream, Cance TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } - if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + if (this.fileHeader.Width <= 0 || this.fileHeader.Height <= 0) { - throw new UnknownImageFormatException("Width or height cannot be 0"); + TgaThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); } Image image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index dc9e1e7296..74914e036f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -126,6 +126,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span< } } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // A pixel can require a 13-bit terminating code. Each row can also require + // an 8-bit zero-length white run and a 12-bit EOL, plus the initial EOL. + return 12L + ((((long)this.Width * 13) + 20) * rowsPerStrip); + } + private void WriteEndOfLine(Span compressedData) { if (this.useModifiedHuffman) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs index 692e088d1c..8cf23fea1c 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -131,6 +131,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span< this.WriteCode(12, 1, compressedData); } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // Alternating pixels use at most 29 bits per two-pixel horizontal mode. + // Allow 16 bits per pixel, row transition overhead, and the final 24-bit EOFB. + return ((((long)this.Width * 16) + 24) * rowsPerStrip) + 24; + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs index 4e04f9cf17..46c686d7c9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -465,6 +465,12 @@ protected void PadByte() /// The destination buffer to write the code to. protected void WriteCode(uint codeLength, uint code, Span compressedData) { + long availableBits = (((long)compressedData.Length - this.bytePosition) * 8) - this.bitPosition; + if (codeLength > availableBits) + { + throw new InvalidMemoryOperationException("The CCITT output buffer is too small for the encoded data."); + } + while (codeLength > 0) { int bitNumber = (int)codeLength; @@ -526,8 +532,20 @@ public override void CompressStrip(Span rows, int height) /// public override void Initialize(int rowsPerStrip) { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip); + ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8); + if (maxNeededBytes > int.MaxValue) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue); + } + + this.compressedDataBuffer = this.Allocator.Allocate((int)maxNeededBytes); } + + /// + /// Gets an upper bound for the encoded strip length in bits. + /// + /// The number of rows in the strip. + /// The maximum encoded length. + protected abstract long GetMaximumEncodedBits(int rowsPerStrip); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs index d2dbedc9cb..5ef572fb1f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs @@ -53,6 +53,12 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (bitReader.RunLength > 0) { + // A decoded run is untrusted and must fit the current row before the unchecked bit writer is used. + if (bitReader.RunLength > (nuint)this.Width - pixelsWritten) + { + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); + } + if (bitReader.IsWhiteRun) { BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.whiteValue); @@ -86,11 +92,6 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int bitReader.StartNewRow(); } - - if (pixelsWritten > (ulong)this.Width) - { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error, decoded more pixels then image width"); - } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs index bfc4f9beed..982f4db8b9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs @@ -319,7 +319,7 @@ public void ReadNextRun() { if (this.CurValueBitsRead > this.maxCodeLength) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: invalid code length read"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: invalid code length read."); } bool isMakeupCode = this.IsMakeupCode(); @@ -401,7 +401,7 @@ protected virtual void ReadEolBeforeFirstData() if (!this.IsEndOfScanLine) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error: expected start of data marker not found"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: expected start of data marker not found."); } this.Reset(); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs index 6bdcad2b81..b586fdc81f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs @@ -72,7 +72,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (bitReader.RunLength > 0) { - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelsWritten); bitsWritten += (int)bitReader.RunLength; pixelsWritten += bitReader.RunLength; @@ -102,12 +102,18 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (pixelsWritten > 0 && pixelsWritten < (ulong)this.width) { bitReader.ReadNextRun(); - this.WritePixelRun(buffer, bitReader, bitsWritten); + this.WritePixelRun(buffer, bitReader, bitsWritten, pixelsWritten); } } - private void WritePixelRun(Span buffer, T4BitReader bitReader, nint bitsWritten) + private void WritePixelRun(Span buffer, T4BitReader bitReader, nint bitsWritten, nuint pixelsWritten) { + // A decoded run is untrusted and must fit the current row before the unchecked bit writer is used. + if (bitReader.RunLength > (nuint)this.width - pixelsWritten) + { + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); + } + if (bitReader.IsWhiteRun) { BitWriterUtils.WriteBits(buffer, bitsWritten, (int)bitReader.RunLength, this.whiteValue); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs index c868fec626..4ad7818da8 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6TiffCompression.cs @@ -163,7 +163,7 @@ private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, Cc int runLength = (int)bitReader.RunLength; if (runLength > (uint)(scanline.Length - unpacked)) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); } scanline.Slice(unpacked, runLength).Fill(fillByte); @@ -175,7 +175,7 @@ private static void Decode2DScanline(T6BitReader bitReader, bool whiteIsZero, Cc runLength = (int)bitReader.RunLength; if (runLength > (uint)(scanline.Length - unpacked)) { - TiffThrowHelper.ThrowImageFormatException("ccitt compression parsing error"); + TiffThrowHelper.ThrowImageFormatException("CCITT compression parsing error: decoded more pixels than the image width."); } scanline.Slice(unpacked, runLength).Fill(fillByte); diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index 2f3176f013..8bd5697756 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -29,6 +29,10 @@ public static TiffBaseDecompressor Create( int tileWidth = 0, int tileHeight = 0) { + // Fax compression emits one decoded row at a time, so tiled images must use + // the tile row width that was used to size the caller's destination buffer. + int faxWidth = isTiled ? tileWidth : width; + switch (method) { case TiffDecoderCompressionType.None: @@ -51,15 +55,15 @@ public static TiffBaseDecompressor Create( case TiffDecoderCompressionType.T4: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); - return new T4TiffCompression(allocator, fillOrder, width, bitsPerPixel, faxOptions, photometricInterpretation); + return new T4TiffCompression(allocator, fillOrder, faxWidth, bitsPerPixel, faxOptions, photometricInterpretation); case TiffDecoderCompressionType.T6: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); - return new T6TiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + return new T6TiffCompression(allocator, fillOrder, faxWidth, bitsPerPixel, photometricInterpretation); case TiffDecoderCompressionType.HuffmanRle: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); - return new ModifiedHuffmanTiffCompression(allocator, fillOrder, width, bitsPerPixel, photometricInterpretation); + return new ModifiedHuffmanTiffCompression(allocator, fillOrder, faxWidth, bitsPerPixel, photometricInterpretation); case TiffDecoderCompressionType.Jpeg: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 134f8e2aa0..43f1a3f93b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -616,7 +616,7 @@ private void DecodeTilesPlanar( tilesBuffers[i] = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); } - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, true, tileWidth, tileLength); TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); int tileIndex = 0; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 6926928ea9..8b6710a61e 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -289,6 +289,12 @@ public static uint ReadChunkSize(Stream stream, Span buffer) if (stream.Read(buffer) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + if (chunkSize == uint.MaxValue) + { + // The mandatory padding byte cannot fit in the returned 32-bit extent. + WebpThrowHelper.ThrowInvalidImageContentException("Invalid WebP chunk size."); + } + return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; } @@ -326,11 +332,12 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType long streamLength = stream.Length; while (stream.Position < streamLength) { - uint chunkLength = ReadChunkSize(stream, buffer); + int chunkLength = ValidateMetadataChunkSize(stream, ReadChunkSize(stream, buffer)); if (ignoreMetaData) { stream.Skip((int)chunkLength); + continue; } int bytesRead; @@ -381,6 +388,22 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType } } + /// + /// Validates a metadata extent before allocating or skipping its payload. + /// + /// The stream positioned at the payload. + /// The declared size including padding. + /// The size supported by the array and stream read APIs. + public static int ValidateMetadataChunkSize(BufferedReadStream stream, uint chunkSize) + { + if (chunkSize > int.MaxValue || chunkSize > stream.Length - stream.Position) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); + } + + return (int)chunkSize; + } + private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag) { if (exifProfile.TryGetValue(tag, out IExifValue? resolution)) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 362677eeee..307d4b905e 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -351,6 +351,13 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, Span buffer) { uint exifChunkSize = ReadChunkSize(stream, buffer); + if (exifChunkSize > int.MaxValue || exifChunkSize > stream.Length - stream.Position) + { + // Preserve v3's recovery from truncated EXIF without allocating its declared size. + stream.Position = stream.Length; + return; + } + if (this.skipMetadata) { stream.Skip((int)exifChunkSize); @@ -401,6 +408,13 @@ private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag buffer) { uint xmpChunkSize = ReadChunkSize(stream, buffer); + if (xmpChunkSize > int.MaxValue || xmpChunkSize > stream.Length - stream.Position) + { + // Preserve v3's recovery from truncated XMP without allocating its declared size. + stream.Position = stream.Length; + return; + } + if (this.skipMetadata) { stream.Skip((int)xmpChunkSize); @@ -427,7 +441,7 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, S /// Temporary buffer. private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, Span buffer) { - uint iccpChunkSize = ReadChunkSize(stream, buffer); + int iccpChunkSize = WebpChunkParsingUtils.ValidateMetadataChunkSize(stream, ReadChunkSize(stream, buffer)); if (this.skipMetadata) { stream.Skip((int)iccpChunkSize); @@ -532,6 +546,12 @@ private static uint ReadChunkSize(BufferedReadStream stream, Span buffer, if (stream.Read(buffer, 0, 4) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + if (chunkSize == uint.MaxValue) + { + // The mandatory padding byte cannot fit in the returned 32-bit extent. + WebpThrowHelper.ThrowInvalidImageContentException("Invalid WebP chunk size."); + } + return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 24b880a770..2c0ee4922d 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -29,8 +29,8 @@ - - 3.1 + + 3.2 diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index d0dcaea9d5..79e7a6011f 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -224,6 +224,13 @@ protected void ReadValues64(List values, ulong offset) this.Seek(offset); ulong count = this.ReadUInt64(); + // Each entry occupies 20 bytes and the directory ends with an 8-byte next-IFD offset. + long remainingDirectoryBytes = this.data.Length - this.data.Position; + if (remainingDirectoryBytes < 8 || count > (ulong)((remainingDirectoryBytes - 8) / 20)) + { + throw new InvalidImageContentException("The BigTIFF directory entry count exceeds the available data."); + } + Span offsetBuffer = stackalloc byte[8]; for (ulong i = 0; i < count; i++) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs index e88dd8d9e1..95d23ccb83 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs @@ -76,13 +76,7 @@ public IccClut ReadClut(int inChannelCount, int outChannelCount, bool isFloat) public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 1); const float Max = byte.MaxValue; @@ -110,13 +104,7 @@ public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPoi public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 2); const float Max = ushort.MaxValue; @@ -144,13 +132,7 @@ public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPo public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChCount); - } - - length /= inChCount; + int length = this.GetClutLength(inChCount, outChCount, gridPointCount, 4); var values = new float[length][]; for (int i = 0; i < length; i++) @@ -165,4 +147,38 @@ public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) this.currentIndex = start + (length * outChCount * 4); return new IccClut(values, gridPointCount, IccClutDataType.Float); } + + /// + /// Gets the CLUT node count after checking its dimensions and available sample data. + /// + /// The number of input dimensions. + /// The number of values at each node. + /// The number of nodes along each input dimension. + /// The encoded size of each value in bytes. + /// The number of CLUT nodes. + private int GetClutLength(int inputChannelCount, int outputChannelCount, byte[] gridPointCount, int bytesPerValue) + { + int length = 1; + for (int i = 0; i < inputChannelCount; i++) + { + int gridPoints = gridPointCount[i]; + if (gridPoints == 0 || length > int.MaxValue / gridPoints) + { + throw new InvalidIccProfileException("Invalid CLUT dimensions."); + } + + length *= gridPoints; + } + + // A multidimensional grid contains the product of its dimension sizes. Check the + // complete encoded payload before allocating the node arrays from untrusted dimensions. + long valueCount = (long)length * outputChannelCount; + long byteCount = valueCount * bytesPerValue; + if (valueCount > int.MaxValue || byteCount > this.data.Length - this.currentIndex) + { + throw new InvalidIccProfileException("The CLUT data is shorter than its declared dimensions."); + } + + return length; + } } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 45074c9a6e..d80ac49275 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -128,6 +128,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) } List table = new((int)tagCount); + uint dataLength = (uint)reader.DataLength; for (int i = 0; i < tagCount; i++) { uint tagSignature = reader.ReadUInt32(); @@ -135,7 +136,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) uint tagSize = reader.ReadUInt32(); // Exclude entries that have nonsense values and could cause exceptions further on - if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128) + if (tagSize >= 8 && tagOffset <= dataLength && tagSize <= dataLength - tagOffset) { table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize)); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 1b4a43129b..195d93c970 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -19,6 +19,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp; [ValidateDisposedMemoryAllocations] public class BmpDecoderTests { + /// + /// Rejects the reported BMP profile extent before allocating its declared size. + /// + [Fact] + public void Decode_WithProfileLargerThanRemainingData_ThrowsInvalidImageContentException() + { + byte[] payload = Convert.FromHexString( + "424D8E000000000000008A0000007C0000000100000001000000010018000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000C800" + + "00000000004000000000000000"); + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; public static readonly string[] MiscBmpFiles = Miscellaneous; diff --git a/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs new file mode 100644 index 0000000000..bcf7df5984 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Formats; + +public class InvalidImageDimensionsTests +{ + [Theory] + [InlineData("Qk1GAAAAAAAAADYAAAAoAAAAAgACAAAAAAABABgAAAAAABAAAAATCwAAEwsAAAAAAAAAAAAAAAD/AP8AAAAAAP8A/wAAAA==")] + [InlineData("R0lGODdhAgIAAIEAAAD/AP8AAAAA/wAAACwAAAQAAgACAAAIBwADABAQICAAOw==")] + [InlineData("AAACAAAAAAAAAAAAAgDCsRgAAgAAAP8A/wD/AAAA//8=")] + public void Load_WithNonPositiveDimensions_ThrowsInvalidImageContentException(string encodedData) + { + byte[] data = Convert.FromBase64String(encodedData); + + Assert.Throws(() => Image.Load(data)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs new file mode 100644 index 0000000000..95c452c0f8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Png; + +namespace SixLabors.ImageSharp.Tests.Formats.Png; + +[Trait("Format", "Png")] +public class PngDecoderCoreTests +{ + [Fact] + public void CalculateScanlineLength_WithLargeGrayscaleWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(536_870_913, 8, 1); + + Assert.Equal(536_870_913, length); + } + + [Fact] + public void CalculateScanlineLength_WithLargeRgbaWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(33_554_432, 16, 8); + + Assert.Equal(268_435_456, length); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index aff8bc12a2..088d7f2c30 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -77,6 +77,19 @@ public void Decode_IncorrectCRCForCriticalChunk_ExceptionIsThrown(uint chunkType } } + [Fact] + public void DecodeAndIdentify_WithDuplicateHeader_ThrowInvalidImageContentException() + { + using MemoryStream payloadStream = new(); + payloadStream.Write(Raw1X1PngIhdrAndpHYs); + payloadStream.Write(Raw1X1PngIhdrAndpHYs.AsSpan(8, 25)); + payloadStream.Write(Raw1X1PngIdatAndIend); + byte[] payload = payloadStream.ToArray(); + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + private static string GetChunkTypeName(uint value) { byte[] data = new byte[4]; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs index 8e90b1dd57..5106355690 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -115,4 +115,17 @@ public void TiffDecoder_SubIfd8(TestImageProvider provider) Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripOffsets)); Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts)); } + + [Fact] + public void TiffDecoder_DirectoryEntryCountExceedsAvailableData_Throws() + { + byte[] data = + [ + 0x49, 0x49, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF2, 0x05, 0x2A, 0x01, 0x00, 0x00, 0x00, + ]; + + Assert.Throws(() => Image.Load(data)); + } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs new file mode 100644 index 0000000000..b27fbbc2b4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/CcittTiffCompressionTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.IO.Compression; +using SixLabors.ImageSharp.Formats; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression; + +[Trait("Format", "Tiff")] +public class CcittTiffCompressionTests +{ + private const string T4PocGzipBase64 = + "H4sICBOogmoCA3BvYy10aWZmLXQ0LnRpZgDty7ENwjAQhlFfjKKUoYGSPlPQZopskGVYiJFgA2zpFGUG9Kz3S19xXtelTKX0xaVEq2dbZPcNUY+u2bVtzO7vmvd72+3095792vJyfsQH" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgH33fP9Zj5xoBYAEA"; + + private const string ModifiedHuffmanPocGzipBase64 = + "H4sICBSogmoCA3BvYy10aWZmLW1oLnRpZgDty7ENgzAQhlEfRBElNKRMnylomSIbZBkWYiTYAFs6RZkhetb7pa84r+urDKW0xa1EraUustu66L/dZ3d19+z2prz/1M0/fx/Z2zsvx2cc" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+jcL3Buw0IBYAEA"; + + private const string TiledT4ReporterPocGzipBase64 = + "H4sIAAAAAAACCu3UKw7CUBCG0RkuIeC6AxQGj8TwEHWshyWxOhI2QC8Z0QWAIDnNmeQXran4xnEf64jYROQyMvo8RtYepltk++x+rXabblW7P6fZ++fZvtS+T3et/djVR8M2n/Btr1v6C/zCQbQQLUQLRAvRQrRAtBAtRAtEC9FCtEC0EC3+Mlpv/6RrkiQmAAA="; + + [Theory] + [InlineData(T4PocGzipBase64)] + [InlineData(ModifiedHuffmanPocGzipBase64)] + public void Decode_CcittStripWithOversizedRun_ThrowsImageFormatException(string gzipBase64) + { + using MemoryStream tiffStream = CreateTiffStream(gzipBase64); + + ImageFormatException exception = Assert.Throws(() => Image.Load(tiffStream)); + + Assert.Equal("CCITT compression parsing error: decoded more pixels than the image width.", exception.Message); + } + + [Fact] + public void Decode_TiledCcittReporterSample_ThrowsImageFormatException() + { + // This is the reporter's exact tiled T4 TIFF sample, gzip-compressed only to keep the test source small. + using MemoryStream tiffStream = CreateTiffStream(TiledT4ReporterPocGzipBase64); + + ImageFormatException exception = Assert.Throws(() => Image.Load(tiffStream)); + + Assert.Equal("CCITT compression parsing error: decoded more pixels than the image width.", exception.Message); + } + + private static MemoryStream CreateTiffStream(string gzipBase64) + { + // Keep the TIFF payloads compressed so the regression source remains small. + byte[] compressed = Convert.FromBase64String(gzipBase64); + using MemoryStream compressedStream = new(compressed); + using GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress); + MemoryStream tiffStream = new(); + + gzipStream.CopyTo(tiffStream); + tiffStream.Position = 0; + + return tiffStream; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 86d518e902..80ac8e4882 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -493,6 +493,45 @@ public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WhiteIsZero_ public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + /// + /// CCITT row framing must fit even when each row contains only one pixel. + /// + /// The image width. + [Theory] + [InlineData(1)] + [InlineData(64)] + public void TiffEncoder_EncodeNarrowCcittGroup3Fax_Works(int width) + { + using Image image = new(width, 2000); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = new L8((byte)(((x + y) & 1) == 0 ? 255 : 0)); + } + } + + TiffFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + metadata.BitsPerPixel = TiffBitsPerPixel.Bit1; + metadata.Compression = TiffCompression.CcittGroup3Fax; + + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(image.Size, decoded.Size); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(image[x, y], decoded[x, y]); + } + } + } + [Theory] [WithFile(Issues2255, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WithoutSpecifyingBitPerPixel_Works(TestImageProvider provider) @@ -508,6 +547,26 @@ public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_WhiteIsZero_ public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); + /// + /// Re-encoding a one-pixel Group 4 image must retain its pixel and fit the end-of-block code. + /// + [Fact] + public void TiffEncoder_ReencodeNarrowCcittGroup4Fax_Works() + { + byte[] data = Convert.FromBase64String( + "SUkqAAgAAAAJAAABAwABAAAAAQAAAAEBAwABAAAAAQAAAAIBAwABAAAAAQAAAAMBAwABAAAABAAAAAYBAwABAAAAAAAAABEBBAABAAAA" + + "egAAABUBAwABAAAAAQAAABYBBAABAAAAAQAAABcBBAABAAAABAAAAAAAAACACACA"); + + using Image image = Image.Load(data); + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(new Size(1, 1), decoded.Size); + Assert.Equal(image[0, 0], decoded[0, 0]); + } + [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index ab8fef60f7..5d8275221e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -13,6 +13,22 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp; [Trait("Format", "Webp")] public class WebpMetaDataTests { + /// + /// Rejects ICC payload sizes that cannot be read before allocating the profile. + /// + /// The reported malformed WebP file. + [Theory] + [InlineData("524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304")] + [InlineData("524946460000000057454250565038580A00000020000000010000010000494343500000004000000000")] + [InlineData("524946462200000057454250565038580A0000002000000000000000000049434350FFFFFFFF01020304")] + public void DecodeAndIdentify_WithInvalidIccExtent_ThrowInvalidImageContentException(string hex) + { + byte[] payload = Convert.FromHexString(hex); + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + [Theory] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs index 63585a3bd4..e271a374a5 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs @@ -52,6 +52,36 @@ internal void ReadClutF32(byte[] data, IccClut expected, int inChannelCount, int Assert.Equal(expected, output); } + [Fact] + public void ReadClut_WithOversizedDimensions_ThrowsInvalidIccProfileException() + { + byte[] gridPointCount = Enumerable.Repeat((byte)3, 15).ToArray(); + + Assert.Throws(() => CreateReader(new byte[8]).ReadClut8(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClut16(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClutF32(15, 15, gridPointCount)); + } + + /// + /// A complete profile header and element table do not make absent CLUT values readable. + /// + [Fact] + public void ReadTagDataEntry_WithTruncatedClut_RejectsMissingValues() + { + // A2B0 starts at byte 144; its element at byte 168 declares a 15-channel, three-point grid. + byte[] data = Convert.FromHexString( + "000000C874657374040000006D6E74725247422058595A200000000000000000000000006163737000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000014132423000000090000000386D7065740000" + + "000000000000000000010000001800000020636C7574000F000F030303030303030303030303030303000000000000000000"); + + IccDataReader reader = new(data); + IccTagTableEntry tag = new(IccProfileTag.AToB0, 144, 56); + + Assert.Throws(() => reader.ReadTagDataEntry(tag)); + Assert.Empty(new IccProfile(data).Entries); + } + [Theory] [MemberData(nameof(IccTestDataLut.Lut8TestData), MemberType = typeof(IccTestDataLut))] internal void ReadLut8(byte[] data, IccLut expected) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs index 9b2ca2a275..96facfd5d8 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Icc; @@ -54,4 +55,29 @@ private IccReader CreateReader() { return new IccReader(); } + + /// + /// Ignores tag entries that cannot contain a complete tag header. + /// + [Fact] + public void ReadProfile_WithUndersizedArrayTags_IgnoresTags() + { + const int tagCount = 100; + const int dataOffset = 132 + (tagCount * 12); + byte[] data = new byte[dataOffset + 16]; + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(128), tagCount); + + for (int i = 0; i < tagCount; i++) + { + Span entry = data.AsSpan(132 + (i * 12), 12); + BinaryPrimitives.WriteUInt32BigEndian(entry, 0x73663332); + BinaryPrimitives.WriteUInt32BigEndian(entry[4..], dataOffset); + BinaryPrimitives.WriteUInt32BigEndian(entry[8..], 0); + } + + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(dataOffset), 0x73663332); + IccProfile profile = new(data); + + Assert.Empty(profile.Entries); + } } diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 60e33835af..0c48547813 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -14,6 +14,18 @@ public class HistogramEqualizationTests { private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0456F); + [Fact] + public void GlobalHistogramEqualization_ClampsDecodedFloatingPointSamples() + { + byte[] data = BuildFloatTiff(8, 2F); + + using Image image = Image.Load(data); + image.Mutate(x => x.HistogramEqualization()); + + Assert.Equal(8, image.Width); + Assert.Equal(1, image.Height); + } + [Theory] [InlineData(256)] [InlineData(65536)] @@ -238,4 +250,47 @@ public unsafe void Issue1640(TestImageProvider provider) ValidatorComparer.VerifySimilarity(referenceResult, processed); } + + private static byte[] BuildFloatTiff(int width, float sample) + { + const int headerLength = 8; + const int entryCount = 10; + const int ifdLength = 2 + (entryCount * 12) + 4; + uint pixelOffset = headerLength + ifdLength; + + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream); + + writer.Write((byte)0x49); + writer.Write((byte)0x49); + writer.Write((ushort)42); + writer.Write((uint)headerLength); + writer.Write((ushort)entryCount); + WriteEntry(writer, 256, 4, 1, (uint)width); + WriteEntry(writer, 257, 4, 1, 1); + WriteEntry(writer, 258, 3, 1, 32); + WriteEntry(writer, 259, 3, 1, 1); + WriteEntry(writer, 262, 3, 1, 1); + WriteEntry(writer, 273, 4, 1, pixelOffset); + WriteEntry(writer, 277, 3, 1, 1); + WriteEntry(writer, 278, 4, 1, 1); + WriteEntry(writer, 279, 4, 1, (uint)(width * sizeof(float))); + WriteEntry(writer, 339, 3, 1, 3); + writer.Write(0U); + + for (int i = 0; i < width; i++) + { + writer.Write(sample); + } + + return stream.ToArray(); + + static void WriteEntry(BinaryWriter writer, ushort tag, ushort type, uint count, uint value) + { + writer.Write(tag); + writer.Write(type); + writer.Write(count); + writer.Write(value); + } + } }