Commit 3dab5859122 for php.net

commit 3dab58591220c6b4da6c10755f47f11bde74a2b4
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Tue Apr 7 11:50:39 2026 -0400

    Fix GH-16811: Crash in zend_test observer on runtime observe_function_names change (GH-21635)

    OnUpdateCommaList called zend_observer_remove/add_begin_handler without
    checking whether observer data was initialized. This null-dereferenced
    when the function had never been called (no runtime cache), and hit
    ZEND_UNREACHABLE() when observe_all had already installed the same
    handler.

    Guard both the remove and add blocks with runtime cache checks. Remove
    existing handlers before re-adding to prevent slot overflow from
    duplicates.

    Fixes GH-16811

diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c
index 85c7d82da0e..e348987359b 100644
--- a/ext/zend_test/observer.c
+++ b/ext/zend_test/observer.c
@@ -334,7 +334,8 @@ static ZEND_INI_MH(zend_test_observer_OnUpdateCommaList)
 	}
 	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))
+					&& RUN_TIME_CACHE(&func->common)) {
 				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);
@@ -357,7 +358,11 @@ 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))
+						&& RUN_TIME_CACHE(&func->common) && *ZEND_OBSERVER_DATA(func)) {
+					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);
 					zend_observer_add_begin_handler(func, observer_begin);
 					zend_observer_add_end_handler(func, observer_end);
 				}
diff --git a/ext/zend_test/tests/gh16811.phpt b/ext/zend_test/tests/gh16811.phpt
new file mode 100644
index 00000000000..1b2b8ece9aa
--- /dev/null
+++ b/ext/zend_test/tests/gh16811.phpt
@@ -0,0 +1,18 @@
+--TEST--
+GH-16811 (Segmentation fault in zend observer)
+--EXTENSIONS--
+zend_test
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.show_output=1
+zend_test.observer.observe_function_names=a,d
+--FILE--
+<?php
+var_dump(ini_set("zend_test.observer.observe_function_names", "bar"));
+function d() {}
+?>
+--EXPECTF--
+<!-- init '%s' -->
+<!-- init ini_set() -->
+<!-- init var_dump() -->
+string(3) "a,d"
diff --git a/ext/zend_test/tests/gh16811_observe_all.phpt b/ext/zend_test/tests/gh16811_observe_all.phpt
new file mode 100644
index 00000000000..466aa0c6d4a
--- /dev/null
+++ b/ext/zend_test/tests/gh16811_observe_all.phpt
@@ -0,0 +1,17 @@
+--TEST--
+GH-16811 (Assertion failure adding duplicate observer handler)
+--EXTENSIONS--
+zend_test
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_output=0
+--FILE--
+<?php
+function foo() {}
+foo();
+ini_set("zend_test.observer.observe_function_names", "foo");
+echo "Done\n";
+?>
+--EXPECT--
+Done