Repository navigation
JPEG decoder fails or corrupts the image when a stray TEM marker or reserved marker bytes appear in the stream #3237
Description
Prerequisites
- I have written a descriptive issue title
- I have verified that I am running the latest version of ImageSharp
- I have verified if the problem exist in both
DEBUGandRELEASEmode - I have searched open and closed issues to ensure it has not already been reported
ImageSharp version
main @ a73c43d (also present in v4.1.3)
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 Pro 10.0.22631, x64
.NET Framework version
.NET 10 (net10.0)
Description
Scanners and other imperfect producers write JPEGs that libjpeg-based viewers open without trouble. ImageSharp rejects or decodes two such cases wrongly.
1. Stray TEM marker between header segments
ITU T.81 B.1.1.4 says that TEM (FF 01) stands alone with no length field, like RSTn. JpegDecoderCore.FindNextFileMarker already skips stray RSTn markers between segments, but it returns TEM as an ordinary marker. ParseStream then reads the next two bytes as a length field. Those bytes are really the next segment's marker, so Identify and Load both throw InvalidImageContentException.
2. Extraneous bytes before restart markers that form a reserved marker
Some producers write junk bytes before the RSTn markers in the entropy-coded data. The decoder already handles plain junk such as 00 00, FF 00 or a single byte. But when the junk forms a TEM or reserved marker (FF 01 to FF BF), HuffmanScanDecoder.HandleRestart takes it as the end of the scan. A marker in that range can never end a restart interval or a scan. The result depends on the layout:
- Baseline interleaved: the image decodes, but everything after the first restart interval is silently wrong.
- Baseline non-interleaved and progressive: the marker parser fails on the junk with
InvalidImageContentException.
Expected: the decoder skips such markers and resyncs at the next marker, as libjpeg's jpeg_resync_to_restart does, and the image decodes the same as the clean file.
A fix is proposed in #3225.
Steps to Reproduce
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
static byte[] EncodeGradient(JpegEncoder encoder)
{
using Image<Rgba32> image = new(32, 24);
for (int y = 0; y < 24; y++)
{
for (int x = 0; x < 32; x++)
{
image[x, y] = new Rgba32((byte)(x * 8), (byte)(y * 10), (byte)((x + y) * 4));
}
}
using MemoryStream ms = new();
image.SaveAsJpeg(ms, encoder);
return ms.ToArray();
}
// Inserts the junk bytes before every RSTn marker in the entropy-coded data after the first SOS header.
static byte[] InsertBeforeRestartMarkers(byte[] jpg, byte[] junk)
{
int i = 2;
while (jpg[i + 1] != 0xDA)
{
i += 2 + ((jpg[i + 2] << 8) | jpg[i + 3]);
}
i += 2 + ((jpg[i + 2] << 8) | jpg[i + 3]);
List<byte> result = [.. jpg[..i]];
for (; i < jpg.Length; i++)
{
if (jpg[i] == 0xFF && i + 1 < jpg.Length && jpg[i + 1] is >= 0xD0 and <= 0xD7)
{
result.AddRange(junk);
}
result.Add(jpg[i]);
}
return [.. result];
}
// 1. TEM marker straight after the APP0 segment, which follows SOI.
byte[] clean = EncodeGradient(new JpegEncoder { ColorType = JpegColorType.YCbCrRatio420 });
int app0End = 4 + ((clean[4] << 8) | clean[5]);
byte[] withTem = [.. clean[..app0End], 0xFF, 0x01, .. clean[app0End..]];
Image.Identify(withTem); // InvalidImageContentException
using (Image<Rgba32> image = Image.Load<Rgba32>(withTem)) { } // InvalidImageContentException
// 2. FF 12 before every restart marker.
foreach ((bool progressive, bool interleaved) in new[] { (false, true), (false, false), (true, false) })
{
byte[] original = EncodeGradient(new JpegEncoder
{
ColorType = JpegColorType.YCbCrRatio420,
Progressive = progressive,
Interleaved = interleaved,
RestartInterval = 1
});
byte[] corrupt = InsertBeforeRestartMarkers(original, [0xFF, 0x12]);
using Image<Rgba32> expected = Image.Load<Rgba32>(original);
using Image<Rgba32> actual = Image.Load<Rgba32>(corrupt); // throws for the second and third layouts
int differ = 0;
for (int y = 0; y < 24; y++)
{
for (int x = 0; x < 32; x++)
{
differ += expected[x, y] == actual[x, y] ? 0 : 1;
}
}
Console.WriteLine($"progressive={progressive} interleaved={interleaved}: {differ} of 768 pixels differ");
}Actual result:
1. Identify and Load:
SixLabors.ImageSharp.InvalidImageContentException: Input stream does not have enough bytes to parse declared contents of the 01 marker.
2. Baseline interleaved:
progressive=False interleaved=True: 512 of 768 pixels differ
2. Baseline non-interleaved and progressive:
SixLabors.ImageSharp.InvalidImageContentException: Input stream does not have enough bytes to parse declared contents of the 12 marker.
Expected result:
- Case 1:
Identifyreports 32x24, andLoadgives the same pixels as the clean file. - Case 2: every layout decodes, with 0 of 768 pixels different from the clean file. The same holds for
FF 01,FF BForFF 12 FF 34as the junk.
Images
No images needed; the repro builds the input in memory.
Activity