Skip to content
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

Fix GH-17941: Stack-use-after-return with lazy objects and hooks #17947

Closed
wants to merge 1 commit into from

Conversation

nielsdos
Copy link
Member

@nielsdos nielsdos commented Feb 27, 2025

zend_std_write_property() can return the variable pointer, but the code
was using a local variable, and so a pointer to a local variable could
be returned. Fix this by using the value pointer instead of the backup
value was written.

zend_std_write_property() can return the variable pointer, but the code
was using a local variable, and so a pointer to a local variable could
be returned. Fix this by using the value pointer instead of the backup
value was written.
This can be more efficient on master by using the safe_assign helper.
@nielsdos
Copy link
Member Author

Actually, now the value between variable_ptr and value can be inconsistent. And it's a bit annoying that we can't "just" add a reference because the backupping of the value influences the semantics (i.e. is observable). I'm not going to deal with this.

@nielsdos nielsdos closed this Feb 27, 2025
@arnaud-lb
Copy link
Member

I think the observable semantic change is right, because the current behavior is wrong.

Currently, $a->prop = rhs evaluates to either:

  1. The value of rhs if the assignment was handled by __set or a set hook
  2. The value of $a->prop after the assignment, in other cases

In the first case, when rhs is a CV, it evaluates to the value of rhs after the assignment. I'm not sure this is by design, but this is the behavior without lazy objects. When $a is a lazy object, this always evaluates to the value of rhs before the assignment, due to the backup.

Your change makes the behavior consistent, so I think it's right.

@nielsdos
Copy link
Member Author

nielsdos commented Mar 6, 2025

Okay, reopening for review then. I closed it because I didn't see an easy way to make the behaviour the same and didn't want to deal with this, but if the new behaviour is right then sure.

@nielsdos nielsdos reopened this Mar 6, 2025
@nielsdos nielsdos marked this pull request as ready for review March 6, 2025 18:50
@nielsdos nielsdos requested a review from dstogov as a code owner March 6, 2025 18:50
@nielsdos nielsdos requested review from arnaud-lb and iluuu1994 and removed request for dstogov March 6, 2025 18:50
@iluuu1994
Copy link
Member

I found another issue:

class C {
    public $prop {
        set {
            echo "set\n";
            $this->prop = $value * 2;
        }
    }
}

$rc = new ReflectionClass(C::class);

$obj = $rc->newLazyProxy(function () {
    echo "init\n";
    return new C;
});

function foo(C $c) {
    $c->prop = 1;
    var_dump($c->prop);
}

foo($obj);
set
init
set
int(4)

As you can see, the original 1 turns into 4. This happens because init() is called twice. That's because lazy init will repeat the assignment by calling zend_std_write_property(), in which zend_should_call_hook() will return true due to the parent frame calling the hook on the proxy, and the child calling it on the actual object. This could be solved in two ways:

  • Continue calling set twice, but pass the original value (1 in this case) again, rather than the result from the last set call.
  • Skip the set call for the second assignment, by considering proxy and object as equal somehow. This sounds like the better approach to me.

WDYT?

@nielsdos
Copy link
Member Author

nielsdos commented Mar 7, 2025

My poor head... Option 2 sounds best, I have yet to dig into the code that deals with proxies though. The first option would be bad because of observable side effects.

@arnaud-lb
Copy link
Member

arnaud-lb commented Mar 7, 2025

I also prefer the second approach, as it’s what would happen if the hook was a method.

It’s consistent with how lazy objects are specified: Interaction with the proxy executes the proxy’s code, and property accesses forward to the real instance. $this->prop = $value * 2 in the hook means “set the backing property”, so it makes sense to forward to the backing store of the real instance directly.

A third approach would have been to initialize before calling the hook, and to call the hook on the real instance. But the second approach is better.

Potentially this can be fixed separately. @nielsdos I can take care of it if you don’t want to.

@nielsdos
Copy link
Member Author

nielsdos commented Mar 7, 2025

A third approach would have been to initialize before calling the hook, and to call the hook on the real instance. But the second approach is better.

Yes, I still prefer the second approach as well, it's the most logical.

Potentially this can be fixed separately. @nielsdos I can take care of it if you don’t want to.

I agree this may be fixed separately. Feel free to give it a shot, I'm kinda busy right now 🙂

@iluuu1994
Copy link
Member

Handling this separately sounds good to me! Feel free to merge this one, I'll create a new issue.

@arnaud-lb zend_should_call_hook() is normally skipped when SIMPLE_GET or READ is set, which should hopefully be most of the time. A call to zend_object_is_lazy() and then fetching the real object should not be too expensive. I can take care of this.

@nielsdos nielsdos closed this in 38e8725 Mar 7, 2025
@arnaud-lb
Copy link
Member

Thank you @nielsdos!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Stack-use-after-return with lazy objects and hooks
3 participants