Skip to content

Commit

Permalink
Handle EOF in Jpeg bit reader when data is bad to prevent DOS attack. (
Browse files Browse the repository at this point in the history
…#2516)

* Handle EOF in bit reader when data is bad.

* Allow parallel processing of multi-megapixel image

* Stream seek can exceed the length of a stream

* Try triggering on release branches

* Update JpegBitReader.cs

* Skin on Win .NET 6

* All Win OS is an issue

* Address feedback

* add validation to CanIterateWithoutIntOverflow

---------

Co-authored-by: antonfirsov <antonfir@gmail.com>
  • Loading branch information
JimBobSquarePants and antonfirsov committed Aug 30, 2023
1 parent e81efa3 commit ed3860c
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 6 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
jobs:
Build:
Expand Down
10 changes: 5 additions & 5 deletions src/ImageSharp/Advanced/ParallelRowIterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static void IterateRows<T>(
int width = rectangle.Width;
int height = rectangle.Height;

int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);

// Avoid TPL overhead in this trivial case:
Expand Down Expand Up @@ -115,7 +115,7 @@ public static void IterateRows<T, TBuffer>(
int width = rectangle.Width;
int height = rectangle.Height;

int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
Expand Down Expand Up @@ -180,7 +180,7 @@ public static void IterateRowIntervals<T>(
int width = rectangle.Width;
int height = rectangle.Height;

int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);

// Avoid TPL overhead in this trivial case:
Expand Down Expand Up @@ -242,7 +242,7 @@ public static void IterateRowIntervals<T, TBuffer>(
int width = rectangle.Width;
int height = rectangle.Height;

int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
Expand Down Expand Up @@ -270,7 +270,7 @@ public static void IterateRowIntervals<T, TBuffer>(
}

[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);

private static void ValidateRectangle(Rectangle rectangle)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ public bool FindNextMarker()
private int ReadStream()
{
int value = this.badData ? 0 : this.stream.ReadByte();
if (value == -1)

// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
// during decoding of the SOS marker.
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
// we know we have hit the EOI and completed decoding the scan buffer.
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
{
// We've encountered the end of the file stream which means there's no EOI marker
// in the image or the SOS marker has the wrong dimensions set.
Expand Down
17 changes: 17 additions & 0 deletions tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,21 @@ public void Issue2478_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
image.DebugSave(provider);
image.CompareToOriginal(provider);
}

[Theory]
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows &&
TestEnvironment.RunsOnCI)
{
// Windows CI runs consistently fail with OOM.
return;
}

using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
Assert.Equal(65503, image.Width);
Assert.Equal(65503, image.Height);
}
}
39 changes: 39 additions & 0 deletions tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.

using System.Numerics;
using System.Runtime.CompilerServices;
using Castle.Core.Configuration;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
Expand Down Expand Up @@ -406,6 +408,43 @@ void RowAction(RowInterval rows, Span<Rgba32> memory)
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
}

[Fact]
public void CanIterateWithoutIntOverflow()
{
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
const int max = 100_000;

Rectangle rect = new(0, 0, max, max);
int intervalMaxY = 0;
void RowAction(RowInterval rows, Span<Rgba32> memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);

TestRowOperation operation = new();
TestRowIntervalOperation<Rgba32> intervalOperation = new(RowAction);

ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
Assert.Equal(max - 1, operation.MaxY.Value);

ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in intervalOperation);
Assert.Equal(max, intervalMaxY);
}

private readonly struct TestRowOperation : IRowOperation
{
public TestRowOperation()
{
}

public StrongBox<int> MaxY { get; } = new StrongBox<int>();

public void Invoke(int y)
{
lock (this.MaxY)
{
this.MaxY.Value = Math.Max(y, this.MaxY.Value);
}
}
}

private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action<RowInterval> action;
Expand Down
1 change: 1 addition & 0 deletions tests/ImageSharp.Tests/TestImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ public static class Issues
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
public const string Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";

public static class Fuzz
{
Expand Down
3 changes: 3 additions & 0 deletions tests/Images/Input/Jpg/issues/Hang_C438A851.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ed3860c

Please sign in to comment.