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.
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.
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.
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.
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.



