This is an automated email from the ASF dual-hosted git repository.
wwbmmm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 4693b0854 OSS-Fuzz: Add new fuzzer and corpora targets rtmp (#3516)
4693b0854 is described below
commit 4693b0854f6c926f33275ef0a4e0b8b4ddc837d9
Author: Arthur Chan <[email protected]>
AuthorDate: Sun Oct 25 18:03:17 2026 +0800
OSS-Fuzz: Add new fuzzer and corpora targets rtmp (#3516)
* OSS-Fuzz: Add new fuzzer and corpora targets rtmp
Signed-off-by: Arthur Chan <[email protected]>
* Provide dynamic seed generation python script
Signed-off-by: Arthur Chan <[email protected]>
---------
Signed-off-by: Arthur Chan <[email protected]>
---
.gitignore | 3 +
test/CMakeLists.txt | 3 +-
test/fuzzing/fuzz_rtmp.cpp | 81 ++++++++++++
test/fuzzing/gen_rtmp_seed_corpus.py | 234 +++++++++++++++++++++++++++++++++++
test/fuzzing/oss-fuzz.sh | 6 +-
5 files changed, 325 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index 7a9ade11d..7ebc2d409 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,6 @@ bazel-out
bazel-testlogs
bazel-brpc
MODULE.bazel.lock
+
+# Generated by test/fuzzing/gen_rtmp_seed_corpus.py
+test/fuzzing/fuzz_rtmp_seed_corpus/
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index e2e9a8475..4c3a09f52 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -343,7 +343,8 @@ if(BUILD_FUZZ_TESTS)
set(FUZZ_TARGETS fuzz_butil fuzz_esp fuzz_hpack fuzz_http
fuzz_hulu fuzz_json fuzz_redis fuzz_shead fuzz_sofa fuzz_uri
fuzz_baidu_rpc fuzz_mongo fuzz_memcache
- fuzz_couchbase fuzz_streaming fuzz_http_parser fuzz_amf)
+ fuzz_couchbase fuzz_streaming fuzz_http_parser fuzz_amf
+ fuzz_rtmp)
foreach(target ${FUZZ_TARGETS})
diff --git a/test/fuzzing/fuzz_rtmp.cpp b/test/fuzzing/fuzz_rtmp.cpp
new file mode 100644
index 000000000..c2621e931
--- /dev/null
+++ b/test/fuzzing/fuzz_rtmp.cpp
@@ -0,0 +1,81 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "brpc/policy/rtmp_protocol.h"
+#include "brpc/rtmp.h"
+#include "brpc/server.h"
+#include "butil/iobuf.h"
+#include "fuzz_common.h"
+
+namespace {
+
+constexpr size_t kMinInputLength = 5;
+constexpr size_t kMaxInputLength = 4096;
+
+class FuzzRtmpService : public brpc::RtmpService {
+public:
+ brpc::RtmpServerStream* NewStream(const brpc::RtmpConnectRequest&)
override {
+ return nullptr;
+ }
+};
+
+brpc::Server* get_fuzz_server() {
+ static brpc::Server* started = []() -> brpc::Server* {
+ static FuzzRtmpService rtmp_service;
+ static brpc::Server server;
+ brpc::ServerOptions options;
+ options.rtmp_service = &rtmp_service;
+ options.has_builtin_services = false;
+ options.num_threads = 0;
+ if (server.Start(0, &options) != 0) {
+ // Fall back to a context with no service
+ LOG(ERROR) << "Fail to start fuzz RTMP server, "
+ "server-side command handlers will be skipped";
+ return nullptr;
+ }
+
+ // Stop again straight away. _options -- and so rtmp_service -- is not
+ // touched by Stop()/Join()
+ server.Stop(0);
+ server.Join();
+ return &server;
+ }();
+ return started;
+}
+
+} // namespace
+
+extern "C" int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+ if (size < kMinInputLength || size > kMaxInputLength) {
+ return 0;
+ }
+
+ brpc::Socket* sock = get_fuzz_socket();
+ if (sock == nullptr) {
+ return 0;
+ }
+ brpc::Server* server = get_fuzz_server();
+
+ butil::IOBuf buf;
+ buf.append(data, size);
+
+ sock->reset_parsing_context(new brpc::policy::RtmpContext(nullptr,
server));
+ brpc::policy::ParseRtmpMessage(&buf, sock, false, server);
+ return 0;
+}
diff --git a/test/fuzzing/gen_rtmp_seed_corpus.py
b/test/fuzzing/gen_rtmp_seed_corpus.py
new file mode 100755
index 000000000..04307005d
--- /dev/null
+++ b/test/fuzzing/gen_rtmp_seed_corpus.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generate the seed corpus for fuzz_rtmp.
+
+Every seed is synthesised here from readable source -- there is no captured
+traffic and no third-party media, so the corpus is kept as this script rather
+than as binary files. oss-fuzz.sh runs it before packaging the corpus; to
+regenerate the seeds by hand:
+
+ python3 gen_rtmp_seed_corpus.py [output-dir]
+"""
+
+import os
+import struct
+import sys
+
+# A simplified-RTMP stream: version byte 3 followed by
+# SIMPLIFIED_RTMP_MAGIC_NUMBER, see src/brpc/policy/rtmp_protocol.cpp.
+MAGIC = b"\x03BDMS"
+
+
+# ----------------------------------------------------------------- AMF0 values
+def num(value):
+ return b"\x00" + struct.pack(">d", value)
+
+
+def boolean(value):
+ return b"\x01" + (b"\x01" if value else b"\x00")
+
+
+def string(value):
+ encoded = value.encode()
+ return b"\x02" + struct.pack(">H", len(encoded)) + encoded
+
+
+def null():
+ return b"\x05"
+
+
+def obj(pairs):
+ out = b"\x03"
+ for key, value in pairs:
+ encoded = key.encode()
+ out += struct.pack(">H", len(encoded)) + encoded + value
+ return out + b"\x00\x00\x09" # object end marker
+
+
+# -------------------------------------------------------- RTMP chunk encoding
+def basic_header(fmt, cs_id):
+ if cs_id < 64:
+ return bytes([(fmt << 6) | cs_id])
+ if cs_id < 320:
+ return bytes([(fmt << 6) | 0, cs_id - 64])
+ return bytes([(fmt << 6) | 1]) + struct.pack("<H", cs_id - 64)
+
+
+def u24(value):
+ return struct.pack(">I", value)[1:]
+
+
+def chunk(cs_id, payload, fmt=0, ts=0, type_id=0, stream_id=0, length=None):
+ """One RTMP chunk.
+
+ `length` overrides the declared message length, which lets a seed advertise
+ a size that disagrees with the bytes that actually follow it.
+ """
+ declared = len(payload) if length is None else length
+ header = basic_header(fmt, cs_id)
+ if fmt == 0:
+ header += (u24(ts) + u24(declared) + bytes([type_id])
+ + struct.pack("<I", stream_id))
+ elif fmt == 1:
+ header += u24(ts) + u24(declared) + bytes([type_id])
+ elif fmt == 2:
+ header += u24(ts)
+ return header + payload
+
+
+def command(name, txid, *args, **kwargs):
+ """An AMF command message (type 20 = AMF0, type 17 = AMF3)."""
+ return chunk(kwargs.pop("cs_id", 3),
+ string(name) + num(txid) + b"".join(args),
+ type_id=kwargs.pop("type_id", 20),
+ stream_id=kwargs.pop("stream_id", 1),
+ **kwargs)
+
+
+def user_control(event, value):
+ return chunk(2, struct.pack(">HI", event, value), type_id=4)
+
+
+def corrupt(buf, offset, byte):
+ """Splice a stray byte in *without* correcting the declared message length.
+
+ This models a mutated seed, and keeps the corruption visible in the source
+ instead of hiding it inside an opaque blob.
+ """
+ return buf[:offset] + bytes([byte]) + buf[offset:]
+
+
+# ---------------------------------------------------------------- AMF payloads
+CONNECT_ARGS = obj([
+ ("app", string("live")),
+ ("flashVer", string("FMLE/3.0 (compatible; FMSc/1.0)")),
+ ("tcUrl", string("rtmp://127.0.0.1/live")),
+ ("fpad", boolean(False)),
+ ("capabilities", num(15)),
+ ("audioCodecs", num(4071)),
+ ("videoCodecs", num(252)),
+ ("videoFunction", num(1)),
+ ("objectEncoding", num(0)),
+])
+
+METADATA = string("@setDataFrame") + string("onMetaData") + obj([
+ ("duration", num(0)),
+ ("width", num(1280)),
+ ("height", num(720)),
+ ("videocodecid", num(7)),
+ ("audiocodecid", num(10)),
+])
+
+# An AVC sequence header (SPS/PPS) and one IDR NALU. These stay as literals
+# because they are opaque codec bytes that the code under test only forwards.
+AVC_SEQ_HEADER = bytes.fromhex(
+ "17000000000142c01effe100096742c01ed90089f96601000468ce3c80")
+AVC_NALU = bytes.fromhex("270100000000000004419a0020")
+
+# Two FLV-style sub-tags whose header fields deliberately disagree with the
+# data that follows them, to exercise the aggregate demuxer's bounds checks.
+AGGREGATE_BODY = bytes.fromhex(
+ "00000000000408000001af0121000000000e000000000006090000012701"
+ "0000004100000010")
+
+
+# -------------------------------------------------------------------- the
seeds
+SEEDS = {}
+
+# Protocol control messages.
+SEEDS["set_chunk_size"] = chunk(2, struct.pack(">I", 4096), type_id=1)
+SEEDS["abort"] = chunk(2, struct.pack(">I", 3), type_id=2)
+SEEDS["ack"] = chunk(2, struct.pack(">I", 12345), type_id=3)
+SEEDS["window_ack_size"] = chunk(2, struct.pack(">I", 2500000), type_id=5)
+SEEDS["set_peer_bandwidth"] = chunk(
+ 2, struct.pack(">I", 2500000) + b"\x02", type_id=6)
+SEEDS["user_control"] = user_control(6, 7777) + user_control(0, 1)
+
+# AMF command messages.
+SEEDS["connect"] = corrupt(command("connect", 1, CONNECT_ARGS), 0x8c, 0xc3)
+SEEDS["create_stream"] = command("createStream", 2, null())
+SEEDS["play"] = command("play", 3, null(), string("stream"), num(-2000))
+SEEDS["publish"] = command(
+ "publish", 4, null(), string("stream"), string("live"))
+SEEDS["release_stream"] = (
+ command("releaseStream", 5, null(), string("stream"))
+ + command("FCPublish", 6, null(), string("stream")))
+SEEDS["close_stream"] = (command("closeStream", 7, null())
+ + command("deleteStream", 8, null(), num(1)))
+# An AMF3 command: type 17, payload prefixed with one AMF0-encoding marker
byte.
+SEEDS["command_amf3"] = chunk(
+ 3, b"\x00" + string("connect") + num(1) + null(), type_id=17, stream_id=1)
+
+# Data and media messages.
+SEEDS["metadata"] = corrupt(
+ chunk(4, METADATA, type_id=18, stream_id=1), 0x8c, 0xc4)
+SEEDS["audio"] = (chunk(5, b"\xaf\x00\x12\x10", type_id=8, stream_id=1)
+ + chunk(5, b"\xaf\x01" + b"\x21\x00" * 8, ts=40,
+ type_id=8, stream_id=1))
+SEEDS["video"] = (chunk(6, AVC_SEQ_HEADER, type_id=9, stream_id=1)
+ + chunk(6, AVC_NALU, ts=33, type_id=9, stream_id=1))
+SEEDS["aggregate"] = chunk(7, AGGREGATE_BODY, type_id=22, stream_id=1)
+
+# Chunk-layer edge cases. All four header formats in one stream: a full
+# header, then one without a message id, then a timestamp-only header, then a
+# continuation that inherits everything from its predecessor.
+SEEDS["header_fmts"] = (
+ chunk(8, b"\xaf\x01" + b"\x11" * 6, ts=100, type_id=8, stream_id=1)
+ + chunk(8, b"\xaf\x01" + b"\x22" * 6, fmt=1, ts=30, type_id=8)
+ + chunk(8, b"\xaf\x01" + b"\x33" * 6, fmt=2, ts=30)
+ + chunk(8, b"\xaf\x01" + b"\x44" * 6, fmt=3))
+# A timestamp of 0xffffff selects the 4-byte extended timestamp that follows
+# the chunk header.
+SEEDS["extended_timestamp"] = chunk(
+ 9, b"\x01\x00\x00\x00" + b"\xaf\x01\x21\x00\x21\x00",
+ ts=0xffffff, type_id=8, stream_id=1, length=6)
+# A 261-byte message, longer than the 128-byte default chunk size, so the
+# parser has to reassemble it; two stray bytes break the chunk boundaries.
+SEEDS["multi_chunk"] = corrupt(
+ corrupt(chunk(10, b"\x27\x01\x00\x00\x00" + bytes(range(256)),
+ type_id=9, stream_id=1), 0x8c, 0xca), 0x10d, 0xca)
+# Chunk stream ids that need the 2-byte and 3-byte basic header forms.
+SEEDS["large_cs_id"] = (chunk(70, struct.pack(">I", 1), type_id=3)
+ + chunk(400, struct.pack(">I", 2), type_id=3))
+
+# A full publish session, replayed as one stream.
+SEEDS["session"] = (SEEDS["set_chunk_size"] + SEEDS["connect"]
+ + SEEDS["create_stream"] + SEEDS["publish"]
+ + SEEDS["metadata"])
+
+# The plain (non-simplified) handshake: C0 + C1 + C2, with no BDMS prefix.
+_C1 = b"\x00" * 8 + bytes((i * 7) & 0xff for i in range(1528))
+_C2 = bytes((i * 11) & 0xff for i in range(1536))
+HANDSHAKE = b"\x03" + _C1 + _C2
+
+
+def build(name):
+ return HANDSHAKE if name == "handshake_c0c1c2" else MAGIC + SEEDS[name]
+
+
+def main(argv):
+ out_dir = argv[1] if len(argv) > 1 else "fuzz_rtmp_seed_corpus"
+ os.makedirs(out_dir, exist_ok=True)
+ names = sorted(list(SEEDS) + ["handshake_c0c1c2"])
+ for name in names:
+ with open(os.path.join(out_dir, name + ".rtmp"), "wb") as out:
+ out.write(build(name))
+ print("Generated %d seeds in %s" % (len(names), out_dir))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/test/fuzzing/oss-fuzz.sh b/test/fuzzing/oss-fuzz.sh
index 3f1649e9b..b6d556d87 100644
--- a/test/fuzzing/oss-fuzz.sh
+++ b/test/fuzzing/oss-fuzz.sh
@@ -37,7 +37,7 @@ make \
fuzz_redis fuzz_shead fuzz_sofa fuzz_uri \
fuzz_baidu_rpc fuzz_mongo fuzz_memcache \
fuzz_couchbase fuzz_streaming fuzz_http_parser \
- fuzz_amf --ignore-errors -j$(nproc)
+ fuzz_amf fuzz_rtmp --ignore-errors -j$(nproc)
cp test/fuzz_* $OUT/
@@ -53,4 +53,8 @@ zip $OUT/fuzz_redis_seed_corpus.zip fuzz_redis_seed_corpus/*
zip $OUT/fuzz_http_seed_corpus.zip fuzz_http_seed_corpus/*
zip $OUT/fuzz_butil_seed_corpus.zip fuzz_butil_seed_corpus/*
zip $OUT/fuzz_hpack_seed_corpus.zip fuzz_hpack_seed_corpus/*
+
+# Generate rtmp's seeds gen_rtmp_seed_corpus.py
+python3 gen_rtmp_seed_corpus.py fuzz_rtmp_seed_corpus
+zip $OUT/fuzz_rtmp_seed_corpus.zip fuzz_rtmp_seed_corpus/*
popd
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]