
Proof of concept for CVE-2026-86476: GStreamer GstVideoMeta heap out-of-bounds read/write via unixfd deserialization, with ASan repro and RCE chain.
A heap buffer overflow in GStreamer's libgstvideo (gst-plugins-base). A GstVideoMeta describes how a video frame's pixels are laid out in memory. When that description is deserialized from an untrusted unixfd peer, nothing checks that the geometry it claims fits inside the buffer backing it. A peer can set oversized plane strides, and the consumer reads or writes past the allocation.
| Field | Value |
|---|---|
| CVE | CVE-2026-86476 |
| Class | CWE-125 (out-of-bounds read), CWE-787 (out-of-bounds write), reached through CWE-502 (deserialization of untrusted data) |
| Component | libgstvideo: gstvideometa.c, video-frame.c |
| Impact | Out-of-bounds read (confirmed), out-of-bounds write to code execution (demonstrated) |
| Trigger | A GstVideoMeta with fabricated plane strides, deserialized from a unixfd peer |
| Fault site | video_orc_convert_I420_BGRA, the videoconvert ORC kernel |
The bug is three missing checks that compound.
No size check in the meta branch of the frame map. gst_video_frame_map_id() in video-frame.c has two branches. The no-meta branch ends with if (frame->map[0].size < info->size) goto invalid_size;. The meta branch, taken whenever a GstVideoMeta is present, only enforces info->width <= meta->width and info->height <= meta->height. Those are lower bounds. Nothing compares the plane geometry against the size of the real memory.
default_map() checks one byte per plane. In gstvideometa.c, the plane memory is resolved with gst_buffer_find_memory(buffer, offset, 1, ...). The size argument is 1. The code confirms a single byte exists at the plane's start offset, then copies the stride out as is. The real extent of the plane, stride * height, is never compared against the allocation. A FIXME use plane size comment sits directly above the check.
The deserializer range-checks nothing. video_meta_deserialize() validates only that the byte reader did not run out of input, that n_planes <= GST_VIDEO_MAX_PLANES, and that version != 0. Offsets and strides are copied straight out of the stream. Each stride is read as a signed gint32, so a negative stride is accepted.
On the receiving side, gst_unix_fd_src_create() deserializes the metas against an empty buffer and attaches the peer's memory afterward. The stride the meta carries and the size of the memory that backs the buffer are never compared.
A hostile peer serializes a GstVideoMeta for a 320x240 I420 frame with every plane stride set to 1000000. The consumer deserializes it without a range check, attaches the small real memory, and maps the frame. default_map() is satisfied because the first byte of each plane exists. videoconvert then reads each input plane for stride * height bytes and walks past the allocation.
That is the read. The write is the mirror image. gst_video_frame_copy_plane() does a memcpy of stride * height bytes, so when a small buffer carrying the lying meta is the destination of a copy, the memcpy writes past its backing.
The unixfd transport copies non-FD buffers into a memfd, so the stock pipeline lands the read against an mmap'd mapping boundary (crash and information leak). Driving the same deserializer with the same untrusted bytes against a heap-backed destination exercises the write primitive. The transport decides whether the overflow hits a mapping boundary or the heap. It does not decide whether the bug exists.
With the heap write, the proof of concept overwrites the free function pointer in the header of a neighbouring GstMiniObject, places the command string where it lands in the first argument register, and triggers an unref so the forged pointer is called as system().
Any pipeline that accepts video buffers over unixfd from a process it does not fully trust. Sandboxed media decoders, portal-style split pipelines, and anything using unixfdsrc to receive frames across a privilege or trust boundary.
Confirmed:
libgstreamer-1.0.so.0.2804.0 (1.28.4-2) with glibc 2.42.Two processes over a unix socket, stock elements only.
fakesrc ! capsfilter ! unixfdsink, pushing a valid 320x240 I420 buffer with real strides 320/160/160 but a GstVideoMeta whose plane strides are 1000000.unixfdsrc ! videoconvert ! capsfilter ! fakesink, where the capsfilter forces I420 to BGRA so videoconvert must read the frame by the meta strides.| Run | Meta strides | Result |
|---|---|---|
| 1 | 1000000 / 1000000 / 1000000 | out-of-bounds read under ASan in video_orc_convert_I420_BGRA |
| 2 | 320 / 160 / 160 | normal completion, buffer reaches fakesink |
Build GStreamer with -Db_sanitize=address for the read-side proof. The RCE proof of concept runs on a stock build, because an ASan build aborts on the first overflowing memcpy.
default_map(), replace the one-byte gst_buffer_find_memory(..., 1, ...) with a check against the stride * height plane extent.gst_video_frame_map_id() the same invalid_size check the no-meta branch has.video_meta_deserialize(), range-check offsets and strides against the buffer, and reject negative strides.Any one of these breaks the chain. All three is the correct fix for a structure that crosses a trust boundary.