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.
filter::Graph::{add,get}returnfilter::Contextby value without tying its lifetime to the graph. FFmpeg owns eachAVFilterContextthrough theAVFilterGraph, 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":Valgrind, trimmed to the relevant frames:
Why the borrow checker permits it
When this changed
filter::Contextused to carry the lifetime, andGraph::getused to return it: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 haspub 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-next9.0.0 on two independent stacks:libavfilter.so.8.44.100)libavfilter.so.10.5.100), rustc 1.98.0 stable, Valgrind 3.24.0The 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 forkffmpeg-the-thirdhas shipped exactly that shape (Context<'a>withadd/getreturningContext<'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.