Skip to content

Commit ccd66b6

Browse files
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 <[email protected]>
1 parent 0198ede commit ccd66b6

File tree

7 files changed

+72
-6
lines changed

7 files changed

+72
-6
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
pull_request:
1010
branches:
1111
- main
12+
- release/*
1213
types: [ labeled, opened, synchronize, reopened ]
1314
jobs:
1415
Build:

src/ImageSharp/Advanced/ParallelRowIterator.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static void IterateRows<T>(
5050
int width = rectangle.Width;
5151
int height = rectangle.Height;
5252

53-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
53+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
5454
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
5555

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

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

183-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
183+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
184184
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
185185

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

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

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

275275
private static void ValidateRectangle(Rectangle rectangle)
276276
{

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,12 @@ public bool FindNextMarker()
212212
private int ReadStream()
213213
{
214214
int value = this.badData ? 0 : this.stream.ReadByte();
215-
if (value == -1)
215+
216+
// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
217+
// during decoding of the SOS marker.
218+
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
219+
// we know we have hit the EOI and completed decoding the scan buffer.
220+
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
216221
{
217222
// We've encountered the end of the file stream which means there's no EOI marker
218223
// in the image or the SOS marker has the wrong dimensions set.

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

+17
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,21 @@ public void Issue2315_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
314314
image.DebugSave(provider);
315315
image.CompareToOriginal(provider);
316316
}
317+
318+
[Theory]
319+
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
320+
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
321+
where TPixel : unmanaged, IPixel<TPixel>
322+
{
323+
if (TestEnvironment.IsWindows &&
324+
TestEnvironment.RunsOnCI)
325+
{
326+
// Windows CI runs consistently fail with OOM.
327+
return;
328+
}
329+
330+
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
331+
Assert.Equal(65503, image.Width);
332+
Assert.Equal(65503, image.Height);
333+
}
317334
}

tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

+39
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Six Labors Split License.
33

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

411+
[Fact]
412+
public void CanIterateWithoutIntOverflow()
413+
{
414+
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
415+
const int max = 100_000;
416+
417+
Rectangle rect = new(0, 0, max, max);
418+
int intervalMaxY = 0;
419+
void RowAction(RowInterval rows, Span<Rgba32> memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);
420+
421+
TestRowOperation operation = new();
422+
TestRowIntervalOperation<Rgba32> intervalOperation = new(RowAction);
423+
424+
ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
425+
Assert.Equal(max - 1, operation.MaxY.Value);
426+
427+
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in intervalOperation);
428+
Assert.Equal(max, intervalMaxY);
429+
}
430+
431+
private readonly struct TestRowOperation : IRowOperation
432+
{
433+
public TestRowOperation()
434+
{
435+
}
436+
437+
public StrongBox<int> MaxY { get; } = new StrongBox<int>();
438+
439+
public void Invoke(int y)
440+
{
441+
lock (this.MaxY)
442+
{
443+
this.MaxY.Value = Math.Max(y, this.MaxY.Value);
444+
}
445+
}
446+
}
447+
409448
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
410449
{
411450
private readonly Action<RowInterval> action;

tests/ImageSharp.Tests/TestImages.cs

+1
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ public static class Issues
284284
public const string Issue2315_NotEnoughBytes = "Jpg/issues/issue-2315.jpg";
285285
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
286286
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
287+
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
287288

288289
public static class Fuzz
289290
{
Loading

0 commit comments

Comments
 (0)