Skip to content

Commit ad9c10b

Browse files
committed
Prefer userfaultfd over mprotect+SIGSEGV signal handling on linux for phpdbg watchpoints
Closes GH-7551.
1 parent adcd43d commit ad9c10b

File tree

4 files changed

+146
-17
lines changed

4 files changed

+146
-17
lines changed

sapi/phpdbg/config.m4

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ if test "$BUILD_PHPDBG" = "" && test "$PHP_PHPDBG" != "no"; then
4545
AC_MSG_RESULT([disabled])
4646
fi
4747

48+
AC_CACHE_CHECK([for userfaultfd faulting on write-protected memory support], ac_phpdbg_userfaultfd_writefault, AC_COMPILE_IFELSE([AC_LANG_SOURCE([[
49+
#include <linux/userfaultfd.h>
50+
#ifndef UFFDIO_WRITEPROTECT_MODE_WP
51+
# error userfaults on write-protected memory not supported
52+
#endif
53+
]])], [ac_phpdbg_userfaultfd_writefault=yes], [ac_phpdbg_userfaultfd_writefault=no]))
54+
if test "$ac_phpdbg_userfaultfd_writefault" = "yes"; then
55+
if test "$enable_zts" != "yes"; then
56+
dnl Add pthreads linker and compiler flags for userfaultfd background thread
57+
if test -n "$ac_cv_pthreads_lib"; then
58+
LIBS="$LIBS -l$ac_cv_pthreads_lib"
59+
fi
60+
if test -n "$ac_cv_pthreads_cflags"; then
61+
CFLAGS="$CFLAGS $ac_cv_pthreads_cflags"
62+
fi
63+
64+
PTHREADS_FLAGS
65+
fi
66+
67+
AC_DEFINE(HAVE_USERFAULTFD_WRITEFAULT, 1, [Whether faulting on write-protected memory support can be compiled for userfaultfd])
68+
fi
69+
4870
PHP_SUBST(PHP_PHPDBG_CFLAGS)
4971
PHP_SUBST(PHP_PHPDBG_FILES)
5072
PHP_SUBST(PHPDBG_EXTRA_LIBS)

sapi/phpdbg/phpdbg.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ static inline void php_phpdbg_globals_ctor(zend_phpdbg_globals *pg) /* {{{ */
140140

141141
pg->cur_command = NULL;
142142
pg->last_line = 0;
143+
144+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
145+
pg->watch_userfaultfd = 0;
146+
pg->watch_userfault_thread = 0;
147+
#endif
143148
} /* }}} */
144149

145150
static PHP_MINIT_FUNCTION(phpdbg) /* {{{ */
@@ -1499,8 +1504,13 @@ int main(int argc, char **argv) /* {{{ */
14991504
}
15001505

15011506
#ifndef _WIN32
1502-
zend_try { zend_sigaction(SIGSEGV, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
1503-
zend_try { zend_sigaction(SIGBUS, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
1507+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
1508+
if (!PHPDBG_G(watch_userfaultfd))
1509+
#endif
1510+
{
1511+
zend_try { zend_sigaction(SIGSEGV, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
1512+
zend_try { zend_sigaction(SIGBUS, &signal_struct, &PHPDBG_G(old_sigsegv_signal)); } zend_end_try();
1513+
}
15041514
#endif
15051515
zend_try { zend_signal(SIGINT, phpdbg_sigint_handler); } zend_end_try();
15061516

sapi/phpdbg/phpdbg.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ ZEND_BEGIN_MODULE_GLOBALS(phpdbg)
246246

247247
#ifndef _WIN32
248248
struct sigaction old_sigsegv_signal; /* segv signal handler */
249+
#endif
250+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
251+
int watch_userfaultfd; /* userfaultfd(2) handler, 0 if unused */
252+
pthread_t watch_userfault_thread; /* thread for watch fault handling */
249253
#endif
250254
phpdbg_btree watchpoint_tree; /* tree with watchpoints */
251255
phpdbg_btree watch_HashTables; /* tree with original dtors of watchpoints */

sapi/phpdbg/phpdbg_watch.c

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@
111111
# include <sys/mman.h>
112112
#endif
113113

114+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
115+
# include <pthread.h>
116+
# include <linux/userfaultfd.h>
117+
# include <sys/ioctl.h>
118+
# include <sys/syscall.h>
119+
#endif
120+
114121
ZEND_EXTERN_MODULE_GLOBALS(phpdbg)
115122

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

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

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

230237
static void phpdbg_change_watchpoint_access(phpdbg_watchpoint_t *watch, int access) {
238+
void *page_addr = phpdbg_get_page_boundary(watch->addr.ptr);
239+
size_t size = phpdbg_get_total_page_size(watch->addr.ptr, watch->size);
240+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
241+
if (PHPDBG_G(watch_userfaultfd)) {
242+
struct uffdio_range range = {
243+
.start = (__u64) page_addr,
244+
.len = size
245+
};
246+
if (access == PROT_READ) {
247+
struct uffdio_register reg = {
248+
.mode = UFFDIO_REGISTER_MODE_WP,
249+
.range = range
250+
};
251+
struct uffdio_writeprotect protect = {
252+
.mode = UFFDIO_WRITEPROTECT_MODE_WP,
253+
.range = range
254+
};
255+
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_REGISTER, &reg);
256+
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT, &protect);
257+
} else {
258+
struct uffdio_register reg = {
259+
.mode = UFFDIO_REGISTER_MODE_WP,
260+
.range = range
261+
};
262+
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_UNREGISTER, &reg);
263+
}
264+
} else
265+
#endif
231266
/* pagesize is assumed to be in the range of 2^x */
232-
mprotect(phpdbg_get_page_boundary(watch->addr.ptr), phpdbg_get_total_page_size(watch->addr.ptr, watch->size), access);
267+
{
268+
mprotect(page_addr, size, access);
269+
}
233270
}
234271

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

258295
/* perhaps unnecessary, but check to be sure to not conflict with other segfault handlers */
259-
if (phpdbg_check_for_watchpoint(page) == NULL) {
296+
if (phpdbg_check_for_watchpoint(&PHPDBG_G(watchpoint_tree), page) == NULL) {
260297
return FAILURE;
261298
}
262299

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

308+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
309+
void *phpdbg_watchpoint_userfaultfd_thread(void *phpdbg_globals) {
310+
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
311+
zend_phpdbg_globals *globals = (zend_phpdbg_globals *) phpdbg_globals;
312+
313+
struct uffd_msg fault_msg = {0};
314+
while (read(globals->watch_userfaultfd, &fault_msg, sizeof(fault_msg)) == sizeof(fault_msg)) {
315+
void *page = phpdbg_get_page_boundary((char *) fault_msg.arg.pagefault.address);
316+
zend_hash_index_add_empty_element(globals->watchlist_mem, (zend_ulong) page);
317+
struct uffdio_writeprotect unprotect = {
318+
.mode = 0,
319+
.range = {
320+
.start = (__u64) page,
321+
.len = phpdbg_pagesize
322+
}
323+
};
324+
ioctl(globals->watch_userfaultfd, UFFDIO_WRITEPROTECT, &unprotect);
325+
}
326+
327+
return NULL;
328+
}
329+
#endif
330+
271331
/* ### REGISTER WATCHPOINT ### To be used only by watch element and collision managers ### */
272332
static inline void phpdbg_store_watchpoint_btree(phpdbg_watchpoint_t *watch) {
273333
phpdbg_btree_result *res;
@@ -331,14 +391,14 @@ void phpdbg_delete_watch_collision(phpdbg_watchpoint_t *watch) {
331391
if ((coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
332392
zend_hash_index_del(&coll->parents, (zend_ulong) watch);
333393
if (zend_hash_num_elements(&coll->parents) == 0) {
334-
phpdbg_deactivate_watchpoint(&coll->ref);
335394
phpdbg_remove_watchpoint_btree(&coll->ref);
395+
phpdbg_deactivate_watchpoint(&coll->ref);
336396

337397
if (coll->ref.type == WATCH_ON_ZVAL) {
338398
phpdbg_delete_watch_collision(&coll->ref);
339399
} else if (coll->reference.addr.ptr) {
340-
phpdbg_deactivate_watchpoint(&coll->reference);
341400
phpdbg_remove_watchpoint_btree(&coll->reference);
401+
phpdbg_deactivate_watchpoint(&coll->reference);
342402
phpdbg_delete_watch_collision(&coll->reference);
343403
if (coll->reference.type == WATCH_ON_STR) {
344404
zend_string_release(coll->reference.backup.str);
@@ -614,8 +674,8 @@ void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
614674
if (zend_hash_num_elements(&hti->watches) == 1) {
615675
zend_hash_destroy(&hti->watches);
616676
phpdbg_btree_delete(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht);
617-
phpdbg_deactivate_watchpoint(&hti->hash_watch);
618677
phpdbg_remove_watchpoint_btree(&hti->hash_watch);
678+
phpdbg_deactivate_watchpoint(&hti->hash_watch);
619679
efree(hti);
620680
} else {
621681
zend_hash_del(&hti->watches, element->name_in_parent);
@@ -887,8 +947,8 @@ void phpdbg_update_watch_collision_elements(phpdbg_watchpoint_t *watch) {
887947
void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
888948
phpdbg_watch_element *element;
889949

890-
phpdbg_deactivate_watchpoint(watch);
891950
phpdbg_remove_watchpoint_btree(watch);
951+
phpdbg_deactivate_watchpoint(watch);
892952
phpdbg_delete_watch_collision(watch);
893953

894954
if (watch->coll) {
@@ -1020,8 +1080,8 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
10201080
return;
10211081
}
10221082

1023-
phpdbg_deactivate_watchpoint(watch);
10241083
phpdbg_remove_watchpoint_btree(watch);
1084+
phpdbg_deactivate_watchpoint(watch);
10251085
watch->addr.zv = new;
10261086
phpdbg_store_watchpoint_btree(watch);
10271087
phpdbg_activate_watchpoint(watch);
@@ -1068,7 +1128,21 @@ void phpdbg_reenable_memory_watches(void) {
10681128
if (res) {
10691129
watch = res->ptr;
10701130
if ((char *) page < (char *) watch->addr.ptr + watch->size) {
1071-
mprotect((void *) page, phpdbg_pagesize, PROT_READ);
1131+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
1132+
if (PHPDBG_G(watch_userfaultfd)) {
1133+
struct uffdio_writeprotect protect = {
1134+
.mode = UFFDIO_WRITEPROTECT_MODE_WP,
1135+
.range = {
1136+
.start = (__u64) page,
1137+
.len = phpdbg_pagesize
1138+
}
1139+
};
1140+
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT, &protect);
1141+
} else
1142+
#endif
1143+
{
1144+
mprotect((void *) page, phpdbg_pagesize, PROT_READ);
1145+
}
10721146
}
10731147
}
10741148
} ZEND_HASH_FOREACH_END();
@@ -1396,23 +1470,42 @@ void phpdbg_setup_watchpoints(void) {
13961470
zend_hash_init(PHPDBG_G(watchlist_mem_backup), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);
13971471

13981472
PHPDBG_G(watch_tmp) = NULL;
1473+
1474+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
1475+
PHPDBG_G(watch_userfaultfd) = syscall(SYS_userfaultfd, O_CLOEXEC);
1476+
if (PHPDBG_G(watch_userfaultfd) < 0) {
1477+
PHPDBG_G(watch_userfaultfd) = 0;
1478+
} else {
1479+
struct uffdio_api userfaultfd_features = {0};
1480+
userfaultfd_features.api = UFFD_API;
1481+
userfaultfd_features.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP;
1482+
ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_API, &userfaultfd_features);
1483+
if (userfaultfd_features.features & UFFD_FEATURE_PAGEFAULT_FLAG_WP) {
1484+
pthread_create(&PHPDBG_G(watch_userfault_thread), NULL, phpdbg_watchpoint_userfaultfd_thread, ZEND_MODULE_GLOBALS_BULK(phpdbg));
1485+
} else {
1486+
PHPDBG_G(watch_userfaultfd) = 0;
1487+
}
1488+
}
1489+
#endif
13991490
}
14001491

14011492
void phpdbg_destroy_watchpoints(void) {
14021493
phpdbg_watch_element *element;
1403-
phpdbg_btree_position pos;
1404-
phpdbg_btree_result *res;
14051494

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

14111500
/* 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. */
1412-
pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), 0, -1);
1413-
while ((res = phpdbg_btree_next(&pos))) {
1414-
phpdbg_deactivate_watchpoint(res->ptr);
1501+
phpdbg_purge_watchpoint_tree();
1502+
1503+
#ifdef HAVE_USERFAULTFD_WRITEFAULT
1504+
if (PHPDBG_G(watch_userfaultfd)) {
1505+
pthread_cancel(PHPDBG_G(watch_userfault_thread));
1506+
close(PHPDBG_G(watch_userfaultfd));
14151507
}
1508+
#endif
14161509

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

0 commit comments

Comments
 (0)