-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Alternative fix for GH-10168 (alternative to GH-10500) #10524
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
Conversation
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]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an interesting discrepancy when using static properties as storage:
class Test
{
static ?Test $a = null;
public function __construct() {
var_dump(self::$a = $this);
}
function __destruct() {
self::$a = null;
}
}
new Test();
new Test();
object(Test)#1 (0) {
}
NULL
The second log looks wrong. This works fine with the existing tests.
Using ZVAL_COPY
for the result looks correct. That was the previous behavior.
@dstogov Just note that there's another issue that was discovered here (Zend/tests/gh10168_3.phpt) that should probably be ported over to the other PR. |
Ugh. This changes everything :( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Lets go with this approach.
Thank you @nielsdos! |
I looked into this again. This was actually never a use-after-free. Here's what happened: https://3v4l.org/EbPmd class Test
{
static $instances;
public function __construct() {
var_dump(self::$instances[0] = $this);
}
function __destruct() {
unset(self::$instances[0]);
}
}
new Test();
new Test();
The same can happen for untyped properties. https://3v4l.org/9l3PK class Box {
public $value;
}
$box = new Box();
class Test
{
public function __construct() {
global $box;
var_dump($box->value = $this);
}
function __destruct() {
global $box;
unset($box->value);
}
}
new Test();
new Test();
The reason I'm documenting this is that for all existing cases, the value is copied into result after the destructor has run. IMO this behavior is pretty unexpected and can lead
Zend/tests/gh10168_3.phpt on the other hand is a true use-after-free. The reason for that is that |
@iluuu1994 |
@nielsdos Oh. You're right of course. A use-after-free is still possible when the array is reallocated. So I guess the only solution is to postpone the destructor after all... |
@iluuu1994 I'm just having some random thoughts now, but I might share them as well because it doesn't hurt to throw some ideas I gues... One possible implementation idea might be to add a (intrusive) linked list field to EX(...) where we add the Z_COUNTEDs of the objects-to-be-destructed to. Then the VM could at the end of each opcode handling check if there is something in the linked list, and if there is loop over it to execute the destructors. This could move all the special handling code until after the opcode. But ig this is both a BC break because the destructors are postponed, and an ABI break. |
@nielsdos Oh, you meant for delaying the constructor, not just freeing the object. Hmm, let me think about that. |
So, I don't think checking for dead objects after every opcode handler is a good idea (pretty sure Dmitry doesn't think so either). We could probably limit it to certain handlers, but determining which ones might not be so easy. Missing one would not be catastrophic (the object would just be cleaned later) but would make the point in which the destructor is called somewhat unstable. We could try this approach to see how it performs and whether it simplifies or complicates the code. IMO while it would be nice for this issue to go away, the cases are quite unlikely to occur in real-life code. |
In PHP-5.0 we had a buffer (array) of "garbage" zvals and cleaned them after each VM instruction through |
Looking into this and the following #10546 proposal, I think this approach is wrong. I propose to revert this and start developing the fix again. I think it should be targeted to master only. I think, I found a simpler approach. It's actually based on @nielsdos original idea. I'm limited in time now, @iluuu1994 can you please verify my patch and think if it can/should be improved. |
@dstogov Thank you for looking into this! From a high level it seems like this approach should also solve the issues #10546 does. I will have a detailed look tomorrow and check all code paths again. I think the following line is wrong: - if (ZEND_VM_SPEC || UNEXPECTED(RETURN_VALUE_USED(opline))) {
+ if (UNEXPECTED(RETURN_VALUE_USED(opline))) { The copy should not be done for all specializations unconditionally, but just the ones actually used when the return value is used. Edit: Although that condition is guarded by |
I use The code: if (!ZEND_VM_SPEC || UNEXPECTED(RETURN_VALUE_USED(opline))) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, OP2_TYPE, EX_USES_STRICT_TYPES(), &garbage);
if (ZEND_VM_SPEC || UNEXPECTED(RETURN_VALUE_USED(opline))) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} if ZEND_VM_SPEC == 1 it's translated as if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, OP2_TYPE, EX_USES_STRICT_TYPES(), &garbage);
if (1) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} if ZEND_VM_SPEC == 0 it's translated as if (1) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, OP2_TYPE, EX_USES_STRICT_TYPES(), &garbage);
if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} Seems right, but please verify this. |
I think my assumption was correct. This is the generated code for specialized handlers: if (0 || UNEXPECTED(0)) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, IS_CONST, EX_USES_STRICT_TYPES(), &garbage);
if (1 || UNEXPECTED(0)) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} In this case, the second This is the unspecialized handler: if (1 || UNEXPECTED(RETURN_VALUE_USED(opline))) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, opline->op2_type, EX_USES_STRICT_TYPES(), &garbage);
if (0 || UNEXPECTED(RETURN_VALUE_USED(opline))) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} In this case, the This is a minor point, I thought it was a little confusing which is why I mentioned it but it doesn't do any harm. Do you think given that this targets Zend/tests/gh10168_8.phpt fails with I'll now look over all cases again to make sure we've caught every case. I'll create a new PR afterwards. |
Probably we should use |
I think the inner if (1 || UNEXPECTED(RETURN_VALUE_USED(opline))) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, opline->op2_type, EX_USES_STRICT_TYPES(), &garbage);
if (0 && UNEXPECTED(RETURN_VALUE_USED(opline))) {
ZVAL_COPY(EX_VAR(opline->result.var), value);
} Keeping the |
SPEC+!RETURN_VALUE_USED if (0 || UNEXPECTED(0)) {
// the reset doesn't matter because we always take the "else" part SPEC+RETRUN_VALUE_USED if (0 || UNEXPECTED(1)) {
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, opline->op2_type, EX_USES_STRICT_TYPES(), &garbage);
if (1 || UNEXPECTED(1)) { // we won't come here if return value is not used according to the first condition
ZVAL_COPY(EX_VAR(opline->result.var), value);
} !SPEC if (1 || UNEXPECTED(RETURN_VALUE_USED(opline))) { // we never take the "else" part
zend_refcounted *garbage = NULL;
value = zend_assign_to_variable_ex(variable_ptr, value, opline->op2_type, EX_USES_STRICT_TYPES(), &garbage);
if (0 || UNEXPECTED(RETURN_VALUE_USED(opline))) { // we always check RETURN_VALUE_USED() here
ZVAL_COPY(EX_VAR(opline->result.var), value);
} |
No problem. You may be still right. It's better to re-check me. :) |
Sorry, I misunderstood your response and so deleted my last comment. I still think the inner
But it's not harmful so I'm happy just keeping it there 😄 |
Alternative to GH-10500 as suggested by @dstogov .
I don't know if I did it 100% correctly though...
According to Callgrind, the instructions count for Zend/bench.php is:
zend_copy_to_variable
(this approach was my first attempt, see commit log, but this caused 2 test failures in readdir tests. I probably did something wrong here...).zend_copy_to_variable
withZVAL_COPY
, I get 3,340,811,227 though (done in last commit).This is slightly higher than my first attempt in Fix GH-10168: heap-buffer-overflow at zval_undefined_cv #10500.