Skip to content

Fix GH-16414: zend_test.observer.observe_function_names may segfault #16438

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 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions ext/zend_test/observer.c
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,12 @@ static ZEND_INI_MH(zend_test_observer_OnUpdateCommaList)
zend_array **p = (zend_array **) ZEND_INI_GET_ADDR();
zend_string *funcname;
zend_function *func;
if (!ZT_G(observer_enabled)) {
return FAILURE;
}
if (stage != PHP_INI_STAGE_STARTUP && stage != PHP_INI_STAGE_ACTIVATE && stage != PHP_INI_STAGE_DEACTIVATE && stage != PHP_INI_STAGE_SHUTDOWN) {
ZEND_HASH_FOREACH_STR_KEY(*p, funcname) {
if ((func = zend_hash_find_ptr(EG(function_table), funcname))) {
if ((func = zend_hash_find_ptr(EG(function_table), funcname)) && ZEND_OBSERVER_DATA(func) != NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this. ZEND_OBSERVER_DATA() expands to &RUN_TIME_CACHE(op_array)[handle], which cannot be NULL (except for RUN_TIME_CACHE(op_array) == NULL && handle == 0, which is undefined behavior). Removing these two conditions still makes the test pass. Should this have dereferenced the pointer?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it probably must handle ZEND_OBSERVER_NOT_OBSERVED and ZEND_OBSERVER_NONE_OBSERVED. But I forgot about the details of this api.

Copy link
Member

@bwoebi bwoebi Oct 22, 2024

Choose a reason for hiding this comment

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

@iluuu1994 The value is NULL if the observer has not been initialized yet. It's non-NULL once the init handler ran, assigning a specific handler or one of these two constants. Here we only care about initialized.
But it seems to be missing a dereference, yes. (In PHP 8.3 and before it evaluated to the first value. Hence I missed this.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll have a look as soon as possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

@iluuu1994, this PR targets PHP-8.4; the definition of ZEND_OBSERVER_DATA() has changed there:

#define ZEND_OBSERVER_DATA(function) \
((zend_observer_fcall_begin_handler *)&ZEND_OP_ARRAY_EXTENSION((&(function)->common), ZEND_OBSERVER_HANDLE(function)))

That part is needed if zend_test.observer.enabled=1 upfront (default for our Circle CI job).

@bwoebi, when running the test with zend_test.observer.enabled=1, ZEND_OBSERVER_DATA(func) is NULL for me.

Copy link
Member

@iluuu1994 iluuu1994 Oct 22, 2024

Choose a reason for hiding this comment

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

It's because RUN_TIME_CACHE(&func->common) is NULL, so doing pointer arithmetics on it is UB. In that case, I think we should check whether RUN_TIME_CACHE(&func->common) is set. Note that you're lucky here, if observer had a higher handle, this check would break.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, thank you! I'll have a closer look again, and might actually go with the hash table.

void *old_handler;
zend_observer_remove_begin_handler(func, observer_begin, (zend_observer_fcall_begin_handler *)&old_handler);
zend_observer_remove_end_handler(func, observer_end, (zend_observer_fcall_end_handler *)&old_handler);
Expand All @@ -329,7 +332,7 @@ static ZEND_INI_MH(zend_test_observer_OnUpdateCommaList)
zend_string_release(str);
if (stage != PHP_INI_STAGE_STARTUP && stage != PHP_INI_STAGE_ACTIVATE && stage != PHP_INI_STAGE_DEACTIVATE && stage != PHP_INI_STAGE_SHUTDOWN) {
ZEND_HASH_FOREACH_STR_KEY(*p, funcname) {
if ((func = zend_hash_find_ptr(EG(function_table), funcname))) {
if ((func = zend_hash_find_ptr(EG(function_table), funcname)) && ZEND_OBSERVER_DATA(func) != NULL) {
zend_observer_add_begin_handler(func, observer_begin);
zend_observer_add_end_handler(func, observer_end);
}
Expand Down
13 changes: 13 additions & 0 deletions ext/zend_test/tests/gh16414.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
GH-16414 (zend_test.observer.observe_function_names may segfault)
--EXTENSIONS--
zend_test
--INI--
zend_test.observer.enabled=0
--FILE--
<?php
function bar() {}
var_dump(ini_set("zend_test.observer.observe_function_names", "bar"));
?>
--EXPECT--
bool(false)