PR #25802 opened by Kilian (lusoris) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25802 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25802.patch
The RTSP muxer can return `AVERROR_EOF` on two TCP paths when the connection breaks. The ffmpeg CLI treats EOF from a muxer as the normal end of output, so a publish that sent no media ends with "Muxer returned EOF" and exit status 0. Both paths now return `AVERROR(EPIPE)`; every other error is returned unchanged. This was raised as a pre-existing issue in the review of #25761. This branch is stacked on #25761 (head 7b7d6f422df7), which is stacked on #25726; only the last two commits are new here. - Interleaved media write: `rtsp_write_packet()` returns the result of `ff_rtsp_tcp_write_packet()`. A TLS write that moves no bytes (`SSL_write()` returning 0) reaches it as `AVERROR_EOF`. - Poll loop: when the peer sends an interleave marker (`$`) and then closes its sending side, `ff_rtsp_skip_packet()` returns `AVERROR_EOF`. This matches the mapping #25761 already applies to `ff_rtsp_read_reply()` a few lines above. Measured with local fault peers on 127.0.0.1: an `LD_PRELOAD` wrapper that makes `SSL_write()` return 0 for interleaved packets after RECORD (rtsps), and a TCP peer that sends `$` and shuts down its write side after RECORD: | Case | Before | After | | --- | --- | --- | | media write EOF (rtsps) | exit 0, "Muxer returned EOF" | exit 224, "Broken pipe" | | marker then peer close | exit 0, "Muxer returned EOF" | exit 224, "Broken pipe" | | normal publish (control) | exit 0 | exit 0 | `make fate-source` and the rtsp FATE tests pass. tools/patcheck reports nothing on the added lines. >From 9fa9b661dfca684d621959362b57ab8fbd72c36e Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Tue, 3 Nov 2026 16:23:55 +0100 Subject: [PATCH 1/8] avformat/rtspenc: check the interleaved frame write ff_rtsp_tcp_write_packet() ignored the result of writing each interleaved frame to the RTSP control connection, so a failed or short write was reported as success and the muxer kept writing. Stop at the first failed write and return its error, or AVERROR(EIO) for a short write. Signed-off-by: lusoris <[email protected]> --- libavformat/rtspenc.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libavformat/rtspenc.c b/libavformat/rtspenc.c index f2f4b6d5258..724b5c6b8b4 100644 --- a/libavformat/rtspenc.c +++ b/libavformat/rtspenc.c @@ -145,7 +145,7 @@ int ff_rtsp_tcp_write_packet(AVFormatContext *s, RTSPStream *rtsp_st) RTSPState *rt = s->priv_data; AVFormatContext *rtpctx = rtsp_st->transport_priv; uint8_t *buf, *ptr; - int size; + int size, ret = 0; uint8_t *interleave_header, *interleaved_packet; size = avio_close_dyn_buf(rtpctx->pb, &buf); @@ -171,12 +171,17 @@ int ff_rtsp_tcp_write_packet(AVFormatContext *s, RTSPStream *rtsp_st) interleave_header[0] = '$'; interleave_header[1] = id; AV_WB16(interleave_header + 2, packet_len); - ffurl_write(rt->rtsp_hd_out, interleaved_packet, 4 + packet_len); + ret = ffurl_write(rt->rtsp_hd_out, interleaved_packet, 4 + packet_len); + if (ret != 4 + packet_len) { + ret = ret < 0 ? ret : AVERROR(EIO); + break; + } ptr += packet_len; size -= packet_len; } av_free(buf); - return ffio_open_dyn_packet_buf(&rtpctx->pb, rt->pkt_size); + size = ffio_open_dyn_packet_buf(&rtpctx->pb, rt->pkt_size); + return ret < 0 ? ret : size; } static int rtsp_write_packet(AVFormatContext *s, AVPacket *pkt) -- 2.54.0 >From 35c136d46b71a3ccdde762f416e6674bb922ca9f Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Tue, 3 Nov 2026 16:23:56 +0100 Subject: [PATCH 2/8] avformat/rtsp: check the write of the automatic reply ff_rtsp_read_reply() answers server requests (OPTIONS, GET_PARAMETER and others) on its own but ignored whether the reply was written. Return the write error, or AVERROR(EIO) for a short write, and update last_cmd_time only after a complete write. Signed-off-by: lusoris <[email protected]> --- libavformat/rtsp.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c index 29f219c366a..186be3def9b 100644 --- a/libavformat/rtsp.c +++ b/libavformat/rtsp.c @@ -1358,14 +1358,16 @@ start: av_base64_encode(base64buf, sizeof(base64buf), buf, strlen(buf)); ptr = base64buf; } - ffurl_write(rt->rtsp_hd_out, ptr, strlen(ptr)); + ret = ffurl_write(rt->rtsp_hd_out, ptr, strlen(ptr)); - rt->last_cmd_time = av_gettime_relative(); /* Even if the request from the server had data, it is not the data * that the caller wants or expects. The memory could also be leaked * if the actual following reply has content data. */ if (content_ptr) av_freep(content_ptr); + if (ret != strlen(ptr)) + return ret < 0 ? ret : AVERROR(EIO); + rt->last_cmd_time = av_gettime_relative(); /* If method is set, this is called from ff_rtsp_send_cmd, * where a reply to exactly this request is awaited. For * callers from within packet receiving, we just want to -- 2.54.0 >From edd7328845e3c91ee9373f5d0f9a19668e7a7229 Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Tue, 3 Nov 2026 16:23:58 +0100 Subject: [PATCH 3/8] avformat/rtspdec: check the listen-mode reply write rtsp_send_reply() always returned 0, so in listen mode a reply that could not be written to the client went unnoticed and the server state advanced anyway. Return the write error, or AVERROR(EIO) for a short write, and propagate it from the ANNOUNCE, OPTIONS, SETUP and RECORD handlers before the state changes. Signed-off-by: lusoris <[email protected]> --- libavformat/rtspdec.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c index 0e592eebaed..e6b9b0c7382 100644 --- a/libavformat/rtspdec.c +++ b/libavformat/rtspdec.c @@ -118,7 +118,7 @@ static int rtsp_send_reply(AVFormatContext *s, enum RTSPStatusCode code, { RTSPState *rt = s->priv_data; char message[MAX_URL_SIZE]; - int index = 0; + int index = 0, ret; while (status_messages[index].code) { if (status_messages[index].code == code) { snprintf(message, sizeof(message), "RTSP/1.0 %d %s\r\n", @@ -135,7 +135,9 @@ static int rtsp_send_reply(AVFormatContext *s, enum RTSPStatusCode code, av_strlcat(message, extracontent, sizeof(message)); av_strlcat(message, "\r\n", sizeof(message)); av_log(s, AV_LOG_TRACE, "Sending response:\n%s", message); - ffurl_write(rt->rtsp_hd_out, message, strlen(message)); + ret = ffurl_write(rt->rtsp_hd_out, message, strlen(message)); + if (ret != strlen(message)) + return ret < 0 ? ret : AVERROR(EIO); return 0; } @@ -225,8 +227,7 @@ static int rtsp_read_announce(AVFormatContext *s) av_free(sdp); if (ret) return ret; - rtsp_send_reply(s, RTSP_STATUS_OK, NULL, request.seq); - return 0; + return rtsp_send_reply(s, RTSP_STATUS_OK, NULL, request.seq); } av_log(s, AV_LOG_ERROR, "Invalid ANNOUNCE Content-Length %d\n", request.content_length); @@ -247,10 +248,9 @@ static int rtsp_read_options(AVFormatContext *s) return ret; rt->seq++; /* Send Reply */ - rtsp_send_reply(s, RTSP_STATUS_OK, - "Public: ANNOUNCE, PAUSE, SETUP, TEARDOWN, RECORD\r\n", - request.seq); - return 0; + return rtsp_send_reply(s, RTSP_STATUS_OK, + "Public: ANNOUNCE, PAUSE, SETUP, TEARDOWN, RECORD\r\n", + request.seq); } static int rtsp_read_setup(AVFormatContext *s, char* host, char *controlurl) @@ -366,7 +366,9 @@ static int rtsp_read_setup(AVFormatContext *s, char* host, char *controlurl) av_strlcatf(responseheaders, sizeof(responseheaders), "Session: %s\r\n", rt->session_id); /* Send Reply */ - rtsp_send_reply(s, RTSP_STATUS_OK, responseheaders, request.seq); + ret = rtsp_send_reply(s, RTSP_STATUS_OK, responseheaders, request.seq); + if (ret < 0) + return ret; rt->state = RTSP_STATE_PAUSED; return 0; @@ -388,7 +390,9 @@ static int rtsp_read_record(AVFormatContext *s) rt->seq++; snprintf(responseheaders, sizeof(responseheaders), "Session: %s\r\n", rt->session_id); - rtsp_send_reply(s, RTSP_STATUS_OK, responseheaders, request.seq); + ret = rtsp_send_reply(s, RTSP_STATUS_OK, responseheaders, request.seq); + if (ret < 0) + return ret; rt->state = RTSP_STATE_STREAMING; return 0; -- 2.54.0 >From 7cf2fb7ae1a6366a76789bb865dbfbe9a94f0b67 Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Wed, 4 Nov 2026 00:06:11 +0100 Subject: [PATCH 4/8] avformat/rtspdec: keep the handler error in rtsp_listen rtsp_listen() replaced every non-zero result of the OPTIONS, ANNOUNCE, RECORD and SETUP handlers with AVERROR_INVALIDDATA, so a failed reply write surfaced as invalid data instead of EPIPE or EIO. Return negative handler errors unchanged and keep the mapping for positive results. Signed-off-by: lusoris <[email protected]> --- libavformat/rtspdec.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c index e6b9b0c7382..69224d3596c 100644 --- a/libavformat/rtspdec.c +++ b/libavformat/rtspdec.c @@ -843,7 +843,8 @@ static int rtsp_listen(AVFormatContext *s) } else if (methodcode == SETUP) ret = rtsp_read_setup(s, host, uri); if (ret) { - ret = AVERROR_INVALIDDATA; + if (ret > 0) + ret = AVERROR_INVALIDDATA; goto fail; } } -- 2.54.0 >From 90782a5ef2c41be6d9c14da3805f78f95654697b Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Wed, 4 Nov 2026 00:06:19 +0100 Subject: [PATCH 5/8] avformat/rtspdec: return the reply write error in check_sessionid When the session id did not match, check_sessionid() sent the error reply and returned AVERROR_STREAM_NOT_FOUND even if that write failed. Return the write error instead, so a closed control connection is not reported as an unknown session. Signed-off-by: lusoris <[email protected]> --- libavformat/rtspdec.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libavformat/rtspdec.c b/libavformat/rtspdec.c index 69224d3596c..40dc80e73ce 100644 --- a/libavformat/rtspdec.c +++ b/libavformat/rtspdec.c @@ -152,9 +152,12 @@ static inline int check_sessionid(AVFormatContext *s, return 0; } if (strcmp(session_id, request->session_id)) { + int ret; av_log(s, AV_LOG_ERROR, "Unexpected session-id %s\n", request->session_id); - rtsp_send_reply(s, RTSP_STATUS_SESSION, NULL, request->seq); + ret = rtsp_send_reply(s, RTSP_STATUS_SESSION, NULL, request->seq); + if (ret < 0) + return ret; return AVERROR_STREAM_NOT_FOUND; } return 0; -- 2.54.0 >From 7b7d6f422df78a21106fd941943b3f4f9d679f15 Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Wed, 4 Nov 2026 00:06:22 +0100 Subject: [PATCH 6/8] avformat/rtspenc: keep the read error in the muxer poll loop rtsp_write_packet() polls the control connection for server requests between packets and replaced any failure of ff_rtsp_read_reply() with AVERROR(EPIPE). Return the error it reported instead, except for AVERROR_EOF: the muxer caller treats EOF as the normal end of output, so a server closing the connection during a publish would otherwise end it with success. Keep AVERROR(EPIPE) for that case. Signed-off-by: lusoris <[email protected]> --- libavformat/rtspenc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libavformat/rtspenc.c b/libavformat/rtspenc.c index 724b5c6b8b4..dd37b47aab8 100644 --- a/libavformat/rtspenc.c +++ b/libavformat/rtspenc.c @@ -206,7 +206,7 @@ static int rtsp_write_packet(AVFormatContext *s, AVPacket *pkt) * interleaved packets internally. */ ret = ff_rtsp_read_reply(s, &reply, NULL, 1, NULL); if (ret < 0) - return AVERROR(EPIPE); + return ret == AVERROR_EOF ? AVERROR(EPIPE) : ret; if (ret == 1) { ret = ff_rtsp_skip_packet(s); if (ret < 0) -- 2.54.0 >From c9c45e3c2cdd917684fffd3cf44e983754c96672 Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Fri, 6 Nov 2026 03:33:11 +0100 Subject: [PATCH 7/8] avformat/rtspenc: map TCP write EOF to EPIPE on media packet send When transmitting interleaved RTP packets over TCP via ff_rtsp_tcp_write_packet(), a peer disconnect or TLS write zero-return (such as OpenSSL SSL_write returning 0) propagates AVERROR_EOF from ffurl_write to the caller. Returning AVERROR_EOF from the muxer is incorrect because the muxing harness (fftools/ffmpeg_mux.c) interprets EOF as a normal end of stream ('Muxer returned EOF') and exits with status 0, masking an aborted media stream as a successful publication. Map AVERROR_EOF to AVERROR(EPIPE) so the failure is correctly surfaced as a broken connection. Measured exit status: - Before (red): FFMPEG_EXIT=0 ('Muxer returned EOF', 0 media bytes sent) - After (green): FFMPEG_EXIT=224 (exit -32, 'Broken pipe') Signed-off-by: lusoris <[email protected]> --- libavformat/rtspenc.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libavformat/rtspenc.c b/libavformat/rtspenc.c index dd37b47aab8..872053a495a 100644 --- a/libavformat/rtspenc.c +++ b/libavformat/rtspenc.c @@ -228,8 +228,11 @@ static int rtsp_write_packet(AVFormatContext *s, AVPacket *pkt) * transport, rtpctx->pb is only a dyn_packet_buf that queues up the * packets, so we need to send them out on the TCP connection separately. */ - if (!ret && rt->lower_transport == RTSP_LOWER_TRANSPORT_TCP) + if (!ret && rt->lower_transport == RTSP_LOWER_TRANSPORT_TCP) { ret = ff_rtsp_tcp_write_packet(s, rtsp_st); + if (ret == AVERROR_EOF) + ret = AVERROR(EPIPE); + } return ret; } -- 2.54.0 >From f2e02d95551ac3c6f93f07285948d314318b7c8a Mon Sep 17 00:00:00 2001 From: lusoris <[email protected]> Date: Fri, 6 Nov 2026 03:33:11 +0100 Subject: [PATCH 8/8] avformat/rtspenc: map skip packet EOF to EPIPE on peer interleave When rtsp_write_packet() polls for incoming RTSP control traffic and detects an interleaved packet marker ('$'), ff_rtsp_skip_packet() is called to discard the payload. If the peer closes its writing side right after the interleave marker, ff_rtsp_skip_packet() returns AVERROR_EOF. Returning AVERROR_EOF directly from the muxer causes the muxing loop in fftools/ffmpeg_mux.c to treat the stream as normally completed ('Muxer returned EOF') and terminate with exit status 0 without error. Consistent with ff_rtsp_read_reply() directly preceding it, map AVERROR_EOF from ff_rtsp_skip_packet() to AVERROR(EPIPE) to reflect the broken stream. Measured exit status: - Before (red): FFMPEG_EXIT=0 ('Muxer returned EOF', 0 media bytes sent) - After (green): FFMPEG_EXIT=224 (exit -32, 'Broken pipe') Signed-off-by: lusoris <[email protected]> --- libavformat/rtspenc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libavformat/rtspenc.c b/libavformat/rtspenc.c index 872053a495a..affbc69524d 100644 --- a/libavformat/rtspenc.c +++ b/libavformat/rtspenc.c @@ -210,7 +210,7 @@ static int rtsp_write_packet(AVFormatContext *s, AVPacket *pkt) if (ret == 1) { ret = ff_rtsp_skip_packet(s); if (ret < 0) - return ret; + return ret == AVERROR_EOF ? AVERROR(EPIPE) : ret; } /* XXX: parse message */ if (rt->state != RTSP_STATE_STREAMING) -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
