fix(rtmp): avoid segfault on connection-refused teardown

Track libavformat RTMP session initialization state so teardown only writes a trailer after the muxer header succeeds. This avoids calling av_write_trailer() on partially initialized sessions when avio_open2() fails with Connection refused.

Add a fault-suite regression for libavformat RTMP connection refusal and update the summary helper to compute all_pass from the actual manifest size instead of a hardcoded seven-row expectation.

Verified by rebuilding cvmmap_streamer and rtmp_output_tester, reproducing the refused-connection path without a crash, and running ./scripts/fault_suite.sh successfully (8/8).
This commit is contained in:
2026-04-14 10:21:04 +08:00
parent b277ed363f
commit 30cd956c5c
3 changed files with 26 additions and 4 deletions
+13 -1
View File
@@ -238,6 +238,18 @@ EOF
--frame-interval-ms 1 \ --frame-interval-ms 1 \
--linger-ms 0 --linger-ms 0
run_expected_failure 8 "libavformat_connection_refused" "libavformat connection refusal surfaces without crashing" \
"failed to open RTMP output 'rtmp://127.0.0.1:1/live/test': Connection refused" \
"${RTMP_OUTPUT_TESTER}" \
--rtmp-url rtmp://127.0.0.1:1/live/test \
--transport libavformat \
--codec h264 \
--frames 1 \
--width 320 \
--height 240 \
--frame-interval-ms 1 \
--linger-ms 0
local finished_at_utc local finished_at_utc
finished_at_utc="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" finished_at_utc="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
@@ -294,7 +306,7 @@ PY
echo "counts_pass=${pass_count}" echo "counts_pass=${pass_count}"
echo "counts_fail=${fail_count}" echo "counts_fail=${fail_count}"
echo "all_pass=${all_pass}" echo "all_pass=${all_pass}"
echo "scenarios=removed_encoder_backend,removed_rtmp_transport,removed_rtmp_mode_cli,removed_rtmp_mode_toml,missing_rtmp_url,invalid_rtp_endpoint,ffmpeg_process_bad_binary" echo "scenarios=removed_encoder_backend,removed_rtmp_transport,removed_rtmp_mode_cli,removed_rtmp_mode_toml,missing_rtmp_url,invalid_rtp_endpoint,ffmpeg_process_bad_binary,libavformat_connection_refused"
} > "${EVIDENCE_TEXT}" } > "${EVIDENCE_TEXT}"
if [[ "${all_pass}" == "true" ]]; then if [[ "${all_pass}" == "true" ]]; then
+1 -1
View File
@@ -86,7 +86,7 @@ def build_summary(args: CliArgs) -> dict[str, object]:
pass_count = sum(1 for row in rows if row["status"] == "PASS") pass_count = sum(1 for row in rows if row["status"] == "PASS")
fail_count = sum(1 for row in rows if row["status"] == "FAIL") fail_count = sum(1 for row in rows if row["status"] == "FAIL")
skip_count = sum(1 for row in rows if row["status"] == "SKIP") skip_count = sum(1 for row in rows if row["status"] == "SKIP")
all_pass = len(rows) == 7 and pass_count == 7 and fail_count == 0 and skip_count == 0 all_pass = len(rows) > 0 and pass_count == len(rows) and fail_count == 0 and skip_count == 0
return { return {
"run_id": args.run_id, "run_id": args.run_id,
+11 -1
View File
@@ -230,6 +230,8 @@ public:
std::string url{}; std::string url{};
AVFormatContext *format_context{nullptr}; AVFormatContext *format_context{nullptr};
AVStream *video_stream{nullptr}; AVStream *video_stream{nullptr};
bool io_opened{false};
bool header_written{false};
}; };
LibavformatRtmpOutput() = default; LibavformatRtmpOutput() = default;
@@ -368,6 +370,7 @@ private:
ERR_NETWORK, ERR_NETWORK,
"failed to open RTMP output '" + url + "': " + av_error_string(open_result)); "failed to open RTMP output '" + url + "': " + av_error_string(open_result));
} }
session.io_opened = true;
} }
// RTMP sockets are non-seekable, so the FLV muxer must not try to backfill // RTMP sockets are non-seekable, so the FLV muxer must not try to backfill
@@ -388,6 +391,7 @@ private:
ERR_PROTOCOL, ERR_PROTOCOL,
"failed to write RTMP header for '" + url + "': " + av_error_string(header_result)); "failed to write RTMP header for '" + url + "': " + av_error_string(header_result));
} }
session.header_written = true;
spdlog::info( spdlog::info(
"RTMP_OUTPUT_READY backend=libavformat codec={} url={}", "RTMP_OUTPUT_READY backend=libavformat codec={} url={}",
@@ -401,9 +405,15 @@ private:
return; return;
} }
if (session.header_written) {
av_write_trailer(session.format_context); av_write_trailer(session.format_context);
if (!(session.format_context->oformat->flags & AVFMT_NOFILE) && session.format_context->pb != nullptr) { session.header_written = false;
}
if (session.io_opened &&
!(session.format_context->oformat->flags & AVFMT_NOFILE) &&
session.format_context->pb != nullptr) {
avio_closep(&session.format_context->pb); avio_closep(&session.format_context->pb);
session.io_opened = false;
} }
avformat_free_context(session.format_context); avformat_free_context(session.format_context);
session.format_context = nullptr; session.format_context = nullptr;