Skip to content

[0.x, 1.x] Race condition in janus_plugin_close_pc_internal() causes SIGSEGV (use-after-free) #3617

@moon8011

Description

@moon8011

Description

A race condition in janus_plugin_close_pc() / janus_plugin_close_pc_internal() can cause a crash (SIGSEGV) due to use-after-free of ice_handle.

Environment

  • Janus version: 0.14.1
  • OS: Linux (RHEL/CentOS)
  • Plugin: VideoRoom
    Note: This issue exists in both 0.x and 1.x branches as the affected code is identical.

Crash Information

Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000452b1a in janus_plugin_close_pc_internal (user_data=0x7f92e8049f00) at janus.c:4114
plugin_session = 0x7f92e8049f00
ice_handle = 0x7f95113f9ff9 <-- invalid/freed pointer (not 8-byte aligned)

Root Cause

In janus_plugin_close_pc(), only plugin_session->ref is increased before registering the timeout callback, but ice_handle->ref is NOT increased:

void janus_plugin_close_pc(janus_plugin_session *plugin_session) {
    // ...
    janus_refcount_increase(&plugin_session->ref);
    // ice_handle->ref is NOT increased here!
    GSource *timeout_source = g_timeout_source_new_seconds(0);
    g_source_set_callback(timeout_source, janus_plugin_close_pc_internal, plugin_session, NULL);
    g_source_attach(timeout_source, sessions_watchdog_context);
    // ...
}

This allows ice_handle to be freed by another thread (e.g., handling a "detach" request) before janus_plugin_close_pc_internal() is executed.

Race Condition Scenario

VideoRoom Handler Thread          Requests Thread              Watchdog Thread
        |                              |                            |
   close_pc()                          |                            |
        |                              |                            |
   timeout registered                  |                            |
        |                              |                            |
        |                     "detach" request received             |
        |                              |                            |
        |                     janus_ice_handle_destroy()            |
        |                              |                            |
        |                     ice_handle freed                      |
        |                              |                            |
        |                              |                   close_pc_internal()
        |                              |                            |
        |                              |                   ice_handle access
        |                              |                            |
        |                              |                   SIGSEGV 💥

Proposed Fix

Add janus_refcount_increase(&ice_handle->ref) in janus_plugin_close_pc() before registering the timeout:

void janus_plugin_close_pc(janus_plugin_session *plugin_session) {
    if(!janus_plugin_session_is_alive(plugin_session))
        return;
    janus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;
    if(!ice_handle || !g_atomic_int_compare_and_exchange(&ice_handle->closepc, 0, 1))
        return;
    janus_refcount_increase(&plugin_session->ref);
    janus_refcount_increase(&ice_handle->ref);  // <-- ADD THIS
    GSource *timeout_source = g_timeout_source_new_seconds(0);
    g_source_set_callback(timeout_source, janus_plugin_close_pc_internal, plugin_session, NULL);
    g_source_attach(timeout_source, sessions_watchdog_context);
    g_source_unref(timeout_source);
}

And update janus_plugin_close_pc_internal() to handle the early return case:

static gboolean janus_plugin_close_pc_internal(gpointer user_data) {
    janus_plugin_session *plugin_session = (janus_plugin_session *) user_data;
    janus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;
    if(!ice_handle || !g_atomic_int_compare_and_exchange(&ice_handle->closepc, 1, 0)) {
        janus_refcount_decrease(&plugin_session->ref);
        if(ice_handle)
            janus_refcount_decrease(&ice_handle->ref);  // <-- ADD THIS
        return G_SOURCE_REMOVE;
    }
    // Remove the existing increase since we already did it in close_pc()
    // janus_refcount_increase(&ice_handle->ref);  // <-- REMOVE THIS (line 4118)

    // ... rest of the function remains the same ...
}

Additional Notes

- The server was running with ~1000 threads at the time of crash (high load)
- The invalid pointer 0x7f95113f9ff9 is not 8-byte aligned, indicating memory corruption/reuse after free

Metadata

Metadata

Assignees

No one assigned

    Labels

    legacyRelated to Janus 0.x

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions