Browse Source

Handle EOF in Jpeg bit reader when data is bad to prevent DOS attack. (#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>
pull/2523/head
James Jackson-South 2 years ago
parent
commit
ed3860cfda
  1. 1
      .github/workflows/build-and-test.yml
  2. 10
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  3. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
  4. 17
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  5. 39
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  6. 1
      tests/ImageSharp.Tests/TestImages.cs
  7. 3
      tests/Images/Input/Jpg/issues/Hang_C438A851.jpg

1
.github/workflows/build-and-test.yml

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
jobs:
Build:

10
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -50,7 +50,7 @@ public static partial class ParallelRowIterator
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:
@ -115,7 +115,7 @@ public static partial class ParallelRowIterator
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);
@ -180,7 +180,7 @@ public static partial class ParallelRowIterator
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:
@ -242,7 +242,7 @@ public static partial class ParallelRowIterator
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);
@ -270,7 +270,7 @@ public static partial class ParallelRowIterator
}
[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)
{

7
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

@ -212,7 +212,12 @@ internal struct JpegBitReader
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.

17
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -325,4 +325,21 @@ public partial class JpegDecoderTests
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
tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -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;
@ -406,6 +408,43 @@ public class ParallelRowIteratorTests
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;

1
tests/ImageSharp.Tests/TestImages.cs

@ -291,6 +291,7 @@ public static class TestImages
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
{

3
tests/Images/Input/Jpg/issues/Hang_C438A851.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:580760756f2e7e3ed0752a4ec53d6b6786a4f005606f3a50878f732b3b2a1bcb
size 413
Loading…
Cancel
Save