Skip to content

[FIX] Out-of-bounds read: unsigned underflow defeats the file buffer bounds checks - #2322

Open
cfsmp3 wants to merge 2 commits into
masterfrom
fix/buffered-seek-unsigned-underflow
Open

[FIX] Out-of-bounds read: unsigned underflow defeats the file buffer bounds checks#2322
cfsmp3 wants to merge 2 commits into
masterfrom
fix/buffered-seek-unsigned-underflow

Conversation

@cfsmp3

@cfsmp3 cfsmp3 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

A WTV recording makes ccextractor read ~2.3 GB past a 1 MB buffer and segfault. The buffer bounds check that should have stopped it is defeated by unsigned arithmetic.

Root cause

buffered_seek() handles a negative offset like this:

ctx->filebuffer_pos += offset;
if (ctx->filebuffer_pos < 0)
{
	if ((ctx->filebuffer_pos + ctx->startbytes_pos) < 0)
		fatal(CCX_COMMON_EXIT_BUG_BUG, "PANIC: Attempt to seek before buffer start, this is a bug!");
	...
}

filebuffer_pos and startbytes_pos are both unsigned int, so the addition wraps instead of going negative and neither guard can ever be true. The "seek before buffer start" path is dead code.

A backward seek past the start of the buffer therefore leaves filebuffer_pos at roughly 2^32. buffered_read() then evaluates

if (bytes <= ctx->bytesinbuffer - ctx->filebuffer_pos)

which underflows for the same reason, so the bounds check passes and memcpy() reads from far outside the buffer.

Observed

On 5f6dfe831e35…wtv the WTV parser reaches buffered_seek(ctx, -1969339576) while filebuffer_pos is 1048576. Values captured at the fault:

bytes                          = 32
ctx->bytesinbuffer             = 1048576        (1 MB)
ctx->filebuffer_pos            = 2326676296     (wrapped)
bytesinbuffer - filebuffer_pos = 1969339576     (underflow -> check passes)

memcpy(dst, ctx->filebuffer + 2326676296, 32)   -> SIGSEGV

Fix

Do the seek arithmetic in int64_t so both existing guards work as written, and route the buffer-space checks through a small buffered_bytes_left() helper that returns 0 when the position is past the end.

Three call sites shared the hazard, so all three are converted:

  • buffered_read() — the crash above
  • buffered_skip() — same comparison
  • buffered_read_byte()if (ctx->bytesinbuffer - ctx->filebuffer_pos) would be truthy on underflow and index filebuffer[] out of bounds

Input files are untrusted, so this is a bounds-check fix rather than only a crash fix.

Verification

  • The sample no longer performs an out-of-bounds read. The previously unreachable guard now fires and the file is rejected cleanly (PANIC: Attempt to seek before buffer start) instead of faulting.
  • No captions are lost: master (before crashing) and this branch both write the same 8662-byte output with 100 cues.
  • ASan on the fixed build reports no SEGV and no invalid access on that file.
  • 55 media files swept (ts, mpg, wtv, mp4, mkv, mxf): output byte-identical to master in every case, with no exit-code changes.

What the remaining PANIC means

With this fix the sample no longer reads out of bounds; the guard that was previously dead now fires:

Error: PANIC: Attempt to seek before buffer start, this is a bug!

That message is accurate and should stay: reaching it means an invariant really was broken, and the invariant was broken by our own code rather than by the input.

Tracing the caller shows why. get_data() reads a 32-bit element length straight from the file and does len -= 32 with no check that len >= 32, then passes len + pad to skip_sized_buffer(uint32_t size), which calls buffered_seek(ctx, size) — and that parameter is an int. The observed size was 2325627720, which is above INT_MAX, so the conversion produced -1969339576 (2325627720 - 2^32). A forward skip silently became a 1.8 GB backward seek.

For the avoidance of doubt, the input is not meaningfully damaged: ffprobe reports a well-formed WTV with four streams and a 358 s duration matching the file size, and ffmpeg decodes the whole file to completion with exit 0 (a couple of localized glitches typical of a broadcast capture, nothing structural). The file is 0.34 GiB, so a 1.8 GiB backward seek could never have been legitimate.

Fixing that properly means validating the parsed length and not narrowing a uint32_t into an int seek. That is a WTV-parser change and belongs in its own PR; this one stops the out-of-bounds read for every caller of the buffer layer.

Related

Found while investigating a different segfault; that one is fixed separately in #2319. This branch does not fix it, and #2319 does not fix this one — they are independent bugs in different subsystems.

…passes

buffered_seek() handled a negative offset with

    ctx->filebuffer_pos += offset;
    if (ctx->filebuffer_pos < 0)

but filebuffer_pos is unsigned int, so the addition wraps instead of going
negative and the check can never be true. The guard below it compares
filebuffer_pos + startbytes_pos against 0 and is dead for the same reason, so
the "seek before buffer start" fatal() is unreachable.

Seeking back further than the current position therefore left filebuffer_pos at
roughly 2^32. buffered_read() then evaluated

    bytes <= ctx->bytesinbuffer - ctx->filebuffer_pos

which underflows too, so the bounds check passed and memcpy() read from
filebuffer + 2.3e9 against a 1 MB buffer.

Observed on a malformed WTV file, where a corrupt chunk offset produces
buffered_seek(ctx, -1969339576) with filebuffer_pos = 1048576:

    filebuffer_pos  -> 2326676296
    bytesinbuffer   =    1048576
    bytesinbuffer - filebuffer_pos = 1969339576   (underflow)
    memcpy(dst, filebuffer + 2326676296, 32)      -> SIGSEGV

Do the seek arithmetic in int64_t so both guards work as written, and route the
three buffer-space checks through a buffered_bytes_left() helper that returns 0
when the position is past the end. buffered_read_byte() had the same hazard and
would have indexed filebuffer[] out of bounds.

Input files are untrusted, so this is a bounds-check fix, not only a crash fix.
MSVC warns C4267 on "filebuffer_pos += bytes": filebuffer_pos is an unsigned
int and bytes is a size_t. The warning predates this branch -- it fires 54
times on master -- but the guard this commit series added is what makes the
conversion provably safe, so state that in the code instead of leaving a
warning that a reader has to re-derive.

bytes is known to be no larger than what is left of the buffer, and that
remainder is itself an unsigned int, so the value always fits.
@ccextractor-bot

Copy link
Copy Markdown
Collaborator
CCExtractor CI platform finished running the test files on linux. Below is a summary of the test results, when compared to test for commit b67effd...:
Report Name Tests Passed
Broken 9/13
CEA-708 2/14
DVB 0/7
DVD 3/3
DVR-MS 2/2
General 22/27
Hardsubx 1/1
Hauppage 3/3
MP4 3/3
NoCC 10/10
Options 69/86
Teletext 0/21
WTV 12/13
XDS 31/34

NOTE: The following tests have been failing on the master branch as well as the PR:


This PR does not introduce any new test failures. However, some tests are failing on both master and this PR (see above).

Check the result page for more info.

@ccextractor-bot

Copy link
Copy Markdown
Collaborator
CCExtractor CI platform finished running the test files on windows. Below is a summary of the test results, when compared to test for commit b67effd...:
Report Name Tests Passed
Broken 9/13
CEA-708 2/14
DVB 0/7
DVD 3/3
DVR-MS 2/2
General 22/27
Hardsubx 1/1
Hauppage 3/3
MP4 3/3
NoCC 10/10
Options 69/86
Teletext 0/21
WTV 12/13
XDS 31/34

NOTE: The following tests have been failing on the master branch as well as the PR:


This PR does not introduce any new test failures. However, some tests are failing on both master and this PR (see above).

Check the result page for more info.

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