Closed
Description
Description
The following code:
<?php
class test
{
protected $_id;
static $instances;
public function __construct($id) {
11 < self::$instances[$this->_id] = $this;
}
function __destruct() { unset(self::$instances[$this->_id]);
}
}
new test(2);
new test(2);
new test(3);
?>
Resulted in this output:
./php-fuzz-execute poc1.php
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2128458864
INFO: Loaded 1 modules (147832 inline 8-bit counters): 147832 [0x1d60c20, 0x1d84d98),
INFO: Loaded 1 PC tables (147832 PCs): 147832 [0x1d84d98,0x1fc6518),
./php-fuzz-execute: Running 1 inputs 1 time(s) each.
Running: poc1.php
=================================================================
==2827130==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000002108 at pc 0x00000120a6bf bp 0x7fffffffd8e0 sp 0x7fffffffd8d8
READ of size 8 at 0x602000002108 thread T0
Git commit: ff42cb0
PHP Version
8.3.0-dev
Operating System
No response
Activity
nielsdos commentedon Dec 26, 2022
I spent some time analyzing the bug and I think I understand what's happening.
Slightly simplified reproducer:
Reasoning:
It is during the constructor of "new test(3)" that PHP crashes. During the ASSIGN_DIM is executed, the destructor of "new test(2)" is called. As both "new test(2)" and "new test(3)" occupy the same slot in the array, the result of the assignment becomes UNDEF. This makes sure that OP2 of ASSIGN_DIM is UNDEF (T3 in the opcodes). Therefore, the RHS of the comparison (T5) is UNDEF.
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.
nielsdos commentedon Dec 26, 2022
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 :)
Fix use-after-free in write_property when object is released
iluuu1994 commentedon Dec 29, 2022
VAR
/TMPVAR
are not supposed to beUNDEF
. I took a look but still don't understand exactly what happens, but it seems like something goes wrong with refcounting at some point. The constructor of the second instance frees the first element by overwriting the given index in the array. There seems to be some confusion happening after the destructor was called, possibly expecting the array element to still be the same? I'll have a closer look soon.nielsdos commentedon Dec 29, 2022
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?
iluuu1994 commentedon Dec 30, 2022
@nielsdos The cycle collector doesn't consider static properties to be internal cycles. Even if one were to call
gc_collect_cycles()
the instance won't be freed until it gets removed from the static property. Thus, the destructor does get called deterministically here, as soon as the instance is removed from the array and the refcount goes to 0.TMP
/TMPVAR
should always be initialized by some opcode handler before being consumed by some other.UNDEF
is not a valid value for those, opcode handlers can assume that aTMP
/TMPVAR
should be a non-UNDEF
value. See this great blog post from Nikita.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
UNDEF
forTMP
/TMPVAR
is likely not the right solution, as this assumption is being made all over in the VM.nielsdos commentedon Dec 31, 2022
Thanks for the comment.
I spent some time on this. I'm not 100% sure about this but maybe it's helpful. I think the following happens (I'm using line numbers from PHP-8.1 at commit 4c9375e:
zend_vm_execute.h
lines 50964-50974.zend_assign_to_variable
. (bullet points 2.x are all inzend_assign_to_variable
)2.1. Line 156 sets
garbage
to thezend_refcounted*
ofvariable_ptr
.2.2. We copy
value
tovariable_ptr
at line 157.2.3. The old
zend_refcounted*
from before the copy (garbage
) must be destroyed, in doing so it callsrc_dtor_func(garbage)
which calls the userland destructor. The userland destructor unsets the array slot in$instances
, which meansvariable_ptr
now corresponds to an array slot that's no longer valid.zend_vm_execute.h
at the returning on line 50974. Note that usingvariable_ptr
is effectively using "unset" data now.value
.So it's actually some sort of use-after-free in disguise.
I tried fixing it by refetching
variable_ptr
from the array when a destructor gets called, but this became quite hairy code. I also wonder whether this isn't a more general problem that's not only limited to arrays.nielsdos commentedon Jan 5, 2023
I've been thinking about a solution for this.
I tried things like refetching
variable_ptr
and temporarily increasing refcounts, but I don't like those solutions because they are complex and have a bad performance impact.I came up with the following alternative (PoC patch below):
The problem is that we're using the
variable_ptr
in the opcode handler after it has already been destroyed.So my idea is to delay the destructors until after the
variable_ptr
is used.To accomplish this I made 2 new API functions
zend_assign_to_variable_delayed_garbage_handling
andzend_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.(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.
Test
Test code:
Now no longer crashes and outputs:
Patch
This is a proof-of-concept patch where I used my new API functions in ASSIGN_DIM.
There are probably places other than ASSIGN_DIM where this would be necessary to fix all instances of the same bug. Since this is a PoC I didn't check that yet.
Also: I don't know if I need to change anything to the JIT yet, I haven't touched that code at all yet, so I'd have to check.
(And function names could probably be improved as well)
nielsdos commentedon Feb 3, 2023
Hey @iluuu1994
I still have my PoC patch (see my last post #10168 (comment)) which I have now finished into a full patch to fix this issue. However, I see you are now assigned to this issue. Is it still worth it to post my patch/PR, or did you already start on your own fix?
iluuu1994 commentedon Feb 3, 2023
@nielsdos Go ahead, I unassigned the issue. 🙂
42 remaining items