CVE-2016-5768: Double Free (CWE-415)
value: Double free vulnerability in the _php_mb_regex_ereg_replace_exec function in php_mbregex.c in the mbstring extension in PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 allows remote attackers to execute arbitrary code or cause a denial of service (application crash) by leveraging a callback exception.
CWE description: The product calls free() twice on the same memory address, potentially leading to modification of unexpected memory locations.
CVSS v3 base score: 9.8
Published: 2016-08-07T10:59Z
Repository: https://github.com/php/php-src
Language: C
Files changed: ext/mbstring/php_mbregex.c, ext/mbstring/tests/bug72402.phpt
Commit date: 2016-06-18 21:48:39 -0700
Commit message: Fix bug #72402: _php_mb_regex_ereg_replace_exec - double free

Diff: diff --git a/ext/mbstring/php_mbregex.c b/ext/mbstring/php_mbregex.c
index d73c8483505..6cdee23c3e5 100644
--- a/ext/mbstring/php_mbregex.c
+++ b/ext/mbstring/php_mbregex.c
@@ -30,11 +30,11 @@
 
 #include "ext/standard/php_smart_str.h"
 #include "ext/standard/info.h"
 #include "php_mbregex.h"
 #include "mbstring.h"
- 
+
 #include "php_onig_compat.h" /* must come prior to the oniguruma header */
 #include <oniguruma.h>
 #undef UChar
 
 ZEND_EXTERN_MODULE_GLOBALS(mbstring)
@@ -53,11 +53,11 @@ struct _zend_mb_regex_globals {
 };
 
 #define MBREX(g) (MBSTRG(mb_regex_globals)->g)
 
 /* {{{ static void php_mb_regex_free_cache() */
-static void php_mb_regex_free_cache(php_mb_regex_t **pre) 
+static void php_mb_regex_free_cache(php_mb_regex_t **pre)
 {
 	onig_free(*pre);
 }
 /* }}} */
 
@@ -76,11 +76,11 @@ static int _php_mb_regex_globals_ctor(zend_mb_regex_globals *pglobals TSRMLS_DC)
 	return SUCCESS;
 }
 /* }}} */
 
 /* {{{ _php_mb_regex_globals_dtor */
-static void _php_mb_regex_globals_dtor(zend_mb_regex_globals *pglobals TSRMLS_DC) 
+static void _php_mb_regex_globals_dtor(zend_mb_regex_globals *pglobals TSRMLS_DC)
 {
 	zend_hash_destroy(&pglobals->ht_rc);
 }
 /* }}} */
 
@@ -464,11 +464,11 @@ static php_mb_regex_t *php_mbregex_compile_pattern(const char *pattern, int patl
 		zend_hash_update(&MBREX(ht_rc), (char *) pattern, patlen + 1, (void *) &retval, sizeof(retval), NULL);
 	} else if (found == SUCCESS) {
 		retval = *rc;
 	}
 out:
-	return retval; 
+	return retval;
 }
 /* }}} */
 
 /* {{{ _php_mb_regex_get_option_string */
 static size_t _php_mb_regex_get_option_string(char *str, size_t len, OnigOptionType option, OnigSyntaxType *syntax)
@@ -481,58 +481,58 @@ static size_t _php_mb_regex_get_option_string(char *str, size_t len, OnigOptionT
 	if ((option & ONIG_OPTION_IGNORECASE) != 0) {
 		if (len_left > 0) {
 			--len_left;
 			*(p++) = 'i';
 		}
-		++len_req;	
+		++len_req;
 	}
 
 	if ((option & ONIG_OPTION_EXTEND) != 0) {
 		if (len_left > 0) {
 			--len_left;
 			*(p++) = 'x';
 		}
-		++len_req;	
+		++len_req;
 	}
 
 	if ((option & (ONIG_OPTION_MULTILINE | ONIG_OPTION_SINGLELINE)) ==
 			(ONIG_OPTION_MULTILINE | ONIG_OPTION_SINGLELINE)) {
 		if (len_left > 0) {
 			--len_left;
 			*(p++) = 'p';
 		}
-		++len_req;	
+		++len_req;
 	} else {
 		if ((option & ONIG_OPTION_MULTILINE) != 0) {
 			if (len_left > 0) {
 				--len_left;
 				*(p++) = 'm';
 			}
-			++len_req;	
+			++len_req;
 		}
 
 		if ((option & ONIG_OPTION_SINGLELINE) != 0) {
 			if (len_left > 0) {
 				--len_left;
 				*(p++) = 's';
 			}
-			++len_req;	
+			++len_req;
 		}
-	}	
+	}
 	if ((option & ONIG_OPTION_FIND_LONGEST) != 0) {
 		if (len_left > 0) {
 			--len_left;
 			*(p++) = 'l';
 		}
-		++len_req;	
+		++len_req;
 	}
 	if ((option & ONIG_OPTION_FIND_NOT_EMPTY) != 0) {
 		if (len_left > 0) {
 			--len_left;
 			*(p++) = 'n';
 		}
-		++len_req;	
+		++len_req;
 	}
 
 	c = 0;
 
 	if (syntax == ONIG_SYNTAX_JAVA) {
@@ -564,26 +564,26 @@ static size_t _php_mb_regex_get_option_string(char *str, size_t len, OnigOptionT
 
 	if (len_left > 0) {
 		--len_left;
 		*(p++) = '\0';
 	}
-	++len_req;	
+	++len_req;
 	if (len < len_req) {
 		return len_req;
 	}
 
 	return 0;
 }
 /* }}} */
 
 /* {{{ _php_mb_regex_init_options */
 static void
-_php_mb_regex_init_options(const char *parg, int narg, OnigOptionType *option, OnigSyntaxType **syntax, int *eval) 
+_php_mb_regex_init_options(const char *parg, int narg, OnigOptionType *option, OnigSyntaxType **syntax, int *eval)
 {
 	int n;
 	char c;
-	int optm = 0; 
+	int optm = 0;
 
 	*syntax = ONIG_SYNTAX_RUBY;
 
 	if (parg != NULL) {
 		n = 0;
@@ -634,17 +634,17 @@ _php_mb_regex_init_options(const char *parg, int narg, OnigOptionType *option, O
 					break;
 				case 'd':
 					*syntax = ONIG_SYNTAX_POSIX_EXTENDED;
 					break;
 				case 'e':
-					if (eval != NULL) *eval = 1; 
+					if (eval != NULL) *eval = 1;
 					break;
 				default:
 					break;
 			}
 		}
-		if (option != NULL) *option|=optm; 
+		if (option != NULL) *option|=optm;
 	}
 }
 /* }}} */
 
 /*
@@ -858,15 +858,15 @@ static void _php_mb_regex_ereg_replace_exec(INTERNAL_FUNCTION_PARAMETERS, OnigOp
 		arg_pattern = Z_STRVAL_PP(arg_pattern_zval);
 		arg_pattern_len = Z_STRLEN_PP(arg_pattern_zval);
 	} else {
 		/* FIXME: this code is not multibyte aware! */
 		convert_to_long_ex(arg_pattern_zval);
-		pat_buf[0] = (char)Z_LVAL_PP(arg_pattern_zval);	
+		pat_buf[0] = (char)Z_LVAL_PP(arg_pattern_zval);
 		pat_buf[1] = '\0';
 
 		arg_pattern = pat_buf;
-		arg_pattern_len = 1;	
+		arg_pattern_len = 1;
 	}
 	/* create regex pattern buffer */
 	re = php_mbregex_compile_pattern(arg_pattern, arg_pattern_len, options, MBREX(current_mbctype), syntax TSRMLS_CC);
 	if (re == NULL) {
 		RETURN_FALSE;
@@ -932,11 +932,11 @@ static void _php_mb_regex_ereg_replace_exec(INTERNAL_FUNCTION_PARAMETERS, OnigOp
 						p += fwd;
 						i += fwd;
 					}
 				}
 			}
-				
+
 			if (eval) {
 				zval v;
 				/* null terminate buffer */
 				smart_str_0(&eval_buf);
 				/* do eval */
@@ -951,36 +951,35 @@ static void _php_mb_regex_ereg_replace_exec(INTERNAL_FUNCTION_PARAMETERS, OnigOp
 				smart_str_appendl(&out_buf, Z_STRVAL(v), Z_STRLEN(v));
 				/* Clean up */
 				eval_buf.len = 0;
 				zval_dtor(&v);
 			} else if (is_callable) {
-				zval *retval_ptr;
+				zval *retval_ptr = NULL;
 				zval **args[1];
 				zval *subpats;
 				int i;
-				
+
 				MAKE_STD_ZVAL(subpats);
 				array_init(subpats);
-				
+
 				for (i = 0; i < regs->num_regs; i++) {
 					add_next_index_stringl(subpats, string + regs->beg[i], regs->end[i] - regs->beg[i], 1);
-				}				
-				
+				}
+
 				args[0] = &subpats;
 				/* null terminate buffer */
 				smart_str_0(&eval_buf);
-				
+
 				arg_replace_fci.param_count = 1;
 				arg_replace_fci.params = args;
 				arg_replace_fci.retval_ptr_ptr = &retval_ptr;
-				if (zend_call_function(&arg_replace_fci, &arg_replace_fci_cache TSRMLS_CC) == SUCCESS && arg_replace_fci.retval_ptr_ptr) {
+				if (zend_call_function(&arg_replace_fci, &arg_replace_fci_cache TSRMLS_CC) == SUCCESS && arg_replace_fci.retval_ptr_ptr && retval_ptr) {
 					convert_to_string_ex(&retval_ptr);
 					smart_str_appendl(&out_buf, Z_STRVAL_P(retval_ptr), Z_STRLEN_P(retval_ptr));
 					eval_buf.len = 0;
 					zval_ptr_dtor(&retval_ptr);
 				} else {
-					efree(description);
 					if (!EG(exception)) {
 						php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call custom replacement function");
 					}
 				}
 				zval_ptr_dtor(&subpats);
@@ -989,11 +988,11 @@ static void _php_mb_regex_ereg_replace_exec(INTERNAL_FUNCTION_PARAMETERS, OnigOp
 			n = regs->end[0];
 			if ((pos - (OnigUChar *)string) < n) {
 				pos = (OnigUChar *)string + n;
 			} else {
 				if (pos < string_lim) {
-					smart_str_appendl(&out_buf, pos, 1); 
+					smart_str_appendl(&out_buf, pos, 1);
 				}
 				pos++;
 			}
 		} else { /* nomatch */
 			/* stick that last bit of string on our output */
@@ -1011,11 +1010,11 @@ static void _php_mb_regex_ereg_replace_exec(INTERNAL_FUNCTION_PARAMETERS, OnigOp
 		onig_region_free(regs, 1);
 	}
 	smart_str_free(&eval_buf);
 
 	if (err <= -2) {
-		smart_str_free(&out_buf);	
+		smart_str_free(&out_buf);
 		RETVAL_FALSE;
 	} else {
 		smart_str_appendc(&out_buf, '\0');
 		RETVAL_STRINGL((char *)out_buf.c, out_buf.len - 1, 0);
 	}
@@ -1061,11 +1060,11 @@ PHP_FUNCTION(mb_split)
 	int n, err;
 	long count = -1;
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|l", &arg_pattern, &arg_pattern_len, &string, &string_len, &count) == FAILURE) {
 		RETURN_FALSE;
-	} 
+	}
 
 	if (count > 0) {
 		count--;
 	}
 
@@ -1315,11 +1314,11 @@ PHP_FUNCTION(mb_ereg_search_init)
 	OnigOptionType option;
 
 	if (zend_parse_parameters(argc TSRMLS_CC, "z|ss", &arg_str, &arg_pattern, &arg_pattern_len, &arg_options, &arg_options_len) == FAILURE) {
 		return;
 	}
-	
+
 	if (argc > 1 && arg_pattern_len == 0) {
 		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Empty pattern");
 		RETURN_FALSE;
 	}
 
@@ -1414,11 +1413,11 @@ PHP_FUNCTION(mb_ereg_search_setpos)
 	RETURN_TRUE;
 }
 /* }}} */
 
 /* {{{ php_mb_regex_set_options */
-static void _php_mb_regex_set_options(OnigOptionType options, OnigSyntaxType *syntax, OnigOptionType *prev_options, OnigSyntaxType **prev_syntax TSRMLS_DC) 
+static void _php_mb_regex_set_options(OnigOptionType options, OnigSyntaxType *syntax, OnigOptionType *prev_options, OnigSyntaxType **prev_syntax TSRMLS_DC)
 {
 	if (prev_options != NULL) {
 		*prev_options = MBREX(regex_default_options);
 	}
 	if (prev_syntax != NULL) {
diff --git a/ext/mbstring/tests/bug72402.phpt b/ext/mbstring/tests/bug72402.phpt
new file mode 100644
index 00000000000..abb290bf4d8
--- /dev/null
+++ b/ext/mbstring/tests/bug72402.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Bug #72402: _php_mb_regex_ereg_replace_exec - double free
+--SKIPIF--
+<?php extension_loaded('mbstring') or die('skip mbstring not available'); ?>
+--FILE--
+<?php
+function throwit() {
+	throw new Exception('it');
+}
+$var10 = "throwit";
+try {
+	$var14 = mb_ereg_replace_callback("", $var10, "");
+} catch(Exception $e) {}
+?>
+DONE
+--EXPECT--
+DONE
\ No newline at end of file

Vulnerable code:  
static void php_mb_regex_free_cache(php_mb_regex_t **pre) 
static void _php_mb_regex_globals_dtor(zend_mb_regex_globals *pglobals TSRMLS_DC) 
	return retval; 
		++len_req;	
		++len_req;	
		++len_req;	
			++len_req;	
			++len_req;	
	}	
		++len_req;	
		++len_req;	
	++len_req;	
_php_mb_regex_init_options(const char *parg, int narg, OnigOptionType *option, OnigSyntaxType **syntax, int *eval) 
	int optm = 0; 
					if (eval != NULL) *eval = 1; 
		if (option != NULL) *option|=optm; 
		pat_buf[0] = (char)Z_LVAL_PP(arg_pattern_zval);	
		arg_pattern_len = 1;	
				
				zval *retval_ptr;
				
				
				}				
				
				
				if (zend_call_function(&arg_replace_fci, &arg_replace_fci_cache TSRMLS_CC) == SUCCESS && arg_replace_fci.retval_ptr_ptr) {
					efree(description);
					smart_str_appendl(&out_buf, pos, 1); 
		smart_str_free(&out_buf);	
	} 
	
static void _php_mb_regex_set_options(OnigOptionType options, OnigSyntaxType *syntax, OnigOptionType *prev_options, OnigSyntaxType **prev_syntax TSRMLS_DC) 
Fixed code: 
static void php_mb_regex_free_cache(php_mb_regex_t **pre)
static void _php_mb_regex_globals_dtor(zend_mb_regex_globals *pglobals TSRMLS_DC)
	return retval;
		++len_req;
		++len_req;
		++len_req;
			++len_req;
			++len_req;
	}
		++len_req;
		++len_req;
	++len_req;
_php_mb_regex_init_options(const char *parg, int narg, OnigOptionType *option, OnigSyntaxType **syntax, int *eval)
	int optm = 0;
					if (eval != NULL) *eval = 1;
		if (option != NULL) *option|=optm;
		pat_buf[0] = (char)Z_LVAL_PP(arg_pattern_zval);
		arg_pattern_len = 1;

				zval *retval_ptr = NULL;


				}


				if (zend_call_function(&arg_replace_fci, &arg_replace_fci_cache TSRMLS_CC) == SUCCESS && arg_replace_fci.retval_ptr_ptr && retval_ptr) {
					smart_str_appendl(&out_buf, pos, 1);
		smart_str_free(&out_buf);
	}

static void _php_mb_regex_set_options(OnigOptionType options, OnigSyntaxType *syntax, OnigOptionType *prev_options, OnigSyntaxType **prev_syntax TSRMLS_DC)
--TEST--
Bug #72402: _php_mb_regex_ereg_replace_exec - double free
--SKIPIF--
<?php extension_loaded('mbstring') or die('skip mbstring not available'); ?>
--FILE--
<?php
function throwit() {
	throw new Exception('it');
}
$var10 = "throwit";
try {
	$var14 = mb_ereg_replace_callback("", $var10, "");
} catch(Exception $e) {}
?>
DONE
--EXPECT--
DONE