diff --git a/sapi/phpdbg/config.m4 b/sapi/phpdbg/config.m4 index 2f9a5e4a0f847..a494e9daae226 100644 --- a/sapi/phpdbg/config.m4 +++ b/sapi/phpdbg/config.m4 @@ -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 + #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) diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c index 193a89bfa687a..dc99e8c263721 100644 --- a/sapi/phpdbg/phpdbg.c +++ b/sapi/phpdbg/phpdbg.c @@ -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) /* {{{ */ @@ -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(); diff --git a/sapi/phpdbg/phpdbg.h b/sapi/phpdbg/phpdbg.h index 45c5eb0593b7b..1da3b055dd9d3 100644 --- a/sapi/phpdbg/phpdbg.h +++ b/sapi/phpdbg/phpdbg.h @@ -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 */ diff --git a/sapi/phpdbg/phpdbg_watch.c b/sapi/phpdbg/phpdbg_watch.c index 4da831019d5ec..05155bdd069ee 100644 --- a/sapi/phpdbg/phpdbg_watch.c +++ b/sapi/phpdbg/phpdbg_watch.c @@ -111,6 +111,13 @@ # include #endif +#ifdef HAVE_USERFAULTFD_WRITEFAULT +# include +# include +# include +# include +#endif + ZEND_EXTERN_MODULE_GLOBALS(phpdbg) const phpdbg_command_t phpdbg_watch_commands[] = { @@ -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; @@ -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, ®); + 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, ®); + } + } 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) { @@ -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; } @@ -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; @@ -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); @@ -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); @@ -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) { @@ -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); @@ -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(); @@ -1396,12 +1470,27 @@ 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) { @@ -1409,10 +1498,14 @@ void phpdbg_destroy_watchpoints(void) { } 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));