PR #23070 opened by flex0geek
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23070
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23070.patch
This series adds two OSS-Fuzz-compatible fuzzer harnesses:
1. An audio path in tools/target_enc_fuzzer.c, extending the existing video
encoder fuzzer to cover audio encoders (vorbis, opus, aac, ac3, mp2,
nellymoser, ...).
2. A new tools/target_dem_dash_fuzzer.c the first DASH-demuxer fuzzer for
FFmpeg. The generic target_dem_fuzzer cannot reach dashdec.c because
dash_probe() requires an explicit profile URI; this harness forces the demuxer
via av_find_input_format("dash") and feeds it via a mem-backed AVIOContext,
with an interrupt callback to bound live-stream loops.
Both harnesses have already surfaced bugs that have been disclosed to
ffmpeg-security@:
- DASH (NULL-deref + divide-by-zero in dashdec.c) see #23057; fixes in review
at #23060 (acked by @Steven_Liu).
- vorbis put_codeword heap-OOB-read see #23056.
The audio fuzzing gap that motivates harness (1) is also evidenced by the
earlier bug at #21013.
Intent: merge these harnesses so the corresponding OSS-Fuzz container build
(separate PR against google/oss-fuzz) can wire them into ClusterFuzz's daily
rotation.
Related: #23057, #23056, #21013
From 550deec8ffe91a1d7d89c400d326163a2b0dba55 Mon Sep 17 00:00:00 2001
From: Mohamed Sayed <[email protected]>
Date: Mon, 11 May 2026 05:59:08 +0000
Subject: [PATCH 1/2] fftools/target_enc_fuzzer: add audio encoder fuzz path,
Extends the encoder fuzzer to exercise audio encoders in addition to the
existing video encoder coverage.
Signed-off-by: Mohamed Sayed <[email protected]>
---
tools/target_dem_dash_fuzzer.c | 228 +++++++++++++++++++++++++++++++++
tools/target_enc_fuzzer.c | 146 ++++++++++++++++++++-
2 files changed, 373 insertions(+), 1 deletion(-)
create mode 100644 tools/target_dem_dash_fuzzer.c
diff --git a/tools/target_dem_dash_fuzzer.c b/tools/target_dem_dash_fuzzer.c
new file mode 100644
index 0000000000..a86c75ac4d
--- /dev/null
+++ b/tools/target_dem_dash_fuzzer.c
@@ -0,0 +1,228 @@
+/*
+ * DASH demuxer fuzzer
+ * Copyright (c) 2026 Google LLC
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/*
+ * Design notes:
+ *
+ * The DASH demuxer (dashdec.c) parses an XML MPD manifest and then opens
+ * sub-URLs (init segments, media segments) referenced within. Unlike most
+ * demuxers it cannot be exercised by the generic target_dem_fuzzer because:
+ *
+ * 1. dash_probe() returns 0 for manifests that lack an explicit DASH profile
+ * URI, so av_probe_input_format() won't select the demuxer.
+ * 2. Even when the format is known, the demuxer calls ffio_open_whitelist()
+ * for every sub-URL it finds — those calls will fail (AVERROR) since the
+ * URLs don't exist, which is fine: the bugs we care about live in the
+ * manifest parsing path (dashdec.c:~600 and ~1499) and fire before any
+ * segment fetch is attempted.
+ *
+ * Strategy:
+ * - Force the demuxer via av_find_input_format("dash").
+ * - Back the AVIOContext with the raw fuzzer buffer so parse_manifest()
+ * reads directly from our in-memory data.
+ * - Set a tight interrupt callback so that any live-stream polling loop
+ * (HLS-style) cannot stall the fuzzer for more than ~1000 iterations.
+ * - Call avformat_find_stream_info() to exercise the full header path;
+ * segment opens will fail gracefully with AVERROR, which is expected.
+ */
+
+#include "libavformat/avformat.h"
+#include "libavformat/avio.h"
+#include "libavutil/error.h"
+#include "libavutil/mem.h"
+
+/* ------------------------------------------------------------------ */
+/* In-memory IO context */
+/* ------------------------------------------------------------------ */
+
+typedef struct {
+ const uint8_t *data;
+ size_t size;
+ size_t pos;
+} MemIOContext;
+
+static int mem_read_packet(void *opaque, uint8_t *buf, int buf_size)
+{
+ MemIOContext *ctx = opaque;
+ int avail = (int)(ctx->size - ctx->pos);
+
+ if (avail <= 0)
+ return AVERROR_EOF;
+
+ if (buf_size > avail)
+ buf_size = avail;
+
+ memcpy(buf, ctx->data + ctx->pos, buf_size);
+ ctx->pos += buf_size;
+ return buf_size;
+}
+
+static int64_t mem_seek(void *opaque, int64_t offset, int whence)
+{
+ MemIOContext *ctx = opaque;
+ int64_t newpos;
+
+ switch (whence) {
+ case SEEK_SET:
+ newpos = offset;
+ break;
+ case SEEK_CUR:
+ newpos = (int64_t)ctx->pos + offset;
+ break;
+ case SEEK_END:
+ newpos = (int64_t)ctx->size + offset;
+ break;
+ case AVSEEK_SIZE:
+ return (int64_t)ctx->size;
+ default:
+ return -1;
+ }
+
+ if (newpos < 0 || newpos > (int64_t)ctx->size)
+ return -1;
+
+ ctx->pos = (size_t)newpos;
+ return newpos;
+}
+
+/* ------------------------------------------------------------------ */
+/* Interrupt callback — prevent infinite loops in live-stream paths */
+/* ------------------------------------------------------------------ */
+
+static int64_t interrupt_counter;
+
+static int interrupt_cb(void *opaque)
+{
+ (void)opaque;
+ return --interrupt_counter < 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Fuzzer entry point */
+/* ------------------------------------------------------------------ */
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ static int initialized = 0;
+ AVFormatContext *fmtctx = NULL;
+ AVIOContext *avio_pb = NULL;
+ MemIOContext mem_ctx;
+ uint8_t *io_buf = NULL;
+ const int IO_BUF_SIZE = 32768;
+ const AVInputFormat *fmt;
+ int ret;
+
+ if (!initialized) {
+ av_log_set_level(AV_LOG_PANIC);
+ initialized = 1;
+ }
+
+ /* Must have at least a few bytes to be worth parsing. */
+ if (size < 4)
+ return 0;
+
+ /* Force DASH demuxer — probe won't auto-select for generic MPDs. */
+ fmt = av_find_input_format("dash");
+ if (!fmt)
+ return 0; /* DASH not compiled in — skip silently */
+
+ /* Allocate the IO buffer (owned by AVIOContext after avio_alloc_context).
*/
+ io_buf = av_malloc(IO_BUF_SIZE);
+ if (!io_buf)
+ return 0;
+
+ mem_ctx.data = data;
+ mem_ctx.size = size;
+ mem_ctx.pos = 0;
+
+ avio_pb = avio_alloc_context(io_buf, IO_BUF_SIZE,
+ /*write_flag=*/0,
+ &mem_ctx,
+ mem_read_packet,
+ /*write_packet=*/NULL,
+ mem_seek);
+ if (!avio_pb) {
+ av_free(io_buf);
+ return 0;
+ }
+
+ fmtctx = avformat_alloc_context();
+ if (!fmtctx)
+ goto cleanup;
+
+ /*
+ * Wire up the interrupt callback so live-stream loops can't spin
+ * forever. 1000 iterations is enough to exercise manifest parsing
+ * without timing out the fuzzer engine.
+ */
+ interrupt_counter = 1000;
+ fmtctx->interrupt_callback.callback = interrupt_cb;
+ fmtctx->interrupt_callback.opaque = NULL;
+
+ /*
+ * Hand the pre-opened pb to the format context. dash_read_header()
+ * calls parse_manifest(s, s->url, s->pb) — it reads the manifest XML
+ * from this pb rather than opening a URL.
+ *
+ * We give a plausible filename so that relative URL construction in
+ * the demuxer doesn't produce garbage. The sub-URL opens will fail
+ * with AVERROR (file not found), which is expected and harmless.
+ */
+ fmtctx->pb = avio_pb;
+
+ ret = avformat_open_input(&fmtctx, "fuzz.mpd", fmt, NULL);
+ if (ret < 0)
+ goto cleanup;
+
+ /*
+ * avformat_find_stream_info drives segment fetches. They will all
+ * fail since the segment files don't exist, but the manifest-parsing
+ * bugs (H-DASH-001..004) trigger during read_header, before this
+ * call. We call it anyway to exercise any post-header paths.
+ */
+ avformat_find_stream_info(fmtctx, NULL);
+
+cleanup:
+ /*
+ * Memory ownership:
+ *
+ * When s->pb is set before avformat_open_input(), demux.c sets
+ * AVFMT_FLAG_CUSTOM_IO, which causes avformat_close_input() to skip
+ * closing s->pb. So close_input will NOT free our avio_pb; we free
+ * it ourselves after close_input.
+ *
+ * If avformat_open_input() was never reached (fmtctx is still
+ * allocated but open_input wasn't called), avformat_free_context()
+ * is the right cleanup.
+ */
+ if (fmtctx)
+ avformat_close_input(&fmtctx);
+
+ /* Free our avio context and its buffer regardless of outcome. */
+ if (avio_pb) {
+ av_freep(&avio_pb->buffer);
+ avio_context_free(&avio_pb);
+ }
+
+ return 0;
+}
diff --git a/tools/target_enc_fuzzer.c b/tools/target_enc_fuzzer.c
index 059d783071..1a0d675a99 100644
--- a/tools/target_enc_fuzzer.c
+++ b/tools/target_enc_fuzzer.c
@@ -23,10 +23,12 @@
#include "config.h"
#include "libavutil/avassert.h"
#include "libavutil/avstring.h"
+#include "libavutil/channel_layout.h"
#include "libavutil/cpu.h"
#include "libavutil/imgutils.h"
#include "libavutil/intreadwrite.h"
#include "libavutil/mem.h"
+#include "libavutil/samplefmt.h"
#include "libavcodec/avcodec.h"
#include "libavcodec/bytestream.h"
@@ -70,6 +72,146 @@ static int encode(AVCodecContext *enc_ctx, AVFrame *frame,
AVPacket *pkt)
av_assert0(0);
}
+static int audio_fuzz(const uint8_t *data, size_t size)
+{
+ const uint8_t *end = data + size;
+ uint32_t it = 0;
+ uint64_t nb_samples_total = 0;
+ uint64_t maxsamples_per_frame = 256 * 1024;
+ uint64_t maxsamples = maxsamples_per_frame * maxiteration;
+ AVDictionary *opts = NULL;
+ int res;
+
+ AVCodecContext* ctx = avcodec_alloc_context3(&c->p);
+ if (!ctx)
+ error("Failed memory allocation");
+
+ ctx->sample_fmt = c->p.sample_fmts ? c->p.sample_fmts[0] :
AV_SAMPLE_FMT_S16;
+ ctx->sample_rate = c->p.supported_samplerates ?
c->p.supported_samplerates[0] : 44100;
+ av_channel_layout_default(&ctx->ch_layout, 2);
+ if (c->p.capabilities & AV_CODEC_CAP_EXPERIMENTAL)
+ ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
+
+ if (size > 1024) {
+ GetByteContext gbc;
+ int flags;
+ uint32_t sr;
+ int64_t br;
+
+ size -= 1024;
+ bytestream2_init(&gbc, data + size, 1024);
+
+ sr = bytestream2_get_le32(&gbc) & 0x7FFFFFFF;
+ if (sr > 0 && sr <= 384000)
+ ctx->sample_rate = sr;
+
+ br = bytestream2_get_le64(&gbc);
+ if (br > 0)
+ ctx->bit_rate = br;
+
+ flags = bytestream2_get_byte(&gbc);
+ if (flags & 2)
+ ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
+ if (flags & 0x40)
+ av_force_cpu_flags(0);
+
+ if (c->p.sample_fmts) {
+ int n = 0;
+ while (c->p.sample_fmts[n] != AV_SAMPLE_FMT_NONE)
+ n++;
+ if (n > 0)
+ ctx->sample_fmt = c->p.sample_fmts[bytestream2_get_byte(&gbc)
% n];
+ }
+ if (c->p.supported_samplerates) {
+ int n = 0;
+ while (c->p.supported_samplerates[n] != 0)
+ n++;
+ if (n > 0)
+ ctx->sample_rate =
c->p.supported_samplerates[bytestream2_get_byte(&gbc) % n];
+ }
+ if (c->p.ch_layouts) {
+ int n = 0;
+ while (c->p.ch_layouts[n].nb_channels)
+ n++;
+ if (n > 0) {
+ av_channel_layout_uninit(&ctx->ch_layout);
+ av_channel_layout_copy(&ctx->ch_layout,
&c->p.ch_layouts[bytestream2_get_byte(&gbc) % n]);
+ }
+ }
+ }
+
+ if (ctx->sample_rate <= 0)
+ ctx->sample_rate = 44100;
+ ctx->time_base = (AVRational){1, ctx->sample_rate};
+
+ /* H006: enable trellis quantisation for Nellymoser to exercise the
+ * dynamic-exponent path (get_exponent_dynamic) and trigger the
+ * UBSAN OOB at nellymoserenc.c:247 */
+ if (c->p.id == AV_CODEC_ID_NELLYMOSER)
+ av_dict_set_int(&opts, "trellis", 1, 0);
+
+ res = avcodec_open2(ctx, &c->p, &opts);
+ if (res < 0) {
+ avcodec_free_context(&ctx);
+ av_dict_free(&opts);
+ return 0;
+ }
+
+ int samples_per_frame = ctx->frame_size > 0 ? ctx->frame_size : 1024;
+ if (samples_per_frame > 65536)
+ samples_per_frame = 65536;
+
+ AVFrame *frame = av_frame_alloc();
+ AVPacket *avpkt = av_packet_alloc();
+ if (!frame || !avpkt)
+ error("Failed memory allocation");
+
+ frame->format = ctx->sample_fmt;
+ frame->sample_rate = ctx->sample_rate;
+ frame->nb_samples = samples_per_frame;
+ if (av_channel_layout_copy(&frame->ch_layout, &ctx->ch_layout) < 0)
+ error("Failed channel layout copy");
+
+ while (data < end && it < maxiteration) {
+ nb_samples_total += samples_per_frame;
+ if (nb_samples_total > maxsamples)
+ goto maximums_reached;
+
+ res = av_frame_get_buffer(frame, 0);
+ if (res < 0)
+ error("Failed av_frame_get_buffer");
+
+ for (int i = 0; i < FF_ARRAY_ELEMS(frame->buf); i++) {
+ if (frame->buf[i]) {
+ int buf_size = FFMIN(end - data, frame->buf[i]->size);
+ memcpy(frame->buf[i]->data, data, buf_size);
+ memset(frame->buf[i]->data + buf_size, 0, frame->buf[i]->size
- buf_size);
+ data += buf_size;
+ }
+ }
+
+ frame->pts = nb_samples_total - samples_per_frame;
+
+ res = encode(ctx, frame, avpkt);
+ if (res < 0)
+ break;
+ it++;
+ for (int i = 0; i < FF_ARRAY_ELEMS(frame->buf); i++)
+ av_buffer_unref(&frame->buf[i]);
+
+ av_packet_unref(avpkt);
+ }
+maximums_reached:
+ encode(ctx, NULL, avpkt);
+ av_packet_unref(avpkt);
+
+ av_frame_free(&frame);
+ avcodec_free_context(&ctx);
+ av_packet_free(&avpkt);
+ av_dict_free(&opts);
+ return 0;
+}
+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
uint64_t maxpixels_per_frame = 512 * 512;
uint64_t maxpixels;
@@ -90,8 +232,10 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t
size) {
av_log_set_level(AV_LOG_PANIC);
}
+ if (c->p.type == AVMEDIA_TYPE_AUDIO)
+ return audio_fuzz(data, size);
if (c->p.type != AVMEDIA_TYPE_VIDEO)
- return 0;
+ return 0; // subtitle encoders use a different API path; not yet
covered
maxpixels = maxpixels_per_frame * maxiteration;
switch (c->p.id) {
--
2.52.0
From eb8a5ecca6c8eeae6622abcc2efc1b485ab2d8ed Mon Sep 17 00:00:00 2001
From: Mohamed Sayed <[email protected]>
Date: Mon, 11 May 2026 06:00:46 +0000
Subject: [PATCH 2/2] fftools/target_dem_dash_fuzzer: new DASH demuxer fuzzer.
The generic target_dem_fuzzer cannot cover dashdec.c because dash_probe()
returnes 0 for bare MPDs that lack a profile URI, so av_probe_input_format()
never selects the demuxer.
Signed-off-by: Mohamed Sayed <[email protected]>
---
tools/Makefile | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/Makefile b/tools/Makefile
index 7ae6e3cb75..96a053f5f0 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -20,6 +20,9 @@ tools/target_dem_fuzzer.o: tools/target_dem_fuzzer.c
tools/target_io_dem_fuzzer.o: tools/target_dem_fuzzer.c
$(COMPILE_C) -DIO_FLAT=0
+tools/target_dem_dash_fuzzer.o: tools/target_dem_dash_fuzzer.c
+ $(COMPILE_C)
+
tools/target_sws_fuzzer.o: tools/target_sws_fuzzer.c
$(COMPILE_C)
--
2.52.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]