From c8800991ee8db8020a1e7aa4aab95ce5aeefdcef Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sun, 26 Jul 2026 15:12:43 -0700 Subject: [PATCH] fix(ffmpeg): heap reads after the end, bad packet, checks at open This is a security audit of the FFmpeg reader (specs/002-format-security-audit). Gray movies read heap memory after the end of the buffer. The GRAY8 and GRAY16 code asked swscale for a gray frame with one plane, but it left nchannels at 3. Each scanline copy then read three times too many bytes. Now nchannels is 1. This was the intent when we added the gray code (#2349). It also makes 16-bit gray files readable, which they were not before. A decoder can change the frame size in the middle of a stream. But we make the scale context and the RGB buffer one time, from the header. A smaller frame thus caused a read after its end. Now read_frame() refuses a frame whose size or pixel format is different from the ImageSpec. It also scales from the spec. Nothing examined the frame size at open. A file of 6 KB can declare a frame of 1.1 GB, and cause an allocation of 2.4 GB. Now check_open() and check_compression_ratio() refuse such a file. The ratio floor stays at 1 GB, because a correct movie with one frame can reach a ratio of 13000:1. read_frame() gave an uninitialized AVPacket to av_read_frame(), which does not initialize it when it fails. The code then read bad values from the packet, and it could continue forever on a bad stream. Now the code uses av_packet_alloc(), and it stops when the decoder is empty. There are smaller changes. Examine the result of av_frame_alloc(). Correct a bad nb_frames and start_time. Protect time_stamp() against a time base of zero. Do the frame-count arithmetic in double, because two int64 times can overflow. Report a decode failure, and do not give back the last good frame. Remove an unnecessary av_free() after avformat_close_input(). New fixtures come from testsuite/ffmpeg/src/make_malformed_movies.py: the gray8 and gray16 tests, a decompression bomb, a truncated stream, and a change of size in the middle of a stream. Assisted-by: Claude Code / claude-opus-5 Signed-off-by: Larry Gritz --- src/ffmpeg.imageio/ffmpeginput.cpp | 198 +++++++++++++----- testsuite/ffmpeg/ref/out-ffmpeg6.1.txt | 14 ++ testsuite/ffmpeg/ref/out-ffmpeg8.0.txt | 14 ++ testsuite/ffmpeg/ref/out-ffmpeg8.1.txt | 14 ++ testsuite/ffmpeg/run.py | 25 +++ testsuite/ffmpeg/src/bomb-16384x9000.avi | Bin 0 -> 6592 bytes testsuite/ffmpeg/src/gray16.avi | Bin 0 -> 6726 bytes testsuite/ffmpeg/src/gray8.avi | Bin 0 -> 6468 bytes testsuite/ffmpeg/src/make_malformed_movies.py | 82 ++++++++ testsuite/ffmpeg/src/resolution-change.mkv | Bin 0 -> 2154 bytes testsuite/ffmpeg/src/truncated.avi | Bin 0 -> 2156 bytes 11 files changed, 295 insertions(+), 52 deletions(-) create mode 100644 testsuite/ffmpeg/src/bomb-16384x9000.avi create mode 100644 testsuite/ffmpeg/src/gray16.avi create mode 100644 testsuite/ffmpeg/src/gray8.avi create mode 100644 testsuite/ffmpeg/src/make_malformed_movies.py create mode 100644 testsuite/ffmpeg/src/resolution-change.mkv create mode 100644 testsuite/ffmpeg/src/truncated.avi diff --git a/src/ffmpeg.imageio/ffmpeginput.cpp b/src/ffmpeg.imageio/ffmpeginput.cpp index ba6a3a8b12..165d81fb77 100644 --- a/src/ffmpeg.imageio/ffmpeginput.cpp +++ b/src/ffmpeg.imageio/ffmpeginput.cpp @@ -74,13 +74,37 @@ receive_frame(AVCodecContext* avctx, AVFrame* picture, AVPacket* avpkt) #include +#include +#include #include +#include +#include #include #include OIIO_PLUGIN_NAMESPACE_BEGIN +// Timestamps, frame rates and time bases all come from the file, so the +// arithmetic below can produce NaN, infinity, or values outside the integer +// range. Convert through these helpers to keep that out of undefined +// behavior. safe_int64's range is well under the type's limits so that a sum +// of two results can't overflow either. +static int64_t +safe_int64(double v) +{ + return std::isfinite(v) ? int64_t(clamp(v, -4.0e18, 4.0e18)) : 0; +} + + +static int +safe_int(double v) +{ + return std::isfinite(v) ? int(clamp(v, double(INT_MIN), double(INT_MAX))) + : 0; +} + + class FFmpegInput final : public ImageInput { public: FFmpegInput(); @@ -120,6 +144,7 @@ class FFmpegInput final : public ImageInput { AVFrame* m_frame = nullptr; AVFrame* m_rgb_frame = nullptr; size_t m_stride; // scanline width in bytes, a.k.a. scanline stride + AVPixelFormat m_decoded_pix_format; // what the decoder promised at open AVPixelFormat m_dst_pix_format; SwsContext* m_sws_rgb_context = nullptr; AVRational m_frame_rate; @@ -133,19 +158,21 @@ class FFmpegInput final : public ImageInput { bool m_offset_time; bool m_codec_cap_delay; bool m_read_frame; + bool m_frame_valid; // did the last read_frame() actually decode? int64_t m_start_time; // init to initialize state void init(void) { m_filename.clear(); - m_format_context = nullptr; - m_codec_context = nullptr; - m_codec = nullptr; - m_frame = nullptr; - m_rgb_frame = nullptr; - m_sws_rgb_context = nullptr; - m_stride = 0; + m_format_context = nullptr; + m_codec_context = nullptr; + m_codec = nullptr; + m_frame = nullptr; + m_rgb_frame = nullptr; + m_sws_rgb_context = nullptr; + m_stride = 0; + m_decoded_pix_format = AV_PIX_FMT_NONE; m_rgb_buffer.clear(); m_video_indexes.clear(); m_video_stream = -1; @@ -155,6 +182,7 @@ class FFmpegInput final : public ImageInput { m_last_decoded_pos = 0; m_offset_time = true; m_read_frame = false; + m_frame_valid = false; m_codec_cap_delay = false; m_subimage = 0; m_start_time = 0; @@ -315,31 +343,48 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) AVStream* stream = m_format_context->streams[m_video_stream]; m_frame_rate = av_guess_frame_rate(m_format_context, stream, NULL); - m_frames = stream->nb_frames; + // nb_frames and start_time come from the file. Clamp them to a range the + // rest of this reader can handle: subimage indices are int, and an + // unset start_time (AV_NOPTS_VALUE == INT64_MIN) would blow up the frame + // arithmetic in read_frame(). + m_frames = OIIO::clamp(stream->nb_frames, int64_t(0), int64_t(INT_MAX)); m_start_time = stream->start_time; + if (m_start_time == int64_t(AV_NOPTS_VALUE)) + m_start_time = 0; if (!m_frames) { seek(0); AVPacket pkt; av_init_packet(&pkt); av_read_frame(m_format_context, &pkt); int64_t first_pts = pkt.pts; - int64_t max_pts = 0; + if (first_pts == int64_t(AV_NOPTS_VALUE)) + first_pts = 0; + int64_t max_pts = 0; av_packet_unref(&pkt); //because seek(int) uses m_format_context seek(1 << 29); av_init_packet(&pkt); //Is this needed? while (stream && av_read_frame(m_format_context, &pkt) >= 0) { - int64_t current_pts = static_cast( - av_q2d(stream->time_base) * (pkt.pts - first_pts) * fps()); + // Do the difference in double: both timestamps are untrusted and + // subtracting them as int64 can overflow. + int64_t current_pts = safe_int64( + av_q2d(stream->time_base) + * (double(pkt.pts) - double(first_pts)) * fps()); if (current_pts > max_pts) { max_pts = current_pts + 1; } av_packet_unref(&pkt); //Always free before format_context usage } - m_frames = max_pts; + m_frames = std::min(max_pts, int64_t(INT_MAX)); } m_frame = av_frame_alloc(); m_rgb_frame = av_frame_alloc(); + if (!m_frame || !m_rgb_frame) { + errorfmt("\"{}\" could not allocate FFmpeg frame", file_name); + close(); + return false; + } + m_decoded_pix_format = m_codec_context->pix_fmt; AVPixelFormat src_pix_format; switch (m_codec_context->pix_fmt) { // deprecation warning for YUV formats case AV_PIX_FMT_YUVJ420P: src_pix_format = AV_PIX_FMT_YUV420P; break; @@ -418,6 +463,7 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) case AV_PIX_FMT_GRAY8: case AV_PIX_FMT_MONOWHITE: case AV_PIX_FMT_MONOBLACK: + nchannels = 1; datatype = TypeUInt8; m_dst_pix_format = AV_PIX_FMT_GRAY8; break; @@ -430,6 +476,7 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) case AV_PIX_FMT_GRAY12LE: case AV_PIX_FMT_GRAY16BE: case AV_PIX_FMT_GRAY16LE: + nchannels = 1; datatype = TypeUInt16; m_dst_pix_format = AV_PIX_FMT_GRAY16; break; @@ -496,8 +543,16 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) default: break; } - m_spec = ImageSpec(m_codec_context->width, m_codec_context->height, - nchannels, datatype); + m_spec = ImageSpec(m_codec_context->width, m_codec_context->height, + nchannels, datatype); + // The frame dimensions come from the container/codec headers, which are + // untrusted. FFmpeg caps the pixel count at only about 2.7e8, which still + // leaves room for a tiny file to declare a multi-GB frame. + if (!check_open(m_spec, { 0, 1 << 16, 0, 1 << 16, 0, 1, 0, 4 }) + || !check_compression_ratio(m_spec, Filesystem::file_size(name))) { + close(); + return false; + } m_stride = (size_t)(m_spec.scanline_bytes()); int rgb_buffer_size @@ -592,8 +647,9 @@ FFmpegInput::seek_subimage(int subimage, int miplevel) if (subimage == m_subimage) { return true; } - m_subimage = subimage; - m_read_frame = false; + m_subimage = subimage; + m_read_frame = false; + m_frame_valid = false; return true; } @@ -633,6 +689,14 @@ FFmpegInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, if (!m_read_frame) { read_frame(m_subimage); } + if (!m_frame_valid) { + // Nothing was decoded for this subimage. Without this check we would + // hand back whatever the previously decoded frame left in the buffer. + if (!has_error()) + errorfmt("Could not decode frame {} of \"{}\"", m_subimage, + m_filename); + return false; + } if (m_spec.format == TypeUInt8 || m_spec.nchannels == 1) { if (m_rgb_frame->data[0]) { memcpy(data, m_rgb_frame->data[0] + y * m_rgb_frame->linesize[0], @@ -676,8 +740,8 @@ FFmpegInput::close(void) if (m_codec_context) avcodec_free_context(&m_codec_context); if (m_format_context) { + // Frees the context and everything it owns, and nulls the pointer. avformat_close_input(&m_format_context); - av_free(m_format_context); // will free m_codec and m_codec_context } if (m_frame) av_frame_free(&m_frame); // free after close input @@ -697,61 +761,87 @@ FFmpegInput::read_frame(int frame) if (m_last_decoded_pos + 1 != frame) { seek(frame); } - AVPacket pkt; - int finished = 0; - int ret = 0; - while ((ret = av_read_frame(m_format_context, &pkt)) == 0 - || m_codec_cap_delay) { - if (ret == AVERROR_EOF) { - break; + m_read_frame = true; + m_frame_valid = false; + // Allocate the packet rather than using a stack AVPacket: av_read_frame + // is not guaranteed to initialize it on failure, and the flush path below + // would then unref an uninitialized packet. + AVPacket* pkt = av_packet_alloc(); + if (!pkt) { + errorfmt("Could not allocate FFmpeg packet"); + return; + } + bool flushing = false; + while (true) { + int ret = av_read_frame(m_format_context, pkt); + if (ret < 0) { + if (!m_codec_cap_delay || ret == AVERROR_EOF) + break; + // The codec buffers delayed frames, so keep going with flush + // packets (data == null, size == 0), but stop as soon as the + // decoder runs dry -- otherwise a stream that keeps returning + // the same error would spin here forever. + flushing = true; + av_packet_unref(pkt); + pkt->stream_index = m_video_stream; } - if (pkt.stream_index == m_video_stream) { - if (ret < 0 && m_codec_cap_delay) { - pkt.data = NULL; - pkt.size = 0; - } - - finished = receive_frame(m_codec_context, m_frame, &pkt); + if (pkt->stream_index == m_video_stream) { + int finished = receive_frame(m_codec_context, m_frame, pkt); + if (flushing && !finished) + break; double pts = 0; if (static_cast(m_frame->pts) != int64_t(AV_NOPTS_VALUE)) { pts = av_q2d( m_format_context->streams[m_video_stream]->time_base) - * m_frame->pts; + * double(m_frame->pts); } - int current_frame = int((pts - m_start_time) * fps() + 0.5f); //??? + int current_frame = safe_int((pts - double(m_start_time)) * fps() + + 0.5); //current_frame = m_frame->display_picture_number; m_last_search_pos = current_frame; if (current_frame == frame && finished) { + // A decoder may change frame geometry or pixel format + // mid-stream, but the scaling context and RGB buffer were + // sized from the header when we opened the file. + if (m_frame->width != m_spec.width + || m_frame->height != m_spec.height + || m_frame->format != m_decoded_pix_format) { + errorfmt("\"{}\" frame {} does not match the {}x{} format " + "declared by the header", + m_filename, frame, m_spec.width, m_spec.height); + break; + } + // Use the spec dimensions, which are what m_rgb_buffer and + // the scaling context were sized for -- the decoder can move + // m_codec_context->width/height out from under us. int fill_ret = avpicture_fill(m_rgb_frame, &m_rgb_buffer[0], - m_dst_pix_format, - m_codec_context->width, - m_codec_context->height); + m_dst_pix_format, m_spec.width, + m_spec.height); if (fill_ret < 0) { errorfmt("Error filling FFmpeg RGB frame"); - av_packet_unref(&pkt); break; } - int scale_ret = sws_scale( - m_sws_rgb_context, - static_cast(m_frame->data), - m_frame->linesize, 0, m_codec_context->height, - m_rgb_frame->data, m_rgb_frame->linesize); + int scale_ret = sws_scale(m_sws_rgb_context, + static_cast( + m_frame->data), + m_frame->linesize, 0, m_spec.height, + m_rgb_frame->data, + m_rgb_frame->linesize); if (scale_ret <= 0) { errorfmt("Error converting FFmpeg frame"); - av_packet_unref(&pkt); break; } m_last_decoded_pos = current_frame; - av_packet_unref(&pkt); + m_frame_valid = true; break; } } - av_packet_unref(&pkt); + av_packet_unref(pkt); } - m_read_frame = true; + av_packet_free(&pkt); } @@ -791,15 +881,19 @@ FFmpegInput::seek(int frame) int64_t FFmpegInput::time_stamp(int frame) const { - int64_t timestamp = static_cast( - (static_cast(frame) - / (fps() - * av_q2d(m_format_context->streams[m_video_stream]->time_base)))); + // A corrupt header can give us a zero or degenerate time base, which + // would make the divisions below produce inf/NaN. + double time_base = av_q2d( + m_format_context->streams[m_video_stream]->time_base); + double scale = fps() * time_base; + if (!(scale > 0) || !(time_base > 0)) + return 0; + int64_t timestamp = safe_int64(static_cast(frame) / scale); if (static_cast(m_format_context->start_time) != int64_t(AV_NOPTS_VALUE)) { - timestamp += static_cast( + timestamp += safe_int64( static_cast(m_format_context->start_time) * AV_TIME_BASE - / av_q2d(m_format_context->streams[m_video_stream]->time_base)); + / time_base); } return timestamp; } diff --git a/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt b/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt index 1f1258b343..8d09187e79 100644 --- a/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt +++ b/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt @@ -110,3 +110,17 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray8.avi : 128 x 64, 1 channel, uint8 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray16.avi : 128 x 64, 1 channel, uint16 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +oiiotool ERROR: read : "src/bomb-16384x9000.avi": FFmpeg movie header claims a 1125 MB image from a 6592 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info --hash src/bomb-16384x9000.avi +oiiotool ERROR: read : "src/truncated.avi" could not open input +Full command line was: +> oiiotool --info --hash src/truncated.avi +src/resolution-change.mkv : 320 x 240, 3 channel, uint8 FFmpeg movie (4 subimages) + SHA-1: "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header +iinfo ERROR: "src/resolution-change.mkv" : "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header diff --git a/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt b/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt index 28084a22e3..022f360f6a 100644 --- a/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt +++ b/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt @@ -110,3 +110,17 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray8.avi : 128 x 64, 1 channel, uint8 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray16.avi : 128 x 64, 1 channel, uint16 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +oiiotool ERROR: read : "src/bomb-16384x9000.avi": FFmpeg movie header claims a 1125 MB image from a 6592 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info --hash src/bomb-16384x9000.avi +oiiotool ERROR: read : "src/truncated.avi" could not open input +Full command line was: +> oiiotool --info --hash src/truncated.avi +src/resolution-change.mkv : 320 x 240, 3 channel, uint8 FFmpeg movie (4 subimages) + SHA-1: "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header +iinfo ERROR: "src/resolution-change.mkv" : "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header diff --git a/testsuite/ffmpeg/ref/out-ffmpeg8.1.txt b/testsuite/ffmpeg/ref/out-ffmpeg8.1.txt index 0a9eef7cb9..e5bcc54445 100644 --- a/testsuite/ffmpeg/ref/out-ffmpeg8.1.txt +++ b/testsuite/ffmpeg/ref/out-ffmpeg8.1.txt @@ -110,3 +110,17 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray8.avi : 128 x 64, 1 channel, uint8 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +src/gray16.avi : 128 x 64, 1 channel, uint16 FFmpeg movie +oiiotool WARNING : oiiotool produced no output. Did you forget -o? +oiiotool ERROR: read : "src/bomb-16384x9000.avi": FFmpeg movie header claims a 1125 MB image from a 6592 byte file; probably a corrupt or malicious header +Full command line was: +> oiiotool --info --hash src/bomb-16384x9000.avi +oiiotool ERROR: read : "src/truncated.avi" could not open input +Full command line was: +> oiiotool --info --hash src/truncated.avi +src/resolution-change.mkv : 320 x 240, 3 channel, uint8 FFmpeg movie (4 subimages) + SHA-1: "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header +iinfo ERROR: "src/resolution-change.mkv" : "src/resolution-change.mkv" frame 0 does not match the 320x240 format declared by the header diff --git a/testsuite/ffmpeg/run.py b/testsuite/ffmpeg/run.py index eca172516c..0522a96e29 100755 --- a/testsuite/ffmpeg/run.py +++ b/testsuite/ffmpeg/run.py @@ -9,6 +9,31 @@ for f in files: command = command + info_command (os.path.join(imagedir, f)) +# Capture stderr too, so the errors from the malformed-input reads below land +# in out.txt and are checked against the ref. +redirect = " >> out.txt 2>&1 " + # Regression test for narrow 10-bit VP9 input that used to make swscale write # past OIIO's destination buffer during pixel reads. command = command + oiiotool ("src/ffmpeg-width1-gbrp16.mkv --hash") + +# Grayscale movies. These used to be described as 3-channel even though the +# frames were requested from swscale as GRAY8/GRAY16, so every scanline copy +# ran past the end of the decoded row. Check the channel count, then read the +# pixels to make sure the copy stays in bounds. (No hash: swscale output is +# not bit-identical across FFmpeg versions.) See src/make_malformed_movies.py. +for f in [ "gray8.avi", "gray16.avi" ]: + command = command + info_command ("src/" + f, verbose=False, hash=False) + command = command + oiiotool ("src/" + f + " --hash") + +# Hostile/truncated input must be rejected cleanly, with no crash and no +# over-allocation ahead of the rejection. +for f in [ "bomb-16384x9000.avi", "truncated.avi" ]: + command = command + info_command ("src/" + f, verbose=False, + failureok=True) + +# A stream whose frames decode smaller than the header claims. swscale used +# to read past the end of those frames; the read must fail instead. (iinfo, +# because oiiotool's --hash exits 0 on a failed read.) +command = command + info_command ("src/resolution-change.mkv", verbose=False, + failureok=True, info_program="iinfo") diff --git a/testsuite/ffmpeg/src/bomb-16384x9000.avi b/testsuite/ffmpeg/src/bomb-16384x9000.avi new file mode 100644 index 0000000000000000000000000000000000000000..654bda29d3f966af97bd6defed64575a8ae2152c GIT binary patch literal 6592 zcmWIYbaUGw$-v+k=BeQ0863hS%)pS5Qk0WemYHF}z`)?(#LuuI5y$`n0Zs-6MhFv1 z3jh@`FlZ=41(7I_S*rvY7>Y}Za)20YR#|3Bv71|%A(Aq%Iml*4Gchpy2LhNH0R{n( z8EKz@0v|zw4iNho86+6MW-)^VQ9&4+oc`Y(Zr2N@8!)ypzI!P1Y_F^DRtxo#8+rn@ zrAJpiwoTBv(boJg+I=Jc(YTM*{~If|)*e(;QrstbEbyiwKgYKEs!b>FoQj-x{O;e! z#_Y%2@4hQ(f0TOU_n#{VWX`HS$?&?)cV8;VbYEEdKZE>z(~c%ES@`IQA2Rl}O(LGmPMBn3EJ0LF~Ku@wXx zAr2Em(gtRN#u`|F2IQyY<^WN~4|D}1hz}aQk^=hI)6dP{GuSPJ56JdOEK4&p(lZ1i z10(Rr%nvMMPa`OhM!h;30wXpAKqW*5aI88vzbq3pDhnF7{>c8}x_4XhYKA)X!z;wD zF)4A?zg{q{boB?%&dUGOA8PukCfCmUo%B2G1?QjEmxrA;r|aEw%-Hsa^Li}T8H49v z)*RH$nC5A0m@J!9U! zzTqwMmXo(4OJu|Q>suym?3)(Hl(b###kaZBw0jyX(q%&bdB$)4tG4&)tCjxt6TkfG z-mvzX(@k^1Y`fpmhu7zvDyZDO`}ipfpWhc+J=p?PU+ez2y7TG(A_j&y2KifGyQZAi zIeK$vmxPV*f}(F`jQ+tlO%#+am8|S9;a)N^LSf-Y<1D|_o-bxc>#pcr1`XQ(|Ff!T zp2)?+MgMgV?5a33*Yb0Y&`rL>cb2bSR~;s~uu_pX{q4b>)~8wb)mXiieEN<>_p8_I zgMR(W+fMN*{gwQ@H%{Z@+3I4$_qXfa>uW;a|NpkZmwSFM$Ne>lTE zcV5nB1C`caf2RGE-OqbjdSz+y`_iLdzHLlCb$fH2cA09mx=pZ&*gm_3uNfE)F~ka` zm`fS$dBesnWc0oIRIz!fn4QG!_?e8wDNiaM=t!r$PHmc3H}_g?V&k&Ei$T8l`0LB- zE7I#fs9&1r9{>1t{I287Pk3)!VEZV*;&DL4_DPNE%`^SA*K-8s9%R(lU^=exo84p1 z%3p$_{c>y9yXAI2VohAn^?RwXw$p_F|BarhY!v2_IkzWj#-E6nae5yaUY9$HFF0?q zXKxxyd9W7qvun%lna^E%>hqt^XRdrX(RokODgQ1{SXkq}Cf&mf3_BTSM~j;?WTsRY Rf@*G1S_9Fb839n50{}dw)hGY} literal 0 HcmV?d00001 diff --git a/testsuite/ffmpeg/src/gray16.avi b/testsuite/ffmpeg/src/gray16.avi new file mode 100644 index 0000000000000000000000000000000000000000..5fd43e5443b3113c08d768d827dd488dbd07d3e2 GIT binary patch literal 6726 zcmWIYbaS(lVqkC#^HlKh3=UxsW?;xjDauJK%gnG~U|?`?;%8Wq2xI_(04DRtGW<+04gG3=IE)0H&q^XgW}T z+6SNj2sZ$+0}wMZNHBoSN&s@uKp2~x{@)#L*9)c_Ft#zidnog4udDA?3-yv4dIGhj zM^`hzAI^elzQa%pDPDs&Z<7i@Vd@-UnXD@2r|1&hxnb%hrI@!fyHUonh!<*Y_2DTeD zziv_$`5*IiTf@IoaZU08EA69g_P_a`@-D^A1CeNui!VgcpwCrtYRKYLa-?5*U zZg_Sy>C@_4pW4?YIQ^5Gu6ub&%Xx>*a%YGZx-F@jA2c)%*YduSwr{_w@4q z(|xbzm~P6PCF$`?c~{@1;G369gB=}Z{$6||Q+4_T<5#cs-hu~D|C_?Zx0I|9|HDlN8PI^Q_78|NjmD zaKBkNUuNGP-;{;NeMKdAzWqDTk2SON$_gIGlJqqf|Fv@;yY;6?!>4e{bMvdMWsAOv zoO@Kf{`uLgonO)y3SM;W<8e3MGc}~bB+w=^I5p04llB5hk%q=X&gPErFwuLK^~np; zqp~9(ozAS@peH_a?lnG*umw|#wJa?KPyJhQW8(jR4~|c7VUSf=%$j*?X1<%N*Gr35 z@s{qNsTTXbIx*XI%`5$Ud4osYVso2nR=?Ccfq`OEwmGP`t$z2g_-a(kl!)`&jx0DE zBo?=H-oAOC=l%cxva{lA>(u#{p34)$`}dt@V3cBr*m2jGAv2}I5I7;h07~;98ZGb24iATlsw0EN~M<`EPmqh1{i zfzc3vhXAOA=mCxe=jNAXf<|Q# z29BHY{}=>~jTj`1=e&FOZdC)5UHJd={|gunGVs_x6>4B+Uvq+i`@i2YrXFVj&Rx?4 zgpy`3xL#e~(7Yy&`F+$b_DTNJ^&FVF!cQMyW??Yc*kE$p=F@2l*OlFSrUgG1=S-Qy zz>v*w!tX%x9GS@=hcob47$i1vI0>{Uaxidoo-3a7RMTq}#NPk^e{?V;G_c8cayl?J zH;4+lvIw~UdtZ63ATU5AfPwi>ZWe<>Puc@U_H4h+YhFhlo3@;RrAL{=O|S(>2u#}l z|C87M|0lIVzkjO#{}+h<|9=fe@Bjag2Mwqb0S+*kq-o0jOg8j*dw`+icT6S&GoRN3 zM#1>cDhC+l)ow+GMlF+MU~Dux#=!k9vx7mrI7fkD=K%(Wdkh^OF75*RZD9c$#~^Y2 zga%{hxuPknR;8t-Feq33|M~y_XYtCE84NrP499lwV30ImP^tqeG^p9(KaHWvlg0Ll z<=SkC?~V748Fnw#zWmaE;#^0?-r3vr8<<=V?>@C=%_<;hjdMnG+Kut}rm%WGGgj_#rc;!VoxF PzyL~*AR07l07{nte94-K literal 0 HcmV?d00001 diff --git a/testsuite/ffmpeg/src/make_malformed_movies.py b/testsuite/ffmpeg/src/make_malformed_movies.py new file mode 100644 index 0000000000..5d04572c78 --- /dev/null +++ b/testsuite/ffmpeg/src/make_malformed_movies.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +# Generate the small movie fixtures used by the ffmpeg security-audit +# regression tests. Requires the `ffmpeg` command line tool; the generated +# files are committed, so this only needs to be re-run if a fixture changes. +# +# gray8.avi valid 8-bit grayscale movie +# gray16.avi valid 16-bit grayscale movie +# bomb-16384x9000.avi tiny file whose header declares a 1.1 GB frame +# truncated.avi valid header followed by a chopped-off stream +# resolution-change.mkv frames smaller than the size the header declares +# +# The two grayscale movies are the regression case for a heap overread: the +# reader asked FFmpeg for GRAY8/GRAY16 frames but described them in the +# ImageSpec as 3-channel, so each scanline copy ran three times past the end +# of the decoded row. +# +# AVI keeps the frame dimensions in two places, the main header (avih) and +# the stream format header (strf, a BITMAPINFOHEADER). Patching both is +# enough to make a small file claim an arbitrary frame size, since ffv1 takes +# its dimensions from the container. + +import os +import struct +import subprocess +import sys + + +def ffmpeg(pix_fmt, size, out): + subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "lavfi", "-i", + "testsrc=size=%s:rate=1:duration=1" % size, + "-pix_fmt", pix_fmt, "-c:v", "ffv1", "-frames:v", "1", + out], check=True) + + +def patch_dimensions(src, dst, width, height): + d = bytearray(open(src, "rb").read()) + avih = d.find(b"avih") + strf = d.find(b"strf") + if avih < 0 or strf < 0: + sys.exit("could not locate AVI headers in " + src) + # avih: dwWidth/dwHeight are the 9th and 10th uint32 of the structure. + d[avih + 40:avih + 44] = struct.pack("YE71L?lx;WzisH1C+0*na{N>K;j?#Vf>5B z-E+_Ldp_Rh{oV7Xeat*1Ee5wK<(a=qYN(eioFGnk@YFPe{g za^OrYnLb~vhCGF$|E&;i5`nvo~N8R>JwZGVvzMtwn`E=jp<*~yzy54?Dqm(Z*OQs{eyAv&F zKtrpC2D;i~{)~pU^=$-2>sB)Aa?cr}W9vnO1&SvUbn`>I? z8#mQ;zd_T^!fj?8bn!x^wI8>;XY=X%PhNWDR}E!ddo+ZSz|Frl2l$Qd5HdW!c+uS4 zmhatXKmFzOhT+}D@9^n0GqZ84qXL^SFUkZn4m*}@R;v~JY<352=fJVS3;`CMLQ@3> z{vgs%cyQE;QbHgB69JJ`Gm_uq*fEZCy z!(vQvVxIGHJj-C21VH+UPZ0SC%Q#`zn2T{jLLn?2w>jxJ!`N_?uptu1O1lrVN-QY} zNCN4V{VZeFc>;l>;W+Vo^@^Yqugt}Qgk>C<56NPb^Fk_vRhbAvSOKjnUghUiQ24ee zB=`yE-zvrkYh{cKg9DtRdL<tp0hx;u z1+-2Q4u(_-v$r*Z!{bOL*Yq`@a-yFLF>!NoKA^Mln`XdJj=@g9Q6SYKrjY_ zwF85=O<$a)%??l{y-wlH#aYKX02Gx-Yy}QWKq`Dd;2dgmZScKx_6sw(lw{Hi^M9p& z{_W9OEjy&4vOAhgbRWs2sM0^rO}w<+k#&t|s7cRT3CC^O;PJBcFRa$j8yv+2$PcIP z?nwU+BVBoSqzfZ$oFD1S`#F5|i?tKuA0*MzffwJsxbK-i(8;7>TD)>r!%1yxmGD{i zTMfnUY;;li{LJqx--po1x!k(15M_UI==`hS5 u&WT*#F%1pxfd2;