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]

Reply via email to