-
Notifications
You must be signed in to change notification settings - Fork 7.8k
heap-buffer-overflow at zval_undefined_cv #10168
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
Comments
I spent some time analyzing the bug and I think I understand what's happening. Slightly simplified reproducer: <?php
class test
{
static $instances;
public function __construct(private $id) {
(self::$instances[$undefined_variable] = $this) > 0;
}
function __destruct() {
unset(self::$instances[NULL]);
}
}
new test(2);
new test(3);
?> Reasoning:
In the VM's comparison code, _zval_undefined_op2 is called because we got an UNDEF for T3. But _zval_undefined_op{1,2} assume that we're using CVs, not TMPs, so the lookup of the variable name crashes. |
I made a quick PoC patch, although I'm not sure if this is the right way to solve this. If it is, I'll happily make a PR :) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index fc54a8e026..a2c98cffc9 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -273,14 +273,26 @@ static zend_never_inline ZEND_COLD zval* zval_undefined_cv(uint32_t var EXECUTE_
return &EG(uninitialized_zval);
}
+static zend_never_inline ZEND_COLD zval* zval_undefined_var(uint32_t var, zend_uchar type EXECUTE_DATA_DC)
+{
+ if (type & IS_CV) {
+ return zval_undefined_cv(var EXECUTE_DATA_CC);
+ } else {
+ if (EXPECTED(EG(exception) == NULL)) {
+ zend_error(E_WARNING, "Undefined operand");
+ }
+ return &EG(uninitialized_zval);
+ }
+}
+
static zend_never_inline ZEND_COLD zval* ZEND_FASTCALL _zval_undefined_op1(EXECUTE_DATA_D)
{
- return zval_undefined_cv(EX(opline)->op1.var EXECUTE_DATA_CC);
+ return zval_undefined_var(EX(opline)->op1.var, EX(opline)->op1_type EXECUTE_DATA_CC);
}
static zend_never_inline ZEND_COLD zval* ZEND_FASTCALL _zval_undefined_op2(EXECUTE_DATA_D)
{
- return zval_undefined_cv(EX(opline)->op2.var EXECUTE_DATA_CC);
+ return zval_undefined_var(EX(opline)->op2.var, EX(opline)->op2_type EXECUTE_DATA_CC);
}
#define ZVAL_UNDEFINED_OP1() _zval_undefined_op1(EXECUTE_DATA_C) |
|
Even if this is a refcounting bug, the destructor does not necessarily need to run deterministically right? For example if there is a reference cycle somewhere, then the cyclic garbage collector may effectively still free the first instance during the second instance's construction and call the destructor of the first one, setting the temporary to UNDEF anyway? |
@nielsdos The cycle collector doesn't consider static properties to be internal cycles. Even if one were to call
Of course, this might be the result of some other error (like copying some other undefined zval into the VAR). I just wanted to point out that adding code to handle |
Thanks for the comment.
So it's actually some sort of use-after-free in disguise. I tried fixing it by refetching |
I've been thinking about a solution for this. The problem is that we're using the (I also think that the same concept could maybe also be used for #10169) I checked and all tests pass, including the reduced test code below. TestTest code: <?php
class test
{
static $instances;
public function __construct(private $id) {
(self::$instances[NULL] = $this) > 0;
var_dump(self::$instances);
}
function __destruct() {
unset(self::$instances[NULL]);
}
}
new test(2);
new test(3); Now no longer crashes and outputs:
PatchThis is a proof-of-concept patch where I used my new API functions in ASSIGN_DIM. (And function names could probably be improved as well) diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index a9b316b8bd..c382f7a3ff 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -137,12 +137,24 @@ static zend_always_inline void zend_copy_to_variable(zval *variable_ptr, zval *v
}
}
-static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval *value, zend_uchar value_type, bool strict)
+static zend_always_inline void zend_assign_to_variable_handle_garbage(zend_refcounted *garbage)
+{
+ if (!garbage)
+ return;
+ if (GC_DELREF(garbage) == 0) {
+ rc_dtor_func(garbage);
+ } else { /* we need to split */
+ /* optimized version of GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr) */
+ if (UNEXPECTED(GC_MAY_LEAK(garbage))) {
+ gc_possible_root(garbage);
+ }
+ }
+}
+
+static zend_always_inline zval* zend_assign_to_variable_delayed_garbage_handling(zval *variable_ptr, zval *value, zend_uchar value_type, bool strict, zend_refcounted **garbage)
{
do {
if (UNEXPECTED(Z_REFCOUNTED_P(variable_ptr))) {
- zend_refcounted *garbage;
-
if (Z_ISREF_P(variable_ptr)) {
if (UNEXPECTED(ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(variable_ptr)))) {
return zend_assign_to_typed_ref(variable_ptr, value, value_type, strict);
@@ -153,16 +165,8 @@ static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval
break;
}
}
- garbage = Z_COUNTED_P(variable_ptr);
+ *garbage = Z_COUNTED_P(variable_ptr);
zend_copy_to_variable(variable_ptr, value, value_type);
- if (GC_DELREF(garbage) == 0) {
- rc_dtor_func(garbage);
- } else { /* we need to split */
- /* optimized version of GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr) */
- if (UNEXPECTED(GC_MAY_LEAK(garbage))) {
- gc_possible_root(garbage);
- }
- }
return variable_ptr;
}
} while (0);
@@ -171,6 +175,14 @@ static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval
return variable_ptr;
}
+static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval *value, zend_uchar value_type, bool strict)
+{
+ zend_refcounted *garbage = NULL;
+ variable_ptr = zend_assign_to_variable_delayed_garbage_handling(variable_ptr, value, value_type, strict, &garbage);
+ zend_assign_to_variable_handle_garbage(garbage);
+ return variable_ptr;
+}
+
ZEND_API zend_result ZEND_FASTCALL zval_update_constant(zval *pp);
ZEND_API zend_result ZEND_FASTCALL zval_update_constant_ex(zval *pp, zend_class_entry *scope);
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 9a1d00d6c7..49dc6b65c5 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -2584,6 +2584,9 @@ ZEND_VM_C_LABEL(try_assign_dim_array):
Z_ADDREF_P(value);
}
}
+ if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
+ ZVAL_COPY(EX_VAR(opline->result.var), value);
+ }
} else {
dim = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
if (OP2_TYPE == IS_CONST) {
@@ -2595,10 +2598,12 @@ ZEND_VM_C_LABEL(try_assign_dim_array):
ZEND_VM_C_GOTO(assign_dim_error);
}
value = GET_OP_DATA_ZVAL_PTR(BP_VAR_R);
- value = zend_assign_to_variable(variable_ptr, value, OP_DATA_TYPE, EX_USES_STRICT_TYPES());
- }
- if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
- ZVAL_COPY(EX_VAR(opline->result.var), value);
+ zend_refcounted *garbage = NULL;
+ value = zend_assign_to_variable_delayed_garbage_handling(variable_ptr, value, OP_DATA_TYPE, EX_USES_STRICT_TYPES(), &garbage);
+ if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
+ ZVAL_COPY(EX_VAR(opline->result.var), value);
+ }
+ zend_assign_to_variable_handle_garbage(garbage);
}
} else {
if (EXPECTED(Z_ISREF_P(object_ptr))) { |
Hey @iluuu1994 |
@nielsdos Go ahead, I unassigned the issue. 🙂 |
Fixes phpGH-10168 The problem is that we're using the variable_ptr in the opcode handler *after* it has already been destroyed. The solution is to delay executing the destructors until after the variable_ptr is used. To accomplish this we introduce 2 new API functions: * zend_assign_to_variable_delay_garbage_handling(); and * zend_assign_to_variable_handle_garbage() that allows users to delay the garbage handling. zend_assign_to_variable() is now a wrapper for those such that there is no BC break. We only have to apply this to ASSIGN_DIM and ASSIGN. That's because the others rely on properties, in which case the variable_ptr can't be destroyed. The first commit fixes the bug, the second one adds a regression test. Comparing the performance before and after this fix using Valgrind on Zend/bench.php. Instruction counts yields 3,339,632,671 before and 3,340,132,746 after. The difference is only a performance decrease of ~0.015%, which seems negligible.
Co-authored-by: Changochen <[email protected]>
The problem is that we're using the variable_ptr in the opcode handler *after* it has already been destroyed. The solution is to create a specialised version of zend_assign_to_variable which takes in two destination zval pointers.
Co-authored-by: Changochen <[email protected]>
* PHP-8.1: Fix GH-10168: heap-buffer-overflow at zval_undefined_cv
* PHP-8.2: Fix GH-10168: heap-buffer-overflow at zval_undefined_cv
Also make expression result of assignments consistent, containing the value of the variable from after the destructor has been executed. See phpGH-10168
Also make expression result of assignments consistent, containing the value of the variable from after the destructor has been executed. See phpGH-10168
* PHP-8.1: Revert "Fix GH-10168: heap-buffer-overflow at zval_undefined_cv"
* PHP-8.2: Revert "Fix GH-10168: heap-buffer-overflow at zval_undefined_cv"
Description
The following code:
Resulted in this output:
Git commit: ff42cb0
PHP Version
8.3.0-dev
Operating System
No response
The text was updated successfully, but these errors were encountered: