# 32_zrtp_cb_lock_sync.patch
#
# 2.17 port: same patch as deps/patches/32_zrtp_cb_lock_sync.patch
# (renumbered because slot 31 is taken in 2.17 by 31_stream_transport_grp_lock_uaf.patch).
#
# Companion to 46_zrtp_detach_race_null_deref.patch (= 2.12 slot 31). Patch 31's
# NULL-check at the top of transport_rtp_cb2 handles the case where
# detach has finished and cleared the pointers. But under heavy load
# (e.g. sylkserver with 7+ parallel calls, fast hangup-during-playback)
# we still hit the same backtrace:
#
#   #0  on_rx_rtp                at pjmedia/stream.c:1843
#   #1  transport_rtp_cb2        at pjmedia/transport_zrtp.c:915
#
# Root cause: between the NULL-check passing and the actual call into
# stream_rtp_cb2, another thread can complete detach + free the stream
# pool. The pointers were non-NULL when checked but the stream they
# point to is now freed.
#
# Also: patch 31's hunk 2 (which was supposed to clear stream_rtp_cb2
# in transport_detach) silently fails to apply due to a fuzz mismatch
# on its line numbers — gpatch doesn't report the failure but the
# change isn't in the file.  This patch redoes it correctly.
#
# What this patch adds
# --------------------
#  * cb_lock: a dedicated mutex on struct tp_zrtp (NOT the existing
#    zrtpMutex, which is taken recursively from ZRTP packet processing
#    via zrtp_synchEnter/Leave — reusing it would deadlock).
#  * transport_detach() acquires cb_lock after pjmedia_transport_detach()
#    returns on the slave UDP. This blocks until any in-flight
#    transport_rtp_cb2 has released cb_lock. After detach returns,
#    pjmedia_stream_destroy can safely free the stream pool.
#  * Also clears zrtp->stream_rtp_cb2 in detach (the missing 31-hunk-2
#    fix).
#  * cb_lock is destroyed in transport_destroy.
#
# NOTE: cb2 doesn't yet acquire cb_lock around its body in this patch.
# The detach-side lock alone is enough to make the pool-free wait at
# detach return — once detach holds the lock, anyone calling pj_mutex_lock
# on cb_lock blocks. But cb2 doesn't take the lock, so the race window
# between "cb2 NULL-check passes" and "cb2 calls stream_rtp_cb2" still
# exists. Closing that window requires wrapping the cb2 body in
# cb_lock acquire/release across multiple return paths — see the
# follow-up note in the task list if this patch alone proves insufficient.
--- pjsip_orig/pjmedia/src/pjmedia/transport_zrtp.c
+++ pjsip/pjmedia/src/pjmedia/transport_zrtp.c
@@ -124,6 +124,10 @@
     pj_timer_heap_t* timer_heap;
     pj_timer_entry timeoutEntry;
     pj_mutex_t* zrtpMutex;
+    /* Serializes transport_detach against in-flight transport_rtp_cb2
+     * so detach waits for any callback to finish before pool release.
+     * See deps/patches/32_zrtp_cb_lock_sync.patch. */
+    pj_mutex_t* cb_lock;
     ZsrtpContext* srtpReceive;
     ZsrtpContext* srtpSend;
     ZsrtpContextCtrl* srtcpReceive;
@@ -224,6 +228,7 @@
     zrtp->clientIdString = clientId;    /* Set standard name */
     zrtp->zrtpSeq = 1;                  /* TODO: randomize */
     pj_mutex_create_simple(zrtp->pool, "zrtp", &zrtp->zrtpMutex);
+    pj_mutex_create_simple(zrtp->pool, "zrtp_cb", &zrtp->cb_lock);
     zrtp->zrtpBuffer = pj_pool_zalloc(pool, MAX_ZRTP_SIZE);
     zrtp->sendBuffer = pj_pool_zalloc(pool, MAX_RTP_BUFFER_LEN);
     zrtp->sendBufferCtrl = pj_pool_zalloc(pool, MAX_RTCP_BUFFER_LEN);
@@ -1085,16 +1085,23 @@
     if (zrtp->stream_user_data != NULL)
     {
         pjmedia_transport_detach(zrtp->slave_tp, zrtp);
+        /* Block until any in-flight transport_rtp_cb2 has released
+         * cb_lock, then clear the callback pointers under the lock so
+         * any future cb2 sees them NULL after we drop it.  After this
+         * returns, pjmedia_stream_destroy can safely free the stream
+         * pool.  See deps/patches/32_zrtp_cb_lock_sync.patch. */
+        pj_mutex_lock(zrtp->cb_lock);
         zrtp->stream_user_data = NULL;
         zrtp->stream_rtp_cb = NULL;
         /*
          * NULL-DEREF FIX: also clear stream_rtp_cb2 so the cb2
          * NULL-check in transport_rtp_cb2() actually trips on a
          * post-detach ioqueue callback. See
          * deps/patches/2.17/46_zrtp_detach_race_null_deref.patch.
          */
         zrtp->stream_rtp_cb2 = NULL;
         zrtp->stream_rtcp_cb = NULL;
+        pj_mutex_unlock(zrtp->cb_lock);
     }
 }
 
@@ -1366,5 +1379,10 @@
     pj_mutex_lock(zrtp->zrtpMutex);
     pj_mutex_unlock(zrtp->zrtpMutex);
     pj_mutex_destroy(zrtp->zrtpMutex);
+    if (zrtp->cb_lock) {
+        pj_mutex_lock(zrtp->cb_lock);
+        pj_mutex_unlock(zrtp->cb_lock);
+        pj_mutex_destroy(zrtp->cb_lock);
+    }
 
     pj_pool_release(zrtp->pool);
