Skip to content

Fixed GH-16233: Observer segfault when calling user function in internal function via trampoline #16252

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
wants to merge 1 commit into from

Conversation

nielsdos
Copy link
Member

@nielsdos nielsdos commented Oct 5, 2024

In the test, I have an internal __call function for _ZendTestMagicCallForward that calls the global function with name $name via call_user_function. Note that observer writes the pointer to the previously observed frame in the last temporary of the new call frame (*prev_observed_frame).

The following happens:
First, we call $test->callee, this will be handled via a trampoline with T=2 for the two arguments. The call frame is allocated at this point. This call frame is not observed because it has ZEND_ACC_CALL_VIA_TRAMPOLINE set. Next we use ZEND_CALL_TRAMPOLINE to call the trampoline, this reuses the stack frame allocated earlier with T=2, but this time it is observed. The pointer to the previous frame is written outside of the call frame because T is too small (should be 3). We are now in the internal function _ZendTestMagicCallForward::__call where we call the global function callee. This will push a new call frame which will overlap *prev_observed_frame. This value gets overwritten by zend_init_func_execute_data when EX(opline) is set because *prev_observed_frame overlaps with EX(opline). From now on, *prev_observed_frame is corrupted. When zend_observer_fcall_end is called this will result in reading wrong value *prev_observed_frame into current_observed_frame. This causes issues in zend_observer_fcall_end_all leading to the segfault we observe.

Despite function with ZEND_ACC_CALL_VIA_TRAMPOLINE not being observed, the reuse of call frames makes problems when T is not large enough. To fix this, we make sure to add 1 to T if ZEND_OBSERVER_ENABLED is true.

…ternal function via trampoline

In the test, I have an internal `__call` function for `_ZendTestMagicCallForward` that calls the global function with name `$name` via `call_user_function`.
Note that observer writes the pointer to the previously observed frame in the last temporary of the new call frame (`*prev_observed_frame`).

The following happens:
First, we call `$test->callee`, this will be handled via a trampoline with T=2 for the two arguments. The call frame is allocated at this point. This call frame is not observed because it has `ZEND_ACC_CALL_VIA_TRAMPOLINE` set. Next we use `ZEND_CALL_TRAMPOLINE` to call the trampoline, this reuses the stack frame allocated earlier with T=2, but this time it is observed. The pointer to the previous frame is written outside of the call frame because `T` is too small (should be 3). We are now in the internal function `_ZendTestMagicCallForward::__call` where we call the global function `callee`. This will push a new call frame which will overlap `*prev_observed_frame`. This value gets overwritten by `zend_init_func_execute_data` when `EX(opline)` is set because `*prev_observed_frame` overlaps with `EX(opline)`. From now on, `*prev_observed_frame` is corrupted. When `zend_observer_fcall_end` is called this will result in reading wrong value `*prev_observed_frame` into `current_observed_frame`. This causes issues in `zend_observer_fcall_end_all` leading to the segfault we observe.

Despite function with `ZEND_ACC_CALL_VIA_TRAMPOLINE` not being observed, the reuse of call frames makes problems when `T` is not large enough.
To fix this, we make sure to add 1 to `T` if `ZEND_OBSERVER_ENABLED` is true.
@nielsdos nielsdos marked this pull request as ready for review October 5, 2024 21:32
@dstogov
Copy link
Member

dstogov commented Oct 7, 2024

Probably, this is right. cc: @bwoebi

Copy link
Member

@bwoebi bwoebi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right.

@bwoebi
Copy link
Member

bwoebi commented Oct 7, 2024

As a side-note: while looking at this I found that EG(trampoline) T and such isn't reset at all for property hooks trampoline. It's not a problem because it's an upper bound and prop hooks don't need temporaries on their own, it'll just alloc more slots than needed.

@nielsdos nielsdos closed this in e715dd0 Oct 7, 2024
@nielsdos
Copy link
Member Author

nielsdos commented Oct 7, 2024

I'll have a look at the property hook thing.

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.

3 participants