Skip to content
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

Fix #2030: Segmentation fault with autoloaders bailing out #2037

Merged
merged 2 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 31 additions & 0 deletions tests/ext/sandbox-regression/class_resolver_bailout_hook.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Assert bailouts are gracefully handled within class autoloading
--SKIPIF--
<?php if (PHP_VERSION_ID >= 70300 && PHP_VERSION_ID < 70400) die('skip: Bailing out in autoloaders is fundamentally broken in PHP 7.3'); ?>
Copy link
Member

Choose a reason for hiding this comment

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

question (non-blocking): What would be the expected behaviour in PHP 7.3, segfault?
Do we document such things somewhere, just in case another customer experiences this problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this segfaults. It's plain and simple a bug in PHP 7.3 itself.

For now I'm not aware of such documentation.

--ENV--
DD_TRACE_DEBUG=1
--FILE--
<?php

ddtrace\trace_function("x", function() { class C extends D {} print "Should not appear...\n"; });

function x() {}

spl_autoload_register(function() use (&$s) {
if ($s) {
trigger_error("No D", E_USER_ERROR);
} else {
$s = true;
x();
class B {}
print "Leaving Autoloader\n";
}
});

class A extends B {}

?>
--EXPECTF--
Error raised in ddtrace's closure defined at %s:%d for x(): No D in %s
Leaving Autoloader
Flushing trace of size 2 to send-queue for %s
7 changes: 5 additions & 2 deletions zend_abstract_interface/interceptor/php7/interceptor.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,11 @@ static int zai_interceptor_fast_ret_handler(zend_execute_data *execute_data) {
return prev_fast_ret_handler ? prev_fast_ret_handler(execute_data) : ZEND_USER_OPCODE_DISPATCH;
}

void zai_interceptor_check_for_opline_before_exception(void);
void zai_interceptor_check_for_opline_before_exception(zend_execute_data *execute_data);
static user_opcode_handler_t prev_handle_exception_handler;
static int zai_interceptor_handle_exception_handler(zend_execute_data *execute_data) {
// not everything goes through zend_throw_exception_hook, in particular when zend_rethrow_exception alone is used (e.g. during zend_call_function)
zai_interceptor_check_for_opline_before_exception();
zai_interceptor_check_for_opline_before_exception(execute_data);

zai_interceptor_frame_memory *frame_memory;
if (ZEND_HANDLE_EXCEPTION == EX(opline)->opcode && zai_hook_memory_table_find(execute_data, &frame_memory)) {
Expand Down Expand Up @@ -1170,7 +1170,10 @@ static void zai_hook_memory_dtor(zval *zv) {
efree(Z_PTR_P(zv));
}

void zai_interceptor_reset_resolver(void);
void zai_interceptor_activate() {
zai_interceptor_reset_resolver();

zend_hash_init(&zai_hook_memory, 8, nothing, zai_hook_memory_dtor, 0);
}

Expand Down
34 changes: 28 additions & 6 deletions zend_abstract_interface/interceptor/php7/resolver.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ PHP_FUNCTION(zai_interceptor_resolve_after_class_alias) {
#define ZAI_INTERCEPTOR_POST_DECLARE_OP 224 // random 8 bit number greater than ZEND_VM_LAST_OPCODE
static zend_op zai_interceptor_post_declare_op;
ZEND_TLS zend_op zai_interceptor_post_declare_ops[4];
struct zai_interceptor_opline { const zend_op *op; struct zai_interceptor_opline *prev; };
struct zai_interceptor_opline { const zend_op *op; const zend_execute_data *execute_data; struct zai_interceptor_opline *prev; };
ZEND_TLS struct zai_interceptor_opline zai_interceptor_opline_before_binding = {0};
static void zai_interceptor_install_post_declare_op(zend_execute_data *execute_data) {
// We replace the current opline *before* it is executed. Thus we need to preserve opline data first:
Expand Down Expand Up @@ -109,10 +109,24 @@ static void zai_interceptor_install_post_declare_op(zend_execute_data *execute_d
zai_interceptor_opline_before_binding.prev = backup;
}
zai_interceptor_opline_before_binding.op = EX(opline);
zai_interceptor_opline_before_binding.execute_data = execute_data;
EX(opline) = zai_interceptor_post_declare_ops;
}

static void zai_interceptor_pop_opline_before_binding() {
static void zai_interceptor_pop_opline_before_binding(zend_execute_data *execute_data) {
// Normally the zai_interceptor_opline_before_binding stack should be in sync with the actual executing stack, but it might not after bailouts
if (execute_data) {
if (zai_interceptor_opline_before_binding.execute_data == execute_data) {
return;
}

while (zai_interceptor_opline_before_binding.prev && zai_interceptor_opline_before_binding.prev->execute_data != execute_data) {
struct zai_interceptor_opline *backup = zai_interceptor_opline_before_binding.prev;
zai_interceptor_opline_before_binding = *backup;
efree(backup);
}
}

struct zai_interceptor_opline *backup = zai_interceptor_opline_before_binding.prev;
if (backup) {
zai_interceptor_opline_before_binding = *backup;
Expand Down Expand Up @@ -171,8 +185,9 @@ static int zai_interceptor_post_declare_handler(zend_execute_data *execute_data)
}
}
// preserve offset
zai_interceptor_pop_opline_before_binding(execute_data);
EX(opline) = zai_interceptor_opline_before_binding.op + (EX(opline) - &zai_interceptor_post_declare_ops[0]);
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
return ZEND_USER_OPCODE_CONTINUE;
} else if (prev_post_declare_handler) {
return prev_post_declare_handler(execute_data);
Expand Down Expand Up @@ -244,10 +259,11 @@ static int zai_interceptor_add_interface_handler(zend_execute_data *execute_data
}
#endif

void zai_interceptor_check_for_opline_before_exception(void) {
void zai_interceptor_check_for_opline_before_exception(zend_execute_data *execute_data) {
if (EG(opline_before_exception) == zai_interceptor_post_declare_ops) {
zai_interceptor_pop_opline_before_binding(execute_data);
EG(opline_before_exception) = zai_interceptor_opline_before_binding.op;
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
}
}

Expand All @@ -256,14 +272,20 @@ static void zai_interceptor_exception_hook(zval *ex) {
zend_function *func = EG(current_execute_data)->func;
if (func && ZEND_USER_CODE(func->type) && EG(current_execute_data)->opline == zai_interceptor_post_declare_ops) {
// called right before setting EG(opline_before_exception), reset to original value to ensure correct throw_op handling
zai_interceptor_pop_opline_before_binding(EG(current_execute_data));
EG(current_execute_data)->opline = zai_interceptor_opline_before_binding.op;
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
}
if (prev_exception_hook) {
prev_exception_hook(ex);
}
}

void zai_interceptor_reset_resolver() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor nit.

Suggested change
void zai_interceptor_reset_resolver() {
void zai_interceptor_reset_resolver(void) {

// reset in case a prior request had a bailout
zai_interceptor_opline_before_binding = (struct zai_interceptor_opline){0};
}

// post startup hook to be after opcache
void zai_interceptor_setup_resolving_post_startup(void) {
prev_compile_file = zend_compile_file;
Expand Down
7 changes: 7 additions & 0 deletions zend_abstract_interface/interceptor/php8/interceptor.c
Original file line number Diff line number Diff line change
Expand Up @@ -818,9 +818,16 @@ static void zai_hook_memory_dtor(zval *zv) {
efree(Z_PTR_P(zv));
}

#if PHP_VERSION_ID < 80200
void zai_interceptor_reset_resolver(void);
#endif
void zai_interceptor_activate(void) {
zend_hash_init(&zai_hook_memory, 8, nothing, zai_hook_memory_dtor, 0);
zend_hash_init(&zai_interceptor_implicit_generators, 8, nothing, NULL, 0);

#if PHP_VERSION_ID < 80200
zai_interceptor_reset_resolver();
#endif
}

void zai_interceptor_deactivate(void) {
Expand Down
32 changes: 27 additions & 5 deletions zend_abstract_interface/interceptor/php8/resolver_pre-8_2.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ PHP_FUNCTION(zai_interceptor_resolve_after_class_alias) {

static zend_op zai_interceptor_post_declare_op;
ZEND_TLS zend_op zai_interceptor_post_declare_ops[4];
struct zai_interceptor_opline { const zend_op *op; struct zai_interceptor_opline *prev; };
struct zai_interceptor_opline { const zend_op *op; const zend_execute_data *execute_data; struct zai_interceptor_opline *prev; };
ZEND_TLS struct zai_interceptor_opline zai_interceptor_opline_before_binding = {0};
static void zai_interceptor_install_post_declare_op(zend_execute_data *execute_data) {
// We replace the current opline *before* it is executed. Thus we need to preserve opline data first:
Expand Down Expand Up @@ -110,10 +110,24 @@ static void zai_interceptor_install_post_declare_op(zend_execute_data *execute_d
zai_interceptor_opline_before_binding.prev = backup;
}
zai_interceptor_opline_before_binding.op = EX(opline);
zai_interceptor_opline_before_binding.execute_data = execute_data;
EX(opline) = zai_interceptor_post_declare_ops;
}

static void zai_interceptor_pop_opline_before_binding() {
static void zai_interceptor_pop_opline_before_binding(zend_execute_data *execute_data) {
// Normally the zai_interceptor_opline_before_binding stack should be in sync with the actual executing stack, but it might not after bailouts
if (execute_data) {
if (zai_interceptor_opline_before_binding.execute_data == execute_data) {
return;
}

while (zai_interceptor_opline_before_binding.prev && zai_interceptor_opline_before_binding.prev->execute_data != execute_data) {
struct zai_interceptor_opline *backup = zai_interceptor_opline_before_binding.prev;
zai_interceptor_opline_before_binding = *backup;
efree(backup);
}
}

struct zai_interceptor_opline *backup = zai_interceptor_opline_before_binding.prev;
if (backup) {
zai_interceptor_opline_before_binding = *backup;
Expand Down Expand Up @@ -154,8 +168,9 @@ static int zai_interceptor_post_declare_handler(zend_execute_data *execute_data)
}
}
// preserve offset
zai_interceptor_pop_opline_before_binding(execute_data);
EX(opline) = zai_interceptor_opline_before_binding.op + (EX(opline) - &zai_interceptor_post_declare_ops[0]);
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
return ZEND_USER_OPCODE_CONTINUE;
} else if (prev_post_declare_handler) {
return prev_post_declare_handler(execute_data);
Expand Down Expand Up @@ -237,8 +252,9 @@ static user_opcode_handler_t prev_handle_exception_handler;
static int zai_interceptor_handle_exception_handler(zend_execute_data *execute_data) {
// not everything goes through zend_throw_exception_hook, in particular when zend_rethrow_exception alone is used (e.g. during zend_call_function)
if (EG(opline_before_exception) == zai_interceptor_post_declare_ops) {
zai_interceptor_pop_opline_before_binding(execute_data);
EG(opline_before_exception) = zai_interceptor_opline_before_binding.op;
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
}

return prev_handle_exception_handler ? prev_handle_exception_handler(execute_data) : ZEND_USER_OPCODE_DISPATCH;
Expand All @@ -249,14 +265,20 @@ static void zai_interceptor_exception_hook(zend_object *ex) {
zend_function *func = EG(current_execute_data)->func;
if (func && ZEND_USER_CODE(func->type) && EG(current_execute_data)->opline == zai_interceptor_post_declare_ops) {
// called right before setting EG(opline_before_exception), reset to original value to ensure correct throw_op handling
zai_interceptor_pop_opline_before_binding(EG(current_execute_data));
EG(current_execute_data)->opline = zai_interceptor_opline_before_binding.op;
zai_interceptor_pop_opline_before_binding();
zai_interceptor_pop_opline_before_binding(NULL);
}
if (prev_exception_hook) {
prev_exception_hook(ex);
}
}

void zai_interceptor_reset_resolver() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

And matching nit.

Suggested change
void zai_interceptor_reset_resolver() {
void zai_interceptor_reset_resolver(void) {

// reset in case a prior request had a bailout
zai_interceptor_opline_before_binding = (struct zai_interceptor_opline){0};
}

static void *opcache_handle;
static void zai_interceptor_find_opcache_handle(void *ext) {
zend_extension *extension = (zend_extension *)ext;
Expand Down