Skip to content

Commit 688e242

Browse files
Merge pull request #2524 from SixLabors/js/backport-fix-jpeg-dos
Backport - Handle EOF in Jpeg bit reader when data is bad to prevent DOS attack.
2 parents d1b52a2 + 0f17a8b commit 688e242

File tree

6 files changed

+68
-6
lines changed

6 files changed

+68
-6
lines changed

src/ImageSharp/Advanced/ParallelRowIterator.cs

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

55-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
55+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
5656
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
5757

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

120-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
120+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
121121
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
122122
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
123123

@@ -181,7 +181,7 @@ public static void IterateRowIntervals<T>(
181181
int width = rectangle.Width;
182182
int height = rectangle.Height;
183183

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

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

246-
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
246+
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
247247
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
248248
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
249249

@@ -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/HuffmanScanBuffer.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,12 @@ public bool FindNextMarker()
211211
private int ReadStream()
212212
{
213213
int value = this.badData ? 0 : this.stream.ReadByte();
214-
if (value == -1)
214+
215+
// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
216+
// during decoding of the SOS marker.
217+
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
218+
// we know we have hit the EOI and completed decoding the scan buffer.
219+
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
215220
{
216221
// We've encountered the end of the file stream which means there's no EOI marker
217222
// 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
@@ -261,5 +261,22 @@ public void ValidateProgressivePdfJsOutput<TPixel>(
261261
this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}");
262262
}
263263
}
264+
265+
[Theory]
266+
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
267+
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
268+
where TPixel : unmanaged, IPixel<TPixel>
269+
{
270+
if (TestEnvironment.IsWindows &&
271+
TestEnvironment.RunsOnCI)
272+
{
273+
// Windows CI runs consistently fail with OOM.
274+
return;
275+
}
276+
277+
using Image<TPixel> image = provider.GetImage(JpegDecoder);
278+
Assert.Equal(65503, image.Width);
279+
Assert.Equal(65503, image.Height);
280+
}
264281
}
265282
}

tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

+36
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Linq;
66
using System.Numerics;
7+
using System.Runtime.CompilerServices;
78
using System.Threading;
89
using SixLabors.ImageSharp.Advanced;
910
using SixLabors.ImageSharp.Memory;
@@ -410,6 +411,41 @@ void RowAction(RowInterval rows, Span<Rgba32> memory)
410411
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
411412
}
412413

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

tests/ImageSharp.Tests/TestImages.cs

+1
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ public static class Issues
266266
public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg";
267267
public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg";
268268
public const string Issue2133DeduceColorSpace = "Jpg/issues/Issue2133.jpg";
269+
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
269270

270271
public static class Fuzz
271272
{
Loading

0 commit comments

Comments
 (0)