Skip to content

[FIX] ISDB: keep the decoder inside the buffer it was handed, and reserve room for get_text()'s terminator - #2436

Open
pranayr710 wants to merge 2 commits into
CCExtractor:masterfrom
pranayr710:fix-isdb-lengths
Open

pranayr710 wants to merge 2 commits into
CCExtractor:masterfrom
pranayr710:fix-isdb-lengths

Conversation

@pranayr710

Copy link
Copy Markdown
Contributor

Fixes #2434.

Two independent out-of-bounds accesses in the ISDB caption decoder. They share
a reproducer corpus, so they are in one PR as two commits; the second is a
two-line change and is easy to review on its own.

1. The decoder never looked at the length it was handed

isdbsub_decode() takes a buf_size and does not use it, and every length
below it comes straight out of the stream:

  • the PES header length nibble could point past the payload;
  • group_size is 16 bits, so a data group could claim 64 KB of a 25-byte
    payload;
  • the data unit length is 24 bits, so parse_statement() could be asked to
    walk 16 MB;
  • nb_lang is one byte, so caption management data could read 255 five-byte
    language entries out of nothing.

parse_statement() also passed the whole statement size to parse_command()
rather than what was left from the current position, and left ret unassigned
on two of its cases — indeterminate on the first pass, and a carried-over 0
would spin buf += ret forever.

This commit carries the real length down the chain and clamps each
stream-supplied length to what remains. Truncating rather than erroring keeps
the existing "decode what arrived" behaviour for data that legitimately spans
packets.

2. get_text() had no room for the terminator it writes

reserve_buf(text, len) guarantees capacity for text->used + len, i.e. len
bytes on top of what is used. Both call sites in get_text() do

sb_text = allocate_text_node(ls);        /* fresh node, used == 0 */
reserve_buf(sb_text, text->used);        /* reserves exactly text->used */
memcpy(sb_text->buf, text->buf, text->used);
sb_text->buf[text->used] = '\0';         /* one past the end */

The allocation rounds up to a multiple of 128, which hides this until
text->used is itself an exact multiple of 128.

Verification

ISDB is selected by a data_component_descriptor (tag 0xFD, component_id
0x0008) on a stream type 0x06 PID, so a ~1 KB transport stream reaches the
decoder. Generator and a worked example are in the issue. Eight files, built
from master (d7eb132).

Sanitizer build (-fsanitize=address,undefined), two runs each:

file master with this PR
isdb_len24 (24-bit unit length 0xFFFFFF) heap-buffer-overflow in parse_statement, 2/2 clean, 2/2
isdb_len256 (text length lands on 256) heap-buffer-overflow in get_text, 2/2 clean, 2/2
isdb_ok (well formed) clean clean
isdb_hdrskip clean clean

Plain build, pristine master vs this PR, three runs each, all eight files:
identical exit codes throughout, and isdb_ok produces a byte-identical SRT
(md5 5d6c804a) on both. Same compiler warning count (20), none in this file.

Two things worth being straight about:

  • A normal build does not crash on these files. The payload lands in
    ptr->buffer, a 2 MB BUFSIZE heap allocation, so the overreads stay inside
    the block on an ordinary build and quietly fold stale bytes from earlier
    packets into the subtitle text. The sanitizer is what calls it.
  • My corpus also produces intermittent AddressSanitizer:DEADLYSIGNAL loops on
    four of the files. Those appear on both builds at random, including on
    files this patch rejects in its first three lines, and never on a build
    without sanitizers. I treated them as a build artifact rather than evidence
    either way; the two findings above are the deterministic ones.

(Prepared with AI assistance; the reproducers and the sanitizer runs are my
own.)

isdbsub_decode() takes a buf_size and never looked at it. Every length
below it comes straight out of the stream and was used unchecked:

  * the PES header length nibble could point past the payload;
  * group_size is 16 bits, so a data group could claim 64 KB of a
    25 byte payload;
  * the data unit length is 24 bits, so parse_statement() could be
    asked to walk 16 MB;
  * nb_lang is a byte, so caption management data could read 255
    five byte language entries out of nothing.

parse_statement() also passed the whole statement size to
parse_command() rather than what was left from the current position,
and left ret unassigned on two of its cases -- indeterminate on the
first pass, and a carried-over zero would spin the loop for ever.

Carry the real length down the chain and clamp each stream-supplied
length to it. Truncating rather than erroring keeps the existing
"decode what arrived" behaviour for data that legitimately spans
packets.

Three crafted .ts files (ISDB is selected by a data_component_descriptor
with component_id 0x0008 in the PMT) make an ASan build report a
heap-buffer-overflow in parse_statement or die outright; after this they
are parsed and discarded.

Fixes CCExtractor#2434
reserve_buf(text, len) guarantees capacity for text->used + len, so
reserve_buf(sb_text, text->used) on a freshly allocated node reserves
exactly text->used bytes -- and the next line writes
sb_text->buf[text->used] = '\0', one byte past the end.

The allocation rounds up to a multiple of 128, so the write stays inside
the block unless text->used is itself an exact multiple of 128. A
crafted .ts that lands on that boundary makes an ASan build report a
heap-buffer-overflow in get_text.

Both copies in get_text() ask for the terminator as well.
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.

[BUG] ISDB decoder ignores the payload length: heap-buffer-overflow in parse_statement, plus a one-byte overflow in get_text

2 participants