CVE-2016-9936: Use After Free (CWE-416)
value: The unserialize implementation in ext/standard/var.c in PHP 7.x before 7.0.14 allows remote attackers to cause a denial of service (use-after-free) or possibly have unspecified other impact via crafted serialized data.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2015-6834.
CWE description: 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.
CVSS v3 base score: 9.8
Published: 2017-01-04T20:59Z
Repository: https://github.com/php/php-src
Language: C
Files changed: ext/standard/tests/serialize/bug70172_2.phpt, ext/standard/var.c
Commit date: 2016-11-05 23:06:27 +0100
Commit message: Complete the fix of bug #70172 for PHP 7

Diff: diff --git a/ext/standard/tests/serialize/bug70172_2.phpt b/ext/standard/tests/serialize/bug70172_2.phpt
index dc9067168a0..2b12a78edb5 100644
--- a/ext/standard/tests/serialize/bug70172_2.phpt
+++ b/ext/standard/tests/serialize/bug70172_2.phpt
@@ -1,9 +1,7 @@
 --TEST--
 Bug #70172 - Use After Free Vulnerability in unserialize()
---XFAIL--
-Unfinished merge, needs fix.
 --FILE--
 <?php
 class obj implements Serializable {
 	var $data;
 	function serialize() {
@@ -59,12 +57,12 @@ function ptr2str($ptr)
   [1]=>
   array(1) {
     [0]=>
     array(1) {
       [0]=>
-      &object(obj2)#%d (1) {
+      object(obj2)#%d (1) {
         ["ryat"]=>
         int(1)
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/ext/standard/var.c b/ext/standard/var.c
index 472cd62d8f5..88719ccb64d 100644
--- a/ext/standard/var.c
+++ b/ext/standard/var.c
@@ -1034,10 +1034,11 @@ PHP_FUNCTION(unserialize)
 	char *buf = NULL;
 	size_t buf_len;
 	const unsigned char *p;
 	php_unserialize_data_t var_hash;
 	zval *options = NULL, *classes = NULL;
+	zval *retval;
 	HashTable *class_hash = NULL;
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a", &buf, &buf_len, &options) == FAILURE) {
 		RETURN_FALSE;
 	}
@@ -1065,26 +1066,25 @@ PHP_FUNCTION(unserialize)
 		        zend_string_release(lcname);
 			} ZEND_HASH_FOREACH_END();
 		}
 	}
 
-	if (!php_var_unserialize_ex(return_value, &p, p + buf_len, &var_hash, class_hash)) {
+	retval = var_tmp_var(&var_hash);
+	if (!php_var_unserialize_ex(retval, &p, p + buf_len, &var_hash, class_hash)) {
 		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
 		if (class_hash) {
 			zend_hash_destroy(class_hash);
 			FREE_HASHTABLE(class_hash);
 		}
-		zval_ptr_dtor(return_value);
 		if (!EG(exception)) {
 			php_error_docref(NULL, E_NOTICE, "Error at offset " ZEND_LONG_FMT " of %zd bytes",
 				(zend_long)((char*)p - buf), buf_len);
 		}
 		RETURN_FALSE;
 	}
-	/* We should keep an reference to return_value to prevent it from being dtor
-	   in case nesting calls to unserialize */
-	var_push_dtor(&var_hash, return_value);
+
+	ZVAL_COPY(return_value, retval);
 
 	PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
 	if (class_hash) {
 		zend_hash_destroy(class_hash);
 		FREE_HASHTABLE(class_hash);

Vulnerable code: Unfinished merge, needs fix.
      &object(obj2)#%d (1) {
}
	if (!php_var_unserialize_ex(return_value, &p, p + buf_len, &var_hash, class_hash)) {
		zval_ptr_dtor(return_value);
	/* We should keep an reference to return_value to prevent it from being dtor
	   in case nesting calls to unserialize */
	var_push_dtor(&var_hash, return_value);
Fixed code:       object(obj2)#%d (1) {
}
	zval *retval;
	retval = var_tmp_var(&var_hash);
	if (!php_var_unserialize_ex(retval, &p, p + buf_len, &var_hash, class_hash)) {

	ZVAL_COPY(return_value, retval);