Inside GStreamer's media parsers: 29 new bugs and a way to find more

GStreamer, the open-source multimedia framework that ships by default with GNOME-based Linux distributions, is a high-value target for security research. It powers playback in GNOME Videos and Rhythmbox, and it's used by Nautilus and tracker-miners for metadata extraction. A flaw in GStreamer can expose multiple attack surfaces across the desktop, as demonstrated by the one-click RCE in GNOME's tracker-miners disclosed last year.

The framework is also enormous — more than 300 sub-modules. For this research, the focus was limited to the "Base" and "Good" plugin sets, both included by default in Ubuntu. That scope yielded 29 newly discovered vulnerabilities, mostly in the MKV and MP4 demuxers. The results are summarized below.

GHSL CVE DESCRIPTION
GHSL-2024-094 CVE-2024-47537 OOB-write in isomp4/qtdemux.c
GHSL-2024-115 CVE-2024-47538 Stack-buffer overflow in vorbis_handle_identification_packet
GHSL-2024-116 CVE-2024-47607 Stack-buffer overflow in gst_opus_dec_parse_header
GHSL-2024-117 CVE-2024-47615 OOB-Write in gst_parse_vorbis_setup_packet
GHSL-2024-118 CVE-2024-47613 OOB-Write in gst_gdk_pixbuf_dec_flush
GHSL-2024-166 CVE-2024-47606 Memcpy parameter overlap in qtdemux_parse_theora_extension leading to OOB-write
GHSL-2024-195 CVE-2024-47539 OOB-write in convert_to_s334_1a
GHSL-2024-197 CVE-2024-47540 Uninitialized variable in gst_matroska_demux_add_wvpk_header leading to function pointer ovewriting
GHSL-2024-228 CVE-2024-47541 OOB-write in subparse/gstssaparse.c
GHSL-2024-235 CVE-2024-47542 Null pointer dereference in id3v2_read_synch_uint
GHSL-2024-236 CVE-2024-47543 OOB-read in qtdemux_parse_container
GHSL-2024-238 CVE-2024-47544 Null pointer dereference in qtdemux_parse_sbgp
GHSL-2024-242 CVE-2024-47545 Integer underflow in FOURCC_strf parsing leading to OOB-read
GHSL-2024-243 CVE-2024-47546 Integer underflow in extract_cc_from_data leading to OOB-read
GHSL-2024-244 CVE-2024-47596 OOB-read in FOURCC_SMI_ parsing
GHSL-2024-245 CVE-2024-47597 OOB-read in qtdemux_parse_samples
GHSL-2024-246 CVE-2024-47598 OOB-read in qtdemux_merge_sample_table
GHSL-2024-247 CVE-2024-47599 Null pointer dereference in gst_jpeg_dec_negotiate
GHSL-2024-248 CVE-2024-47600 OOB-read in format_channel_mask
GHSL-2024-249 CVE-2024-47601 Null pointer dereference in gst_matroska_demux_parse_blockgroup_or_simpleblock
GHSL-2024-250 CVE-2024-47602 Null pointer dereference in gst_matroska_demux_add_wvpk_header
GHSL-2024-251 CVE-2024-47603 Null pointer dereference in gst_matroska_demux_update_tracks
GHSL-2024-258 CVE-2024-47778 OOB-read in gst_wavparse_adtl_chunk
GHSL-2024-259 CVE-2024-47777 OOB-read in gst_wavparse_smpl_chunk
GHSL-2024-260 CVE-2024-47776 OOB-read in gst_wavparse_cue_chunk
GHSL-2024-261 CVE-2024-47775 OOB-read in parse_ds64
GHSL-2024-262 CVE-2024-47774 OOB-read in gst_avi_subtitle_parse_gab2_chunk
GHSL-2024-263 CVE-2024-47835 Null pointer dereference in parse_lrc
GHSL-2024-280 CVE-2024-47834 Use-After-Free read in Matroska CodecPrivate

Why media files break the usual fuzzing playbook

Coverage-guided fuzzing is the standard approach for hunting bugs in C/C++ code, and the usual starting point is a corpus of real-world sample files. Media files, however, make poor fuzzing seeds. They run from megabytes to gigabytes, and since the fuzzer tends to step through the entire file, large inputs slow the process to a crawl.

Minimization tools can shrink files, but they're crude. On a complex format, naively trimming bytes often corrupts the logic of the file structure itself.

An alternative is to skip sample collection entirely and generate a corpus from scratch. There are two routes for that: a grammar-based generator (such as Grammarinator or AFL++ Grammar-Mutator, both used in previous research on Apache), or a purpose-built generator that mimics how the target parses the format. The second option is more work — it demands dissecting both the file structure and the target software — but it pays off in two ways: generated files are dramatically smaller, speeding up fuzzing, and these "synthetic" files tend to reach code paths that real files don't. This generator-based approach is what uncovered the most interesting MP4 and MKV bugs.

Building the corpus generator for MP4

An MP4 file (MPEG-4 Part 14, an ISO-standardized descendent of Apple's QuickTime format) is a set of nested boxes, or atoms. Each box has a 32-bit size field, a 4-character type code (FourCC), and a data payload. Some boxes also carry a 64-bit extended size (for boxes greater than 4 GB) or a 16-byte user type UUID for custom boxes.

The typical top-level hierarchy: an ftyp box declares file type and compatibility, an mdat box holds the actual audio/video frames, a moov box contains presentation metadata with nested trak boxes for individual tracks, and a udta box stores user-defined data.

Mp4 box structure Common MP4 file structure

Why can't a general-purpose fuzzer handle this structure on its own? Look at how a fuzzer like AFL mutates input. Bit/byte flips don't alter file size. But block insertion and deletion do, and here's the problem: when the fuzzer inserts or deletes bytes inside an MP4 box, the size field for that box becomes stale. If the box is nested, all parent box sizes must be recalculated and propagated up the tree. Getting a generic mutator to track and update that implicit hierarchy is impractical, so structural mutations quickly produce files that the parser rejects before reaching deeper code.

Making the generator: from random trees to valid boxes

The generator treats an MP4 file as a tree, each node a box. The implementation proceeds in four stages:

Step 1: Generate unlabelled trees. Random tree shapes with varying node counts are created; at this stage nodes carry no type yet.

3 different 9-node unlabelled trees

The RandomTree class constructor below builds a tree up to a specified node count by walking level by level and assigning a random number of children to each node.

RandomTree::RandomTree(uint32_t total_nodes){
uint32_t curr_level = 0;

//Root node
new_node(-1, curr_level);
curr_level++;

uint32_t rem_nodes = total_nodes - 1;
uint32_t current_node = 0;

while(rem_nodes > 0){

uint32_t num_children = rand_uint32(1, rem_nodes);
uint32_t min_value = this->levels[curr_level-1].front();
uint32_t max_value = this->levels[curr_level-1].back();

for(int i=0; i<num_children; i++){
uint32_t parent_id = rand_uint32(min_value, max_value);
new_node(parent_id, curr_level);
}

curr_level++;
rem_nodes -= num_children;
}
}

Using a random number of children per node produces highly varied tree shapes.

Random generation of child nodes

Once the node budget is spent, generation moves on to the next random tree.

Step 2: Assign FourCC tags. Each node gets a FourCC label, either from the leaf set FOURCC_LIST or the container set CONTAINER_LIST. A fourcc_info struct holds the four-byte ID, a description, and a minimum data size for that box type.

const fourcc_info CONTAINER_LIST[] = {

{FOURCC_moov, "movie", 0,},
{FOURCC_vttc, "VTTCueBox 14496-30", 0},
{FOURCC_clip, "clipping", 0,},
{FOURCC_trak, "track", 0,},
{FOURCC_udta, "user data", 0,},
…

const fourcc_info FOURCC_LIST[] = {

{FOURCC_crgn, "clipping region", 0,},
{FOURCC_kmat, "compressed matte", 0,},
{FOURCC_elst, "edit list", 0,},
{FOURCC_load, "track load settings", 0,},

The labeler walks the tree, selecting a leaf label for childless nodes and a container label otherwise.

…

MP4_labeler::MP4_labeler(RandomTree *in_tree) {
…
for(int i=1; i < this->tree->size(); i++){

Node &node = this->tree->get_node(i);
…
if(node.children().size() == 0){
//LEAF
uint32_t random = rand_uint32(0, FOURCC_LIST_SIZE-1);
fourcc = FOURCC_LIST[random].fourcc;
…
}else{
//CONTAINER
uint32_t random = rand_uint32(0, CONTAINER_LIST_SIZE-1);
fourcc = CONTAINER_LIST[random].fourcc;
…
}
…
node.set_label(label);
}
}

This yields a fully labeled tree structure.

Labeled trees with MP4 box tags

Step 3: Inject random-size padding. Each node is padded with null bytes up to the minimum size required by its FourCC. The variable padding gives the fuzzer room to inject data into box payloads without needing to resize the input itself.

if(node.children().size() == 0){
//LEAF
…
padding = FOURCC_LIST[random].min_size;
random_data = rand_uint32(4, 16);
}else{
//CONTAINER
…
padding = CONTAINER_LIST[random].min_size;
random_data = 0;
}
…
std::string label = uint32_to_string(fourcc);
label += std::string(padding, '\x00');
label += std::string(random_data, '\x41');

Step 4: Compute and propagate box sizes. A recursive traverse method serializes each node, computes the box size, and bubbles size updates up to parent boxes so every container reflects the combined size of its children.

std::string MP4_labeler::traverse(Node &node){
…
for(int i=0; i < node.children().size(); i++){ Node &child = tree->get_node(node.children()[i]);

output += traverse(child);
}

uint32_t size;
if(node.get_id() == 0){
size = 20;
}else{
size = node.get_label().size() + output.size() + 4;
}

std::string label = node.get_label();
uint32_t label_size = label.size();

output = uint32_to_string_BE(size) + label + output;
…
}

The size of the generated corpus scales with time and resources available. The corpus used for this GStreamer research held roughly four million files.

Practical takeaways

Generating a structured corpus from scratch is more upfront work than hoarding samples, but for hierarchical formats like MP4 and MKV it avoids the fundamental mutation problem: when the fuzzer alters inner content, the file's size fields stay valid, so the parser traverses deeper into the demuxing logic. The source code for this generator and the fuzzing setup is available in the GitHub Security Lab repository.