From ccbe8b009dbc4599a73d9786b1d583be4de319b5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 15 Aug 2026 16:25:25 -0400 Subject: [PATCH 1/5] Fix segfault: keep Graph alive from held filter ctx A FilterContext held a weakref to its Graph, so dropping the Graph freed the underlying AVFilterContext while the context was still held. Using it (push/link_to/process_command) then dereferenced freed memory. Make `_graph` a strong ref; the Graph<->context cycle is collected by gc. --- av/filter/context.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/av/filter/context.py b/av/filter/context.py index 1dfd6e7a4..857e9270e 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -1,5 +1,3 @@ -import weakref - import cython import cython.cimports.libav as lib from cython.cimports.av.audio.frame import alloc_audio_frame @@ -23,7 +21,7 @@ def wrap_filter_context( graph: Graph, filter: Filter, ptr: cython.pointer[lib.AVFilterContext] ) -> FilterContext: self: FilterContext = FilterContext(_cinit_sentinel) - self._graph = weakref.ref(graph) + self._graph = graph self.filter = filter self.ptr = ptr @@ -110,10 +108,7 @@ def link_to( @property def graph(self): - if graph := self._graph(): - return graph - else: - raise RuntimeError("graph is unallocated") + return self._graph def push(self, frame: Frame | None): res: cython.int From d75b9b3bfcaf0a8c59c6add44f57b6b78df28fb5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 15 Aug 2026 16:38:32 -0400 Subject: [PATCH 2/5] Fix two more filter/stream segfaults - push() to a buffer source before the graph is configured dereferenced uninitialized graph internals. Auto-configure first, mirroring pull(). - add_stream_from_template() dereferenced a template Stream's AVStream ptr without checking its source container was still open. Guard with _assert_open(). --- av/container/output.py | 2 ++ av/filter/context.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/av/container/output.py b/av/container/output.py index 44032bacd..1f424e10a 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -282,6 +282,8 @@ def add_stream_from_template( :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ + template.container._assert_open() + if opaque is None: opaque = template.type != "video" diff --git a/av/filter/context.py b/av/filter/context.py index 857e9270e..1ec3674d1 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -113,6 +113,11 @@ def graph(self): def push(self, frame: Frame | None): res: cython.int + # av_buffersrc_write_frame() dereferences graph internals that only + # exist after configuration; pushing first would segfault. + if self._kind == _KIND_SOURCE or frame is None: + self._graph.configure() + if frame is None: with cython.nogil: res = lib.av_buffersrc_write_frame(self.ptr, cython.NULL) From 184e2f8ab392cbfc712bb88c23527f100e3f443f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 15 Aug 2026 16:38:32 -0400 Subject: [PATCH 3/5] Fix AttributeError in FilterPad repr: add missing type property FilterPad.__repr__ and FilterContextPad.__repr__ referenced self.type, which was never defined, so repr() of any pad raised AttributeError. Add a type property via avfilter_pad_get_type, mirroring the name property. --- av/filter/link.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/av/filter/link.py b/av/filter/link.py index d9a9bddf5..6cc3b023a 100644 --- a/av/filter/link.py +++ b/av/filter/link.py @@ -89,6 +89,13 @@ def is_output(self): def name(self): return lib.avfilter_pad_get_name(self.base_ptr, self.index) + @property + def type(self): + media_type = lib.av_get_media_type_string( + lib.avfilter_pad_get_type(self.base_ptr, self.index) + ) + return "unknown" if media_type == cython.NULL else media_type + @cython.final @cython.cclass From ff0d071833db7f8f5b17ade0aadfb551640da7ac Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 15 Aug 2026 16:46:14 -0400 Subject: [PATCH 4/5] Drop Graph's redundant buffer-source lists `_context_by_type` already indexes them, which is how the sink side reads its contexts. --- av/filter/graph.pxd | 2 -- av/filter/graph.py | 16 ++++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index bf22cd34d..17775902e 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -20,5 +20,3 @@ cdef class Graph: cdef int _nb_filters_seen cdef dict[size_t, FilterContext] _context_by_ptr cdef dict[str, list[FilterContext]] _context_by_type - cdef list[FilterContext] _video_sources - cdef list[FilterContext] _audio_sources diff --git a/av/filter/graph.py b/av/filter/graph.py index d2409a0dc..d2067facb 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -22,8 +22,6 @@ def __cinit__(self): self._nb_filters_seen = 0 self._context_by_ptr = {} self._context_by_type = {} - self._video_sources = [] - self._audio_sources = [] def __dealloc__(self): if self.ptr: @@ -113,10 +111,6 @@ def _register_context(self, ctx: FilterContext) -> cython.void: name: str = ctx.filter.ptr.name self._context_by_ptr[cython.cast(cython.size_t, ctx.ptr)] = ctx self._context_by_type.setdefault(name, []).append(ctx) - if name == "buffer": - self._video_sources.append(ctx) - elif name == "abuffer": - self._audio_sources.append(ctx) @cython.cfunc def _auto_register(self) -> cython.void: @@ -250,11 +244,13 @@ def push(self, frame, at: cython.int = -1): every buffer source matching the frame's type. """ if frame is None: - contexts = self._video_sources + self._audio_sources + contexts = self._get_context_by_type("buffer") + self._get_context_by_type( + "abuffer" + ) elif isinstance(frame, VideoFrame): - contexts = self._video_sources + contexts = self._get_context_by_type("buffer") elif isinstance(frame, AudioFrame): - contexts = self._audio_sources + contexts = self._get_context_by_type("abuffer") else: raise ValueError( f"can only AudioFrame, VideoFrame or None; got {type(frame)}" @@ -273,7 +269,7 @@ def push(self, frame, at: cython.int = -1): def vpush(self, frame: VideoFrame | None, at: cython.int = -1): """Like :meth:`push`, but only for :class:`.VideoFrame`.""" - contexts = self._video_sources + contexts = self._get_context_by_type("buffer") if at >= 0: if at >= len(contexts): raise IndexError( From a6fc5bd2792fd1aed5a0e88772856a85fa468e76 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 15 Aug 2026 16:46:56 -0400 Subject: [PATCH 5/5] Pack Graph's two int fields together __basicsize__ 80 -> 72. --- av/filter/graph.pxd | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index 17775902e..bedf924e9 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -4,19 +4,18 @@ from av.filter.context cimport FilterContext cdef class Graph: + # Fields are laid out in declaration order: pointers first, then the two + # ints paired up, so there are no padding holes between them. cdef object __weakref__ - cdef lib.AVFilterGraph *ptr - + cdef dict _name_counts + cdef dict[size_t, FilterContext] _context_by_ptr + cdef dict[str, list[FilterContext]] _context_by_type cdef readonly bint configured - cpdef configure(self, bint auto_buffer=*, bint force=*) + cdef int _nb_filters_seen - cdef dict _name_counts + cpdef configure(self, bint auto_buffer=*, bint force=*) cdef str _get_unique_name(self, str name) cdef list[FilterContext] _get_context_by_type(self, str type) - cdef void _register_context(self, FilterContext) cdef void _auto_register(self) - cdef int _nb_filters_seen - cdef dict[size_t, FilterContext] _context_by_ptr - cdef dict[str, list[FilterContext]] _context_by_type