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]

Reply via email to