Skip to content

Commit df18834

Browse files
committed
Fix segfaults on streams without a codec context
`Stream.codec_context` is cdef-typed, so with Cython's default nonecheck=False, calling into it while None causes a segfault. A stream has no codec context when avcodec_find_decoder comes up empty on demux, or when it was created by `add_mux_stream`. Decoding and encoding such a stream now raise DecoderNotFoundError and EncoderNotFoundError through a shared guard. `BitStreamFilterContext` instead skips the dereference entirely: it only mirrored codecpar into the context, and avcodec_parameters_copy has already given the muxer what it needs, so `add_mux_stream` with h264_mp4toannexb now works rather than crashing. Closes #2344.
1 parent 49b2e96 commit df18834

8 files changed

Lines changed: 54 additions & 6 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ Features:
3636
- Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`).
3737
- ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`.
3838

39+
Fixes:
40+
41+
- Fix a crash when using a stream that has no ``CodecContext`` (a demuxed stream with no available decoder, such as one from a truncated file, or a stream created by ``add_mux_stream``); decoding now raises ``DecoderNotFoundError``, encoding now raises ``EncoderNotFoundError``, and ``BitStreamFilterContext`` accepts such a stream as ``out_stream`` by :gh-user:`WyattBlue`, reported by :gh-user:`justinrmiller` (:issue:`2344`).
42+
3943
v18.0.0
4044
-------
4145

av/audio/stream.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import cython
2+
from cython.cimports import libav as lib
23
from cython.cimports.av.audio.frame import AudioFrame
34
from cython.cimports.av.packet import Packet
45

@@ -31,7 +32,7 @@ def encode(self, frame: AudioFrame | None = None):
3132
3233
.. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`.
3334
"""
34-
35+
self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND)
3536
packets = self.codec_context.encode(frame)
3637
packet: Packet
3738
for packet in packets:
@@ -49,5 +50,5 @@ def decode(self, packet: Packet | None = None):
4950
5051
.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
5152
"""
52-
53+
self._assert_has_codec_context()
5354
return self.codec_context.decode(packet)

av/bitstream.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ def __cinit__(
6161
out_stream.ptr.codecpar, self.ptr.par_out
6262
)
6363
err_check(res)
64-
lib.avcodec_parameters_to_context(
65-
out_stream.codec_context.ptr, out_stream.ptr.codecpar
66-
)
64+
# codecpar carries everything the muxer needs; a mux-only stream
65+
# (add_mux_stream) has no context to keep in sync.
66+
if out_stream.codec_context is not None:
67+
lib.avcodec_parameters_to_context(
68+
out_stream.codec_context.ptr, out_stream.ptr.codecpar
69+
)
6770

6871
def __dealloc__(self):
6972
if self.ptr:

av/stream.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ cdef class Stream:
2121

2222
# Private API.
2323
cdef _init(self, Container, lib.AVStream*, CodecContext)
24+
cdef _assert_has_codec_context(self, int err=*)
2425
cdef _finalize_for_output(self)
2526
cdef _set_id(self, value)
2627

av/stream.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ def _init(
128128
errors=self.container.metadata_errors,
129129
)
130130

131+
@cython.cfunc
132+
def _assert_has_codec_context(
133+
self, err: cython.int = lib.AVERROR_DECODER_NOT_FOUND
134+
):
135+
# Calling into a NULL codec_context is a segfault, not an AttributeError.
136+
if self.codec_context is None:
137+
err_check(err)
138+
131139
def __repr__(self):
132140
name = getattr(self, "name", None)
133141
return (

av/subtitles/stream.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def decode(self, packet: Packet | None = None):
1818
1919
.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
2020
"""
21+
self._assert_has_codec_context()
2122
if not packet:
2223
packet = Packet()
2324

av/video/stream.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def encode(self, frame: VideoFrame | None = None):
4040
4141
.. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`.
4242
"""
43-
43+
self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND)
4444
packets = self.codec_context.encode(frame)
4545
packet: Packet
4646
for packet in packets:
@@ -57,6 +57,7 @@ def decode(self, packet: Packet | None = None):
5757
5858
.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
5959
"""
60+
self._assert_has_codec_context()
6061
return self.codec_context.decode(packet)
6162

6263
@cython.cfunc

tests/test_decode.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import io
23
import os
34
import pathlib
45
from fractions import Fraction
@@ -52,6 +53,34 @@ def make_h264_test_video(path: str) -> None:
5253

5354

5455
class TestDecode(TestCase):
56+
def test_decode_stream_without_codec_context(self) -> None:
57+
buffer = io.BytesIO()
58+
with av.open(buffer, "w", format="mp4") as output:
59+
stream = output.add_mux_stream("h264", width=16, height=16)
60+
packet = av.Packet(b"invalid")
61+
packet.stream = stream
62+
packet.pts = packet.dts = 0
63+
packet.time_base = Fraction(1, 1000)
64+
output.mux(packet)
65+
66+
# Keep the MP4 video stream while making its codec unknown to FFmpeg.
67+
data = buffer.getvalue().replace(b"avc1", b"zzzz")
68+
with av.open(io.BytesIO(data)) as container:
69+
stream = container.streams.video[0]
70+
assert stream.codec_context is None
71+
with pytest.raises(av.DecoderNotFoundError):
72+
list(container.decode(stream))
73+
74+
def test_mux_stream_without_codec_context(self) -> None:
75+
with av.open(io.BytesIO(), "w", format="mp4") as output:
76+
stream = output.add_mux_stream("h264", width=16, height=16)
77+
assert stream.codec_context is None
78+
with pytest.raises(av.EncoderNotFoundError):
79+
stream.encode(None)
80+
81+
# A bitstream filter only needs to update codecpar for a mux stream.
82+
av.BitStreamFilterContext("h264_mp4toannexb", "h264", out_stream=stream)
83+
5584
def test_decoded_video_frame_count(self) -> None:
5685
container = av.open(fate_suite("h264/interlaced_crop.mp4"))
5786
video_stream = next(s for s in container.streams if s.type == "video")

0 commit comments

Comments
 (0)