Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Features:
- 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`).
- ``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`.

Fixes:

- ``VideoStream.decode`` and ``AudioStream.decode`` now raise ``ValueError`` instead of crashing the process when the stream has no codec context (e.g. a bitstream truncated inside its first coded frame); decoding such input ran FFmpeg 8.1's slice-multithreaded decoder over unvalidated state, which could perform an out-of-bounds read and abort the process with ``SIGSEGV``/``SIGBUS`` by :gh-user:`justinrmiller` in (:pr:`2345`).

v18.0.0
-------

Expand Down
8 changes: 7 additions & 1 deletion av/audio/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,11 @@ def decode(self, packet: Packet | None = None):

.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
"""

if self.codec_context is None:
raise ValueError(
"cannot decode: stream has no codec context (the container could "
"not identify a decoder for this stream, e.g. a truncated or "
"corrupt bitstream). Decoding it would run FFmpeg's multithreaded "
"decoder on unvalidated state, which can crash the process."
)
return self.codec_context.decode(packet)
7 changes: 7 additions & 0 deletions av/video/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ def decode(self, packet: Packet | None = None):

.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
"""
if self.codec_context is None:
raise ValueError(
"cannot decode: stream has no codec context (the container could "
"not identify a decoder for this stream, e.g. a truncated or "
"corrupt bitstream). Decoding it would run FFmpeg's multithreaded "
"decoder on unvalidated state, which can crash the process."
)
return self.codec_context.decode(packet)

@cython.cfunc
Expand Down
51 changes: 51 additions & 0 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import io
import os
import pathlib
from fractions import Fraction
Expand Down Expand Up @@ -190,6 +191,56 @@ def test_decode_video_corrupt(self) -> None:
assert packet_count == 1
assert frame_count == 0

def test_decode_truncated_no_codec_context_raises(self) -> None:
# A bitstream truncated inside its first coded frame can leave the
# container able to open but unable to establish codec parameters
# (`stream.codec_context is None`). Decoding it previously ran FFmpeg's
# multithreaded decoder over unvalidated state and crashed the whole
# process (SIGSEGV/SIGBUS) on a broad class of such inputs; it must now
# raise a catchable error instead.
try:
av.codec.Codec("libx264", "w")
except Exception:
pytest.skip("libx264 not available")

# Build a short faststart H.264/mp4 (moov up front, so a prefix still
# opens while its first frame is incomplete). faststart rewrites the
# file on close, so encode to a real path rather than a BytesIO.
path = self.sandboxed("truncation_fixture.mp4")
with av.open(path, "w", format="mp4", options={"movflags": "faststart"}) as out:
stream = out.add_stream("libx264", rate=30)
stream.width, stream.height, stream.pix_fmt = 320, 240, "yuv420p"
for _ in range(30):
frame = av.VideoFrame.from_ndarray(
np.zeros((240, 320, 3), dtype=np.uint8), format="rgb24"
)
for packet in stream.encode(frame):
out.mux(packet)
for packet in stream.encode():
out.mux(packet)
with open(path, "rb") as fh:
data = fh.read()

# Find a prefix that opens but yields no codec context.
target = None
for cut in range(200, len(data)):
try:
with av.open(io.BytesIO(data[:cut])) as c:
if c.streams.video and c.streams.video[0].codec_context is None:
target = cut
break
except av.FFmpegError:
continue
assert target is not None, "expected a prefix that opens with no codec context"

with av.open(io.BytesIO(data[:target])) as container:
stream = container.streams.video[0]
assert stream.codec_context is None
# Must raise (not crash the process) when asked to decode.
with pytest.raises(ValueError):
for packet in container.demux(stream):
packet.decode()

def test_decode_close_then_use(self) -> None:
container = av.open(fate_suite("h264/interlaced_crop.mp4"))
container.close()
Expand Down