Notes. Scope: Debian bookworm’s gst-rtsp-server 1.22 C library, driving a live RTSP relay for camera feeds. The crash and the fix are measured; the sequence that leaves a stream “complete but frameless” is a plausible mechanism, not fully isolated. Corrections welcome.

At 10,000 feet

Setting: an RTSP relay built on GStreamer’s gst-rtsp-server library (the standard C library behind most self-hosted RTSP servers on Linux), which serves a placeholder stream when a source is offline.

Problem: whenever a source stayed offline, the whole server process SIGABRTed about every 30 seconds — one process serves several feeds, so every abort took all of them down.

What this note establishes: the abort is a gratuitous g_assert(FALSE) in gst_rtsp_media_get_rates() (rtsp-media.c:2766), triggered by a stream that reached “complete sender” state without ever carrying data; a 3-line patch onto the graceful result = FALSE path directly beside it fixes it.

Takeaway: if any stream can reach SETUP without ever carrying data (dead upstream, placeholder that hits EOS, teardown/PLAY race), you are exposed to a process-wide abort. Patch the library or keep such streams from existing.

The crash: an assert that should have been a return

In rtsp-media.c (the library, not application code):

if (gst_rtsp_stream_is_complete (stream) && gst_rtsp_stream_is_sender (stream)) {
  if (gst_rtsp_stream_get_rates (stream, rate, applied_rate)) {
    ... if (save_rate != *rate ...) { g_assert (FALSE); /* line 2759 */ }
  } else {
    /* complete stream without rate and applied_rate, weird */
    g_assert (FALSE);                          /* line 2766 — the crash */
    result = FALSE;
    break;
  }
}

A stream whose SETUP finished (is_complete and is_sender true) but that never carried data produces no segment event, so no rate. On the next client PLAY, get_rates returns FALSE and g_assert(FALSE) fires — with glib assertions live, an abort() of the entire process. The graceful path (result = FALSE, caller logs “failed to obtain consistent rate” and tears down one session) sits directly below the assert: a debug-time invariant check left load-bearing in production.

Measured: ~130 aborts/hour while a source stayed offline, 949 aborts in retained logs, ~30 s cadence matching the placeholder-then-reconnect cycle.

How a stream ends up “complete but frameless” (plausible mechanism)

A placeholder test pattern runs for a fixed number of buffers, emits EOS, and the cached media’s streams stay registered as complete senders with no data ever coming; the next PLAY walks into the assert. It could equally be a race between source teardown and a client attaching. The fix is correct either way — the state (complete sender, no rate) is what the library mishandles.

The fix: de-assert the library (3-line patch)

Rebuild Debian’s libgstrtspserver-1.0-0 with the three assert sites in get_rates (2746 guarded, 2759 and 2766 rerouted) sent onto the existing result = FALSE path: a process-wide SIGABRT becomes a failed PLAY for one client (500, session torn down, client retries). Install via quilt patch + apt-get source rebuild, then apt-mark hold so an upgrade cannot restore the aborting binary. Optionally add an application-side liveness gate that refuses to build pipelines for dead sources — that shrinks the window but is not the fix.

Verified: the repro (dead source + scripted client re-attaching) crashed the unpatched library with the exact signature; patched, it ran 5000+ attaches over ~8 minutes with zero aborts, plus a 20+ minute live soak with zero aborts and healthy feeds unaffected.

Honesty and prior art

  • Measured: the assert-to-abort mechanism and the patch (before/after repro).
  • Prior art: CVE-2024-44331 / GStreamer SA-2024-0004 documents an assertion-abort DoS in the same rtsp-media.c, so the class is known. New here is the trigger: an empty complete-sender stream. A GitHub-wide search for gst_rtsp_media_get_rates returned zero hits — absence of public record, not proof no one has hit it privately.
  • Plausible mechanism, not isolated: the application-side sequence that creates the empty stream. The fix does not depend on it.

Licensed under the site footer’s CC BY 4.0. If this saved you a debugging session, the optional thanks link in the footer is appreciated, no obligation.

Validation (2026-08-31)

Re-checked in a throwaway debian:bookworm Docker container:

  • Version: apt-get source gst-rtsp-server1.0 pulled 1.22.0-2 (upstream 1.22.0), matching the installed libgstrtspserver-1.0-0 binary.
  • Source: gst_rtsp_media_get_rates() (line 2725) contains the three asserts at 2746, 2759, 2766 (claimed crash site: exact), each g_assert(FALSE) followed immediately by result = FALSE; break; and a tail that logs and returns.
  • Shipped binary: debian/rules sets no -Db_ndebug / G_DISABLE_ASSERT; disassembly of the shipped .so shows exactly 3 g_assertion_message_expr calls in the function. Assertions enabled: YES.
  • Runtime: a minimal glib g_assert(FALSE) program dies with SIGABRT, so line 2766 aborts the whole process with ERROR:../gst/rtsp-server/rtsp-media.c:2766:...: assertion failed: (FALSE).
  • Not reproduced: the end-to-end live trigger (real media reaching complete-sender-no-rate, then PLAY) was not built in the time box.

Verdict: source-confirmed and binary-confirmed on 1.22.0-2; every mechanistic link measured; end-to-end live trigger not reproduced.