From 8d2df86b060657bb10e1e9a8692c8972b4cc2083 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Sat, 25 Nov 2023 00:54:02 +0100 Subject: [PATCH 1/2] Fix invalid opline in OOM handlers within ZEND_FUNC_GET_ARGS and ZEND_BIND_STATIC (#12768) * fix segfault in `ZEND_BIND_STATIC` In case a `ZEND_BIND_STATIC` is being executed, while the current chunk is full, the `zend_array_dup()` call will trigger a OOM in ZendMM which will crash, as the opline might be a dangling pointer. * add missing test * `assert()`ing seems easier than trying to make the compiler to not optimize * moved from function call to INI setting, so we can use this in other places as well * make `assert()` work no NDEBUG builds * document magic number * fix segfault in `ZEND_FUNC_GET_ARGS` In case a `ZEND_FUNC_GET_ARGS` is being executed, while the current chunk is full, the `zend_new_array()` call will trigger a OOM in ZendMM which will crash, as the opline might be a dangling pointer. --------- Co-authored-by: Florian Engelhardt --- Zend/zend_vm_def.h | 4 +- Zend/zend_vm_execute.h | 5 +- ext/zend_test/php_test.h | 3 + ext/zend_test/test.c | 77 +++++++++++++++++++++ ext/zend_test/tests/opline_dangling.phpt | 33 +++++++++ ext/zend_test/tests/opline_dangling_02.phpt | 36 ++++++++++ 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 ext/zend_test/tests/opline_dangling.phpt create mode 100644 ext/zend_test/tests/opline_dangling_02.phpt diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index fd5b7242ba6..a6f86a9eab6 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -8788,6 +8788,8 @@ ZEND_VM_HANDLER(183, ZEND_BIND_STATIC, CV, UNUSED, REF) variable_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_W); + SAVE_OPLINE(); + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ht = zend_array_dup(EX(func)->op_array.static_variables); @@ -8797,7 +8799,6 @@ ZEND_VM_HANDLER(183, ZEND_BIND_STATIC, CV, UNUSED, REF) value = (zval*)((char*)ht->arData + (opline->extended_value & ~(ZEND_BIND_REF|ZEND_BIND_IMPLICIT|ZEND_BIND_EXPLICIT))); - SAVE_OPLINE(); if (opline->extended_value & ZEND_BIND_REF) { if (Z_TYPE_P(value) == IS_CONSTANT_AST) { if (UNEXPECTED(zval_update_constant_ex(value, EX(func)->op_array.scope) != SUCCESS)) { @@ -9250,6 +9251,7 @@ ZEND_VM_HANDLER(172, ZEND_FUNC_GET_ARGS, UNUSED|CONST, UNUSED) } if (result_size) { + SAVE_OPLINE(); uint32_t first_extra_arg = EX(func)->op_array.num_args; ht = zend_new_array(result_size); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index bc098148541..5ee1c2c94da 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -10700,6 +10700,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FUNC_GET_ARGS_SPEC_CONST_UNUSE } if (result_size) { + SAVE_OPLINE(); uint32_t first_extra_arg = EX(func)->op_array.num_args; ht = zend_new_array(result_size); @@ -36064,6 +36065,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FUNC_GET_ARGS_SPEC_UNUSED_UNUS } if (result_size) { + SAVE_OPLINE(); uint32_t first_extra_arg = EX(func)->op_array.num_args; ht = zend_new_array(result_size); @@ -48471,6 +48473,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_BIND_STATIC_SPEC_CV_UNUSED_HAN variable_ptr = EX_VAR(opline->op1.var); + SAVE_OPLINE(); + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ht = zend_array_dup(EX(func)->op_array.static_variables); @@ -48480,7 +48484,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_BIND_STATIC_SPEC_CV_UNUSED_HAN value = (zval*)((char*)ht->arData + (opline->extended_value & ~(ZEND_BIND_REF|ZEND_BIND_IMPLICIT|ZEND_BIND_EXPLICIT))); - SAVE_OPLINE(); if (opline->extended_value & ZEND_BIND_REF) { if (Z_TYPE_P(value) == IS_CONSTANT_AST) { if (UNEXPECTED(zval_update_constant_ex(value, EX(func)->op_array.scope) != SUCCESS)) { diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index 05f658e91bd..9d0ca087ce6 100644 --- a/ext/zend_test/php_test.h +++ b/ext/zend_test/php_test.h @@ -52,6 +52,9 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test) HashTable global_weakmap; int replace_zend_execute_ex; int register_passes; + int observe_opline_in_zendmm; + zend_mm_heap* zend_orig_heap; + zend_mm_heap* zend_test_heap; zend_test_fiber *active_fiber; ZEND_END_MODULE_GLOBALS(zend_test) diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 2f3ff6d9009..c2a6bd18a54 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -30,8 +30,15 @@ #include "zend_interfaces.h" #include "zend_weakrefs.h" #include "Zend/Optimizer/zend_optimizer.h" +#include "Zend/zend_alloc.h" #include "test_arginfo.h" +// `php.h` sets `NDEBUG` when not `PHP_DEBUG` which will make `assert()` from +// assert.h a no-op. In order to have `assert()` working on NDEBUG builds, we +// undefine `NDEBUG` and re-include assert.h +#undef NDEBUG +#include "assert.h" + #if defined(HAVE_LIBXML) && !defined(PHP_WIN32) # include # include @@ -364,6 +371,68 @@ static ZEND_FUNCTION(zend_test_crash) php_printf("%s", invalid); } +static bool has_opline(zend_execute_data *execute_data) +{ + return execute_data + && execute_data->func + && ZEND_USER_CODE(execute_data->func->type) + && execute_data->opline + ; +} + +void * zend_test_custom_malloc(size_t len) +{ + if (has_opline(EG(current_execute_data))) { + assert(EG(current_execute_data)->opline->lineno != (uint32_t)-1); + } + return _zend_mm_alloc(ZT_G(zend_orig_heap), len ZEND_FILE_LINE_EMPTY_CC ZEND_FILE_LINE_EMPTY_CC); +} + +void zend_test_custom_free(void *ptr) +{ + if (has_opline(EG(current_execute_data))) { + assert(EG(current_execute_data)->opline->lineno != (uint32_t)-1); + } + _zend_mm_free(ZT_G(zend_orig_heap), ptr ZEND_FILE_LINE_EMPTY_CC ZEND_FILE_LINE_EMPTY_CC); +} + +void * zend_test_custom_realloc(void * ptr, size_t len) +{ + if (has_opline(EG(current_execute_data))) { + assert(EG(current_execute_data)->opline->lineno != (uint32_t)-1); + } + return _zend_mm_realloc(ZT_G(zend_orig_heap), ptr, len ZEND_FILE_LINE_EMPTY_CC ZEND_FILE_LINE_EMPTY_CC); +} + +static PHP_INI_MH(OnUpdateZendTestObserveOplineInZendMM) +{ + if (new_value == NULL) { + return FAILURE; + } + + int int_value = zend_ini_parse_bool(new_value); + + if (int_value == 1) { + // `zend_mm_heap` is a private struct, so we have not way to find the + // actual size, but 4096 bytes should be enough + ZT_G(zend_test_heap) = malloc(4096); + memset(ZT_G(zend_test_heap), 0, 4096); + zend_mm_set_custom_handlers( + ZT_G(zend_test_heap), + zend_test_custom_malloc, + zend_test_custom_free, + zend_test_custom_realloc + ); + ZT_G(zend_orig_heap) = zend_mm_get_heap(); + zend_mm_set_heap(ZT_G(zend_test_heap)); + } else if (ZT_G(zend_test_heap)) { + free(ZT_G(zend_test_heap)); + ZT_G(zend_test_heap) = NULL; + zend_mm_set_heap(ZT_G(zend_orig_heap)); + } + return OnUpdateBool(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage); +} + static ZEND_FUNCTION(zend_test_is_pcre_bundled) { ZEND_PARSE_PARAMETERS_NONE(); @@ -558,6 +627,7 @@ static ZEND_METHOD(ZendTestChildClassWithMethodWithParameterAttribute, override) PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("zend_test.replace_zend_execute_ex", "0", PHP_INI_SYSTEM, OnUpdateBool, replace_zend_execute_ex, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.register_passes", "0", PHP_INI_SYSTEM, OnUpdateBool, register_passes, zend_zend_test_globals, zend_test_globals) + STD_PHP_INI_BOOLEAN("zend_test.observe_opline_in_zendmm", "0", PHP_INI_ALL, OnUpdateZendTestObserveOplineInZendMM, observe_opline_in_zendmm, zend_zend_test_globals, zend_test_globals) PHP_INI_END() void (*old_zend_execute_ex)(zend_execute_data *execute_data); @@ -700,6 +770,13 @@ PHP_RSHUTDOWN_FUNCTION(zend_test) zend_weakrefs_hash_del(&ZT_G(global_weakmap), (zend_object *)(uintptr_t)objptr); } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&ZT_G(global_weakmap)); + + if (ZT_G(zend_test_heap)) { + free(ZT_G(zend_test_heap)); + ZT_G(zend_test_heap) = NULL; + zend_mm_set_heap(ZT_G(zend_orig_heap)); + } + return SUCCESS; } diff --git a/ext/zend_test/tests/opline_dangling.phpt b/ext/zend_test/tests/opline_dangling.phpt new file mode 100644 index 00000000000..b411223cecf --- /dev/null +++ b/ext/zend_test/tests/opline_dangling.phpt @@ -0,0 +1,33 @@ +--TEST-- +possible segfault in `ZEND_BIND_STATIC` +--DESCRIPTION-- +https://github.com/php/php-src/pull/12758 +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observe_opline_in_zendmm=1 +--FILE-- + +--EXPECT-- +int(1) +string(1) "x" +int(1) +int(5) +Done. diff --git a/ext/zend_test/tests/opline_dangling_02.phpt b/ext/zend_test/tests/opline_dangling_02.phpt new file mode 100644 index 00000000000..1537bc68c18 --- /dev/null +++ b/ext/zend_test/tests/opline_dangling_02.phpt @@ -0,0 +1,36 @@ +--TEST-- +possible segfault in `ZEND_FUNC_GET_ARGS` +--DESCRIPTION-- +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observe_opline_in_zendmm=1 +--FILE-- + +--EXPECT-- +int(1) +string(1) "x" +int(1) +array(2) { + [0]=> + string(6) "string" + [1]=> + int(0) +} +Done. From 1305ea23cee98e91092641c0ec39f17e09f29882 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Sat, 25 Nov 2023 00:57:22 +0100 Subject: [PATCH 2/2] Add NEWS entry for GH-12768 --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index b8d860bd3d9..a528a8f878f 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,8 @@ PHP NEWS error handler). (ilutov) . Fixed oss-fuzz #64209 (In-place modification of filename in php_message_handler_for_zend). (ilutov) + . Fixed bug GH-12758 / GH-12768 (Invalid opline in OOM handlers within + ZEND_FUNC_GET_ARGS and ZEND_BIND_STATIC). (Florian Engelhardt) - DOM: . Fixed bug GH-12616 (DOM: Removing XMLNS namespace node results in invalid