From 02c2ac0b600c3a5e625862db6af39f7e2f2561a3 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:23:15 +0100 Subject: [PATCH] Fix creating a first-class callable from FFI This causes a NULL pointer access because the scope field is being accessed in `zend_closure_call_magic`. We get to that function because of the ZEND_ACC_CALL_VIA_TRAMPOLINE fn_flag that is checked in `zend_closure_from_frame`. The `zend_closure_from_frame` function does not take into account the reserved data or the internal handler function, causing us to end up in `zend_closure_call_magic` instead of the FFI trampoline. Get rid of the flag, and store the temporary internal function in another reserved slot so we can clean that up at the end of the FFI trampoline's execution. --- ext/ffi/ffi.c | 9 +++++---- ext/ffi/tests/bug79177.phpt | 2 +- ext/ffi/tests/first_class_callable.phpt | 27 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 ext/ffi/tests/first_class_callable.phpt diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c index 9be5ac340590c..8f5f488ced55a 100644 --- a/ext/ffi/ffi.c +++ b/ext/ffi/ffi.c @@ -2148,7 +2148,6 @@ static zend_result zend_ffi_cdata_get_closure(zend_object *obj, zend_class_entry func->common.arg_flags[0] = 0; func->common.arg_flags[1] = 0; func->common.arg_flags[2] = 0; - func->common.fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE; func->common.function_name = ZSTR_KNOWN(ZEND_STR_MAGIC_INVOKE); /* set to 0 to avoid arg_info[] allocation, because all values are passed by value anyway */ func->common.num_args = 0; @@ -2161,6 +2160,7 @@ static zend_result zend_ffi_cdata_get_closure(zend_object *obj, zend_class_entry func->internal_function.reserved[0] = type; func->internal_function.reserved[1] = *(void**)cdata->ptr; + func->internal_function.reserved[2] = func; *ce_ptr = NULL; *fptr_ptr= func; @@ -2843,8 +2843,9 @@ static ZEND_FUNCTION(ffi_trampoline) /* {{{ */ exit: zend_string_release(EX(func)->common.function_name); - if (EX(func)->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { - zend_free_trampoline(EX(func)); + zend_function *func = EX(func)->internal_function.reserved[2]; + if (func) { + zend_free_trampoline(func); EX(func) = NULL; } } @@ -2899,7 +2900,6 @@ static zend_function *zend_ffi_get_func(zend_object **obj, zend_string *name, co func->common.arg_flags[0] = 0; func->common.arg_flags[1] = 0; func->common.arg_flags[2] = 0; - func->common.fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE; func->common.function_name = zend_string_copy(name); /* set to 0 to avoid arg_info[] allocation, because all values are passed by value anyway */ func->common.num_args = 0; @@ -2912,6 +2912,7 @@ static zend_function *zend_ffi_get_func(zend_object **obj, zend_string *name, co func->internal_function.reserved[0] = type; func->internal_function.reserved[1] = sym->addr; + func->internal_function.reserved[2] = func; return func; } diff --git a/ext/ffi/tests/bug79177.phpt b/ext/ffi/tests/bug79177.phpt index aaabfdbca012f..9ee3f1ec377d5 100644 --- a/ext/ffi/tests/bug79177.phpt +++ b/ext/ffi/tests/bug79177.phpt @@ -32,7 +32,7 @@ echo "done\n"; --EXPECTF-- Warning: Uncaught RuntimeException: Not allowed in %s:%d Stack trace: -#0 %s(%d): {closure}() +#0 [internal function]: {closure}() #1 %s(%d): FFI->bug79177() #2 {main} thrown in %s on line %d diff --git a/ext/ffi/tests/first_class_callable.phpt b/ext/ffi/tests/first_class_callable.phpt new file mode 100644 index 0000000000000..c5e60ef5b1bd1 --- /dev/null +++ b/ext/ffi/tests/first_class_callable.phpt @@ -0,0 +1,27 @@ +--TEST-- +Bug: Cannot create first-class callable from CData function +--EXTENSIONS-- +ffi +--SKIPIF-- + +--INI-- +ffi.enable=1 +--FILE-- +printf("Hello world\n"); +$closure = $libc->printf; +$closure("%s %s\n", "Hello", "world"); +$closure = $libc->printf(...); +$closure("%s %s\n", "Hello", "world"); +?> +--EXPECT-- +Hello world +Hello world +Hello world