Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package gstreamer-plugins-base for 
openSUSE:Factory checked in at 2026-11-09 16:03:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/gstreamer-plugins-base (Old)
 and      /work/SRC/openSUSE:Factory/.gstreamer-plugins-base.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "gstreamer-plugins-base"

Mon Nov  9 16:03:09 2026 rev:136 rq:1395965 version:1.28.9

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/gstreamer-plugins-base/gstreamer-plugins-base.changes
    2026-10-10 14:59:27.547179151 +0200
+++ 
/work/SRC/openSUSE:Factory/.gstreamer-plugins-base.new.3759290/gstreamer-plugins-base.changes
       2026-11-09 16:03:16.991390774 +0100
@@ -1,0 +2,21 @@
+Fri Nov  6 20:04:14 UTC 2026 - Bjørn Lie <[email protected]>
+
+- Update to version 1.28.9:
+  * appsink: keep queued sticky events across a flush
+  * eagl and vulkan: Remove internal_view from the superview when finalizing
+  * decodebin3 aborts on giostreamsrc + parsebin with ID3-tagged MP3
+  * decodebin3: Crash in GstPad playing a MP3 file
+  * decodebin3: mq_slot_handle_stream_start: assertion failed: (collection)
+  * decodebin3: Null-deref crash in gst_decodebin_input_link_to_slot with
+    Strawberry player
+  * discoverer: Fix use-after-free when a stream reuses its parent
+  * parsebin: Handle lazy stream-id generation
+  * qsv,d3d11,d3d12: Build fixes on latest msys2 environment
+  * samiparse: Drop already parsed text from the HTML context buffer
+  * samiparse: Fix when encountering unclosed tags to prevent slowdown
+  * udmabuf: Allow disabling via env var
+  * video: fix undefined behaviour on empty unregistered user data meta
+  * video: vbi: reject pixel widths narrower than one conversion group
+  * videoconvertscale: Don't scale in the padding described by GstVideoMeta
+
+-------------------------------------------------------------------

Old:
----
  gstreamer-1.28.8.tar.xz

New:
----
  gstreamer-1.28.9.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ gstreamer-plugins-base.spec ++++++
--- /var/tmp/diff_new_pack.gLVYP4/_old  2026-11-09 16:03:18.092436927 +0100
+++ /var/tmp/diff_new_pack.gLVYP4/_new  2026-11-09 16:03:18.094437011 +0100
@@ -20,7 +20,7 @@
 %define gst_branch 1.0
 
 Name:           gstreamer-plugins-base
-Version:        1.28.8
+Version:        1.28.9
 Release:        0
 Summary:        GStreamer Streaming-Media Framework Plug-Ins
 License:        GPL-2.0-or-later AND LGPL-2.1-or-later

++++++ _service ++++++
--- /var/tmp/diff_new_pack.gLVYP4/_old  2026-11-09 16:03:18.133438646 +0100
+++ /var/tmp/diff_new_pack.gLVYP4/_new  2026-11-09 16:03:18.137438814 +0100
@@ -4,7 +4,7 @@
     <param name="scm">git</param>
     <param 
name="url">https://gitlab.freedesktop.org/gstreamer/gstreamer.git</param>
     <param name="subdir">subprojects/gst-plugins-base</param>
-    <param name="revision">1.28.8</param>
+    <param name="revision">1.28.9</param>
     <param name="versionformat">@PARENT_TAG@+@TAG_OFFSET@</param>
     <param name="versionrewrite-pattern">v?(.*)\+0</param>
     <param name="versionrewrite-replacement">\1</param>

++++++ gstreamer-1.28.8.tar.xz -> gstreamer-1.28.9.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst/playback/gstdecodebin3.c 
new/gstreamer-1.28.9/gst/playback/gstdecodebin3.c
--- old/gstreamer-1.28.8/gst/playback/gstdecodebin3.c   2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst/playback/gstdecodebin3.c   2026-11-06 
13:54:37.000000000 +0100
@@ -3378,7 +3378,13 @@
 
   collection =
       find_collection_for_stream (dbin, (gchar *) slot->active_stream_id);
-  g_assert (collection);
+  if (G_UNLIKELY (collection == NULL)) {
+    GST_ELEMENT_ERROR (dbin, STREAM, FAILED,
+        ("Stream %s is not part of any known stream collection",
+            slot->active_stream_id),
+        ("pad %s:%s", GST_DEBUG_PAD_NAME (slot->src_pad)));
+    goto beach;
+  }
 
   /* check if all streams are present for that collection. We do it now since 
we
    * might just have a single stream in the collection */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst/playback/gstparsebin.c 
new/gstreamer-1.28.9/gst/playback/gstparsebin.c
--- old/gstreamer-1.28.8/gst/playback/gstparsebin.c     2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst/playback/gstparsebin.c     2026-11-06 
13:54:37.000000000 +0100
@@ -3148,9 +3148,17 @@
     }
   }
 
-  if (chain->parsed) {
-    complete = TRUE;
-    goto out;
+  if (chain->parsed && chain->endpad) {
+    /* Plugging a parser is not enough to expose: the parser creates its
+     * stream-id lazily, on the first data it pushes, so a pad exposed here
+     * would publish a stream-collection built from an earlier stream-start
+     * and then emit a different one. The ghost mirrors its target's sticky
+     * events, so wait until the pad we are about to publish knows its own
+     * identity; the block probe on the target triggers exposure once it does. 
*/
+    gchar *sid = gst_pad_get_stream_id (GST_PAD (chain->endpad));
+
+    complete = (sid != NULL);
+    g_free (sid);
   }
 
 out:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst/subparse/samiparse.c 
new/gstreamer-1.28.9/gst/subparse/samiparse.c
--- old/gstreamer-1.28.8/gst/subparse/samiparse.c       2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst/subparse/samiparse.c       2026-11-06 
13:54:37.000000000 +0100
@@ -23,6 +23,11 @@
 #include <string.h>
 #include <stdlib.h>
 
+/* Maximum size kept for an unterminated HTML tag. A dangling '<' swallows
+ * input until the next '>' or '<'; this bounds how much is buffered and
+ * rescanned before the pending tag is dropped. */
+#define MAX_PENDING_TAG_SIZE 4096
+
 #define ITALIC_TAG 'i'
 #define SPAN_TAG   's'
 #define RUBY_TAG   'r'
@@ -599,7 +604,25 @@
       gchar *element = NULL;
       /* find <blahblah> */
       if (!strchr (next, '>')) {
-        /* no tag end point. buffer will be process in next time */
+        /* no tag end point. drop what was already parsed, the pending tag
+         * stays buffered until the next text completes it. Truncate the
+         * buffer to a maximum size so that we don't do O(n^2) work
+         * rescanning an ever-growing buffer on subsequent calls. */
+        g_string_erase (ctxt->buf, 0, next - ctxt->buf->str);
+        if (ctxt->buf->len > MAX_PENDING_TAG_SIZE) {
+          /* Restart the pending tag at the next '<' inside the kept window:
+           * the buffer must keep starting with '<', otherwise the text left
+           * in front of it is emitted as caption content, and cutting a
+           * multibyte sequence in half would emit invalid UTF-8. Without a
+           * '<' in the window the pending tag is dropped, which ends the
+           * swallow window after at most MAX_PENDING_TAG_SIZE bytes. */
+          const gchar *p = memchr (ctxt->buf->str + ctxt->buf->len -
+              MAX_PENDING_TAG_SIZE, '<', MAX_PENDING_TAG_SIZE);
+          if (p)
+            g_string_erase (ctxt->buf, 0, p - ctxt->buf->str);
+          else
+            g_string_truncate (ctxt->buf, 0);
+        }
         return TRUE;
       }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/gstreamer-1.28.8/gst/videoconvertscale/gstvideoconvertscale.c 
new/gstreamer-1.28.9/gst/videoconvertscale/gstvideoconvertscale.c
--- old/gstreamer-1.28.8/gst/videoconvertscale/gstvideoconvertscale.c   
2026-10-08 17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst/videoconvertscale/gstvideoconvertscale.c   
2026-11-06 13:54:37.000000000 +0100
@@ -2007,6 +2007,7 @@
   if (priv->converter_config_changed ||
       !gst_video_info_is_equal (&priv->last_frame_vinfo, &in_frame->info)) {
     GstStructure *options;
+    GstVideoInfo converter_in_info = in_frame->info;
 
     if (priv->converter_config)
       options = gst_structure_copy (priv->converter_config);
@@ -2022,10 +2023,18 @@
       options =
           gst_structure_copy (gst_video_converter_get_config (priv->convert));
 
+    if (!priv->converter_config) {
+      /* The meta may describe padded dimensions. Keep its layout, but use the
+       * visible dimensions from caps to avoid resampling the padding. */
+      converter_in_info.width = filter->in_info.width;
+      converter_in_info.height = filter->in_info.height;
+    }
+
     gst_video_converter_free (priv->convert);
     priv->convert =
         gst_video_convert_scale_create_converter (GST_VIDEO_CONVERT_SCALE
-        (filter), &in_frame->info, &filter->out_info, options);
+        (filter), &converter_in_info, &filter->out_info, options);
+    priv->last_frame_vinfo = in_frame->info;
 
     priv->converter_config_changed = FALSE;
   }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/gstreamer-1.28.8/gst-libs/gst/allocators/gstudmabufallocator.c 
new/gstreamer-1.28.9/gst-libs/gst/allocators/gstudmabufallocator.c
--- old/gstreamer-1.28.8/gst-libs/gst/allocators/gstudmabufallocator.c  
2026-10-08 17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/allocators/gstudmabufallocator.c  
2026-11-06 13:54:37.000000000 +0100
@@ -29,6 +29,9 @@
  * gst_allocator_alloc() method using `memfd_create()` and `UDMABUF_CREATE`.
  * Platforms not supporting that (most non-Linux) will always return %NULL.
  *
+ * The allocator can be disabled by setting the GST_DISABLE_UDMABUF environment
+ * variable.
+ *
  * Since: 1.28
  */
 
@@ -204,12 +207,15 @@
 
   alloc->mem_type = GST_ALLOCATOR_UDMABUF;
 
+  self->udmabuf_dev_fd = -1;
 #if defined(HAVE_MEMFD_CREATE) && defined(HAVE_LINUX_UDMABUF_H)
-  self->udmabuf_dev_fd = open ("/dev/udmabuf", O_RDONLY | O_CLOEXEC, 0);
-  if (self->udmabuf_dev_fd == -1)
-    GST_WARNING_OBJECT (self,
-        "Udmabuf allocator not available, can't open /dev/udmabuf: %s",
-        strerror (errno));
+  if (!g_getenv ("GST_DISABLE_UDMABUF")) {
+    self->udmabuf_dev_fd = open ("/dev/udmabuf", O_RDONLY | O_CLOEXEC, 0);
+    if (self->udmabuf_dev_fd == -1)
+      GST_WARNING_OBJECT (self,
+          "Udmabuf allocator not available, can't open /dev/udmabuf: %s",
+          strerror (errno));
+  }
 #endif
 
   /* Inherited from GstFdAllocator. Unset as we implement alloc(). */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/app/gstappsink.c 
new/gstreamer-1.28.9/gst-libs/gst/app/gstappsink.c
--- old/gstreamer-1.28.8/gst-libs/gst/app/gstappsink.c  2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/app/gstappsink.c  2026-11-06 
13:54:37.000000000 +0100
@@ -58,6 +58,11 @@
  * is stopped or in the EOS state. You can check for the EOS state with the
  * "eos" property or with the gst_app_sink_is_eos() method.
  *
+ * Serialized events can be pulled along with the samples with
+ * gst_app_sink_pull_object(). Upstream doesn't send sticky events again after
+ * a flush, so the ones that were queued but not pulled yet are kept, as long
+ * as the pad still has them.
+ *
  * The eos signal can also be used to be informed when the EOS state is reached
  * to avoid polling.
  */
@@ -1006,8 +1011,31 @@
   gst_sample_set_segment (priv->sample, NULL);
 }
 
+typedef struct
+{
+  GQueue *sticky_events;
+  GQueue *requeue;
+} KeepStickyData;
+
+static gboolean
+gst_app_sink_keep_sticky_event (GstPad * pad, GstEvent ** event,
+    gpointer user_data)
+{
+  KeepStickyData *data = user_data;
+  GList *l = g_queue_find (data->sticky_events, *event);
+
+  if (l) {
+    g_queue_unlink (data->sticky_events, l);
+    g_queue_push_tail_link (data->requeue, l);
+  }
+
+  return TRUE;
+}
+
+/* With @sticky_events, the queued sticky events are moved there instead of
+ * being dropped, for the caller to keep the ones the pad still has. */
 static void
-gst_app_sink_flush_unlocked (GstAppSink * appsink)
+gst_app_sink_flush_unlocked (GstAppSink * appsink, GQueue * sticky_events)
 {
   GstMiniObject *obj;
   GstAppSinkPrivate *priv = appsink->priv;
@@ -1015,8 +1043,14 @@
   GST_DEBUG_OBJECT (appsink, "flush stop appsink");
   priv->is_eos = FALSE;
   gst_buffer_replace (&priv->preroll_buffer, NULL);
-  while ((obj = gst_vec_deque_pop_head (priv->queue)))
+  while ((obj = gst_vec_deque_pop_head (priv->queue))) {
+    if (sticky_events && gst_app_keep_on_flush (obj)) {
+      g_queue_push_tail (sticky_events, obj);
+      continue;
+    }
+
     gst_mini_object_unref (obj);
+  }
 
   gst_queue_status_info_reset (&priv->queue_status_info);
 
@@ -1057,7 +1091,7 @@
   priv->flushing = TRUE;
   priv->started = FALSE;
   priv->wait_status = NOONE_WAITING;
-  gst_app_sink_flush_unlocked (appsink);
+  gst_app_sink_flush_unlocked (appsink, NULL);
   gst_buffer_replace (&priv->preroll_buffer, NULL);
   gst_caps_replace (&priv->preroll_caps, NULL);
   gst_caps_replace (&priv->last_caps, NULL);
@@ -1095,6 +1129,9 @@
 {
   GstAppSink *appsink = GST_APP_SINK_CAST (sink);
   GstAppSinkPrivate *priv = appsink->priv;
+  GQueue sticky_events = G_QUEUE_INIT;
+  GQueue requeue = G_QUEUE_INIT;
+  KeepStickyData keep = { &sticky_events, &requeue };
 
   GST_DEBUG_OBJECT (appsink, "%" GST_PTR_FORMAT, event);
 
@@ -1179,9 +1216,17 @@
     case GST_EVENT_FLUSH_STOP:
       g_mutex_lock (&priv->mutex);
       GST_DEBUG_OBJECT (appsink, "received FLUSH_STOP");
-      gst_app_sink_flush_unlocked (appsink);
+      gst_app_sink_flush_unlocked (appsink, &sticky_events);
       g_mutex_unlock (&priv->mutex);
 
+      /* Upstream won't send the queued sticky events again. Keep the ones
+       * the pad still has, in its order: it already dropped the replaced
+       * ones and the ones the flush invalidates. */
+      gst_pad_sticky_events_foreach (GST_BASE_SINK_PAD (sink),
+          gst_app_sink_keep_sticky_event, &keep);
+      g_queue_clear_full (&sticky_events,
+          (GDestroyNotify) gst_mini_object_unref);
+
       if (!priv->silent) {
         g_object_notify (G_OBJECT (appsink), "dropped");
       }
@@ -1195,6 +1240,7 @@
     gboolean emit;
     Callbacks *callbacks = NULL;
     GstAppSinkSimpleCallbacks *simple_callbacks = NULL;
+    GstMiniObject *obj;
     gboolean ret;
 
     g_mutex_lock (&priv->mutex);
@@ -1209,6 +1255,13 @@
     gst_vec_deque_push_tail (priv->queue, gst_event_ref (event));
     gst_queue_status_info_push_event (&priv->queue_status_info);
 
+    /* Sticky events kept by a FLUSH_STOP go right after it */
+    while ((obj = g_queue_pop_head (&requeue))) {
+      GST_DEBUG_OBJECT (appsink, "keeping %" GST_PTR_FORMAT, obj);
+      gst_vec_deque_push_tail (priv->queue, obj);
+      gst_queue_status_info_push_event (&priv->queue_status_info);
+    }
+
     if ((priv->wait_status & APP_WAITING))
       g_cond_signal (&priv->cond);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/app/gstappsrc.c 
new/gstreamer-1.28.9/gst-libs/gst/app/gstappsrc.c
--- old/gstreamer-1.28.8/gst-libs/gst/app/gstappsrc.c   2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/app/gstappsrc.c   2026-11-06 
13:54:37.000000000 +0100
@@ -841,17 +841,40 @@
   gst_base_src_set_live (GST_BASE_SRC (appsrc), DEFAULT_PROP_IS_LIVE);
 }
 
+/* Not stream-start and caps, as the segment would go out before them */
+static gboolean
+gst_app_src_keep_on_flush (GstMiniObject * obj)
+{
+  return gst_app_keep_on_flush (obj)
+      && GST_EVENT_TYPE (obj) != GST_EVENT_STREAM_START
+      && GST_EVENT_TYPE (obj) != GST_EVENT_CAPS;
+}
+
 /* Must be called with priv->mutex */
 static void
-gst_app_src_flush_queued (GstAppSrc * src, gboolean retain_last_caps)
+gst_app_src_flush_queued (GstAppSrc * src, gboolean retain_last_caps,
+    gboolean keep_sticky_events)
 {
   GstMiniObject *obj;
   GstAppSrcPrivate *priv = src->priv;
   GstCaps *requeue_caps = NULL;
+  GQueue sticky_events = G_QUEUE_INIT;
+
+  /* Nothing sends the queued sticky events again, keep them */
+  while ((obj = gst_vec_deque_pop_head (priv->delayed_events))) {
+    if (keep_sticky_events && gst_app_src_keep_on_flush (obj))
+      g_queue_push_tail (&sticky_events, obj);
+    else
+      gst_mini_object_unref (obj);
+  }
 
   while (!gst_vec_deque_is_empty (priv->queue)) {
     obj = gst_vec_deque_pop_head (priv->queue);
     if (obj) {
+      if (keep_sticky_events && gst_app_src_keep_on_flush (obj)) {
+        g_queue_push_tail (&sticky_events, obj);
+        continue;
+      }
       if (GST_IS_CAPS (obj) && retain_last_caps) {
         gst_caps_replace (&requeue_caps, GST_CAPS_CAST (obj));
       }
@@ -862,8 +885,9 @@
   if (requeue_caps) {
     gst_vec_deque_push_tail (priv->queue, requeue_caps);
   }
+  while ((obj = g_queue_pop_head (&sticky_events)))
+    gst_vec_deque_push_tail (priv->queue, obj);
 
-  gst_vec_deque_clear (priv->delayed_events);
   priv->pushed_buffer = FALSE;
 
   gst_queue_status_info_reset (&priv->queue_status_info);
@@ -896,7 +920,7 @@
     callbacks = g_steal_pointer (&priv->callbacks);
   if (priv->simple_callbacks)
     simple_callbacks = g_steal_pointer (&priv->simple_callbacks);
-  gst_app_src_flush_queued (appsrc, FALSE);
+  gst_app_src_flush_queued (appsrc, FALSE, FALSE);
   g_mutex_unlock (&priv->mutex);
 
   g_clear_pointer (&callbacks, callbacks_unref);
@@ -1124,7 +1148,7 @@
   switch (GST_EVENT_TYPE (event)) {
     case GST_EVENT_FLUSH_STOP:
       g_mutex_lock (&priv->mutex);
-      gst_app_src_flush_queued (appsrc, TRUE);
+      gst_app_src_flush_queued (appsrc, TRUE, TRUE);
       priv->is_eos = FALSE;
       g_mutex_unlock (&priv->mutex);
 
@@ -1223,7 +1247,7 @@
   priv->flushing = TRUE;
   priv->started = FALSE;
   priv->posted_latency_msg = FALSE;
-  gst_app_src_flush_queued (appsrc, TRUE);
+  gst_app_src_flush_queued (appsrc, TRUE, FALSE);
   g_cond_broadcast (&priv->cond);
   priv->in = priv->out = priv->dropped = 0;
   g_mutex_unlock (&priv->mutex);
@@ -1382,7 +1406,7 @@
   if (res) {
     GST_DEBUG_OBJECT (appsrc, "flushing queue");
     g_mutex_lock (&priv->mutex);
-    gst_app_src_flush_queued (appsrc, TRUE);
+    gst_app_src_flush_queued (appsrc, TRUE, TRUE);
     gst_segment_copy_into (segment, &priv->last_segment);
     gst_segment_copy_into (segment, &priv->current_segment);
     priv->pending_custom_segment = FALSE;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/app/gstapputils.c 
new/gstreamer-1.28.9/gst-libs/gst/app/gstapputils.c
--- old/gstreamer-1.28.8/gst-libs/gst/app/gstapputils.c 2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/app/gstapputils.c 2026-11-06 
13:54:37.000000000 +0100
@@ -248,3 +248,14 @@
       " buffers, %" GST_TIME_FORMAT, info->queued_bytes,
       info->queued_buffers, GST_TIME_ARGS (info->queued_time));
 }
+
+/* Whether @obj is a sticky event that a flush doesn't invalidate, as on a
+ * pad: nothing sends it again after the flush. */
+gboolean
+gst_app_keep_on_flush (GstMiniObject * obj)
+{
+  return GST_IS_EVENT (obj) && GST_EVENT_IS_STICKY (obj)
+      && GST_EVENT_TYPE (obj) != GST_EVENT_SEGMENT
+      && GST_EVENT_TYPE (obj) != GST_EVENT_EOS
+      && GST_EVENT_TYPE (obj) != GST_EVENT_STREAM_GROUP_DONE;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/app/gstapputils.h 
new/gstreamer-1.28.9/gst-libs/gst/app/gstapputils.h
--- old/gstreamer-1.28.8/gst-libs/gst/app/gstapputils.h 2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/app/gstapputils.h 2026-11-06 
13:54:37.000000000 +0100
@@ -44,4 +44,6 @@
     GstSegment * current_segment, GstSegment * last_segment,
     GstObject * log_context);
 
+gboolean gst_app_keep_on_flush (GstMiniObject * obj);
+
 #endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/gstreamer-1.28.8/gst-libs/gst/gl/eagl/gstglwindow_eagl.m 
new/gstreamer-1.28.9/gst-libs/gst/gl/eagl/gstglwindow_eagl.m
--- old/gstreamer-1.28.8/gst-libs/gst/gl/eagl/gstglwindow_eagl.m        
2026-10-08 17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/gl/eagl/gstglwindow_eagl.m        
2026-11-06 13:54:37.000000000 +0100
@@ -115,6 +115,22 @@
 {
   GstGLWindowEagl *window = GST_GL_WINDOW_EAGL (object);
 
+  if (window->priv->internal_view) {
+    GstGLUIView *view = (__bridge GstGLUIView *) window->priv->internal_view;
+    /* UIKit teardown must run on the main thread. The block retains the view
+     * until removal, and its weak window reference makes callbacks safe. */
+    if ([NSThread isMainThread]) {
+      [view setGstWindow:NULL];
+      [view removeFromSuperview];
+    } else {
+      dispatch_async (dispatch_get_main_queue (), ^{
+        [view setGstWindow:NULL];
+        [view removeFromSuperview];
+      });
+    }
+    CFRelease (window->priv->internal_view);
+    window->priv->internal_view = NULL;
+  }
   if (window->priv->layer)
     CFRelease (window->priv->layer);
   window->priv->layer = NULL;
@@ -362,7 +378,7 @@
 }
 
 @implementation GstGLUIView {
-    GstGLWindowEagl * window_eagl;
+    GWeakRef window_eagl;
 };
 
 +(Class) layerClass
@@ -370,9 +386,22 @@
   return [CAEAGLLayer class];
 }
 
+-(instancetype) initWithFrame:(CGRect) frame
+{
+  self = [super initWithFrame:frame];
+  if (self)
+    g_weak_ref_init (&window_eagl, NULL);
+  return self;
+}
+
+-(void) dealloc
+{
+  g_weak_ref_clear (&window_eagl);
+}
+
 -(void) setGstWindow:(GstGLWindowEagl *) window
 {
-  window_eagl = window;
+  g_weak_ref_set (&window_eagl, window);
 }
 
 -(void) didMoveToWindow
@@ -386,13 +415,14 @@
   [super layoutSubviews];
   [self updateScale];
 
-  if (window_eagl) {
-    GstGLWindow *window = GST_GL_WINDOW (window_eagl);
+  GstGLWindow *window = g_weak_ref_get (&window_eagl);
+  if (window) {
     GstGLContext *context = gst_gl_window_get_context (window);
     if (context) {
       gst_gl_window_queue_resize (window);
       gst_object_unref (context);
     }
+    gst_object_unref (window);
   }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/gl/meson.build 
new/gstreamer-1.28.9/gst-libs/gst/gl/meson.build
--- old/gstreamer-1.28.8/gst-libs/gst/gl/meson.build    2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/gl/meson.build    2026-11-06 
13:54:37.000000000 +0100
@@ -822,8 +822,10 @@
     windows_graphics_h = cc.has_header('windows.graphics.h')
     windows_app_dep = cc.find_library('WindowsApp', required: false)
     runtimeobject_lib = cc.find_library('runtimeobject', required: false)
+    windows_ui_xaml_h = cc.has_header('windows.ui.xaml.h')
+    windows_ui_xaml_media_dxinterop_h = 
cc.has_header('windows.ui.xaml.media.dxinterop.h')
 
-    if windows_graphics_h and windows_app_dep.found() and 
runtimeobject_lib.found()
+    if windows_graphics_h and windows_ui_xaml_h and 
windows_ui_xaml_media_dxinterop_h and windows_app_dep.found() and 
runtimeobject_lib.found()
       enabled_gl_winsys += 'winrt'
       glconf.set('GST_GL_HAVE_WINDOW_WINRT', 1)
       gl_priv_sources += [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/gstreamer-1.28.8/gst-libs/gst/pbutils/gstdiscoverer.c 
new/gstreamer-1.28.9/gst-libs/gst/pbutils/gstdiscoverer.c
--- old/gstreamer-1.28.8/gst-libs/gst/pbutils/gstdiscoverer.c   2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/pbutils/gstdiscoverer.c   2026-11-06 
13:54:37.000000000 +0100
@@ -1234,6 +1234,7 @@
 
   if (nval == NULL || GST_VALUE_HOLDS_STRUCTURE (nval)) {
     GstStructure *st = find_stream_for_node (dc, topology);
+    GstDiscovererStreamInfo *parent_arg = parent;
     gboolean add_to_list = TRUE;
 
     if (st) {
@@ -1270,19 +1271,29 @@
           /* We sometimes get an extra sub-stream from the parser. If this is
            * the case, we just replace the parent caps with this stream's caps
            * since they might contain more information */
+          GstDiscovererStreamInfo *sub;
+
           gst_caps_replace (&parent->caps, caps);
 
-          parse_stream_topology (dc, st, parent);
+          sub = parse_stream_topology (dc, st, parent);
+          if (sub)
+            gst_discoverer_stream_info_unref (sub);
           add_to_list = FALSE;
         } else if (child_is_raw_stream (parent->caps, caps)) {
           /* This is the "raw" stream corresponding to the parent. This
            * contains more information than the parent, tags etc. */
-          parse_stream_topology (dc, st, parent);
+          GstDiscovererStreamInfo *sub;
+
+          sub = parse_stream_topology (dc, st, parent);
+          if (sub)
+            gst_discoverer_stream_info_unref (sub);
           add_to_list = FALSE;
         } else {
           GstDiscovererStreamInfo *next = parse_stream_topology (dc, st, NULL);
-          res->next = next;
-          next->previous = res;
+          if (next) {
+            res->next = next;
+            next->previous = res;
+          }
         }
         gst_caps_unref (caps);
       }
@@ -1292,8 +1303,13 @@
       res->stream_number = dc->priv->current_info_stream_count++;
       dc->priv->current_info->stream_list =
           g_list_append (dc->priv->current_info->stream_list, res);
-    } else {
+    } else if (res == parent_arg) {
+      /* collect_information() re-used the caller's parent object: drop the
+       * reference this call acquired on it and report that no standalone
+       * stream info was created. Unreffing a distinct res would drop the
+       * caller's own reference to the parent instead. */
       gst_discoverer_stream_info_unref (res);
+      res = NULL;
     }
 
   } else if (GST_VALUE_HOLDS_LIST (nval)) {
@@ -1331,6 +1347,8 @@
       GST_DEBUG_OBJECT (dc, "%d %" GST_PTR_FORMAT, i, subst);
 
       substream = parse_stream_topology (dc, subst, NULL);
+      if (!substream)
+        continue;
 
       substream->previous = res;
       cont->streams =
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/video/video-anc.c 
new/gstreamer-1.28.9/gst-libs/gst/video/video-anc.c
--- old/gstreamer-1.28.8/gst-libs/gst/video/video-anc.c 2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/video/video-anc.c 2026-11-06 
13:54:37.000000000 +0100
@@ -338,6 +338,7 @@
 gst_video_vbi_parser_new (GstVideoFormat format, guint32 pixel_width)
 {
   GstVideoVBIParser *parser;
+  guint min_width;
 
   g_return_val_if_fail (pixel_width > 0, NULL);
 
@@ -345,16 +346,27 @@
     case GST_VIDEO_FORMAT_v210:
       parser = g_new0 (GstVideoVBIParser, 1);
       parser->bit16 = TRUE;
+      min_width = 6;            /* v210 packs 6 pixels per 16-byte word */
       break;
     case GST_VIDEO_FORMAT_UYVY:
       parser = g_new0 (GstVideoVBIParser, 1);
       parser->bit16 = FALSE;
+      min_width = 4;            /* UYVY lines convert 4 pixels at a time */
       break;
     default:
       GST_WARNING ("Format not supported by GstVideoVBIParser");
       return NULL;
   }
 
+  /* Line conversion walks whole pixel groups and indexes the workspace
+   * relative to info.width; a line narrower than one group underflows the
+   * loop bounds. */
+  if (pixel_width < min_width) {
+    GST_WARNING ("Pixel width %u too small for this format", pixel_width);
+    g_free (parser);
+    return NULL;
+  }
+
   gst_video_info_init (&parser->info);
   if (!gst_video_info_set_format (&parser->info, format, pixel_width, 1)) {
     GST_ERROR ("Could not create GstVideoInfo");
@@ -619,6 +631,7 @@
 gst_video_vbi_encoder_new (GstVideoFormat format, guint32 pixel_width)
 {
   GstVideoVBIEncoder *encoder;
+  guint min_width;
 
   g_return_val_if_fail (pixel_width > 0, NULL);
 
@@ -626,16 +639,27 @@
     case GST_VIDEO_FORMAT_v210:
       encoder = g_new0 (GstVideoVBIEncoder, 1);
       encoder->bit16 = TRUE;
+      min_width = 6;            /* v210 packs 6 pixels per 16-byte word */
       break;
     case GST_VIDEO_FORMAT_UYVY:
       encoder = g_new0 (GstVideoVBIEncoder, 1);
       encoder->bit16 = FALSE;
+      min_width = 4;            /* UYVY lines convert 4 pixels at a time */
       break;
     default:
       GST_WARNING ("Format not supported by GstVideoVBIEncoder");
       return NULL;
   }
 
+  /* Line conversion walks whole pixel groups and indexes the workspace
+   * relative to info.width; a line narrower than one group underflows the
+   * loop bounds. */
+  if (pixel_width < min_width) {
+    GST_WARNING ("Pixel width %u too small for this format", pixel_width);
+    g_free (encoder);
+    return NULL;
+  }
+
   gst_video_info_init (&encoder->info);
   if (!gst_video_info_set_format (&encoder->info, format, pixel_width, 1)) {
     GST_ERROR ("Could not create GstVideoInfo");
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-libs/gst/video/video-sei.c 
new/gstreamer-1.28.9/gst-libs/gst/video/video-sei.c
--- old/gstreamer-1.28.8/gst-libs/gst/video/video-sei.c 2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/gst-libs/gst/video/video-sei.c 2026-11-06 
13:54:37.000000000 +0100
@@ -172,13 +172,18 @@
 {
   GstVideoSEIUserDataUnregisteredMeta *meta;
   g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);
+  g_return_val_if_fail (uuid != NULL, NULL);
+  g_return_val_if_fail (data != NULL || size == 0, NULL);
 
   meta = (GstVideoSEIUserDataUnregisteredMeta *) gst_buffer_add_meta (buffer,
       GST_VIDEO_SEI_USER_DATA_UNREGISTERED_META_INFO, NULL);
   g_assert (meta != NULL);
   memcpy (meta->uuid, uuid, 16);
-  meta->data = g_malloc (size);
-  memcpy (meta->data, data, size);
+  meta->data = NULL;
+  if (size > 0) {
+    meta->data = g_malloc (size);
+    memcpy (meta->data, data, size);
+  }
   meta->size = size;
 
   return meta;
@@ -200,7 +205,13 @@
     gst_video_sei_user_data_unregistered_parse_precision_time_stamp
     (GstVideoSEIUserDataUnregisteredMeta * user_data, guint8 * status,
     guint64 * precision_time_stamp) {
-  guint8 *data = user_data->data;
+  guint8 *data;
+
+  g_return_val_if_fail (user_data != NULL, FALSE);
+  g_return_val_if_fail (status != NULL, FALSE);
+  g_return_val_if_fail (precision_time_stamp != NULL, FALSE);
+
+  data = user_data->data;
 
   if (memcmp (user_data->uuid, &H264_MISP_MICROSECTIME, 16) != 0 &&
       memcmp (user_data->uuid, &H265_MISP_MICROSECONDS, 16) != 0 &&
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/gst-plugins-base.doap 
new/gstreamer-1.28.9/gst-plugins-base.doap
--- old/gstreamer-1.28.8/gst-plugins-base.doap  2026-10-08 17:20:32.000000000 
+0200
+++ new/gstreamer-1.28.9/gst-plugins-base.doap  2026-11-06 13:54:37.000000000 
+0100
@@ -36,6 +36,16 @@
 
  <release>
   <Version>
+   <revision>1.28.9</revision>
+   <branch>1.28</branch>
+   <name></name>
+   <created>2026-11-06</created>
+   <file-release 
rdf:resource="https://gstreamer.freedesktop.org/src/gst-plugins-base/gst-plugins-base-1.28.9.tar.xz";
 />
+  </Version>
+ </release>
+
+ <release>
+  <Version>
    <revision>1.28.8</revision>
    <branch>1.28</branch>
    <name></name>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/meson.build 
new/gstreamer-1.28.9/meson.build
--- old/gstreamer-1.28.8/meson.build    2026-10-08 17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/meson.build    2026-11-06 13:54:37.000000000 +0100
@@ -1,5 +1,5 @@
 project('gst-plugins-base', 'c',
-  version : '1.28.8',
+  version : '1.28.9',
   meson_version : '>= 1.4',
   default_options : [ 'warning_level=1',
                       'buildtype=debugoptimized',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/scripts/dist-common-files.py 
new/gstreamer-1.28.9/scripts/dist-common-files.py
--- old/gstreamer-1.28.8/scripts/dist-common-files.py   2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/scripts/dist-common-files.py   2026-11-06 
13:54:37.000000000 +0100
@@ -42,10 +42,14 @@
 
     # Release notes (instead of NEWS) - could also write it out as NEWS.md
     print('Copying release notes into dist staging directory ..')
-    relnotes_src = os.path.join(source_root, '..', '..', 'release-notes', 
major_minor, f'release-notes-{major_minor}.md')
+    relnotes_src = os.path.join(source_root, '..', '..', 'release-notes',
+                                major_minor, f'release-notes-{major_minor}.md')
     with open(relnotes_src, 'r') as f:
         lines = f.readlines()
-    if not f'### {project_version}\n' in lines:
+    if not f'  - version: "{project_version}"\n' in lines:
+        sys.exit(
+            f'Update {relnotes_src} first, must contain a header entry for 
{project_version}')
+    if not f'### {project_version} {{#{project_version}}}\n' in lines:
         sys.exit(f'Update {relnotes_src} first, must contain a section for 
{project_version}')
     if not project_version.endswith('.0'):
         found = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/elements/appsink.c 
new/gstreamer-1.28.9/tests/check/elements/appsink.c
--- old/gstreamer-1.28.8/tests/check/elements/appsink.c 2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/elements/appsink.c 2026-11-06 
13:54:37.000000000 +0100
@@ -1087,6 +1087,125 @@
 
 GST_END_TEST;
 
+static GstEvent *
+pull_event (GstAppSink * app_sink)
+{
+  GstMiniObject *obj = gst_app_sink_try_pull_object (app_sink, 0);
+
+  fail_unless (obj != NULL);
+  fail_unless (GST_IS_EVENT (obj), "expected an event, got %" GST_PTR_FORMAT,
+      obj);
+
+  return GST_EVENT_CAST (obj);
+}
+
+GST_START_TEST (test_sticky_events_kept_on_flush)
+{
+  GstElement *sink;
+  GstAppSink *app_sink;
+  GstTagList *tags;
+  GstEvent *event;
+  GstMiniObject *obj;
+  GstSegment segment;
+  gchar *title = NULL;
+  gint expected_buffer_number = 3;
+
+  sink = setup_appsink ();
+  app_sink = GST_APP_SINK (sink);
+
+  ASSERT_SET_STATE (sink, GST_STATE_PLAYING, GST_STATE_CHANGE_ASYNC);
+
+  /* Queue stream-start, caps, segment, two stream tags for the same slot, a
+   * global tag, two custom sticky events with different names, a custom
+   * non-sticky event, and a buffer, then flush. */
+  push_buffer_with_number (1);
+  tags = gst_tag_list_new (GST_TAG_TITLE, "first", NULL);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_tag (tags)));
+  tags = gst_tag_list_new (GST_TAG_TITLE, "second", NULL);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_tag (tags)));
+  tags = gst_tag_list_new (GST_TAG_TITLE, "global", NULL);
+  gst_tag_list_set_scope (tags, GST_TAG_SCOPE_GLOBAL);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_tag (tags)));
+  fail_unless (gst_pad_push_event (mysrcpad,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
+              gst_structure_new_empty ("sticky"))));
+  fail_unless (gst_pad_push_event (mysrcpad,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
+              gst_structure_new_empty ("other-sticky"))));
+  fail_unless (gst_pad_push_event (mysrcpad,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM,
+              gst_structure_new_empty ("not-sticky"))));
+  push_buffer_with_number (2);
+
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_flush_start ()));
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_flush_stop (TRUE)));
+  ASSERT_SET_STATE (sink, GST_STATE_PLAYING, GST_STATE_CHANGE_ASYNC);
+
+  /* Upstream won't send them again: after the FLUSH_STOP, the sticky events
+   * are still there, the latest one per slot, but not the old segment and
+   * buffers, nor the non-sticky event. Pull them before anything else is
+   * queued, so they must be accounted for to be pulled. */
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_FLUSH_STOP);
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_STREAM_START);
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_CAPS);
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_TAG);
+  gst_event_parse_tag (event, &tags);
+  fail_unless (gst_tag_list_get_string (tags, GST_TAG_TITLE, &title));
+  assert_equals_string (title, "second");
+  g_free (title);
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_TAG);
+  gst_event_parse_tag (event, &tags);
+  assert_equals_int (gst_tag_list_get_scope (tags), GST_TAG_SCOPE_GLOBAL);
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event),
+      GST_EVENT_CUSTOM_DOWNSTREAM_STICKY);
+  fail_unless (gst_event_has_name (event, "sticky"));
+  gst_event_unref (event);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event),
+      GST_EVENT_CUSTOM_DOWNSTREAM_STICKY);
+  fail_unless (gst_event_has_name (event, "other-sticky"));
+  gst_event_unref (event);
+
+  fail_unless (gst_app_sink_try_pull_object (app_sink, 0) == NULL);
+
+  gst_segment_init (&segment, GST_FORMAT_TIME);
+  fail_unless (gst_pad_push_event (mysrcpad, gst_event_new_segment 
(&segment)));
+  push_buffer_with_number (3);
+
+  event = pull_event (app_sink);
+  assert_equals_int (GST_EVENT_TYPE (event), GST_EVENT_SEGMENT);
+  gst_event_unref (event);
+
+  obj = gst_app_sink_try_pull_object (app_sink, 0);
+  fail_unless (GST_IS_SAMPLE (obj));
+  gst_check_buffer_data (gst_sample_get_buffer (GST_SAMPLE_CAST (obj)),
+      &expected_buffer_number, sizeof (gint));
+  gst_mini_object_unref (obj);
+
+  fail_unless (gst_app_sink_try_pull_object (app_sink, 0) == NULL);
+
+  cleanup_appsink (sink);
+}
+
+GST_END_TEST;
+
 static gboolean
 propose_allocation_cb (GstAppSink * appsink, GstQuery * query,
     gpointer callback_data)
@@ -1299,6 +1418,7 @@
   tcase_add_test (tc_chain, test_event_paused);
   tcase_add_test (tc_chain, test_reverse_stepping);
   tcase_add_test (tc_chain, test_caps_before_flush_race_condition);
+  tcase_add_test (tc_chain, test_sticky_events_kept_on_flush);
   tcase_add_test (tc_chain, test_query_allocation_callback);
   tcase_add_test (tc_chain, test_query_allocation_signals);
   tcase_add_test (tc_chain, test_eos_reset);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/elements/appsrc.c 
new/gstreamer-1.28.9/tests/check/elements/appsrc.c
--- old/gstreamer-1.28.8/tests/check/elements/appsrc.c  2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/elements/appsrc.c  2026-11-06 
13:54:37.000000000 +0100
@@ -1557,6 +1557,145 @@
 
 GST_END_TEST;
 
+GST_START_TEST (test_appsrc_sticky_events_kept_on_flush)
+{
+  GstHarness *h;
+  GstAppSrc *src;
+  GstCaps *caps;
+  GstEvent *event;
+  gboolean got_sticky = FALSE;
+
+  h = gst_harness_new ("appsrc");
+  src = GST_APP_SRC (h->element);
+  gst_harness_play (h);
+
+  /* Without caps, the events wait while appsrc goes EOS, then flush */
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
+              gst_structure_new_empty ("sticky"))));
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_stream_group_done (1)));
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM,
+              gst_structure_new_empty ("not-sticky"))));
+  fail_unless_equals_int (gst_app_src_end_of_stream (src), GST_FLOW_OK);
+  while ((event = gst_harness_pull_event (h))) {
+    gboolean eos = GST_EVENT_TYPE (event) == GST_EVENT_EOS;
+
+    gst_event_unref (event);
+    if (eos)
+      break;
+  }
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_flush_start ()));
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_flush_stop (TRUE)));
+
+  caps = gst_caps_new_empty_simple ("foo/bar");
+  gst_app_src_set_caps (src, caps);
+  gst_caps_unref (caps);
+  fail_unless_equals_int (gst_app_src_push_buffer (src,
+          gst_buffer_new_and_alloc (1)), GST_FLOW_OK);
+  gst_buffer_unref (gst_harness_pull (h));
+
+  /* Nothing sends the sticky event again, it must be kept */
+  while ((event = gst_harness_try_pull_event (h))) {
+    if (gst_event_has_name (event, "sticky"))
+      got_sticky = TRUE;
+    fail_if (gst_event_has_name (event, "not-sticky"));
+    fail_if (GST_EVENT_TYPE (event) == GST_EVENT_STREAM_GROUP_DONE);
+    gst_event_unref (event);
+  }
+  fail_unless (got_sticky);
+
+  gst_harness_teardown (h);
+}
+
+GST_END_TEST;
+
+static GMutex buffer_blocked_mutex;
+static GCond buffer_blocked_cond;
+static gboolean buffer_blocked;
+
+static GstPadProbeReturn
+buffer_blocked_probe (GstPad * pad, GstPadProbeInfo * info, gpointer user_data)
+{
+  g_mutex_lock (&buffer_blocked_mutex);
+  buffer_blocked = TRUE;
+  g_cond_signal (&buffer_blocked_cond);
+  g_mutex_unlock (&buffer_blocked_mutex);
+
+  return GST_PAD_PROBE_OK;
+}
+
+GST_START_TEST (test_appsrc_queued_sticky_events_kept_on_flush)
+{
+  GstHarness *h;
+  GstAppSrc *src;
+  GstPad *srcpad;
+  GstCaps *caps, *current;
+  GstEvent *event;
+  gboolean flushed = FALSE, got_sticky = FALSE;
+  gulong probe;
+
+  h = gst_harness_new ("appsrc");
+  src = GST_APP_SRC (h->element);
+  srcpad = gst_element_get_static_pad (h->element, "src");
+  gst_harness_play (h);
+
+  /* Block the streaming thread on a buffer, so the caps and sticky event
+   * stay queued, then flush */
+  buffer_blocked = FALSE;
+  probe = gst_pad_add_probe (srcpad,
+      GST_PAD_PROBE_TYPE_BLOCK | GST_PAD_PROBE_TYPE_BUFFER,
+      buffer_blocked_probe, NULL, NULL);
+  caps = gst_caps_new_empty_simple ("foo/first");
+  gst_app_src_set_caps (src, caps);
+  gst_caps_unref (caps);
+  fail_unless_equals_int (gst_app_src_push_buffer (src,
+          gst_buffer_new_and_alloc (1)), GST_FLOW_OK);
+  g_mutex_lock (&buffer_blocked_mutex);
+  while (!buffer_blocked)
+    g_cond_wait (&buffer_blocked_cond, &buffer_blocked_mutex);
+  g_mutex_unlock (&buffer_blocked_mutex);
+
+  caps = gst_caps_new_empty_simple ("foo/second");
+  gst_app_src_set_caps (src, caps);
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
+              gst_structure_new_empty ("sticky"))));
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_flush_start ()));
+  gst_pad_remove_probe (srcpad, probe);
+  fail_unless (gst_element_send_event (h->element,
+          gst_event_new_flush_stop (TRUE)));
+
+  fail_unless_equals_int (gst_app_src_push_buffer (src,
+          gst_buffer_new_and_alloc (1)), GST_FLOW_OK);
+  gst_buffer_unref (gst_harness_pull (h));
+
+  /* The sticky event is kept, and goes after the new caps */
+  while ((event = gst_harness_try_pull_event (h))) {
+    if (GST_EVENT_TYPE (event) == GST_EVENT_FLUSH_STOP)
+      flushed = TRUE;
+    if (flushed && GST_EVENT_TYPE (event) == GST_EVENT_CAPS)
+      fail_if (got_sticky);
+    if (gst_event_has_name (event, "sticky"))
+      got_sticky = TRUE;
+    gst_event_unref (event);
+  }
+  fail_unless (got_sticky);
+  current = gst_pad_get_current_caps (h->sinkpad);
+  fail_unless (gst_caps_is_equal (current, caps));
+  gst_caps_unref (current);
+  gst_caps_unref (caps);
+
+  gst_object_unref (srcpad);
+  gst_harness_teardown (h);
+}
+
+GST_END_TEST;
+
 enum ExpectedObj
 {
   EXPECTED_STREAM_START,
@@ -1765,6 +1904,8 @@
   tcase_add_test (tc_chain, test_appsrc_send_event_before_buffer);
   tcase_add_test (tc_chain, test_appsrc_send_event_before_sample);
   tcase_add_test (tc_chain, test_appsrc_send_event_between_caps_buffer);
+  tcase_add_test (tc_chain, test_appsrc_sticky_events_kept_on_flush);
+  tcase_add_test (tc_chain, test_appsrc_queued_sticky_events_kept_on_flush);
 
   if (RUNNING_ON_VALGRIND)
     tcase_add_loop_test (tc_chain, test_appsrc_block_deadlock, 0, 5);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/elements/subparse.c 
new/gstreamer-1.28.9/tests/check/elements/subparse.c
--- old/gstreamer-1.28.8/tests/check/elements/subparse.c        2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/elements/subparse.c        2026-11-06 
13:54:37.000000000 +0100
@@ -21,6 +21,10 @@
 # include <config.h>
 #endif
 
+#ifdef HAVE_VALGRIND
+# include <valgrind/valgrind.h>
+#endif
+
 #include <gst/check/gstcheck.h>
 #include <gst/check/gstharness.h>
 
@@ -970,6 +974,116 @@
 
 GST_END_TEST;
 
+/* Per HTML 4.01 tag syntax (which SAMI Sync-block markup follows), a
+ * dangling '<' turns everything up to the next '>' into one tag: the
+ * following <SYNC> is swallowed as part of the garbage tag, not an
+ * error. The parse must complete and simply yield no caption. */
+GST_START_TEST (test_sami_unclosed_tags_swallow)
+{
+  GString *garbage = g_string_new ("<SAMI>\n<BODY>\n");
+  guint i;
+
+  for (i = 0; i < 200; i++)
+    g_string_append (garbage, "<unclosed tag without end\n");
+
+  g_string_append (garbage,
+      "    <SYNC Start=5000>\n"
+      "        <P Class=CC>\n"
+      "            Swallowed caption.\n" "</BODY>\n" "</SAMI>\n");
+
+  setup_subparse ();
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          buffer_from_static_string (garbage->str)), GST_FLOW_OK);
+  gst_pad_push_event (mysrcpad, gst_event_new_eos ());
+
+  fail_unless_equals_int (g_list_length (buffers), 0);
+
+  teardown_subparse ();
+  g_string_free (garbage, TRUE);
+}
+
+GST_END_TEST;
+
+/* The dangling '<' swallows at most the parser's pending-tag size cap:
+ * once the pending tag exceeds it and no '<' restarts it within the kept
+ * window, the pending tag is dropped and parsing resumes, so the caption
+ * after the garbage is still emitted. */
+GST_START_TEST (test_sami_unclosed_tags_recovery)
+{
+  GString *garbage = g_string_new ("<SAMI>\n<BODY>\n<x");
+  guint i;
+
+  for (i = 0; i < 5000; i++)
+    g_string_append_c (garbage, 'a');
+  g_string_append (garbage, "\n"
+      "    <SYNC Start=5000>\n"
+      "        <P Class=CC>\n"
+      "            Recovered caption.\n" "</BODY>\n" "</SAMI>\n");
+
+  setup_subparse ();
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          buffer_from_static_string (garbage->str)), GST_FLOW_OK);
+  gst_pad_push_event (mysrcpad, gst_event_new_eos ());
+
+  fail_unless_equals_int (g_list_length (buffers), 1);
+  fail_unless_equals_uint64 (GST_BUFFER_TIMESTAMP (buffers->data),
+      5000 * GST_MSECOND);
+  {
+    GstMapInfo map;
+
+    gst_buffer_map (buffers->data, &map, GST_MAP_READ);
+    fail_unless_equals_int (map.size, strlen ("Recovered caption."));
+    fail_unless (!memcmp (map.data, "Recovered caption.", map.size));
+    gst_buffer_unmap (buffers->data, &map);
+  }
+
+  teardown_subparse ();
+  g_string_free (garbage, TRUE);
+}
+
+GST_END_TEST;
+
+/* Unclosed '<' lines must not make the HTML context buffer grow without
+ * bound nor cause quadratic rescanning: each call must do O(1) work.
+ * The header is sent once, then pure dangling-'<' lines in chunks, so the
+ * context buffer accumulates exactly like the fuzzing reproducer.
+ * Regression test for clusterfuzz-testcase-gst-subparse-5193624668405760. */
+GST_START_TEST (test_sami_unclosed_tags_perf)
+{
+  GString *chunk = g_string_new (NULL);
+  guint i;
+  gint64 start;
+
+  for (i = 0; i < 2000; i++)
+    g_string_append (chunk, "<unclosed\n");
+
+  setup_subparse ();
+  /* header once, so nothing later flushes the pending tag */
+  fail_unless_equals_int (gst_pad_push (mysrcpad,
+          buffer_from_static_string ("<SAMI>\n<BODY>\n")), GST_FLOW_OK);
+  start = g_get_monotonic_time ();
+  /* 500k dangling lines in ~20KB buffers */
+  for (i = 0; i < 250; i++) {
+    fail_unless_equals_int (gst_pad_push (mysrcpad,
+            buffer_from_static_string (chunk->str)), GST_FLOW_OK);
+  }
+  gst_pad_push_event (mysrcpad, gst_event_new_eos ());
+  /* no sync tag was ever reached */
+  fail_unless_equals_int (g_list_length (buffers), 0);
+#ifdef HAVE_VALGRIND
+  /* valgrind slows everything down enough that the wall-clock bound below
+   * is meaningless */
+  if (!RUNNING_ON_VALGRIND)
+#endif
+    fail_unless ((g_get_monotonic_time () - start) < 5 * G_USEC_PER_SEC,
+        "500k unclosed-tag lines took >5s (quadratic rescan / unbounded 
buffer?)");
+
+  teardown_subparse ();
+  g_string_free (chunk, TRUE);
+}
+
+GST_END_TEST;
+
 GST_START_TEST (test_sami_html_entities)
 {
   SubParseInputChunk sami_input[] = {
@@ -1065,6 +1179,28 @@
 
 GST_END_TEST;
 
+GST_START_TEST (test_sami_split_tag)
+{
+  SubParseInputChunk sami_input[] = {
+    {"<SAMI>\n"
+          "<BODY>\n"
+          "    <SYNC Start=1000>\n"
+          "        <P Class=CC>\n" "            This line splits a tag<b\n",
+          1000 * GST_MSECOND, 2000 * GST_MSECOND,
+        "This line splits a tag\nin the middle."},
+    {"r> in the middle.\n"
+          "    <SYNC Start=2000>\n"
+          "        <P Class=CC>\n"
+          "            Second line.\n" "</BODY>\n" "</SAMI>\n",
+          2000 * GST_MSECOND, GST_CLOCK_TIME_NONE,
+        "Second line."}
+  };
+
+  do_test (sami_input, G_N_ELEMENTS (sami_input), "pango-markup");
+}
+
+GST_END_TEST;
+
 GST_START_TEST (test_lrc)
 {
   SubParseInputChunk lrc_input[] = {
@@ -1142,10 +1278,14 @@
   tcase_add_test (tc_chain, test_dks);
   tcase_add_test (tc_chain, test_sami);
   tcase_add_test (tc_chain, test_sami_xml_entities);
+  tcase_add_test (tc_chain, test_sami_unclosed_tags_swallow);
+  tcase_add_test (tc_chain, test_sami_unclosed_tags_recovery);
+  tcase_add_test (tc_chain, test_sami_unclosed_tags_perf);
   tcase_add_test (tc_chain, test_sami_html_entities);
   tcase_add_test (tc_chain, test_sami_bad_entities);
   tcase_add_test (tc_chain, test_sami_comment);
   tcase_add_test (tc_chain, test_sami_self_contained_tags);
+  tcase_add_test (tc_chain, test_sami_split_tag);
   tcase_add_test (tc_chain, test_lrc);
   tcase_add_test (tc_chain, test_raw_conversion);
   return s;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/elements/videoconvert.c 
new/gstreamer-1.28.9/tests/check/elements/videoconvert.c
--- old/gstreamer-1.28.8/tests/check/elements/videoconvert.c    2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/elements/videoconvert.c    2026-11-06 
13:54:37.000000000 +0100
@@ -256,6 +256,57 @@
 
 GST_END_TEST;
 
+static GstBuffer *
+create_black_gray8_2x2_buffer (void)
+{
+  GstBuffer *buffer;
+  GstMapInfo map;
+  gsize offsets[GST_VIDEO_MAX_PLANES] = { 0, 0, 0, 0 };
+  gint strides[GST_VIDEO_MAX_PLANES] = { 2, 0, 0, 0 };
+
+  buffer = gst_buffer_new_allocate (NULL, 4, NULL);
+  fail_unless (gst_buffer_map (buffer, &map, GST_MAP_WRITE));
+  memset (map.data, 0, map.size);
+  gst_buffer_unmap (buffer, &map);
+
+  gst_buffer_add_video_meta_full (buffer, GST_VIDEO_FRAME_FLAG_NONE,
+      GST_VIDEO_FORMAT_GRAY8, 2, 2, 1, offsets, strides);
+
+  return buffer;
+}
+
+/* A GstVideoMeta may describe a buffer that is larger than the negotiated
+ * caps, for example a decoder output buffer carrying its coded size, or any
+ * buffer allocated with a GstVideoAlignment. Without a GstVideoCropMeta the
+ * picture is the top-left region of that buffer, so the padding must be
+ * cropped out and not scaled in. */
+GST_START_TEST (test_videometa_larger_than_caps)
+{
+  GstHarness *h;
+  GstBuffer *padded;
+  GstBuffer *exact;
+
+  h = gst_harness_new ("videoconvertscale");
+  gst_harness_set_src_caps_str (h,
+      "video/x-raw,format=GRAY8,width=2,height=2,framerate=30/1");
+  gst_harness_set_sink_caps_str (h,
+      "video/x-raw,format=RGB,width=2,height=2,framerate=30/1");
+
+  /* The top-left 2x2 region of this 4x4 buffer is black, the padding is not */
+  padded = gst_harness_push_and_pull (h, create_gray8_4x4_buffer ());
+  fail_unless (padded != NULL);
+  exact = gst_harness_push_and_pull (h, create_black_gray8_2x2_buffer ());
+  fail_unless (exact != NULL);
+
+  fail_unless (rgb_buffer_pixels_equal (padded, exact));
+
+  gst_buffer_unref (exact);
+  gst_buffer_unref (padded);
+  gst_harness_teardown (h);
+}
+
+GST_END_TEST;
+
 /* Test that runtime converter configuration updates rebuild the converter */
 GST_START_TEST (test_converter_config_update)
 {
@@ -274,9 +325,11 @@
   full_frame = gst_harness_push_and_pull (h, create_gray8_4x4_buffer ());
   fail_unless (full_frame != NULL);
 
+  /* The default source rectangle is the top-left 2x2 region described by the
+   * caps, so configure the opposite corner to tell the two apart. */
   config = gst_structure_new ("GstVideoConverter",
-      GST_VIDEO_CONVERTER_OPT_SRC_X, G_TYPE_INT, 0,
-      GST_VIDEO_CONVERTER_OPT_SRC_Y, G_TYPE_INT, 0,
+      GST_VIDEO_CONVERTER_OPT_SRC_X, G_TYPE_INT, 2,
+      GST_VIDEO_CONVERTER_OPT_SRC_Y, G_TYPE_INT, 2,
       GST_VIDEO_CONVERTER_OPT_SRC_WIDTH, G_TYPE_INT, 2,
       GST_VIDEO_CONVERTER_OPT_SRC_HEIGHT, G_TYPE_INT, 2, NULL);
   g_object_set (h->element, "converter-config", config, NULL);
@@ -299,6 +352,129 @@
 
 GST_END_TEST;
 
+
+/* A single top field of a 2x4 alternate-interlaced picture, in a buffer whose
+ * GstVideoMeta claims @meta_height rows. Everything past the first two rows
+ * of the field is padding, filled with @pad_value. */
+static GstBuffer *
+create_alternate_gray8_field (gint meta_height, guint8 pad_value)
+{
+  GstBuffer *buffer;
+  GstMapInfo map;
+  gsize offsets[GST_VIDEO_MAX_PLANES] = { 0, 0, 0, 0 };
+  gint strides[GST_VIDEO_MAX_PLANES] = { 2, 0, 0, 0 };
+
+  buffer = gst_buffer_new_allocate (NULL, 2 * meta_height, NULL);
+  fail_unless (gst_buffer_map (buffer, &map, GST_MAP_WRITE));
+  memset (map.data, 0, map.size);
+  if (map.size > 4)
+    memset (map.data + 4, pad_value, map.size - 4);
+  gst_buffer_unmap (buffer, &map);
+
+  gst_buffer_add_video_meta_full (buffer, GST_VIDEO_FRAME_FLAG_NONE,
+      GST_VIDEO_FORMAT_GRAY8, 2, meta_height, 1, offsets, strides);
+  GST_BUFFER_FLAG_SET (buffer, GST_VIDEO_BUFFER_FLAG_TOP_FIELD);
+
+  return buffer;
+}
+
+/* Same as test_videometa_larger_than_caps, but for split-field (alternate)
+ * interlacing, where the caps height is the height of the whole frame while
+ * each buffer only holds a single field. */
+GST_START_TEST (test_videometa_larger_than_caps_alternate)
+{
+  GstHarness *h;
+  GstBuffer *padded;
+  GstBuffer *exact;
+
+  h = gst_harness_new ("videoconvertscale");
+  gst_harness_set_src_caps_str (h,
+      "video/x-raw(format:Interlaced),format=GRAY8,width=2,height=4,"
+      "interlace-mode=alternate,framerate=30/1");
+  gst_harness_set_sink_caps_str (h,
+      "video/x-raw(format:Interlaced),format=RGB,width=2,height=4,"
+      "interlace-mode=alternate,framerate=30/1");
+
+  /* The caps describe 4 rows, so 2 rows per field. The meta describes 6 rows,
+   * so 3 rows per field, making the third row of the field padding. */
+  padded = gst_harness_push_and_pull (h, create_alternate_gray8_field (6, 
255));
+  fail_unless (padded != NULL);
+  exact = gst_harness_push_and_pull (h, create_alternate_gray8_field (4, 0));
+  fail_unless (exact != NULL);
+
+  /* Each output buffer contains one 2x2 field; ignore row padding. */
+  fail_unless (rgb_buffer_pixels_equal (padded, exact));
+
+  gst_buffer_unref (exact);
+  gst_buffer_unref (padded);
+  gst_harness_teardown (h);
+}
+
+GST_END_TEST;
+
+static GstBuffer *
+create_nv12_10x8_buffer (gint meta_width, gint meta_height)
+{
+  GstVideoInfo info;
+  GstBuffer *buffer;
+  GstMapInfo map;
+  guint x, y;
+
+  fail_unless (gst_video_info_set_format (&info, GST_VIDEO_FORMAT_NV12,
+          meta_width, meta_height));
+  buffer = gst_buffer_new_allocate (NULL, info.size, NULL);
+  fail_unless (gst_buffer_map (buffer, &map, GST_MAP_WRITE));
+  memset (map.data, 255, map.size);
+  for (y = 0; y < 8; y++)
+    for (x = 0; x < 10; x++)
+      map.data[info.offset[0] + y * info.stride[0] + x] = x + y;
+  for (y = 0; y < 4; y++)
+    for (x = 0; x < 10; x++)
+      map.data[info.offset[1] + y * info.stride[1] + x] = x + y;
+  gst_buffer_unmap (buffer, &map);
+  gst_buffer_add_video_meta_full (buffer, GST_VIDEO_FRAME_FLAG_NONE,
+      GST_VIDEO_FORMAT_NV12, meta_width, meta_height, 2, info.offset,
+      info.stride);
+  return buffer;
+}
+
+/* Padded GstVideoMeta must not shift chroma during NV12 to I420 conversion. */
+GST_START_TEST (test_videometa_larger_than_caps_nv12)
+{
+  GstHarness *h;
+  GstBuffer *padded;
+  GstBuffer *exact;
+  GstVideoInfo info;
+  GstVideoFrame a, b;
+  guint plane, y;
+
+  h = gst_harness_new ("videoconvertscale");
+  gst_harness_set_src_caps_str (h,
+      "video/x-raw,format=NV12,width=10,height=8,framerate=30/1");
+  gst_harness_set_sink_caps_str (h,
+      "video/x-raw,format=I420,width=10,height=8,framerate=30/1");
+  padded = gst_harness_push_and_pull (h, create_nv12_10x8_buffer (16, 16));
+  exact = gst_harness_push_and_pull (h, create_nv12_10x8_buffer (10, 8));
+  fail_unless (padded != NULL && exact != NULL);
+  fail_unless (gst_video_info_set_format (&info, GST_VIDEO_FORMAT_I420, 10, 
8));
+  fail_unless (gst_video_frame_map (&a, &info, padded, GST_MAP_READ));
+  fail_unless (gst_video_frame_map (&b, &info, exact, GST_MAP_READ));
+  for (plane = 0; plane < 3; plane++)
+    for (y = 0; y < (plane ? 4 : 8); y++)
+      fail_unless (memcmp ((guint8 *) GST_VIDEO_FRAME_PLANE_DATA (&a, plane) +
+              y * GST_VIDEO_FRAME_PLANE_STRIDE (&a, plane),
+              (guint8 *) GST_VIDEO_FRAME_PLANE_DATA (&b, plane) +
+              y * GST_VIDEO_FRAME_PLANE_STRIDE (&b, plane),
+              plane ? 5 : 10) == 0);
+  gst_video_frame_unmap (&b);
+  gst_video_frame_unmap (&a);
+  gst_buffer_unref (exact);
+  gst_buffer_unref (padded);
+  gst_harness_teardown (h);
+}
+
+GST_END_TEST;
+
 static Suite *
 videoconvert_suite (void)
 {
@@ -310,6 +486,9 @@
   tcase_add_test (tc_chain, test_template_formats);
   tcase_add_test (tc_chain, test_negotiate_alternate);
   tcase_add_test (tc_chain, test_videometa_layout_changes);
+  tcase_add_test (tc_chain, test_videometa_larger_than_caps);
+  tcase_add_test (tc_chain, test_videometa_larger_than_caps_alternate);
+  tcase_add_test (tc_chain, test_videometa_larger_than_caps_nv12);
   tcase_add_test (tc_chain, test_converter_config_update);
 
   return s;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/libs/discoverer.c 
new/gstreamer-1.28.9/tests/check/libs/discoverer.c
--- old/gstreamer-1.28.8/tests/check/libs/discoverer.c  2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/libs/discoverer.c  2026-11-06 
13:54:37.000000000 +0100
@@ -29,7 +29,7 @@
 #include <glib/gstdio.h>
 #include <glib/gprintf.h>
 
-static gboolean have_theora, have_ogg;
+static gboolean have_theora, have_ogg, have_mp3_parser;
 
 GST_START_TEST (test_disco_init)
 {
@@ -106,6 +106,75 @@
 
 GST_END_TEST;
 
+/* Minimal MP3 stream (tests/files/mp3-topology-uaf.bin, from OSS-Fuzz
+ * testcase 4764783547842560) whose stream topology contains a nested "next"
+ * node whose caps match its own parent. Parsing that node used to unref the
+ * parent stream info while a caller still held it, crashing when the caller
+ * wrote through the freed pointer. Push-based discovery (appsrc) as in the
+ * fuzzing harness. */
+static void
+push_data_on_source_setup (GstDiscoverer * dc, GstElement * source,
+    gpointer data)
+{
+  GBytes *bytes = data;
+  gsize size;
+  const guint8 *map;
+  GstBuffer *buf;
+  GstFlowReturn ret;
+
+  map = g_bytes_get_data (bytes, &size);
+  buf = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY,
+      (gpointer) map, size, 0, size, NULL, NULL);
+  g_object_set (G_OBJECT (source), "size", (gint64) size, NULL);
+  g_signal_emit_by_name (G_OBJECT (source), "push-buffer", buf, &ret);
+  gst_buffer_unref (buf);
+}
+
+GST_START_TEST (test_disco_push_nested_same_caps)
+{
+  GError *err = NULL;
+  GstDiscoverer *dc;
+  GstDiscovererInfo *info;
+  gchar *path;
+  gpointer contents;
+  gsize size;
+  GBytes *bytes;
+
+  if (!have_mp3_parser) {
+    GST_INFO ("No mp3 parser, skipping");
+    return;
+  }
+
+  path = g_build_filename (GST_TEST_FILES_PATH, "mp3-topology-uaf.bin", NULL);
+
+  if (!g_file_get_contents (path, (gchar **) & contents, &size, &err)) {
+    g_free (path);
+    fail ("Could not read test file: %s", err->message);
+  }
+  g_free (path);
+  bytes = g_bytes_new_take (contents, size);
+
+  /* high timeout, in case we're running under valgrind */
+  dc = gst_discoverer_new (30 * GST_SECOND, &err);
+  fail_unless (dc != NULL);
+  fail_unless (err == NULL);
+
+  g_signal_connect (dc, "source-setup",
+      (GCallback) push_data_on_source_setup, bytes);
+
+  /* the point of the test is that discovery completes without crashing;
+   * the result itself depends on which plugins are available */
+  info = gst_discoverer_discover_uri (dc, "appsrc://", &err);
+  fail_unless (info != NULL);
+  g_clear_error (&err);
+
+  gst_discoverer_info_unref (info);
+  g_object_unref (dc);
+  g_bytes_unref (bytes);
+}
+
+GST_END_TEST;
+
 GST_START_TEST (test_disco_sync)
 {
   GError *err = NULL;
@@ -394,6 +463,10 @@
       "theoradec", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0);
   have_ogg = gst_registry_check_feature_version (gst_registry_get (),
       "oggdemux", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0);
+  /* the nested-topology reproducer only exercises the topology parser, so
+   * the mp3 parser alone is enough to trigger it */
+  have_mp3_parser = gst_registry_check_feature_version (gst_registry_get (),
+      "mpegaudioparse", GST_VERSION_MAJOR, GST_VERSION_MINOR, 0);
 
   suite_add_tcase (s, tc_chain);
   tcase_add_test (tc_chain, test_disco_init);
@@ -403,6 +476,7 @@
   tcase_add_test (tc_chain, test_disco_sync_reuse_timeout);
   tcase_add_test (tc_chain, test_disco_missing_plugins);
   tcase_add_test (tc_chain, test_disco_serializing);
+  tcase_add_test (tc_chain, test_disco_push_nested_same_caps);
   tcase_add_test (tc_chain, test_disco_async);
   tcase_add_test (tc_chain, test_disco_async_custom_context);
   tcase_add_test (tc_chain, test_disco_async_reuse);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gstreamer-1.28.8/tests/check/libs/videoanc.c 
new/gstreamer-1.28.9/tests/check/libs/videoanc.c
--- old/gstreamer-1.28.8/tests/check/libs/videoanc.c    2026-10-08 
17:20:32.000000000 +0200
+++ new/gstreamer-1.28.9/tests/check/libs/videoanc.c    2026-11-06 
13:54:37.000000000 +0100
@@ -339,6 +339,57 @@
 
 GST_END_TEST;
 
+/* A line narrower than one conversion group underflows the loop bounds in the
+ * v210/UYVY line converters, corrupting the heap. The parser/encoder must
+ * reject such widths instead. */
+GST_START_TEST (reject_small_pixel_width)
+{
+  /* v210 packs 6 pixels per 16-byte word, UYVY converts 4 pixels at a time */
+  const guint v210_min_width = 6;
+  const guint uyvy_min_width = 4;
+  GstVideoVBIParser *parser;
+  GstVideoVBIEncoder *encoder;
+  guint8 line[64] = { 0, };
+  GstVideoAncillary vanc;
+  guint w;
+
+  /* Below the minimum: rejected, no object leaks back to the caller. */
+  for (w = 1; w < v210_min_width; w++) {
+    fail_if (gst_video_vbi_parser_new (GST_VIDEO_FORMAT_v210, w) != NULL);
+    fail_if (gst_video_vbi_encoder_new (GST_VIDEO_FORMAT_v210, w) != NULL);
+  }
+  for (w = 1; w < uyvy_min_width; w++) {
+    fail_if (gst_video_vbi_parser_new (GST_VIDEO_FORMAT_UYVY, w) != NULL);
+    fail_if (gst_video_vbi_encoder_new (GST_VIDEO_FORMAT_UYVY, w) != NULL);
+  }
+
+  /* At the minimum: accepted, and a full line converts without overrunning the
+   * workspace (empty line yields no ancillary data). */
+  parser = gst_video_vbi_parser_new (GST_VIDEO_FORMAT_v210, v210_min_width);
+  fail_unless (parser != NULL);
+  gst_video_vbi_parser_add_line (parser, line);
+  fail_unless_equals_int (gst_video_vbi_parser_get_ancillary (parser, &vanc),
+      GST_VIDEO_VBI_PARSER_RESULT_DONE);
+  gst_video_vbi_parser_free (parser);
+
+  parser = gst_video_vbi_parser_new (GST_VIDEO_FORMAT_UYVY, uyvy_min_width);
+  fail_unless (parser != NULL);
+  gst_video_vbi_parser_add_line (parser, line);
+  fail_unless_equals_int (gst_video_vbi_parser_get_ancillary (parser, &vanc),
+      GST_VIDEO_VBI_PARSER_RESULT_DONE);
+  gst_video_vbi_parser_free (parser);
+
+  encoder = gst_video_vbi_encoder_new (GST_VIDEO_FORMAT_v210, v210_min_width);
+  fail_unless (encoder != NULL);
+  gst_video_vbi_encoder_free (encoder);
+
+  encoder = gst_video_vbi_encoder_new (GST_VIDEO_FORMAT_UYVY, uyvy_min_width);
+  fail_unless (encoder != NULL);
+  gst_video_vbi_encoder_free (encoder);
+}
+
+GST_END_TEST;
+
 static Suite *
 gst_videoanc_suite (void)
 {
@@ -353,6 +404,8 @@
   tcase_add_test (tc, encode_8bit);
   tcase_add_test (tc, encode_10bit);
 
+  tcase_add_test (tc, reject_small_pixel_width);
+
   return s;
 }
 
Binary files old/gstreamer-1.28.8/tests/files/mp3-topology-uaf.bin and 
new/gstreamer-1.28.9/tests/files/mp3-topology-uaf.bin differ

++++++ gstreamer.obsinfo ++++++
--- /var/tmp/diff_new_pack.gLVYP4/_old  2026-11-09 16:03:18.686461828 +0100
+++ /var/tmp/diff_new_pack.gLVYP4/_new  2026-11-09 16:03:18.690461995 +0100
@@ -1,5 +1,5 @@
 name: gstreamer
-version: 1.28.8
-mtime: 1791472832
-commit: 1b2a95a1b72787cda146b46df9605b168e5b683b
+version: 1.28.9
+mtime: 1793969677
+commit: 3ced45da096032bdac8e0922faa492a079c3180e
 

Reply via email to