CVE-2023-38703 (C, published 2023-10-06T14:15Z)
Description: value: PJSIP is a free and open source multimedia communication library written in C with high level API in C, C++, Java, C#, and Python languages. SRTP is a higher level media transport which is stacked upon a lower level media transport such as UDP and ICE. Currently a higher level transport is not synchronized with its lower level transport that may introduce use-after-free issue. This vulnerability affects applications that have SRTP capability (`PJMEDIA_HAS_SRTP` is set) and use underlying media transport other than UDP. This vulnerability’s impact may range from unexpected application termination to control flow hijack/memory corruption. The patch is available as a commit in the master branch.

Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-416 \u2014 Use After Free
The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
Repository: https://github.com/pjsip/pjproject
Files changed:
pjmedia/include/pjmedia/transport.h
pjmedia/src/pjmedia/transport_adapter_sample.c
pjmedia/src/pjmedia/transport_ice.c
pjmedia/src/pjmedia/transport_loop.c
pjmedia/src/pjmedia/transport_srtp.c
pjmedia/src/pjmedia/transport_srtp_dtls.c
pjmedia/src/pjmedia/transport_udp.c
Commit message: Merge pull request from GHSA-f76w-fh7c-pc66

* Add group lock to media transport

* Also add group lock to SRTP-DTLS

* Put lock protection to avoid race condition between destroy() & dtls_on_recv()

Vulnerable code:
    /* Self destruct.. */
    pj_pool_release(adapter->pool);
    struct transport_loop *loop = (struct transport_loop*) tp;

    pj_pool_release(loop->pool);
    /* In case mutex is being acquired by other thread */
    pj_lock_acquire(srtp->mutex);
    pj_lock_release(srtp->mutex);
    pj_lock_destroy(srtp->mutex);
    pj_pool_release(srtp->pool);
	pj_status_t status;
    status = pj_lock_create_simple_mutex(ds->pool, "dtls_ssl_lock%p",
                                         &ds->ossl_lock);
    if (status != PJ_SUCCESS)
        return status;
    pj_lock_acquire(ds->ossl_lock);
    pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
    pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
    pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
            pj_lock_release(ds->ossl_lock);
            pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
    pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
    pj_lock_acquire(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
        pj_lock_release(ds->ossl_lock);
    pj_lock_release(ds->ossl_lock);
    if (size < 1 || !IS_DTLS_PKT(pkt, size))
            if (status != PJ_SUCCESS)
        if (status != PJ_SUCCESS)
    if (ds->ossl_lock) {
        pj_lock_destroy(ds->ossl_lock);
        ds->ossl_lock = NULL;
    }
    pj_pool_safe_release(&ds->pool);
    status = pj_ioqueue_register_sock(pool, ioqueue, tp->rtp_sock, tp,
                                      &rtp_cb, &tp->rtp_key);
    status = pj_ioqueue_register_sock(pool, ioqueue, tp->rtcp_sock, tp,
                                      &rtcp_cb, &tp->rtcp_key);
    
        /* This will block the execution if callback is still
         * being called.
         */

Fixed code:

    /** Group lock, for synchronization between destroy() & callbacks. */
    pj_grp_lock_t           *grp_lock;
static void adapter_on_destroy(void *arg);


    /* Setup group lock handler for destroy and callback synchronization */
    if (transport && transport->grp_lock) {
        pj_grp_lock_t *grp_lock = transport->grp_lock;

        adapter->base.grp_lock = grp_lock;
        pj_grp_lock_add_ref(grp_lock);
        pj_grp_lock_add_handler(grp_lock, pool, adapter, &adapter_on_destroy);
    }


static void adapter_on_destroy(void *arg)
{
    struct tp_adapter *adapter = (struct tp_adapter*)arg;

    pj_pool_release(adapter->pool);
}

    if (adapter->base.grp_lock) {
        pj_grp_lock_dec_ref(adapter->base.grp_lock);
    } else {
        adapter_on_destroy(tp);
    }
        tp_ice->base.grp_lock = grp_lock;

    PJ_LOG(4, (tp_ice->base.name, "ICE transport destroyed"));
    PJ_LOG(4, (tp_ice->base.name, "Destroying ICE transport"));

static void tp_loop_on_destroy(void *arg);
    pj_grp_lock_t *grp_lock;
    pj_status_t status;
    /* Create group lock */
    status = pj_grp_lock_create(pool, NULL, &grp_lock);
    if (status != PJ_SUCCESS)
        return status;

    pj_grp_lock_add_ref(grp_lock);
    pj_grp_lock_add_handler(grp_lock, pool, tp, &tp_loop_on_destroy);


static void tp_loop_on_destroy(void *arg)
{
    struct transport_loop *loop = (struct transport_loop*) arg;

    PJ_LOG(4, (loop->base.name, "Loop transport destroyed"));
    pj_pool_release(loop->pool);
}


    pj_grp_lock_dec_ref(tp->grp_lock);
    pj_grp_lock_add_ref(tp->grp_lock);

    pj_grp_lock_dec_ref(tp->grp_lock);

    pj_grp_lock_add_ref(tp->grp_lock);

    pj_grp_lock_dec_ref(tp->grp_lock);

/* SRTP destroy handler */
static void srtp_on_destroy(void *arg);
    /* Setup group lock handler for destroy and callback synchronization */
    if (tp && tp->grp_lock) {
        srtp->base.grp_lock = tp->grp_lock;
        pj_grp_lock_add_ref(tp->grp_lock);
        pj_grp_lock_add_handler(tp->grp_lock, pool, srtp, &srtp_on_destroy);
    }

    PJ_LOG(4, (srtp->pool->obj_name, "SRTP transport created"));


/* SRTP real destroy */
static void srtp_on_destroy(void *arg)
{
    transport_srtp *srtp = (transport_srtp*)arg;

    PJ_LOG(4, (srtp->pool->obj_name, "SRTP transport destroyed"));

    pj_lock_destroy(srtp->mutex);
    pj_pool_safe_release(&srtp->pool);
}


    PJ_LOG(4, (srtp->pool->obj_name, "Destroying SRTP transport"));

    if (srtp->base.grp_lock) {
        pj_grp_lock_dec_ref(srtp->base.grp_lock);
    } else {
        /* Only get here when the underlying transport does not have
         * a group lock, race condition with callbacks may occur.
         * Currently UDP, ICE, and loop have a group lock already.
         */
        PJ_LOG(4,(srtp->pool->obj_name,
                  "Warning: underlying transport does not have group lock"));
        /* In case mutex is being acquired by other thread.
         * An effort to synchronize destroy() & callbacks when the underlying
         * transport does not provide a group lock.
         */
        pj_lock_acquire(srtp->mutex);
        pj_lock_release(srtp->mutex);

        srtp_on_destroy(srtp);
    }
static void dtls_on_destroy(void *arg);

    pj_bool_t            is_destroying;     /* DTLS being destroyed?        */
    pj_status_t status;
    /* Setup group lock handler for destroy and callback synchronization */
    if (srtp->base.grp_lock) {
        pj_grp_lock_t *grp_lock = srtp->base.grp_lock;

        ds->base.grp_lock = grp_lock;
        pj_grp_lock_add_ref(grp_lock);
        pj_grp_lock_add_handler(grp_lock, pool, ds, &dtls_on_destroy);
    } else {
        status = pj_lock_create_simple_mutex(ds->pool, "dtls_ssl_lock%p",
                                             &ds->ossl_lock);
        if (status != PJ_SUCCESS)
            return status;
    }
/* Lock/unlock for DTLS states access protection */

static void DTLS_LOCK(dtls_srtp *ds) {
    if (ds->base.grp_lock)
        pj_grp_lock_acquire(ds->base.grp_lock);
    else
        pj_lock_acquire(ds->ossl_lock);
}


static void DTLS_UNLOCK(dtls_srtp *ds) {
    if (ds->base.grp_lock)
        pj_grp_lock_release(ds->base.grp_lock);
    else
        pj_lock_release(ds->ossl_lock);
}


    DTLS_LOCK(ds);
    DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
    DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
    DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
            DTLS_UNLOCK(ds);
            DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
    DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
        DTLS_UNLOCK(ds);
    DTLS_UNLOCK(ds);
    DTLS_LOCK(ds);

    if (size < 1 || !IS_DTLS_PKT(pkt, size) || ds->is_destroying) {
        DTLS_UNLOCK(ds);
    }
            if (status != PJ_SUCCESS) {
                DTLS_UNLOCK(ds);
            }
        if (status != PJ_SUCCESS) {
            DTLS_UNLOCK(ds);
        }

    DTLS_UNLOCK(ds);

static void dtls_on_destroy(void *arg) {
    dtls_srtp *ds = (dtls_srtp *)arg;

    if (ds->ossl_lock)
        pj_lock_destroy(ds->ossl_lock);

    pj_pool_safe_release(&ds->pool);
}

    ds->is_destroying = PJ_TRUE;

    DTLS_LOCK(ds);

    DTLS_UNLOCK(ds);
    if (ds->base.grp_lock) {
        pj_grp_lock_dec_ref(ds->base.grp_lock);
    } else {
        dtls_on_destroy(tp);
    }
    pj_grp_lock_t *grp_lock;
    /* Create group lock */
    status = pj_grp_lock_create(pool, NULL, &grp_lock);
    if (status != PJ_SUCCESS)
        goto on_error;

    pj_grp_lock_add_ref(grp_lock);
    tp->base.grp_lock = grp_lock;

    status = pj_ioqueue_register_sock2(pool, ioqueue, tp->rtp_sock, grp_lock,
                                       tp, &rtp_cb, &tp->rtp_key);
     *
     * Note that we still need this even after group lock is added to
     * maintain the above behavior.
    status = pj_ioqueue_register_sock2(pool, ioqueue, tp->rtcp_sock, grp_lock,
                                       tp, &rtcp_cb, &tp->rtcp_key);

    /* The following calls to pj_ioqueue_unregister() will block the execution
     * if callback is still being called because allow_concurrent is false.
     * So it is safe to release the pool immediately after.
     */
    pj_grp_lock_dec_ref(tp->grp_lock);