Skip to content

Raise instead of crashing when decoding a stream with no codec context#2345

Closed
justinrmiller wants to merge 1 commit into
PyAV-Org:mainfrom
justinrmiller:fix/threaded-decode-crash-truncated-input
Closed

Raise instead of crashing when decoding a stream with no codec context#2345
justinrmiller wants to merge 1 commit into
PyAV-Org:mainfrom
justinrmiller:fix/threaded-decode-crash-truncated-input

Conversation

@justinrmiller

Copy link
Copy Markdown

Raise instead of crashing when decoding a stream with no codec context

Fixes #2344.

Summary

Calling container.decode() / Stream.decode() on a video (or audio) stream
whose codec_context is None — which happens when the container cannot
establish codec parameters from a truncated or corrupt bitstream — currently
crashes the entire process with a fatal memory-access fault (SIGSEGV or
SIGBUS; EXC_BAD_ACCESS on macOS) rather than raising a catchable Python
exception. Because the fault is a native memory access inside FFmpeg's
multithreaded decoder, no try/except can recover it — one bad input takes
down the whole interpreter.

This PR adds a guard to VideoStream.decode and AudioStream.decode: if the
stream has no codec context, raise a clear ValueError instead of handing the
packet to a decoder that will run FFmpeg's multithreaded decode over
unvalidated state.

Reproducer

import av, io

data = open("some_h264.mp4", "rb").read()
truncated = data[:406]           # cut inside the first coded frame

container = av.open(io.BytesIO(truncated))
stream = container.streams.video[0]
assert stream.codec_context is None          # container couldn't build one
for _ in container.decode(stream):           # <-- SIGBUS, process dies
    pass

Exit status is 138 (128 + SIGBUS). A Python try/except around the loop does
not help — the process is gone before any exception is raised.

Root cause

Traced with a fault report (maceOS .ips), the faulting frame is in PyAV's own
compiled decode path, dereferencing a wild/misaligned pointer:

exception: EXC_BAD_ACCESS, signal SIGBUS, subtype EXC_ARM_DA_ALIGN at 0xb40001e9f9405109
faulting thread 0:
  ...
  stream.abi3.so   VideoStream.decode        <-- here
  packet.abi3.so   Packet.decode
  ...

The decode context PyAV creates for container streams uses FFmpeg's default
threading (thread_count = 0 → auto; here 4 threads, thread_type = SLICE).
FFmpeg 8.1's slice-multithreaded H.264 decoder performs an out-of-bounds /
misaligned read when handed a stream truncated inside its first coded frame.
Three independent observations confirm the threading provenance:

Path Result on the same bytes
ffmpeg CLI (same libav 62.x) graceful — "Output file does not contain any stream", no crash
PyAV, default auto-threaded container.decode() SIGBUS
PyAV, thread_count = 1 / thread_type = "NONE" graceful — decodes 0 frames, no crash

It is a general class of inputs, not one file

Sweeping truncation offsets across four synthetic H.264/mp4 variants (different
durations, GOP sizes, total sizes), decoding each prefix in an isolated
subprocess
so a crash only kills the child:

Variant Auto-threaded (default) Single-threaded
dur 3.0 gop 10 (2442 B) 8 / 222 cuts crash 0 / 222
dur 3.0 gop 1 (2545 B) 8 / 232 cuts crash 0 / 232
dur 5.0 gop 15 (3124 B) 8 / 284 cuts crash 0 / 284
dur 2.0 gop 5 (2154 B) 8 / 196 cuts crash 0 / 196

The crashing cuts fall in a contiguous band at the same structural byte
offsets
across every variant (truncations landing inside the first coded
frame), i.e. ~3–4% of arbitrary truncation points. Every crashing cut has
stream.codec_context is None; single-threaded decode is immune to all of them.

The fix

VideoStream.decode / AudioStream.decode are thin passthroughs to
self.codec_context.decode(packet). When codec_context is None this should
never proceed to a native decode. The guard converts the process-killing crash
into a clear, catchable error:

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)

This is not a perf change for valid streams (they have a codec context and
are unaffected — default multithreaded decode is preserved). It only affects the
previously-crashing codec_context is None path.

Note on scope

The underlying out-of-bounds read is arguably an FFmpeg bug in the
slice-threaded H.264 decoder; it should also be reported upstream. This PR is
the PyAV-side containment: a library should raise, not SIGBUS, on input it
already knows it cannot decode (codec_context is None). It does not attempt to
make the multithreaded decoder itself safe on corrupt input.

Tests

Adds a regression test (tests/test_decode.py::TestDecode::test_decode_truncated_no_codec_context_raises)
that encodes a short faststart H.264/mp4, finds a truncation prefix that opens
but yields codec_context is None, and asserts decode() raises ValueError
(rather than crashing the test process).

Verification

Built from source against FFmpeg 8.1.2 (libavcodec 62.28.102, matching the
released wheel) and confirmed both directions:

  • Without the patch: the regression test segfaults the interpreter
    (pytest exits 139); a direct container.decode() on the crashing bytes dies
    the same way.
  • With the patch: the regression test passes, and the full
    tests/test_decode.py suite is green (14 passed, 4 skipped).

container.decode()/Stream.decode() on a video or audio stream whose
codec_context is None (the container could not establish codec parameters
from a truncated/corrupt bitstream) crashed the whole process with a fatal
memory-access fault (SIGSEGV/SIGBUS): FFmpeg 8.1's slice-multithreaded decoder
does an out-of-bounds read on such input, which no try/except can recover.
Guard VideoStream.decode and AudioStream.decode to raise a clear ValueError
instead, and add a regression test plus a CHANGELOG entry.
@justinrmiller
justinrmiller force-pushed the fix/threaded-decode-crash-truncated-input branch from 7879cb6 to 7ed413c Compare July 21, 2026 04:20
@WyattBlue

Copy link
Copy Markdown
Member

slop

@WyattBlue WyattBlue closed this Jul 21, 2026
@justinrmiller

justinrmiller commented Jul 21, 2026

Copy link
Copy Markdown
Author

Hey, sorry but this isn’t slop. I’m writing a video processing pipeline that will be processing a lot of uploaded videos and this situation came up if the user uploaded an invalid file.

I have successfully reproduced this on an M4 Pro MBP.

@WyattBlue

Copy link
Copy Markdown
Member

I can repo too, but your diagnosis is wrong and reads like an non-frontier LLM wrote it.

@justinrmiller

Copy link
Copy Markdown
Author

Alright, looks like you have a PR with the proper fix. Sorry for the hassle.

@WyattBlue

Copy link
Copy Markdown
Member

My fix is better because it correctly identifies the real cause and fixes related problems you didn't mention. #2346

@justinrmiller

Copy link
Copy Markdown
Author

Thanks. Looks interesting, I’m not an expert in this space. Appreciate the quick fix, I’ll rerun our tests when the new version comes out.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decoding a stream with no codec context (truncated bitstream) crashes the process (SIGSEGV/SIGBUS) instead of raising

2 participants