Skip to content

filter::Context can outlive its Graph, causing use-after-free from safe code #281

Description

@marius-momeu

filter::Graph::{add,get} return filter::Context by value without tying its lifetime to the graph. FFmpeg owns each AVFilterContext through the AVFilterGraph, so dropping the graph leaves the returned safe wrapper dangling.

This is a soundness report, not a claim of exploitability. The trigger is a caller-selected program shape rather than media input.

Minimal reproduction

With ffmpeg-next = "=9.0.0":

use ffmpeg_next as ffmpeg;
use ffmpeg::{filter, format, frame};

fn dangling_source() -> filter::Context {
    let mut graph = filter::Graph::new();
    let args = "video_size=16x16:pix_fmt=2:time_base=1/25:pixel_aspect=1/1";
    graph.add(&filter::find("buffer").unwrap(), "in", args).unwrap();
    graph.add(&filter::find("buffersink").unwrap(), "out", "").unwrap();
    graph.output("in", 0).unwrap().input("out", 0).unwrap().parse("null").unwrap();
    graph.validate().unwrap();
    graph.get("in").unwrap() // `graph` is dropped on return
}

fn main() {
    ffmpeg::init().unwrap();
    let mut source = dangling_source();
    let mut input = frame::Video::new(format::Pixel::RGB24, 16, 16);
    input.set_pts(Some(0));
    println!("{:?}", source.source().add(&input));
}
$ cargo run
Segmentation fault (core dumped)    # exit 139

Valgrind, trimmed to the relevant frames:

==2989== Invalid read of size 8
==2989==    at av_buffersrc_add_frame_flags (libavfilter.so.10.5.100)
==2989==    by <ffmpeg_next::filter::context::source::Source>::add (source.rs:24)
==2989==    by repro::main (main.rs:19)
==2989==  Address 0x140a2a48 is 72 bytes inside a block of size 168 free'd
==2989==    at free (vg_replace_malloc.c:989)
==2989==    by avfilter_graph_free (libavfilter.so.10.5.100)
==2989==    by <ffmpeg_next::filter::graph::Graph as core::ops::drop::Drop>::drop (graph.rs:114)

Why the borrow checker permits it

// src/filter/context/context.rs:6-8 -- no lifetime parameter, no Drop
pub struct Context {
    ptr: *mut AVFilterContext,
}

// src/filter/graph.rs:52, :71 -- both safe, both return it by value
pub fn add(&mut self, filter: &Filter, name: &str, args: &str) -> Result<Context, Error>
pub fn get(&mut self, name: &str) -> Option<Context>

// src/filter/graph.rs:111-117 -- Graph::drop frees every context in the graph

When this changed

filter::Context used to carry the lifetime, and Graph::get used to return it:

-pub struct Context<'a> {
+pub struct Context {
     ptr: *mut AVFilterContext,
-
-    _marker: PhantomData<&'a ()>,
 }
-    pub fn get<'a, 'b>(&'b mut self, name: &str) -> Option<Context<'b>>
-    where 'a: 'b,
+    pub fn get(&mut self, name: &str) -> Option<Context> {

That is #194 ("remove filter::Context generic lifetime parameter"), which removed the lifetime to fix the ergonomics complaint in #193. Neither mentions soundness.

Checked against the published tarballs: 7.0.3 still has pub struct Context<'a>; 7.0.4 has pub struct Context. Both endpoints verified by hand, so the exposure runs from 7.0.4 through the current 9.0.0.

Environment

Reproduced with ffmpeg-next/ffmpeg-sys-next 9.0.0 on two independent stacks:

  • Debian 12 — FFmpeg 5.1.9 (libavfilter.so.8.44.100)
  • Debian 13 — FFmpeg 7.1.5 (libavfilter.so.10.5.100), rustc 1.98.0 stable, Valgrind 3.24.0

The ownership relationship is part of the FFmpeg API rather than specific to a release.

Full reproduction, control, and instrument logs available on request.

Proposed direction

The returned context needs to borrow the graph. Restoring Context<'a> directly would reintroduce the ergonomics problem from #193/#194, so this needs an API decision. For reference, the downstream fork ffmpeg-the-third has shipped exactly that shape (Context<'a> with add/get returning Context<'b>), if a worked example is useful—for example, graph-owned name/index tokens plus borrowing accessors, or borrowing context handles with an API for obtaining multiple contexts together.

I have not opened a PR because the sound fix is API-breaking. Would you welcome a PR, and which API direction would you prefer?

Found by Crustify, an experimental UB/soundness auditing agent developed at UC Berkeley and running on Claude Opus 5, then manually reviewed and independently reproduced.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions