Skip to content

x DTS: fix infinite loop in Extensions() when handler overflows asset boundary - #2631

Open
mjuhasz wants to merge 2 commits into
MediaArea:masterfrom
mjuhasz:fix/dts-extensions-unsigned-underflow
Open

x DTS: fix infinite loop in Extensions() when handler overflows asset boundary#2631
mjuhasz wants to merge 2 commits into
MediaArea:masterfrom
mjuhasz:fix/dts-extensions-unsigned-underflow

Conversation

@mjuhasz

@mjuhasz mjuhasz commented Jun 1, 2026

Copy link
Copy Markdown

Problem

MediaInfo 26.05 hangs indefinitely when parsing the attached MKV file that contains both DTS-HD MA audio and an attachment (an SRT subtitle file). Version 26.01 handled this file without issue. Remuxing using mkvmerge does not help.

Root Cause

Extensions() iterates DTS extension assets with an unsigned while-loop condition:

while (Element_Size - Element_Offset >= 4)

XLL() temporarily expands Element_Size to cover the full XLL frame. If parsing fails inside XLL() (e.g. a segment-size mismatch causes Trusted_IsNot() to fire), Element_Offset is set to the expanded Element_Size. XLL() then restores Element_Size = Element_Size_Save (the smaller outer asset boundary), leaving Element_Offset > Element_Size. The unsigned subtraction wraps to ~2^64, and the loop runs forever.

This regression was introduced by commit 892a9a6, which restored the Skip_XX integrity check that calls Trusted_IsNot().

Fix

Clamp Element_Offset to Element_Size after each extension handler call, before Element_End0(). One line added.

Reproduction

I have an MKV with a DTS-HD MA audio track and an attached file (srt) that will reproduce the hang with mediainfo 26.05.

Test.mkv.zip

… boundary

XLL() temporarily expands Element_Size to cover the full XLL frame.
If parsing fails inside XLL(), Trusted_IsNot() fires and sets
Element_Offset=Element_Size (the expanded value). XLL() then restores
Element_Size=Element_Size_Save (the smaller outer asset boundary), leaving
Element_Offset>Element_Size. The unsigned subtraction in the while-loop
condition wraps around, making the loop run forever.

Reproducible: MKV file with DTS-HD MA audio and any attached file (e.g. SRT).
The hang was introduced when commit 892a9a6 restored the Skip_XX integrity
check, enabling the Trusted_IsNot() code path for the first time.
@cjee21

cjee21 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Need to be careful when reassigning Element_Size. Maybe (I did not read DTS specs or see what is actually going on with that sample file) check should be done before reassigning Element_Size instead. Usually Element_Size is assigned smaller then restored and not expanded. If Element_Size is expanded beyond the end of buffer, integrity checks will fail and there may be buffer overread.

@cjee21

cjee21 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

I think something else is wrong and this PR just covers up the symptoms.

AssetFsize: 3428 bytes and the first XLL is 3304 bytes. The 2nd XLL thus only has 124 bytes but the Segments in it are 3167 bytes.

001E368       Sizes (2 bytes)
001E368        AssetFsize:                      3427 (0x0D63) - (16 bits) - 3428 (0xD64) bytes
001E36A       AssetDescriptor (5 bytes)
001E36A        AssetDescriptFsize:              14 (0x00E) - (9 bits) - 15 (0xF) bytes?
001E36B        AssetIndex:                      0 (0x0) - (3 bits)
001E36B        AssetTypeDescrPresent:           No
001E36B        LanguageDescrPresent:            No
001E36C        InfoTextPresent:                 No
001E36C        BitResolution:                   23 (0x17) - (5 bits) - 24 (0x18) bits
001E36C        MaxSampleRate:                   12 (0xC) - (4 bits) - 48000 (0xBB80) Hz
001E36D        TotalNumChs:                     5 (0x05) - (8 bits) - 6 (0x6) channels
001E36E        One2OneMapChannels2Speakers (1 bytes)
001E36E         One2OneMapChannels2Speakers:    Yes
001E36E         TotalNumChs:                    No
001E36E         SpkrMaskEnabled (1 bytes)
001E36E          SpkrMaskEnabled:               Yes
001E36E          NumBits4SAMask:                0 (0x0) - (2 bits) - 4 (0x4) bits
001E36E          SpkrActivityMask:              15 (0xF) - (4 bits) - Front: L C R, Side: L R, LFE
001E36F         NumSpkrRemapSets:               0 (0x0) - (3 bits)
001E36F       (Not parsed):                     (Data)
001E37B       CRC16ExtSSHeader:                 62396 (0xF3BC)
001E37D      Asset (3428 bytes)
001E37D       XLL (LossLess) (3304 bytes)
001E37D        Sync Word:                       1101174087 (0x41A29547)
001E381        Header (9 bytes)
001E381         Version:                        0 (0x0) - (4 bits)
001E382         HeaderSize:                     12 (0xC) - (8 bits) - 12 (0xC)
001E382         Bits4FrameFsize:                11 (0x0B) - (5 bits) - 12 (0xC)
001E383         LLFrameSize minus 1:            3303 (0xCE7) - (12 bits)
001E384         NumChSetsInFrame:               0 (0x0) - (4 bits) - 1 (0x1)
001E385         SegmentsInFrame:                2 (0x2) - (4 bits) - 4 (0x4)
001E385         SmplInSeg:                      7 (0x7) - (4 bits) - 128 (0x80)
001E386         Bits4SSize:                     10 (0x0A) - (5 bits) - 11 (0xB)
001E386         BandDataCRCEn:                  0 (0x0) - (2 bits)
001E387         ScalableLSBs:                   No
001E387         Bits4ChMask:                    5 (0x05) - (5 bits) - 6 (0x6)
001E387         Padding:                        0 (0x0) - (2 bits)
001E388         CRC16Header:                    9564 (0x255C) - OK
001E38A        Channel Set (65 bytes)
001E38A         ChSetHeaderSize:                64 (0x040) - (10 bits)
001E38B         ChSetLLChannel:                 5 (0x5) - (4 bits)
001E38B         ResidualChEncode:               0 (0x00) - (6 bits)
001E38C         BitResolution:                  23 (0x17) - (5 bits)
001E38D         BitWidth:                       23 (0x17) - (5 bits)
001E38D         FreqIndex:                      12 (0xC) - (4 bits) - 48000 (0xBB80)
001E38E         FsInterpolate:                  0 (0x0) - (2 bits)
001E38E         ReplacementSet:                 0 (0x0) - (2 bits)
001E38E         PrimaryChSet:                   Yes
001E38E         DownmixCoeffCodeEmbedded:       No
001E38F         HierChSet:                      Yes
001E38F         ChMaskEnabled:                  Yes
001E38F         Mask:                           63 (0x3F) - (6 bits) - Front: L C R, Side: L R, Back: C, High: L R, LFE
001E390         (Not parsed):                   (Data)
001E3C9         CRC16SubHeader:                 35863 (0x8C17) - OK
001E3CB        NAVI (8 bytes)
001E3CB         SegmentSize:                    792 (0x318)
001E3CC         SegmentSize:                    806 (0x326)
001E3CD         SegmentSize:                    804 (0x324)
001E3CF         SegmentSize:                    811 (0x32B)
001E3D1         CRC16:                          62302 (0xF35E) - OK
001E3D3        Segments:                        (3213 bytes)
001F060        1 byte per segment?:             (4 bytes)
001F064        Padding:                         (1 bytes)
001F065       XLL (LossLess) (124 bytes)
001F065        Sync Word:                       1101174087 (0x41A29547)
001F069        Header (9 bytes)
001F069         Version:                        0 (0x0) - (4 bits)
001F06A         HeaderSize:                     12 (0xC) - (8 bits) - 12 (0xC)
001F06A         Bits4FrameFsize:                11 (0x0B) - (5 bits) - 12 (0xC)
001F06B         LLFrameSize minus 1:            3259 (0xCBB) - (12 bits)
001F06C         NumChSetsInFrame:               0 (0x0) - (4 bits) - 1 (0x1)
001F06D         SegmentsInFrame:                1 (0x1) - (4 bits) - 2 (0x2)
001F06D         SmplInSeg:                      8 (0x8) - (4 bits) - 256 (0x100)
001F06E         Bits4SSize:                     10 (0x0A) - (5 bits) - 11 (0xB)
001F06E         BandDataCRCEn:                  0 (0x0) - (2 bits)
001F06F         ScalableLSBs:                   No
001F06F         Bits4ChMask:                    5 (0x05) - (5 bits) - 6 (0x6)
001F06F         Padding:                        0 (0x0) - (2 bits)
001F070         CRC16Header:                    37375 (0x91FF) - OK
001F072        Channel Set (72 bytes)
001F072         ChSetHeaderSize:                71 (0x047) - (10 bits)
001F073         ChSetLLChannel:                 5 (0x5) - (4 bits)
001F073         ResidualChEncode:               0 (0x00) - (6 bits)
001F074         BitResolution:                  23 (0x17) - (5 bits)
001F075         BitWidth:                       23 (0x17) - (5 bits)
001F075         FreqIndex:                      12 (0xC) - (4 bits) - 48000 (0xBB80)
001F076         FsInterpolate:                  0 (0x0) - (2 bits)
001F076         ReplacementSet:                 0 (0x0) - (2 bits)
001F076         PrimaryChSet:                   Yes
001F076         DownmixCoeffCodeEmbedded:       No
001F077         HierChSet:                      Yes
001F077         ChMaskEnabled:                  Yes
001F077         Mask:                           63 (0x3F) - (6 bits) - Front: L C R, Side: L R, Back: C, High: L R, LFE
001F078         (Not parsed):                   (Data)
001F0B8         CRC16SubHeader:                 2884 (0x0B44) - OK
001F0BA        NAVI (5 bytes)
001F0BA         SegmentSize:                    1558 (0x616)
001F0BB         SegmentSize:                    1609 (0x649)
001F0BD         CRC16:                          20306 (0x4F52) - OK
001F0BF        Segments:                        (3167 bytes)
001FD1E        1 byte per segment?:             (2 bytes)
001FD20        (Unknown):                       (1 bytes)

…e exceeds asset boundary

When the declared XLL logical frame size (LLFrameSize) exceeds the remaining
bytes in the current ExSS asset, XLL() was unconditionally expanding
Element_Size beyond the actual buffer boundary. Subsequent Skip_XX calls then
triggered the INTEGRITY_SIZE_ATLEAST integrity check (restored by 892a9a6),
which called Trusted_IsNot() and set Element_Offset = Element_Size (the
oversized value). When XLL() restored Element_Size to the outer asset boundary,
Element_Offset was left far past it, causing the unsigned underflow and infinite
loop in Extensions().

Add a bounds check before expanding Element_Size: if the computed XLL frame end
exceeds the current asset boundary, unwind the parser state (BS_End +
Element_End0) and skip to the end of the asset, mirroring the existing
LLFrameSize < 6 guard pattern directly above.
@mjuhasz

mjuhasz commented Jun 4, 2026

Copy link
Copy Markdown
Author

@cjee21 you are right. The original fix only addresses the symptom. Here is an attempt to find the root cause and a proper fix.


Root cause

XLL() expands Element_Size to cover the full logical XLL frame before parsing it:

  // File_Dts.cpp, inside XLL()
  auto Element_Size_Save = Element_Size;
  Element_Size = Element_Offset_Start - 3 + LLFrameSize;  // ← no upper-bound check

The formula is correct for valid data. The sync word (4 bytes) is consumed by Extensions() before XLL() is called, so at entry Element_Offset_Start = frame_start + 4. Since LLFrameSize is "minus one" encoded, the logical XLL frame ends at:

  (frame_start + 4) - 3 + LLFrameSize
  = frame_start + 1 + LLFrameSize
  = frame_start + (LLFrameSize + 1)     ← actual frame size

The problem is that there is no check that this computed end stays within the outer asset boundary stored in Element_Size_Save.


The file in question

The 3428-byte asset contains two XLL frames. The 1st (3304 bytes) parses cleanly. The 2nd starts at asset offset 3304, leaving only 124 bytes available. Its header declares LLFrameSize = 3259 (→ 3260 bytes), which means Element_Size gets expanded to asset_start + 6564 which is 3136 bytes past the actual asset boundary of asset_start + 3428.

Concrete numbers from the sample with the asset starting at a relative origin:

1st XLL 2nd XLL
Element_Offset_Start 4 3308
LLFrameSize (raw) 3303 3259
Computed Element_Size 3304 6564
Element_Size_Save (asset end) 3428 3428
Overflows? No Yes (+3136)

Exact failure chain (with commit 892a9a6)
  1. XLL() sets Element_Size = 6564 (far past the real buffer boundary).
  2. Parsing continues through the header and channel set (both fit in the 124 bytes).
  3. The NAVI claims 3167 bytes of segment data. Skip_XX(3167, "Segments") is called.
  4. INTEGRITY_SIZE_ATLEAST(3167) fires because 3167 > Element_Size - Element_Offset (the remaining "space" before Element_Size is large, but the actual buffer ends long before 6564). The check was restored by 892a9a6 - this is correct defensive code; it prevents an actual buffer overread.
  5. Trusted_IsNot() is called. Since the bit-reader is idle at this point (past BS_End()), the else branch executes: Element_Offset = Element_Size = 6564.
  6. Skip_XX returns without advancing anything.
  7. XLL() eventually restores Element_Size = Element_Size_Save = 3428.
  8. Back in Extensions(): Element_Size (3428) - Element_Offset (6564) wraps as unsigned 64-bit → ~2⁶⁴ → infinite loop.

My original PR's clamp in Extensions() breaks the loop but leaves XLL() still attempting to parse 3136 bytes of garbage, with Trusted_IsNot() degrading parser trust unnecessarily.


The fix

Check before expanding Element_Size:

  auto Element_Size_Save = Element_Size;
  if (Element_Offset_Start - 3 + LLFrameSize > Element_Size_Save)
  {
      BS_End();
      Element_End0();
      Skip_XX(Element_Size - Element_Offset, "(Unknown)");
      return;
  }
  Element_Size = Element_Offset_Start - 3 + LLFrameSize;

This mirrors the existing LLFrameSize < 6 guard directly above it (same BS_End() + Element_End0() + Skip_XX + return pattern), so it follows an already-established convention in this function.

The Skip_XX call skips the remaining asset bytes (all 116 of them, which fit cleanly within the asset boundary), advancing Element_Offset to Element_Size. When XLL() returns, Extensions() sees Element_Size - Element_Offset = 0 < 4 and exits the loop normally.


Why early return rather than partial parsing

The second XLL's LLFrameSize (3260 bytes) exceeds the 124-byte asset slice. This is consistent with a DTS-HD multi-frame XLL scenario where a logical lossless frame spans multiple ExSS subframes. LLFrameSize refers to the total logical frame, not just the per-subframe slice. MediaInfo does not implement cross-frame XLL continuation, so there is no way to complete this frame. The metadata we would extract from this partial frame (bit depth, sample rate, channel mask) was already captured from the first, complete XLL in the same asset. Early return is both correct and sufficient.

Attempting to parse the header and channel set of the partial frame anyway would require adding additional overflow guards deeper in the function; the simple early return is cleaner and costs nothing in the normal (valid-data) path.


Regarding the original Extensions() clamp

The if (Element_Offset > Element_Size) Element_Offset = Element_Size guard proposed in the original PR can be kept as cheap defense-in-depth for other extension handlers, but it is no longer the load-bearing fix. With the XLL() bounds check in place, Trusted_IsNot() is never triggered for this input, Element_Offset never overshoots, and the Extensions() loop exits cleanly.

return;
}
auto Element_Size_Save=Element_Size;
if (Element_Offset_Start-3+LLFrameSize>Element_Size_Save)

@cjee21 cjee21 Jun 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that Element_Offset_Start - 3+ LLFrameSize can not overflow. Maybe LLFrameSize>Element_Size - Element_Offset_Start - 3 is better?

continue;
}
auto Element_Size_Save=Element_Size;
Element_Size=Element_Offset+Size;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do similar checks here to prevent malformed file from expanding Element_Size and causing over-reads.

@cjee21

cjee21 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Looks like a corrupt stream, all header CRCs are correct but the LLFrameSize and SegmentSize of second XLL element exceeds the space available before the next frame.

Anyway, MediaInfo needs to check boundaries before re-assigning Element_Size based on data from stream that may be malformed. Also should validate sizes that are read from stream before using them in Get_BS that only handles max 32-bits.

001E3D1         CRC16:                          62302 (0xF35E) - OK
001E3D3        Segments:                        (3213 bytes)
001F060        1 byte per segment?:             (4 bytes)
001F064        Padding:                         (1 bytes)
001F065       XLL (LossLess) (124 bytes)
001F065        Sync Word:                       1101174087 (0x41A29547)
001F069        Header (9 bytes)
001F069         Version:                        0 (0x0) - (4 bits)
001F06A         HeaderSize:                     12 (0xC) - (8 bits) - 12 (0xC)
001F06A         Bits4FrameFsize:                11 (0x0B) - (5 bits) - 12 (0xC)
001F06B         LLFrameSize minus 1:            3259 (0xCBB) - (12 bits)
001F06C         NumChSetsInFrame:               0 (0x0) - (4 bits) - 1 (0x1)
001F06D         SegmentsInFrame:                1 (0x1) - (4 bits) - 2 (0x2)
001F06D         SmplInSeg:                      8 (0x8) - (4 bits) - 256 (0x100)
001F06E         Bits4SSize:                     10 (0x0A) - (5 bits) - 11 (0xB)
001F06E         BandDataCRCEn:                  0 (0x0) - (2 bits)
001F06F         ScalableLSBs:                   No
001F06F         Bits4ChMask:                    5 (0x05) - (5 bits) - 6 (0x6)
001F06F         Padding:                        0 (0x0) - (2 bits)
001F070         CRC16Header:                    37375 (0x91FF) - OK
001F072        Channel Set (72 bytes)
001F072         ChSetHeaderSize:                71 (0x047) - (10 bits)
001F073         ChSetLLChannel:                 5 (0x5) - (4 bits)
001F073         ResidualChEncode:               0 (0x00) - (6 bits)
001F074         BitResolution:                  23 (0x17) - (5 bits)
001F075         BitWidth:                       23 (0x17) - (5 bits)
001F075         FreqIndex:                      12 (0xC) - (4 bits) - 48000 (0xBB80)
001F076         FsInterpolate:                  0 (0x0) - (2 bits)
001F076         ReplacementSet:                 0 (0x0) - (2 bits)
001F076         PrimaryChSet:                   Yes
001F076         DownmixCoeffCodeEmbedded:       No
001F077         HierChSet:                      Yes
001F077         ChMaskEnabled:                  Yes
001F077         Mask:                           63 (0x3F) - (6 bits) - Front: L C R, Side: L R, Back: C, High: L R, LFE
001F078         (Not parsed):                   (Data)
001F0B8         CRC16SubHeader:                 2884 (0x0B44) - OK
001F0BA        NAVI (5 bytes)
001F0BA         SegmentSize:                    1558 (0x616)
001F0BB         SegmentSize:                    1609 (0x649)
001F0BD         CRC16:                          20306 (0x4F52) - OK
001F0BF        (Unknown):                       (34 bytes)
001F0E1    Lace - Parser=Dts (5620 bytes)
001F0E1     Frame - 13 - PTS 00:00:00.138 (2012 bytes)
001F0E1      Sync:                              2147385345 (0x7FFE8001)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants