Skip to content

Prefer userfaultfd over mprotect+SIGSEGV signal handling on linux for phpdbg watchpoints #7551

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 1 commit 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
22 changes: 22 additions & 0 deletions sapi/phpdbg/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ if test "$BUILD_PHPDBG" = "" && test "$PHP_PHPDBG" != "no"; then
AC_MSG_RESULT([disabled])
fi

AC_CACHE_CHECK([for userfaultfd faulting on write-protected memory support], ac_phpdbg_userfaultfd_writefault, AC_COMPILE_IFELSE([AC_LANG_SOURCE([[
#include <linux/userfaultfd.h>
#ifndef UFFDIO_WRITEPROTECT_MODE_WP
# error userfaults on write-protected memory not supported
#endif
]])], [ac_phpdbg_userfaultfd_writefault=yes], [ac_phpdbg_userfaultfd_writefault=no]))
if test "$ac_phpdbg_userfaultfd_writefault" = "yes"; then
if test "$enable_zts" != "yes"; then
dnl Add pthreads linker and compiler flags for userfaultfd background thread
if test -n "$ac_cv_pthreads_lib"; then
LIBS="$LIBS -l$ac_cv_pthreads_lib"
fi
if test -n "$ac_cv_pthreads_cflags"; then
CFLAGS="$CFLAGS $ac_cv_pthreads_cflags"
fi

PTHREADS_FLAGS
fi

AC_DEFINE(HAVE_USERFAULTFD_WRITEFAULT, 1, [Whether faulting on write-protected memory support can be compiled for userfaultfd])
fi

PHP_SUBST(PHP_PHPDBG_CFLAGS)
PHP_SUBST(PHP_PHPDBG_FILES)
PHP_SUBST(PHPDBG_EXTRA_LIBS)
Expand Down
14 changes: 12 additions & 2 deletions sapi/phpdbg/phpdbg.c
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ static inline void php_phpdbg_globals_ctor(zend_phpdbg_globals *pg) /* {{{ */

pg->cur_command = NULL;
pg->last_line = 0;

#ifdef HAVE_USERFAULTFD_WRITEFAULT
pg->watch_userfaultfd = 0;
pg->watch_userfault_thread = 0;
#endif
} /* }}} */

static PHP_MINIT_FUNCTION(phpdbg) /* {{{ */
Expand Down Expand Up @@ -1499,8 +1504,13 @@ int main(int argc, char **argv) /* {{{ */
}

#ifndef _WIN32
zend_try { zend_sigaction(SIGSEGV, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
zend_try { zend_sigaction(SIGBUS, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
#ifdef HAVE_USERFAULTFD_WRITEFAULT
if (!PHPDBG_G(watch_userfaultfd))
#endif
{
zend_try { zend_sigaction(SIGSEGV, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
zend_try { zend_sigaction(SIGBUS, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
}
#endif
zend_try { zend_signal(SIGINT, phpdbg_sigint_handler); } zend_end_try();

Expand Down
4 changes: 4 additions & 0 deletions sapi/phpdbg/phpdbg.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ ZEND_BEGIN_MODULE_GLOBALS(phpdbg)

#ifndef _WIN32
struct sigaction old_sigsegv_signal; /* segv signal handler */
#endif
#ifdef HAVE_USERFAULTFD_WRITEFAULT
int watch_userfaultfd; /* userfaultfd(2) handler, 0 if unused */
pthread_t watch_userfault_thread; /* thread for watch fault handling */
#endif
phpdbg_btree watchpoint_tree; /* tree with watchpoints */
phpdbg_btree watch_HashTables; /* tree with original dtors of watchpoints */
Expand Down
123 changes: 108 additions & 15 deletions sapi/phpdbg/phpdbg_watch.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@
# include <sys/mman.h>
#endif

#ifdef HAVE_USERFAULTFD_WRITEFAULT
# include <pthread.h>
# include <linux/userfaultfd.h>
# include <sys/ioctl.h>
# include <sys/syscall.h>
#endif

ZEND_EXTERN_MODULE_GLOBALS(phpdbg)

const phpdbg_command_t phpdbg_watch_commands[] = {
Expand Down Expand Up @@ -208,9 +215,9 @@ void phpdbg_print_watch_diff(phpdbg_watchtype type, zend_string *name, void *old
}

/* ### LOW LEVEL WATCHPOINT HANDLING ### */
static phpdbg_watchpoint_t *phpdbg_check_for_watchpoint(void *addr) {
static phpdbg_watchpoint_t *phpdbg_check_for_watchpoint(phpdbg_btree *tree, void *addr) {
phpdbg_watchpoint_t *watch;
phpdbg_btree_result *result = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), (zend_ulong) phpdbg_get_page_boundary(addr) + phpdbg_pagesize - 1);
phpdbg_btree_result *result = phpdbg_btree_find_closest(tree, (zend_ulong) phpdbg_get_page_boundary(addr) + phpdbg_pagesize - 1);

if (result == NULL) {
return NULL;
Expand All @@ -228,8 +235,38 @@ static phpdbg_watchpoint_t *phpdbg_check_for_watchpoint(void *addr) {
}

static void phpdbg_change_watchpoint_access(phpdbg_watchpoint_t *watch, int access) {
void *page_addr = phpdbg_get_page_boundary(watch->addr.ptr);
size_t size = phpdbg_get_total_page_size(watch->addr.ptr, watch->size);
#ifdef HAVE_USERFAULTFD_WRITEFAULT
if (PHPDBG_G(watch_userfaultfd)) {
struct uffdio_range range = {
.start = (__u64) page_addr,
.len = size
};
if (access == PROT_READ) {
struct uffdio_register reg = {
.mode = UFFDIO_REGISTER_MODE_WP,
.range = range
};
struct uffdio_writeprotect protect = {
.mode = UFFDIO_WRITEPROTECT_MODE_WP,
.range = range
};
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_REGISTER, &reg);
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT, &protect);
} else {
struct uffdio_register reg = {
.mode = UFFDIO_REGISTER_MODE_WP,
.range = range
};
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_UNREGISTER, &reg);
}
} else
#endif
/* pagesize is assumed to be in the range of 2^x */
mprotect(phpdbg_get_page_boundary(watch->addr.ptr), phpdbg_get_total_page_size(watch->addr.ptr, watch->size), access);
{
mprotect(page_addr, size, access);
}
}

static inline void phpdbg_activate_watchpoint(phpdbg_watchpoint_t *watch) {
Expand All @@ -256,7 +293,7 @@ int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context) {
);

/* perhaps unnecessary, but check to be sure to not conflict with other segfault handlers */
if (phpdbg_check_for_watchpoint(page) == NULL) {
if (phpdbg_check_for_watchpoint(&PHPDBG_G(watchpoint_tree), page) == NULL) {
return FAILURE;
}

Expand All @@ -268,6 +305,29 @@ int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context) {
return SUCCESS;
}

#ifdef HAVE_USERFAULTFD_WRITEFAULT
void *phpdbg_watchpoint_userfaultfd_thread(void *phpdbg_globals) {
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
zend_phpdbg_globals *globals = (zend_phpdbg_globals *) phpdbg_globals;

struct uffd_msg fault_msg = {0};
while (read(globals->watch_userfaultfd, &fault_msg, sizeof(fault_msg)) == sizeof(fault_msg)) {
void *page = phpdbg_get_page_boundary((char *) fault_msg.arg.pagefault.address);
zend_hash_index_add_empty_element(globals->watchlist_mem, (zend_ulong) page);
struct uffdio_writeprotect unprotect = {
.mode = 0,
.range = {
.start = (__u64) page,
.len = phpdbg_pagesize
}
};
ioctl(globals->watch_userfaultfd, UFFDIO_WRITEPROTECT, &unprotect);
}

return NULL;
}
#endif

/* ### REGISTER WATCHPOINT ### To be used only by watch element and collision managers ### */
static inline void phpdbg_store_watchpoint_btree(phpdbg_watchpoint_t *watch) {
phpdbg_btree_result *res;
Expand Down Expand Up @@ -331,14 +391,14 @@ void phpdbg_delete_watch_collision(phpdbg_watchpoint_t *watch) {
if ((coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
zend_hash_index_del(&coll->parents, (zend_ulong) watch);
if (zend_hash_num_elements(&coll->parents) == 0) {
phpdbg_deactivate_watchpoint(&coll->ref);
phpdbg_remove_watchpoint_btree(&coll->ref);
phpdbg_deactivate_watchpoint(&coll->ref);

if (coll->ref.type == WATCH_ON_ZVAL) {
phpdbg_delete_watch_collision(&coll->ref);
} else if (coll->reference.addr.ptr) {
phpdbg_deactivate_watchpoint(&coll->reference);
phpdbg_remove_watchpoint_btree(&coll->reference);
phpdbg_deactivate_watchpoint(&coll->reference);
phpdbg_delete_watch_collision(&coll->reference);
if (coll->reference.type == WATCH_ON_STR) {
zend_string_release(coll->reference.backup.str);
Expand Down Expand Up @@ -614,8 +674,8 @@ void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
if (zend_hash_num_elements(&hti->watches) == 1) {
zend_hash_destroy(&hti->watches);
phpdbg_btree_delete(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht);
phpdbg_deactivate_watchpoint(&hti->hash_watch);
phpdbg_remove_watchpoint_btree(&hti->hash_watch);
phpdbg_deactivate_watchpoint(&hti->hash_watch);
efree(hti);
} else {
zend_hash_del(&hti->watches, element->name_in_parent);
Expand Down Expand Up @@ -887,8 +947,8 @@ void phpdbg_update_watch_collision_elements(phpdbg_watchpoint_t *watch) {
void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
phpdbg_watch_element *element;

phpdbg_deactivate_watchpoint(watch);
phpdbg_remove_watchpoint_btree(watch);
phpdbg_deactivate_watchpoint(watch);
phpdbg_delete_watch_collision(watch);

if (watch->coll) {
Expand Down Expand Up @@ -1020,8 +1080,8 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
return;
}

phpdbg_deactivate_watchpoint(watch);
phpdbg_remove_watchpoint_btree(watch);
phpdbg_deactivate_watchpoint(watch);
watch->addr.zv = new;
phpdbg_store_watchpoint_btree(watch);
phpdbg_activate_watchpoint(watch);
Expand Down Expand Up @@ -1068,7 +1128,21 @@ void phpdbg_reenable_memory_watches(void) {
if (res) {
watch = res->ptr;
if ((char *) page < (char *) watch->addr.ptr + watch->size) {
mprotect((void *) page, phpdbg_pagesize, PROT_READ);
#ifdef HAVE_USERFAULTFD_WRITEFAULT
if (PHPDBG_G(watch_userfaultfd)) {
struct uffdio_writeprotect protect = {
.mode = UFFDIO_WRITEPROTECT_MODE_WP,
.range = {
.start = (__u64) page,
.len = phpdbg_pagesize
}
};
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT, &protect);
} else
#endif
{
mprotect((void *) page, phpdbg_pagesize, PROT_READ);
}
}
}
} ZEND_HASH_FOREACH_END();
Expand Down Expand Up @@ -1396,23 +1470,42 @@ void phpdbg_setup_watchpoints(void) {
zend_hash_init(PHPDBG_G(watchlist_mem_backup), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);

PHPDBG_G(watch_tmp) = NULL;

#ifdef HAVE_USERFAULTFD_WRITEFAULT
PHPDBG_G(watch_userfaultfd) = syscall(SYS_userfaultfd, O_CLOEXEC);
if (PHPDBG_G(watch_userfaultfd) < 0) {
PHPDBG_G(watch_userfaultfd) = 0;
} else {
struct uffdio_api userfaultfd_features = {0};
userfaultfd_features.api = UFFD_API;
userfaultfd_features.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP;
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_API, &userfaultfd_features);
if (userfaultfd_features.features & UFFD_FEATURE_PAGEFAULT_FLAG_WP) {
pthread_create(&PHPDBG_G(watch_userfault_thread), NULL, phpdbg_watchpoint_userfaultfd_thread, ZEND_MODULE_GLOBALS_BULK(phpdbg));
} else {
PHPDBG_G(watch_userfaultfd) = 0;
}
}
#endif
}

void phpdbg_destroy_watchpoints(void) {
phpdbg_watch_element *element;
phpdbg_btree_position pos;
phpdbg_btree_result *res;

/* unconditionally free all remaining elements to avoid memory leaks */
ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
phpdbg_automatic_dequeue_free(element);
} ZEND_HASH_FOREACH_END();

/* upon fatal errors etc. (i.e. CG(unclean_shutdown) == 1), some watchpoints may still be active. Ensure memory is not watched anymore for next run. Do not care about memory freeing here, shutdown is unclean and near anyway. */
pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), 0, -1);
while ((res = phpdbg_btree_next(&pos))) {
phpdbg_deactivate_watchpoint(res->ptr);
phpdbg_purge_watchpoint_tree();

#ifdef HAVE_USERFAULTFD_WRITEFAULT
if (PHPDBG_G(watch_userfaultfd)) {
pthread_cancel(PHPDBG_G(watch_userfault_thread));
close(PHPDBG_G(watch_userfaultfd));
}
#endif

zend_hash_destroy(&PHPDBG_G(watch_elements)); PHPDBG_G(watch_elements).nNumOfElements = 0; /* phpdbg_watch_efree() is checking against this arrays size */
zend_hash_destroy(&PHPDBG_G(watch_recreation));
Expand Down