Skip to content

Heap buffer overflow in ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER #11016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Changochen opened this issue Apr 4, 2023 · 6 comments · Fixed by #11021
Closed

Heap buffer overflow in ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER #11016

Changochen opened this issue Apr 4, 2023 · 6 comments · Fixed by #11021

Comments

@Changochen
Copy link

Description

The following code:

<?php
ob_flush ( ) ;
echo [ 6.5 => 0.5 , ... [ [ 1 ] , [ 1 ] , 1 ]] ;

Using the asan build and USE_TRACKED_ALLOC=1 USE_ZEND_ALLOC=0 php-src/asan/sapi/cli/php -f poc1.php
Resulted in this output:

heap-buffer-overflow write

But I expected this output instead:

Array

PHP Version

PHP 8.3.0-dev

Operating System

No response

@nielsdos
Copy link
Member

nielsdos commented Apr 4, 2023

Very slightly simplified reproducer:

<?php
$x = [6.5 => 0, ...[1, 1, 1]];

Note: The floating point key is important to make it overflow, and the length of the nested list must be at least 3.
EDIT: the reason is that the index needs to be high enough for it to overflow, we're not reserving enough hash buckets in the unpacked VM handler.

@nielsdos
Copy link
Member

nielsdos commented Apr 4, 2023

My gut feeling says this is the right approach, but I would need to read more about the hashtables to know for sure.
It does get rid of the Valgrind complaint.
If someone confirms this is the right idea, I'll check other places in the VM and PR this up with a test.

diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 7ef8cf1922..b085a30f67 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -6128,7 +6128,7 @@ ZEND_VM_C_LABEL(add_unpack_again):
 		zval *val;
 
 		if (HT_IS_PACKED(ht) && (zend_hash_num_elements(result_ht) == 0 || HT_IS_PACKED(result_ht))) {
-			zend_hash_extend(result_ht, zend_hash_num_elements(result_ht) + zend_hash_num_elements(ht), 1);
+			zend_hash_extend(result_ht, result_ht->nNumUsed + ht->nNumUsed, 1);
 			ZEND_HASH_FILL_PACKED(result_ht) {
 				ZEND_HASH_PACKED_FOREACH_VAL(ht, val) {
 					if (UNEXPECTED(Z_ISREF_P(val)) &&
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 19149c8c18..bbba8b6da5 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -2652,7 +2652,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER(
 		zval *val;
 
 		if (HT_IS_PACKED(ht) && (zend_hash_num_elements(result_ht) == 0 || HT_IS_PACKED(result_ht))) {
-			zend_hash_extend(result_ht, zend_hash_num_elements(result_ht) + zend_hash_num_elements(ht), 1);
+			zend_hash_extend(result_ht, result_ht->nNumUsed + ht->nNumUsed, 1);
 			ZEND_HASH_FILL_PACKED(result_ht) {
 				ZEND_HASH_PACKED_FOREACH_VAL(ht, val) {
 					if (UNEXPECTED(Z_ISREF_P(val)) &&

@iluuu1994
Copy link
Member

I think you're right for result_ht. We're appending to the existing array which may have holes (nNumUsed > nNumOfElements). However, we only need to grow to result_ht->nNumOfElements + zend_hash_num_elements(ht) in that case (since it doesn't matter if ht has holes).

@nielsdos
Copy link
Member

nielsdos commented Apr 4, 2023

Makes sense to me.
I'll check that and PR it tomorrow evening.
I'll add a Suggested-by / Co-authored-by tag for you for the correction, if that's okay.

nielsdos added a commit to nielsdos/php-src that referenced this issue Apr 5, 2023
…ANDLER

Not enough space was reserved for the packed resulting array because of
some confusion in the meaning of nr of used slots vs nr of elements.
nielsdos added a commit to nielsdos/php-src that referenced this issue Apr 5, 2023
…ANDLER

Not enough space was reserved for the packed resulting array because of
some confusion in the meaning of nr of used slots vs nr of elements.

Co-authored-by: Ilija Tovilo <[email protected]>
nielsdos added a commit to nielsdos/php-src that referenced this issue Apr 5, 2023
…ANDLER

Not enough space was reserved for the packed resulting array because of
some confusion in the meaning of nr of used slots vs nr of elements.

Co-authored-by: Ilija Tovilo <[email protected]>
nielsdos added a commit to nielsdos/php-src that referenced this issue Apr 5, 2023
After a hash filling routine the number of elements are set to the fill
index. However, if the fill index is larger than the number of elements,
the number of elements are no longer correct. This is observable at
least via count() and var_dump(). E.g. the attached test case would
incorrectly show int(17) instead of int(11).

Solve this by only increasing the number of elements by the actual
number that got added. Instead of adding a variable that increments per
iteration, I wanted to save some cycles in the iteration and simply
compute the number of added elements at the end.

I removed the assignment in ZEND_HASH_FILL_GROW() because the number of
elements is set anyway at the end of the hash function and this avoids
an unnecessary computation.

I discovered this behaviour while fixing phpGH-11016, where this filling
routine is easily exposed to userland via a specialised VM path [1].
Since this seems to be more a general problem with the macros, and may
be triggered outside of the VM handlers, I fixed it in the macros
instead of modifying the VM to fixup the number of elements.

[1] https://github.com/php/php-src/blob/b2c5acbb010f4bbc7ea9b53ba9bc81d672dd0f34/Zend/zend_vm_def.h#L6132-L6141
@dstogov
Copy link
Member

dstogov commented Apr 6, 2023

I think, the proper fix in conjunction with #9796 should be

zend_hash_extend(result_ht, result_ht->nNumUsed + zend_hash_num_elements(ht), 1);

@nielsdos
Copy link
Member

nielsdos commented Apr 6, 2023

@dstogov I had already made a PR last night which fixes it the way you propose: #11021

nielsdos added a commit to nielsdos/php-src that referenced this issue Apr 6, 2023
After a hash filling routine the number of elements are set to the fill
index. However, if the fill index is larger than the number of elements,
the number of elements are no longer correct. This is observable at
least via count() and var_dump(). E.g. the attached test case would
incorrectly show int(17) instead of int(11).

Solve this by only increasing the number of elements by the actual
number that got added. Instead of adding a variable that increments per
iteration, I wanted to save some cycles in the iteration and simply
compute the number of added elements at the end.

I discovered this behaviour while fixing phpGH-11016, where this filling
routine is easily exposed to userland via a specialised VM path [1].
Since this seems to be more a general problem with the macros, and may
be triggered outside of the VM handlers, I fixed it in the macros
instead of modifying the VM to fixup the number of elements.

[1] https://github.com/php/php-src/blob/b2c5acbb010f4bbc7ea9b53ba9bc81d672dd0f34/Zend/zend_vm_def.h#L6132-L6141
nielsdos added a commit that referenced this issue Apr 6, 2023
After a hash filling routine the number of elements are set to the fill
index. However, if the fill index is larger than the number of elements,
the number of elements are no longer correct. This is observable at
least via count() and var_dump(). E.g. the attached test case would
incorrectly show int(17) instead of int(11).

Solve this by only increasing the number of elements by the actual
number that got added. Instead of adding a variable that increments per
iteration, I wanted to save some cycles in the iteration and simply
compute the number of added elements at the end.

I discovered this behaviour while fixing GH-11016, where this filling
routine is easily exposed to userland via a specialised VM path [1].
Since this seems to be more a general problem with the macros, and may
be triggered outside of the VM handlers, I fixed it in the macros
instead of modifying the VM to fixup the number of elements.

[1] https://github.com/php/php-src/blob/b2c5acbb010f4bbc7ea9b53ba9bc81d672dd0f34/Zend/zend_vm_def.h#L6132-L6141
nielsdos added a commit that referenced this issue Apr 6, 2023
…LER (#11021)

Not enough space was reserved for the packed resulting array because of
some confusion in the meaning of nr of used slots vs nr of elements.

Co-authored-by: Ilija Tovilo <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants