[BUG] MXF: update_tid_lut() writes past tracks[32] onto nb_tracks, turning a file-supplied track id into the write offset

Open Beginner friendly
#2,457 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 7 days

@pranayr710 is already working on this.

Since Nov 10, 2026.

  • #2458 by @pranayr710 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
65/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c
Domain
backend, security

Research direction

Start in src/lib_ccx/ccx_demuxer_mxf.c at update_tid_lut() and check how it handles a full tracks array before indexing it. Read the callers and track-count loops to confirm the limit behavior, then build and exercise an MXF file declaring more than 32 tracks; done when excess tracks are safely ignored and the track count and neighboring fields remain intact.

Written by the indexing model from the issue text.

Description

Summary

update_tid_lut() in src/lib_ccx/ccx_demuxer_mxf.c appends to a fixed
MXFTrack tracks[32] without checking nb_tracks first. nb_tracks is the
struct field directly behind that array, so the 33rd track a file declares
overwrites the bound itself with a 32-bit value taken from the file — and the
very next statement re-reads that bound to choose where to write.

The overlap is exact
typedef struct MXFContext
{
        enum MXFCaptionType type;        /* byte   0 */
        int cap_track_id;                /* byte   4 */
        UID cap_essence_key;             /* byte   8 */
        MXFTrack tracks[32];             /* byte  24 .. 279 */
        int nb_tracks;                   /* byte 280 */
        int cap_count;                   /* byte 284 */
        struct ccx_rational edit_rate;   /* byte 288 */
} MXFContext;                            /* 296 bytes, heap allocated in ccx_mxf_init() */

sizeof(MXFTrack) is 8, so tracks[32] begins at 24 + 32*8 = 280 — the
same address as nb_tracks.

What happens on the 33rd track
ctx->tracks[ctx->nb_tracks].track_id = track_id;
memcpy(ctx->tracks[ctx->nb_tracks].track_number, track_number, 4);
ctx->edit_rate = edit_rate;
ctx->nb_tracks++;
  • statement 1 writes track_id at byte 280, i.e. into nb_tracks;
  • statement 2 re-evaluates ctx->tracks[ctx->nb_tracks] using the value just
    written, so its destination is 24 + 8*track_id + 4.

track_id is read straight from the file in mxf_read_timeline_track_metadata():

case MXF_TAG_TRACK_ID:
        track_id = buffered_get_be32(demux);

With track_id = 0x7FFFFFF0 the memcpy targets byte 17179869084 of a 296
byte allocation — about 16 GB past it. Smaller ids give smaller, more usable
offsets; the write is at 8 * track_id bytes from the block, with four bytes of
file-supplied content.

Everything afterwards is affected too: every for (i = 0; i < ctx->nb_tracks; i++) over tracks[] (here and in update_cap_essence_key()) becomes an
unbounded read, and cap_count at byte 284 is overwritten by track_number.

Reachability

ccx_probe_mxf() scans the first bytes for the 14-byte header partition pack
key and nothing else, so any file carrying that key is handed to this demuxer.
Each Timeline Track set in the header metadata calls update_tid_lut() once —
33 of them is enough. The C path is live; the Rust side only re-exports
ccx_probe_mxf / ccx_mxf_init ("shall be removed after mxf" in
src/rust/build.rs).

Demonstrating the mechanism

To be clear about what this evidence is and is not: the run below is a
standalone program that reproduces MXFContext and update_tid_lut()
verbatim, with no other CCExtractor code. It demonstrates the pointer
arithmetic and the crash; it is not CCExtractor itself crashing. My sanitizer
build is unavailable at the moment, so I have not yet put the crafted .mxf
files through the real binary end to end — I will add that as a comment rather
than claim it now. The struct offsets below are computed from the real
declaration, and the reachability argument is from the source.

Reproducing the exact struct and function (no other CCExtractor code):

MASTER build
  after 32 tracks: nb_tracks = 32
  adding 33rd track, track_id = 0x7FFFFFF0 ...
  Segmentation fault

FIXED build
  after 32 tracks: nb_tracks = 32
  adding 33rd track, track_id = 0x7FFFFFF0 ...
    [guard] table full at 32, ignoring track 0x7FFFFFF0
  survived: nb_tracks = 32, cap_count = 0x00000000

and the intermediate state, printed between the two statements:

nb_tracks before        = 32
nb_tracks after stmt 1  = 0x7FFFFFF0   <-- clobbered by the file's track_id
statement 2 will memcpy to tracks[2147483632].track_number = byte 17179869084 of a 296 byte block
Suggested fix

Refuse to append once the table is full:

if (ctx->nb_tracks >= (int)(sizeof(ctx->tracks) / sizeof(ctx->tracks[0])))
{
        mprint("MXF: more than %d tracks, ignoring the rest\n", ...);
        return;
}

I have a patch and a generator for the test files, and will open a PR
referencing this issue.

(Analysis and patch prepared with AI assistance; the reproducers are my own.)

Dominant language
C
Stars
913
Forks
603
Avg merge
6d 8h
Merged PRs (30d)
33

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from CCExtractor/ccextractor

All issues in CCExtractor/ccextractor

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.