Skip to content

JPEG decoder fails or corrupts the image when a stray TEM marker or reserved marker bytes appear in the stream #3237

Open

Description

@jafin

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 DEBUG and RELEASE mode
  • 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: Identify reports 32x24, and Load gives 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 BF or FF 12 FF 34 as the junk.

Images

No images needed; the repro builds the input in memory.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions