Vulnerability: PJSIP is a free and open source multimedia communication library written in C language implementing standard based protocols such as SIP, SDP, RTP, STUN, TURN, and ICE. In versions up to and including 2.11.1 when in a dialog set (or forking) scenario, a hash key shared by multiple UAC dialogs can potentially be prematurely freed when one of the dialogs is destroyed . The issue may cause a dialog set to be registered in the hash table multiple times (with different hash keys) leading to undefined behavior such as dialog list collision which eventually leading to endless loop. A patch is available in commit db3235953baa56d2fb0e276ca510fefca751643f which will be included in the next release. There are no known workarounds for this issue.
Vulnerability ID: CVE-2022-23608
CWE: CWE-416
Severity score: 6.4
Project: https://github.com/pjsip/pjproject
Published: 2022-02-22
File: pjsip/src/pjsip/sip_ua_layer.c
Commit message: Merge pull request from GHSA-ffff-m5fm-qm62

* Update pjsip_ua_unregister_dlg():
- update the hash key if the dialog being unregistered is used as hash key.
- add an assertion check to make sure that the dlg_set to be removed is valid (can be found in the hash table).

* Change hash key string comparison method.
Commit author: Nanang Izzuddin

Vulnerable version (before fix):
 struct dlg_set     /* This is the buffer to store this entry in the hash table. */     pj_hash_entry_buf ht_entry;      /* List of dialog in this dialog set. */     struct dlg_set_head  dlg_list; }; PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua, 	     * Create the dialog set and add this dialog to it. 	     */ 	    dlg_set = alloc_dlgset_node(); 	    pj_list_init(&dlg_set->dlg_list); 	    pj_list_push_back(&dlg_set->dlg_list, dlg);  	    dlg->dlg_set = dlg_set;  	    /* Register the dialog set in the hash table. */ 	    pj_hash_set_np_lower(mod_ua.dlg_table,  			         dlg->local.info->tag.ptr,                                  (unsigned)dlg->local.info->tag.slen, 			         dlg->local.tag_hval, dlg_set->ht_entry,                                  dlg_set); 	} PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua, 	struct dlg_set *dlg_set;  	dlg_set = alloc_dlgset_node(); 	pj_list_init(&dlg_set->dlg_list); 	pj_list_push_back(&dlg_set->dlg_list, dlg);  	dlg->dlg_set = dlg_set;  	pj_hash_set_np_lower(mod_ua.dlg_table,  		             dlg->local.info->tag.ptr,                              (unsigned)dlg->local.info->tag.slen, 		             dlg->local.tag_hval, dlg_set->ht_entry, dlg_set);     }  PJ_DEF(pj_status_t) pjsip_ua_unregister_dlg( pjsip_user_agent *ua,      /* If dialog list is empty, remove the dialog set from the hash table. */     if (pj_list_empty(&dlg_set->dlg_list)) { 	pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg->local.info->tag.ptr, 		          (unsigned)dlg->local.info->tag.slen,  			  dlg->local.tag_hval, NULL);  	/* Return dlg_set to free nodes. */ 	pj_list_push_back(&mod_ua.free_dlgset_nodes, dlg_set);     }      /* Unlock user agent. */

Fixed version (after fix):
 struct dlg_set     /* This is the buffer to store this entry in the hash table. */     pj_hash_entry_buf ht_entry;      /* Entry key in the hash table */     pj_str_t ht_key;      /* List of dialog in this dialog set. */     struct dlg_set_head  dlg_list; }; PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua, 	     * Create the dialog set and add this dialog to it. 	     */ 	    dlg_set = alloc_dlgset_node(); 	    dlg_set->ht_key = dlg->local.info->tag; 	    pj_list_init(&dlg_set->dlg_list); 	    pj_list_push_back(&dlg_set->dlg_list, dlg);  	    dlg->dlg_set = dlg_set;  	    /* Register the dialog set in the hash table. */ 	    pj_hash_set_np_lower(mod_ua.dlg_table,  			         dlg_set->ht_key.ptr,                                  (unsigned)dlg_set->ht_key.slen, 			         dlg->local.tag_hval, dlg_set->ht_entry,                                  dlg_set); 	} PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua, 	struct dlg_set *dlg_set;  	dlg_set = alloc_dlgset_node(); 	dlg_set->ht_key = dlg->local.info->tag; 	pj_list_init(&dlg_set->dlg_list); 	pj_list_push_back(&dlg_set->dlg_list, dlg);  	dlg->dlg_set = dlg_set;  	pj_hash_set_np_lower(mod_ua.dlg_table,  		             dlg_set->ht_key.ptr,                              (unsigned)dlg_set->ht_key.slen, 		             dlg->local.tag_hval, dlg_set->ht_entry, dlg_set);     }  PJ_DEF(pj_status_t) pjsip_ua_unregister_dlg( pjsip_user_agent *ua,      /* If dialog list is empty, remove the dialog set from the hash table. */     if (pj_list_empty(&dlg_set->dlg_list)) {  	/* Verify that the dialog set is valid */ 	pj_assert(pj_hash_get_lower(mod_ua.dlg_table, dlg_set->ht_key.ptr, 				    (unsigned)dlg_set->ht_key.slen, 				    &dlg->local.tag_hval) == dlg_set);  	pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg_set->ht_key.ptr, 		          (unsigned)dlg_set->ht_key.slen, 			  dlg->local.tag_hval, NULL);  	/* Return dlg_set to free nodes. */ 	pj_list_push_back(&mod_ua.free_dlgset_nodes, dlg_set);     } else { 	/* If the just unregistered dialog is being used as hash key, 	 * reset the dlg_set entry with a new key (i.e: from the first dialog 	 * in dlg_set). 	 */ 	if (dlg_set->ht_key.ptr  == dlg->local.info->tag.ptr && 	    dlg_set->ht_key.slen == dlg->local.info->tag.slen) 	{ 	    pjsip_dialog* key_dlg = dlg_set->dlg_list.next;  	    /* Verify that the old & new keys share the hash value */ 	    pj_assert(key_dlg->local.tag_hval == dlg->local.tag_hval);  	    pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg_set->ht_key.ptr, 			      (unsigned)dlg_set->ht_key.slen, 			      dlg->local.tag_hval, NULL);  	    dlg_set->ht_key = key_dlg->local.info->tag;  	    pj_hash_set_np_lower(mod_ua.dlg_table, 				 dlg_set->ht_key.ptr, 				 (unsigned)dlg_set->ht_key.slen, 				 key_dlg->local.tag_hval, dlg_set->ht_entry, 				 dlg_set); 	}     }      /* Unlock user agent. */

Patch:
patch: @@ -65,6 +65,9 @@ struct dlg_set
     /* This is the buffer to store this entry in the hash table. */
     pj_hash_entry_buf ht_entry;
 
+    /* Entry key in the hash table */
+    pj_str_t ht_key;
+
     /* List of dialog in this dialog set. */
     struct dlg_set_head  dlg_list;
 };
@@ -327,15 +330,16 @@ PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua,
 	     * Create the dialog set and add this dialog to it.
 	     */
 	    dlg_set = alloc_dlgset_node();
+	    dlg_set->ht_key = dlg->local.info->tag;
 	    pj_list_init(&dlg_set->dlg_list);
 	    pj_list_push_back(&dlg_set->dlg_list, dlg);
 
 	    dlg->dlg_set = dlg_set;
 
 	    /* Register the dialog set in the hash table. */
 	    pj_hash_set_np_lower(mod_ua.dlg_table, 
-			         dlg->local.info->tag.ptr,
-                                 (unsigned)dlg->local.info->tag.slen,
+			         dlg_set->ht_key.ptr,
+                                 (unsigned)dlg_set->ht_key.slen,
 			         dlg->local.tag_hval, dlg_set->ht_entry,
                                  dlg_set);
 	}
@@ -345,14 +349,15 @@ PJ_DEF(pj_status_t) pjsip_ua_register_dlg( pjsip_user_agent *ua,
 	struct dlg_set *dlg_set;
 
 	dlg_set = alloc_dlgset_node();
+	dlg_set->ht_key = dlg->local.info->tag;
 	pj_list_init(&dlg_set->dlg_list);
 	pj_list_push_back(&dlg_set->dlg_list, dlg);
 
 	dlg->dlg_set = dlg_set;
 
 	pj_hash_set_np_lower(mod_ua.dlg_table, 
-		             dlg->local.info->tag.ptr,
-                             (unsigned)dlg->local.info->tag.slen,
+		             dlg_set->ht_key.ptr,
+                             (unsigned)dlg_set->ht_key.slen,
 		             dlg->local.tag_hval, dlg_set->ht_entry, dlg_set);
     }
 
@@ -397,12 +402,43 @@ PJ_DEF(pj_status_t) pjsip_ua_unregister_dlg( pjsip_user_agent *ua,
 
     /* If dialog list is empty, remove the dialog set from the hash table. */
     if (pj_list_empty(&dlg_set->dlg_list)) {
-	pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg->local.info->tag.ptr,
-		          (unsigned)dlg->local.info->tag.slen, 
+
+	/* Verify that the dialog set is valid */
+	pj_assert(pj_hash_get_lower(mod_ua.dlg_table, dlg_set->ht_key.ptr,
+				    (unsigned)dlg_set->ht_key.slen,
+				    &dlg->local.tag_hval) == dlg_set);
+
+	pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg_set->ht_key.ptr,
+		          (unsigned)dlg_set->ht_key.slen,
 			  dlg->local.tag_hval, NULL);
 
 	/* Return dlg_set to free nodes. */
 	pj_list_push_back(&mod_ua.free_dlgset_nodes, dlg_set);
+    } else {
+	/* If the just unregistered dialog is being used as hash key,
+	 * reset the dlg_set entry with a new key (i.e: from the first dialog
+	 * in dlg_set).
+	 */
+	if (dlg_set->ht_key.ptr  == dlg->local.info->tag.ptr &&
+	    dlg_set->ht_key.slen == dlg->local.info->tag.slen)
+	{
+	    pjsip_dialog* key_dlg = dlg_set->dlg_list.next;
+
+	    /* Verify that the old & new keys share the hash value */
+	    pj_assert(key_dlg->local.tag_hval == dlg->local.tag_hval);
+
+	    pj_hash_set_lower(NULL, mod_ua.dlg_table, dlg_set->ht_key.ptr,
+			      (unsigned)dlg_set->ht_key.slen,
+			      dlg->local.tag_hval, NULL);
+
+	    dlg_set->ht_key = key_dlg->local.info->tag;
+
+	    pj_hash_set_np_lower(mod_ua.dlg_table,
+				 dlg_set->ht_key.ptr,
+				 (unsigned)dlg_set->ht_key.slen,
+				 key_dlg->local.tag_hval, dlg_set->ht_entry,
+				 dlg_set);
+	}
     }
 
     /* Unlock user agent. */
path: pjsip/src/pjsip/sip_ua_layer.c