xref: /php-src/sapi/phpdbg/phpdbg_watch.c (revision 6ddab74d)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Felipe Pena <felipe@php.net>                                |
14    | Authors: Joe Watkins <joe.watkins@live.co.uk>                        |
15    | Authors: Bob Weinand <bwoebi@php.net>                                |
16    +----------------------------------------------------------------------+
17 */
18 
19 /* Some information for the reader...
20  *
21  * The main structure managing the direct observations is the watchpoint (phpdbg_watchpoint_t). There are several types of watchpoints currently:
22  * WATCH_ON_BUCKET: a watchpoint on a Bucket element, used to monitor values inside HashTables (largely handled equivalently to WATCH_ON_ZVAL, it just monitors also for IS_UNDEF and key changes)
23  * WATCH_ON_ZVAL: a watchpoint on a bare zval (&zend_reference.val, zval.value.indirect)
24  * WATCH_ON_STR: a watchpoint on a zend_string (put on &ZSTR_LEN() in order to not watch refcount/hash)
25  * WATCH_ON_HASHTABLE: a watchpoint on a HashTable (currently only used to observe size changes, put after flags in order to not watch refcount)
26  * WATCH_ON_REFCOUNTED: a watchpoint on a zend_refcounted, observes the refcount and serves as reference pointer in the custom efree handler
27  * WATCH_ON_HASHDATA: special watchpoint to watch for HT_GET_DATA_ADDR(ht) being efree()'d to be able to properly relocate Bucket watches
28  *
29  * Watch elements are either simple, recursive or implicit (PHPDBG_WATCH_* flags)
30  * Simple means that a particular watchpoint was explicitly defined
31  * Recursive watch elements are created recursively (recursive root flag is to distinguish the root element easily from its children recursive elements)
32  * Implicit  watch elements are implicitly created on all ancestors of simple or recursive watch elements
33  * Recursive and (simple or implicit) watch elements are mutually exclusive
34  * Array/Object to distinguish watch elements on arrays
35  *
36  * Watch elements all contain a reference to a watchpoint (except if scheduled for recreation); a "watch" is a watch element created by the user with a specific id
37  * Each watch has its independent structure of watch elements, watchpoints are responsible for managing collisions and preventing pointers being watched multiple times
38  *
39  * PHPDBG_G(watchpoint_tree) contains all watchpoints identified by the watch target address
40  * PHPDBG_G(watch_HashTables) contains the addresses of parent_containers of watch elements
41  * PHPDBG_G(watch_elements) contains all directly defined watch elements (i.e. those which have an individual id)
42  * PHPDBG_G(watch_collisions) is indexed by a zend_refcounted * pointer (phpdbg_watchpoint_t.ref). It stores information about collisions (everything which contains a zend_refcounted * may be referenced by multiple watches)
43  * PHPDBG_G(watch_free) is a set of pointers to watch for being freed (like HashTables referenced by phpdbg_watch_element.parent_container)
44  * PHPDBG_G(watch_recreation) is the list of watch elements whose watchpoint has been removed (via efree() for example) and needs to be recreated
45  * PHPDBG_G(watchlist_mem) is the list of unprotected memory pages; used to watch which pages need their PROT_WRITE attribute removed after checking
46  *
47  * Watching on addresses:
48  * * Address and size are transformed into memory page aligned address and size
49  * * mprotect() enables or disables them (depending on flags) - Windows has a transparent compatibility layer in phpdbg_win.c
50  * * segfault handler stores the address of the page and marks it again as writable
51  * * later watchpoints pointing inside these pages are compared against their current value and eventually reactivated (or deleted)
52  *
53  * Creating a watch:
54  * * Implicit watch elements for each element in the hierarchy (starting from base, which typically is current symbol table) except the last one
55  * * Create a watch element with either simple flag or recursive [+ root] flags
56  * * If the element has recursive flag, create elements recursively for every referenced HashTable and zval
57  *
58  * Creating a watch element:
59  * * For each watch element a related watchpoint is created, if there's none yet; add itself then into the list of parents of that watchpoint
60  * * If the watch has a parent_container, add itself also into a phpdbg_watch_ht_info (inside PHPDBG_G(watch_HashTables)) [and creates it if not yet existing]
61  *
62  * Creation of watchpoints:
63  * * Watchpoints create a watch collision for each refcounted or indirect on the zval (if type is WATCH_ON_BUCKET or WATCH_ON_ZVAL)
64  * * Backs the current value of the watched pointer up
65  * * Installs the watchpoint in PHPDBG_G(watchpoint_tree) and activates it (activation of a watchpoint = remove PROT_WRITE from the pages the watched pointer resides on)
66  *
67  * Watch collisions:
68  * * Manages a watchpoint on the refcount (WATCH_ON_REFCOUNTED) or indirect zval (WATCH_ON_ZVAL)
69  * * Guarantees that every pointer is watched at most once (by having a pointer to collision mapping in PHPDBG_G(watch_collisions), which have the unique watchpoints for the respective collision)
70  * * Contains a list of parents, i.e. which watchpoints reference it (via watch->ref)
71  * * If no watchpoint is referencing it anymore, the watch collision and its associated watchpoints (phpdbg_watch_collision.ref/reference) are removed
72  *
73  * Deleting a watch:
74  * * Watches are stored by an id in PHPDBG_G(watch_elements); the associated watch element is then deleted
75  * * Deletes all parent and children implicit watch elements
76  *
77  * Deleting a watch element:
78  * * Removes itself from the parent list of the associated watchpoints; if that parent list is empty, also delete the watchpoint
79  * * Removes itself from the related phpdbg_watch_ht_info if it has a parent_container
80  *
81  * Deleting a watchpoint:
82  * * Remove itself from watch collisions this watchpoint participates in
83  * * Removes the watchpoint from PHPDBG_G(watchpoint_tree) and deactivates it (deactivation of a watchpoint = add PROT_WRITE to the pages the watched pointer resides on)
84  *
85  * A watched pointer is efree()'d:
86  * * Needs immediate action as we else may run into dereferencing a pointer into freed memory
87  * * Deletes the associated watchpoint, and for each watch element, if recursive, all its children elements
88  * * If the its watch elements are implicit, recursive roots or simple, they and all their children are dissociated from their watchpoints (i.e. removed from the watchpoint, if no other element is referencing it, it is deleted); adds these elements to PHPDBG_G(watch_recreation)
89  *
90  * Recreating watchpoints:
91  * * Upon each opcode, PHPDBG_G(watch_recreation) is checked and all its elements are searched for whether the watch is still reachable via the tree given by its implicits
92  * * In case they are not reachable, the watch is deleted (and thus all the related watch elements), else a new watchpoint is created for all the watch elements
93  * * The old and new values of the watches are compared and shown if changed
94  *
95  * Comparing watchpoints:
96  * * The old and new values of the watches are compared and shown if changed
97  * * If changed, it is checked whether the refcounted/indirect changed and watch collisions removed or created accordingly
98  * * If a zval/bucket watchpoint is recursive, watch elements are added or removed accordingly
99  * * If an array watchpoint is recursive, new array watchpoints are added if there are new ones in the array
100  * * If the watch (element with an id) is not reachable anymore due to changes in implicits, the watch is removed
101  */
102 
103 #include "zend.h"
104 #include "phpdbg.h"
105 #include "phpdbg_btree.h"
106 #include "phpdbg_watch.h"
107 #include "phpdbg_utils.h"
108 #include "phpdbg_prompt.h"
109 #include "zend_portability.h"
110 #ifndef _WIN32
111 # include <unistd.h>
112 # include <sys/mman.h>
113 #endif
114 
115 #ifdef HAVE_USERFAULTFD_WRITEFAULT
116 # include <pthread.h>
117 # include <linux/userfaultfd.h>
118 # include <sys/ioctl.h>
119 # include <sys/syscall.h>
120 #endif
121 
122 ZEND_EXTERN_MODULE_GLOBALS(phpdbg)
123 
124 const phpdbg_command_t phpdbg_watch_commands[] = {
125 	PHPDBG_COMMAND_D_EX(array,      "create watchpoint on an array", 'a', watch_array,     &phpdbg_prompt_commands[24], "s", 0),
126 	PHPDBG_COMMAND_D_EX(delete,     "delete watchpoint",             'd', watch_delete,    &phpdbg_prompt_commands[24], "n", 0),
127 	PHPDBG_COMMAND_D_EX(recursive,  "create recursive watchpoints",  'r', watch_recursive, &phpdbg_prompt_commands[24], "s", 0),
128 	PHPDBG_END_COMMAND
129 };
130 
131 #define HT_FROM_ZVP(zvp) (Z_TYPE_P(zvp) == IS_OBJECT ? Z_OBJPROP_P(zvp) : Z_TYPE_P(zvp) == IS_ARRAY ? Z_ARRVAL_P(zvp) : NULL)
132 
133 #define HT_WATCH_OFFSET (sizeof(zend_refcounted *) + sizeof(uint32_t)) /* we are not interested in gc and flags */
134 #define HT_PTR_HT(ptr) ((HashTable *) (((char *) (ptr)) - HT_WATCH_OFFSET))
135 #define HT_WATCH_HT(watch) HT_PTR_HT((watch)->addr.ptr)
136 
137 /* ### PRINTING POINTER DIFFERENCES ### */
phpdbg_check_watch_diff(phpdbg_watchtype type,void * oldPtr,void * newPtr)138 bool phpdbg_check_watch_diff(phpdbg_watchtype type, void *oldPtr, void *newPtr) {
139 	switch (type) {
140 		case WATCH_ON_BUCKET:
141 			if (memcmp(&((Bucket *) oldPtr)->h, &((Bucket *) newPtr)->h, sizeof(Bucket) - sizeof(zval) /* hash+key comparison */) != 0) {
142 				return 2;
143 			}
144 			/* Fall through to also compare the value from the bucket. */
145 			ZEND_FALLTHROUGH;
146 		case WATCH_ON_ZVAL:
147 			return memcmp(oldPtr, newPtr, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */) != 0;
148 		case WATCH_ON_HASHTABLE:
149 			return zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_PTR_HT(newPtr));
150 		case WATCH_ON_REFCOUNTED:
151 			return memcmp(oldPtr, newPtr, sizeof(uint32_t) /* no zend_refcounted metadata info */) != 0;
152 		case WATCH_ON_STR:
153 			return memcmp(oldPtr, newPtr, *(size_t *) oldPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len)) != 0;
154 		case WATCH_ON_HASHDATA:
155 			ZEND_UNREACHABLE();
156 	}
157 	return 0;
158 }
159 
phpdbg_print_watch_diff(phpdbg_watchtype type,zend_string * name,void * oldPtr,void * newPtr)160 void phpdbg_print_watch_diff(phpdbg_watchtype type, zend_string *name, void *oldPtr, void *newPtr) {
161 	int32_t elementDiff;
162 
163 	PHPDBG_G(watchpoint_hit) = 1;
164 
165 	phpdbg_notice("Breaking on watchpoint %.*s", (int) ZSTR_LEN(name), ZSTR_VAL(name));
166 
167 	switch (type) {
168 		case WATCH_ON_BUCKET:
169 		case WATCH_ON_ZVAL:
170 			if (Z_REFCOUNTED_P((zval *) oldPtr)) {
171 				phpdbg_writeln("Old value inaccessible or destroyed");
172 			} else if (Z_TYPE_P((zval *) oldPtr) == IS_INDIRECT) {
173 				phpdbg_writeln("Old value inaccessible or destroyed (was indirect)");
174 			} else {
175 				phpdbg_out("Old value: ");
176 				zend_print_flat_zval_r((zval *) oldPtr);
177 				phpdbg_out("\n");
178 			}
179 
180 			while (Z_TYPE_P((zval *) newPtr) == IS_INDIRECT) {
181 				newPtr = Z_INDIRECT_P((zval *) newPtr);
182 			}
183 
184 			phpdbg_out("New value%s: ", Z_ISREF_P((zval *) newPtr) ? " (reference)" : "");
185 			zend_print_flat_zval_r((zval *) newPtr);
186 			phpdbg_out("\n");
187 			break;
188 
189 		case WATCH_ON_HASHTABLE:
190 			elementDiff = zend_hash_num_elements(HT_PTR_HT(oldPtr)) - zend_hash_num_elements(HT_PTR_HT(newPtr));
191 			if (elementDiff > 0) {
192 				phpdbg_writeln("%d elements were removed from the array", (int) elementDiff);
193 			} else if (elementDiff < 0) {
194 				phpdbg_writeln("%d elements were added to the array", (int) -elementDiff);
195 			}
196 			break;
197 
198 		case WATCH_ON_REFCOUNTED:
199 			phpdbg_writeln("Old refcount: %d", GC_REFCOUNT((zend_refcounted *) oldPtr));
200 			phpdbg_writeln("New refcount: %d", GC_REFCOUNT((zend_refcounted *) newPtr));
201 			break;
202 
203 		case WATCH_ON_STR:
204 			phpdbg_out("Old value: ");
205 			zend_write((char *) oldPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) oldPtr);
206 			phpdbg_out("\n");
207 
208 			phpdbg_out("New value: ");
209 			zend_write((char *) newPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) newPtr);
210 			phpdbg_out("\n");
211 			break;
212 
213 		case WATCH_ON_HASHDATA:
214 			ZEND_UNREACHABLE();
215 	}
216 }
217 
218 /* ### LOW LEVEL WATCHPOINT HANDLING ### */
phpdbg_check_for_watchpoint(phpdbg_btree * tree,void * addr)219 static phpdbg_watchpoint_t *phpdbg_check_for_watchpoint(phpdbg_btree *tree, void *addr) {
220 	phpdbg_watchpoint_t *watch;
221 	phpdbg_btree_result *result = phpdbg_btree_find_closest(tree, (zend_ulong) phpdbg_get_page_boundary(addr) + phpdbg_pagesize - 1);
222 
223 	if (result == NULL) {
224 		return NULL;
225 	}
226 
227 	watch = result->ptr;
228 
229 	/* check if that addr is in a mprotect()'ed memory area */
230 	if ((char *) phpdbg_get_page_boundary(watch->addr.ptr) > (char *) addr || (char *) phpdbg_get_page_boundary(watch->addr.ptr) + phpdbg_get_total_page_size(watch->addr.ptr, watch->size) < (char *) addr) {
231 		/* failure */
232 		return NULL;
233 	}
234 
235 	return watch;
236 }
237 
phpdbg_change_watchpoint_access(phpdbg_watchpoint_t * watch,int access)238 static void phpdbg_change_watchpoint_access(phpdbg_watchpoint_t *watch, int access) {
239 	void *page_addr = phpdbg_get_page_boundary(watch->addr.ptr);
240 	size_t size = phpdbg_get_total_page_size(watch->addr.ptr, watch->size);
241 #ifdef HAVE_USERFAULTFD_WRITEFAULT
242 	if (PHPDBG_G(watch_userfaultfd)) {
243 		struct uffdio_range range = {
244 			.start = (__u64)(uintptr_t) page_addr,
245 			.len = size
246 		};
247 		if (access == PROT_READ) {
248 			struct uffdio_register reg = {
249 				.mode = UFFDIO_REGISTER_MODE_WP,
250 				.range = range
251 			};
252 			struct uffdio_writeprotect protect = {
253 				.mode = UFFDIO_WRITEPROTECT_MODE_WP,
254 				.range = range
255 			};
256 			ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_REGISTER,  &reg);
257 			ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT,  &protect);
258 		} else {
259 			struct uffdio_register reg = {
260 				.mode = UFFDIO_REGISTER_MODE_WP,
261 				.range = range
262 			};
263 			ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_UNREGISTER,  &reg);
264 		}
265 	} else
266 #endif
267 	/* pagesize is assumed to be in the range of 2^x */
268 	{
269 		mprotect(page_addr, size, access);
270 	}
271 }
272 
phpdbg_activate_watchpoint(phpdbg_watchpoint_t * watch)273 static inline void phpdbg_activate_watchpoint(phpdbg_watchpoint_t *watch) {
274 	phpdbg_change_watchpoint_access(watch, PROT_READ);
275 }
276 
phpdbg_deactivate_watchpoint(phpdbg_watchpoint_t * watch)277 static inline void phpdbg_deactivate_watchpoint(phpdbg_watchpoint_t *watch) {
278 	phpdbg_change_watchpoint_access(watch, PROT_READ | PROT_WRITE);
279 }
280 
281 /* Note that consecutive pages need to be merged in order to avoid watchpoints spanning page boundaries to have part of their data in the one page, part in the other page */
282 #ifdef _WIN32
phpdbg_watchpoint_segfault_handler(void * addr)283 int phpdbg_watchpoint_segfault_handler(void *addr) {
284 #else
285 int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context) {
286 #endif
287 
288 	void *page = phpdbg_get_page_boundary(
289 #ifdef _WIN32
290 		addr
291 #else
292 		info->si_addr
293 #endif
294 	);
295 
296 	/* perhaps unnecessary, but check to be sure to not conflict with other segfault handlers */
297 	if (phpdbg_check_for_watchpoint(&PHPDBG_G(watchpoint_tree), page) == NULL) {
298 		return FAILURE;
299 	}
300 
301 	/* re-enable writing */
302 	mprotect(page, phpdbg_pagesize, PROT_READ | PROT_WRITE);
303 
304 	zend_hash_index_add_empty_element(PHPDBG_G(watchlist_mem), (zend_ulong) page);
305 
306 	return SUCCESS;
307 }
308 
309 #ifdef HAVE_USERFAULTFD_WRITEFAULT
310 # if defined(__GNUC__) && !defined(__clang__)
311 __attribute__((no_sanitize_address))
312 # endif
313 void *phpdbg_watchpoint_userfaultfd_thread(void *phpdbg_globals_ptr) {
314 	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
315 	zend_phpdbg_globals *globals = (zend_phpdbg_globals *) phpdbg_globals_ptr;
316 
317 	struct uffd_msg fault_msg = {0};
318 	while (read(globals->watch_userfaultfd, &fault_msg, sizeof(fault_msg)) == sizeof(fault_msg)) {
319 		void *page = phpdbg_get_page_boundary((char *)(uintptr_t) fault_msg.arg.pagefault.address);
320 		zend_hash_index_add_empty_element(globals->watchlist_mem, (zend_ulong) page);
321 		struct uffdio_writeprotect unprotect = {
322 			.mode = 0,
323 			.range = {
324 				.start = (__u64)(uintptr_t) page,
325 				.len = phpdbg_pagesize
326 			}
327 		};
328 		ioctl(globals->watch_userfaultfd, UFFDIO_WRITEPROTECT, &unprotect);
329 	}
330 
331 	return NULL;
332 }
333 #endif
334 
335 /* ### REGISTER WATCHPOINT ### To be used only by watch element and collision managers ### */
336 static inline void phpdbg_store_watchpoint_btree(phpdbg_watchpoint_t *watch) {
337 #if ZEND_DEBUG
338 	phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr);
339 	ZEND_ASSERT(res == NULL || res->ptr == watch);
340 #endif
341 	phpdbg_btree_insert(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr, watch);
342 }
343 
344 static inline void phpdbg_remove_watchpoint_btree(phpdbg_watchpoint_t *watch) {
345 	phpdbg_btree_delete(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr);
346 }
347 
348 /* ### SET WATCHPOINT ADDR ### To be used only by watch element and collision managers ### */
349 void phpdbg_set_addr_watchpoint(void *addr, size_t size, phpdbg_watchpoint_t *watch) {
350 	watch->addr.ptr = addr;
351 	watch->size = size;
352 	watch->ref = NULL;
353 	watch->coll = NULL;
354 	zend_hash_init(&watch->elements, 8, NULL, NULL, 0);
355 }
356 
357 void phpdbg_set_zval_watchpoint(zval *zv, phpdbg_watchpoint_t *watch) {
358 	phpdbg_set_addr_watchpoint(zv, sizeof(zval) - sizeof(uint32_t), watch);
359 	watch->type = WATCH_ON_ZVAL;
360 }
361 
362 void phpdbg_set_bucket_watchpoint(Bucket *bucket, phpdbg_watchpoint_t *watch) {
363 	phpdbg_set_addr_watchpoint(bucket, sizeof(Bucket), watch);
364 	watch->type = WATCH_ON_BUCKET;
365 }
366 
367 void phpdbg_set_ht_watchpoint(HashTable *ht, phpdbg_watchpoint_t *watch) {
368 	phpdbg_set_addr_watchpoint(((char *) ht) + HT_WATCH_OFFSET, sizeof(HashTable) - HT_WATCH_OFFSET, watch);
369 	watch->type = WATCH_ON_HASHTABLE;
370 }
371 
372 void phpdbg_watch_backup_data(phpdbg_watchpoint_t *watch) {
373 	switch (watch->type) {
374 		case WATCH_ON_BUCKET:
375 		case WATCH_ON_ZVAL:
376 		case WATCH_ON_REFCOUNTED:
377 			memcpy(&watch->backup, watch->addr.ptr, watch->size);
378 			break;
379 		case WATCH_ON_STR:
380 			if (watch->backup.str) {
381 				zend_string_release(watch->backup.str);
382 			}
383 			watch->backup.str = zend_string_init((char *) watch->addr.ptr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) watch->addr.ptr, 1);
384 			GC_MAKE_PERSISTENT_LOCAL(watch->backup.str);
385 			break;
386 		case WATCH_ON_HASHTABLE:
387 			memcpy((char *) &watch->backup + HT_WATCH_OFFSET, watch->addr.ptr, watch->size);
388 		case WATCH_ON_HASHDATA:
389 			break;
390 	}
391 }
392 
393 /* ### MANAGE WATCH COLLISIONS ### To be used only by watch element manager and memory differ ### */
394 /* watch collisions are responsible for having only one watcher on a given refcounted/refval and having a mapping back to the parent zvals */
395 void phpdbg_delete_watch_collision(phpdbg_watchpoint_t *watch) {
396 	phpdbg_watch_collision *coll;
397 	if ((coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
398 		zend_hash_index_del(&coll->parents, (zend_ulong) watch);
399 		if (zend_hash_num_elements(&coll->parents) == 0) {
400 			phpdbg_remove_watchpoint_btree(&coll->ref);
401 			phpdbg_deactivate_watchpoint(&coll->ref);
402 
403 			if (coll->ref.type == WATCH_ON_ZVAL) {
404 				phpdbg_delete_watch_collision(&coll->ref);
405 			} else if (coll->reference.addr.ptr) {
406 				phpdbg_remove_watchpoint_btree(&coll->reference);
407 				phpdbg_deactivate_watchpoint(&coll->reference);
408 				phpdbg_delete_watch_collision(&coll->reference);
409 				if (coll->reference.type == WATCH_ON_STR) {
410 					zend_string_release(coll->reference.backup.str);
411 				}
412 			}
413 
414 			zend_hash_index_del(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref);
415 			zend_hash_destroy(&coll->parents);
416 			efree(coll);
417 		}
418 	}
419 }
420 
421 void phpdbg_update_watch_ref(phpdbg_watchpoint_t *watch) {
422 	phpdbg_watch_collision *coll;
423 
424 	ZEND_ASSERT(watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET);
425 	if (Z_REFCOUNTED_P(watch->addr.zv)) {
426 		if (Z_COUNTED_P(watch->addr.zv) == watch->ref) {
427 			return;
428 		}
429 
430 		if (watch->ref != NULL) {
431 			phpdbg_delete_watch_collision(watch);
432 		}
433 
434 		watch->ref = Z_COUNTED_P(watch->addr.zv);
435 
436 		if (!(coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
437 			coll = emalloc(sizeof(*coll));
438 			coll->ref.type = WATCH_ON_REFCOUNTED;
439 			phpdbg_set_addr_watchpoint(Z_COUNTED_P(watch->addr.zv), sizeof(uint32_t), &coll->ref);
440 			coll->ref.coll = coll;
441 			phpdbg_store_watchpoint_btree(&coll->ref);
442 			phpdbg_activate_watchpoint(&coll->ref);
443 			phpdbg_watch_backup_data(&coll->ref);
444 
445 			if (Z_ISREF_P(watch->addr.zv)) {
446 				phpdbg_set_zval_watchpoint(Z_REFVAL_P(watch->addr.zv), &coll->reference);
447 				coll->reference.coll = coll;
448 				phpdbg_update_watch_ref(&coll->reference);
449 				phpdbg_store_watchpoint_btree(&coll->reference);
450 				phpdbg_activate_watchpoint(&coll->reference);
451 				phpdbg_watch_backup_data(&coll->reference);
452 			} else if (Z_TYPE_P(watch->addr.zv) == IS_STRING) {
453 				coll->reference.type = WATCH_ON_STR;
454 				phpdbg_set_addr_watchpoint(&Z_STRLEN_P(watch->addr.zv), XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len) + Z_STRLEN_P(watch->addr.zv) + 1, &coll->reference);
455 				coll->reference.coll = coll;
456 				phpdbg_store_watchpoint_btree(&coll->reference);
457 				phpdbg_activate_watchpoint(&coll->reference);
458 				coll->reference.backup.str = NULL;
459 				phpdbg_watch_backup_data(&coll->reference);
460 			} else {
461 				coll->reference.addr.ptr = NULL;
462 			}
463 
464 			zend_hash_init(&coll->parents, 8, NULL, NULL, 0);
465 			zend_hash_index_add_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref, coll);
466 		}
467 		zend_hash_index_add_ptr(&coll->parents, (zend_long) watch, watch);
468 	} else if (Z_TYPE_P(watch->addr.zv) == IS_INDIRECT) {
469 		if ((zend_refcounted *) Z_INDIRECT_P(watch->addr.zv) == watch->ref) {
470 			return;
471 		}
472 
473 		if (watch->ref != NULL) {
474 			phpdbg_delete_watch_collision(watch);
475 		}
476 
477 		watch->ref = (zend_refcounted *) Z_INDIRECT_P(watch->addr.zv);
478 
479 		if (!(coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
480 			coll = emalloc(sizeof(*coll));
481 			phpdbg_set_zval_watchpoint(Z_INDIRECT_P(watch->addr.zv), &coll->ref);
482 			coll->ref.coll = coll;
483 			phpdbg_update_watch_ref(&coll->ref);
484 			phpdbg_store_watchpoint_btree(&coll->ref);
485 			phpdbg_activate_watchpoint(&coll->ref);
486 			phpdbg_watch_backup_data(&coll->ref);
487 
488 			zend_hash_init(&coll->parents, 8, NULL, NULL, 0);
489 			zend_hash_index_add_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref, coll);
490 		}
491 		zend_hash_index_add_ptr(&coll->parents, (zend_long) watch, watch);
492 	} else if (watch->ref) {
493 		phpdbg_delete_watch_collision(watch);
494 		watch->ref = NULL;
495 	}
496 }
497 
498 /* ### MANAGE WATCH ELEMENTS ### */
499 /* watchpoints must be unique per element. Only one watchpoint may point to one element. But many elements may point to one watchpoint. */
500 void phpdbg_recurse_watch_element(phpdbg_watch_element *element);
501 void phpdbg_remove_watch_element_recursively(phpdbg_watch_element *element);
502 void phpdbg_free_watch_element(phpdbg_watch_element *element);
503 void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch);
504 void phpdbg_watch_parent_ht(phpdbg_watch_element *element);
505 
506 phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element) {
507 	phpdbg_btree_result *res;
508 	if ((res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr)) == NULL) {
509 		phpdbg_watchpoint_t *mem = emalloc(sizeof(*mem));
510 		*mem = *watch;
511 		watch = mem;
512 		phpdbg_store_watchpoint_btree(watch);
513 		if (watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) {
514 			phpdbg_update_watch_ref(watch);
515 		}
516 		phpdbg_activate_watchpoint(watch);
517 		phpdbg_watch_backup_data(watch);
518 	} else {
519 		phpdbg_watch_element *old_element;
520 		watch = res->ptr;
521 		if ((old_element = zend_hash_find_ptr(&watch->elements, element->str))) {
522 			if (element != old_element) {
523 				phpdbg_free_watch_element(element);
524 			}
525 			return old_element;
526 		}
527 	}
528 
529 	element->watch = watch;
530 	zend_hash_add_ptr(&watch->elements, element->str, element);
531 
532 	if (element->flags & PHPDBG_WATCH_RECURSIVE) {
533 		phpdbg_recurse_watch_element(element);
534 	}
535 
536 	return element;
537 }
538 
539 phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element) {
540 	phpdbg_watchpoint_t watch;
541 	phpdbg_set_bucket_watchpoint(bucket, &watch);
542 	element = phpdbg_add_watch_element(&watch, element);
543 	phpdbg_watch_parent_ht(element);
544 	return element;
545 }
546 
547 phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element) {
548 	phpdbg_watchpoint_t watch;
549 	HashTable *ht = HT_FROM_ZVP(zv);
550 
551 	if (!ht) {
552 		return NULL;
553 	}
554 
555 	element->flags |= Z_TYPE_P(zv) == IS_ARRAY ? PHPDBG_WATCH_ARRAY : PHPDBG_WATCH_OBJECT;
556 	phpdbg_set_ht_watchpoint(ht, &watch);
557 	return phpdbg_add_watch_element(&watch, element);
558 }
559 
560 bool phpdbg_is_recursively_watched(void *ptr, phpdbg_watch_element *element) {
561 	phpdbg_watch_element *next = element;
562 	do {
563 		element = next;
564 		if (element->watch->addr.ptr == ptr) {
565 			return 1;
566 		}
567 		next = element->parent;
568 	} while (!(element->flags & PHPDBG_WATCH_RECURSIVE_ROOT));
569 
570 	return 0;
571 }
572 
573 void phpdbg_add_recursive_watch_from_ht(phpdbg_watch_element *element, zend_long idx, zend_string *str, zval *zv) {
574 	phpdbg_watch_element *child;
575 	if (phpdbg_is_recursively_watched(zv, element)) {
576 		return;
577 	}
578 
579 	child = emalloc(sizeof(*child));
580 	child->flags = PHPDBG_WATCH_RECURSIVE;
581 	if (str) {
582 		child->str = strpprintf(0, (element->flags & PHPDBG_WATCH_ARRAY) ? "%.*s[%s]" : "%.*s->%s", (int) ZSTR_LEN(element->str) - 2, ZSTR_VAL(element->str), phpdbg_get_property_key(ZSTR_VAL(str)));
583 	} else {
584 		child->str = strpprintf(0, (element->flags & PHPDBG_WATCH_ARRAY) ? "%.*s[" ZEND_LONG_FMT "]" : "%.*s->" ZEND_LONG_FMT, (int) ZSTR_LEN(element->str) - 2, ZSTR_VAL(element->str), idx);
585 	}
586 	if (!str) {
587 		str = zend_long_to_str(idx); // TODO: hack, use proper int handling for name in parent
588 	} else { str = zend_string_copy(str); }
589 	child->name_in_parent = str;
590 	child->parent = element;
591 	child->child = NULL;
592 	child->parent_container = HT_WATCH_HT(element->watch);
593 	zend_hash_add_ptr(&element->child_container, child->str, child);
594 	phpdbg_add_bucket_watch_element((Bucket *) zv, child);
595 }
596 
597 void phpdbg_recurse_watch_element(phpdbg_watch_element *element) {
598 	phpdbg_watch_element *child;
599 	zval *zv;
600 
601 	if (element->watch->type == WATCH_ON_ZVAL || element->watch->type == WATCH_ON_BUCKET) {
602 		zv = element->watch->addr.zv;
603 		while (Z_TYPE_P(zv) == IS_INDIRECT) {
604 			zv = Z_INDIRECT_P(zv);
605 		}
606 		ZVAL_DEREF(zv);
607 
608 		if (element->child) {
609 			phpdbg_remove_watch_element_recursively(element->child);
610 		}
611 
612 		if ((Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT)
613 		    || phpdbg_is_recursively_watched(HT_WATCH_OFFSET + (char *) HT_FROM_ZVP(zv), element)) {
614 			if (element->child) {
615 				phpdbg_free_watch_element(element->child);
616 				element->child = NULL;
617 			}
618 			return;
619 		}
620 
621 		if (element->child) {
622 			child = element->child;
623 		} else {
624 			child = emalloc(sizeof(*child));
625 			child->flags = PHPDBG_WATCH_RECURSIVE;
626 			child->str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
627 			child->name_in_parent = NULL;
628 			child->parent = element;
629 			child->child = NULL;
630 			element->child = child;
631 		}
632 		zend_hash_init(&child->child_container, 8, NULL, NULL, 0);
633 		phpdbg_add_ht_watch_element(zv, child);
634 	} else if (zend_hash_num_elements(&element->child_container) == 0) {
635 		zend_string *str;
636 		zend_long idx;
637 
638 		ZEND_ASSERT(element->watch->type == WATCH_ON_HASHTABLE);
639 		ZEND_HASH_FOREACH_KEY_VAL(HT_WATCH_HT(element->watch), idx, str, zv) {
640 			phpdbg_add_recursive_watch_from_ht(element, idx, str, zv);
641 		} ZEND_HASH_FOREACH_END();
642 	}
643 }
644 
645 void phpdbg_watch_parent_ht(phpdbg_watch_element *element) {
646 	if (element->watch->type == WATCH_ON_BUCKET) {
647 		phpdbg_btree_result *res;
648 		phpdbg_watch_ht_info *hti;
649 		ZEND_ASSERT(element->parent_container);
650 		if (!(res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) element->parent_container))) {
651 			hti = emalloc(sizeof(*hti));
652 			hti->ht = element->parent_container;
653 
654 			zend_hash_init(&hti->watches, 0, NULL, ZVAL_PTR_DTOR, 0);
655 			phpdbg_btree_insert(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht, hti);
656 
657 			phpdbg_set_addr_watchpoint(HT_GET_DATA_ADDR(hti->ht), HT_HASH_SIZE(hti->ht->nTableMask), &hti->hash_watch);
658 			hti->hash_watch.type = WATCH_ON_HASHDATA;
659 			phpdbg_store_watchpoint_btree(&hti->hash_watch);
660 			phpdbg_activate_watchpoint(&hti->hash_watch);
661 		} else {
662 			hti = (phpdbg_watch_ht_info *) res->ptr;
663 		}
664 
665 		zend_hash_add_ptr(&hti->watches, element->name_in_parent, element);
666 	}
667 }
668 
669 void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
670 	if (element->watch && element->watch->type == WATCH_ON_BUCKET) {
671 		phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) element->parent_container);
672 		ZEND_ASSERT(element->parent_container);
673 		if (res) {
674 			phpdbg_watch_ht_info *hti = res->ptr;
675 
676 			if (zend_hash_num_elements(&hti->watches) == 1) {
677 				zend_hash_destroy(&hti->watches);
678 				phpdbg_btree_delete(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht);
679 				phpdbg_remove_watchpoint_btree(&hti->hash_watch);
680 				phpdbg_deactivate_watchpoint(&hti->hash_watch);
681 				efree(hti);
682 			} else {
683 				zend_hash_del(&hti->watches, element->name_in_parent);
684 			}
685 		}
686 	}
687 }
688 
689 /* ### DE/QUEUE WATCH ELEMENTS ### to be used by watch element manager only */
690 /* implicit watchpoints may change (especially because of separation); elements updated by remove & re-add etc.; thus we need to wait a little bit (until next opcode) and then compare whether the watchpoint still exists and if not, remove it */
691 
692 void phpdbg_dissociate_watch_element(phpdbg_watch_element *element, phpdbg_watch_element *until);
693 void phpdbg_free_watch_element_tree(phpdbg_watch_element *element);
694 
695 void phpdbg_queue_element_for_recreation(phpdbg_watch_element *element) {
696 	/* store lowermost element */
697 	phpdbg_watch_element *prev;
698 
699 	if ((prev = zend_hash_find_ptr(&PHPDBG_G(watch_recreation), element->str))) {
700 		phpdbg_watch_element *child = prev;
701 		do {
702 			if (child == element) {
703 				return;
704 			}
705 			child = child->child;
706 		} while (child);
707 	}
708 	zend_hash_update_ptr(&PHPDBG_G(watch_recreation), element->str, element);
709 
710 	/* dissociate from watchpoint to avoid dangling memory watches */
711 	phpdbg_dissociate_watch_element(element, prev);
712 
713 	if (!element->parent) {
714 		/* HERE BE DRAGONS; i.e. we assume HashTable is directly allocated via emalloc() ... (which *should be* the case for every user-accessible array and symbol tables) */
715 		zend_hash_index_add_empty_element(&PHPDBG_G(watch_free), (zend_ulong) element->parent_container);
716 	}
717 }
718 
719 bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *element) {
720 	zval *zv;
721 	HashTable *ht = HT_FROM_ZVP(parent);
722 
723 	if (!ht) {
724 		return 0;
725 	} else if (element->flags & (PHPDBG_WATCH_ARRAY | PHPDBG_WATCH_OBJECT)) {
726 		char *htPtr = ((char *) ht) + HT_WATCH_OFFSET;
727 		char *oldPtr = ((char *) &element->backup.ht) + HT_WATCH_OFFSET;
728 		if (phpdbg_check_watch_diff(WATCH_ON_HASHTABLE, oldPtr, htPtr)) {
729 			phpdbg_print_watch_diff(WATCH_ON_HASHTABLE, element->str, oldPtr, htPtr);
730 		}
731 
732 		phpdbg_add_ht_watch_element(parent, element);
733 	} else if ((zv = zend_symtable_find(ht, element->name_in_parent))) {
734 		if (element->flags & PHPDBG_WATCH_IMPLICIT) {
735 			zval *next = zv;
736 
737 			while (Z_TYPE_P(next) == IS_INDIRECT) {
738 				next = Z_INDIRECT_P(next);
739 			}
740 			if (Z_ISREF_P(next)) {
741 				next = Z_REFVAL_P(next);
742 			}
743 
744 			if (!phpdbg_try_re_adding_watch_element(next, element->child)) {
745 				return 0;
746 			}
747 		} else if (phpdbg_check_watch_diff(WATCH_ON_ZVAL, &element->backup.zv, zv)) {
748 			phpdbg_print_watch_diff(WATCH_ON_ZVAL, element->str, &element->backup.zv, zv);
749 		}
750 
751 		element->parent_container = ht;
752 		phpdbg_add_bucket_watch_element((Bucket *) zv, element);
753 		phpdbg_watch_parent_ht(element);
754 	} else {
755 		return 0;
756 	}
757 
758 	return 1;
759 }
760 
761 void phpdbg_automatic_dequeue_free(phpdbg_watch_element *element) {
762 	phpdbg_watch_element *child = element;
763 	while (child->child && !(child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) {
764 		child = child->child;
765 	}
766 	PHPDBG_G(watchpoint_hit) = 1;
767 	if (zend_hash_index_del(&PHPDBG_G(watch_elements), child->id) == SUCCESS) {
768 		phpdbg_notice("%.*s has been removed, removing watchpoint%s", (int) ZSTR_LEN(child->str), ZSTR_VAL(child->str), (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT) ? " recursively" : "");
769 	}
770 	phpdbg_free_watch_element_tree(element);
771 }
772 
773 void phpdbg_dequeue_elements_for_recreation(void) {
774 	phpdbg_watch_element *element;
775 
776 	ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
777 		ZEND_ASSERT(element->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_SIMPLE));
778 		if (element->parent || zend_hash_index_find(&PHPDBG_G(watch_free), (zend_ulong) element->parent_container)) {
779 			zval _zv, *zv = &_zv;
780 			if (element->parent) {
781 				ZEND_ASSERT(element->parent->watch->type == WATCH_ON_ZVAL || element->parent->watch->type == WATCH_ON_BUCKET);
782 				zv = element->parent->watch->addr.zv;
783 				while (Z_TYPE_P(zv) == IS_INDIRECT) {
784 					zv = Z_INDIRECT_P(zv);
785 				}
786 				ZVAL_DEREF(zv);
787 			} else {
788 				ZVAL_ARR(zv, element->parent_container);
789 			}
790 			if (!phpdbg_try_re_adding_watch_element(zv, element)) {
791 				phpdbg_automatic_dequeue_free(element);
792 			}
793 		} else {
794 			phpdbg_automatic_dequeue_free(element);
795 		}
796 	} ZEND_HASH_FOREACH_END();
797 
798 	zend_hash_clean(&PHPDBG_G(watch_recreation));
799 	zend_hash_clean(&PHPDBG_G(watch_free));
800 }
801 
802 /* ### WATCH ELEMENT DELETION ### only use phpdbg_remove_watch_element from the exterior */
803 void phpdbg_clean_watch_element(phpdbg_watch_element *element);
804 
805 void phpdbg_free_watch_element(phpdbg_watch_element *element) {
806 	zend_string_release(element->str);
807 	if (element->name_in_parent) {
808 		zend_string_release(element->name_in_parent);
809 	}
810 	efree(element);
811 }
812 
813 /* note: does *not* free the passed element, only clean */
814 void phpdbg_remove_watch_element_recursively(phpdbg_watch_element *element) {
815 	if (element->child) {
816 		phpdbg_remove_watch_element_recursively(element->child);
817 		phpdbg_free_watch_element(element->child);
818 		element->child = NULL;
819 	} else if (element->flags & (PHPDBG_WATCH_ARRAY | PHPDBG_WATCH_OBJECT)) {
820 		phpdbg_watch_element *child;
821 		ZEND_HASH_MAP_FOREACH_PTR(&element->child_container, child) {
822 			phpdbg_remove_watch_element_recursively(child);
823 			phpdbg_free_watch_element(child);
824 		} ZEND_HASH_FOREACH_END();
825 		zend_hash_destroy(&element->child_container);
826 	}
827 
828 	phpdbg_clean_watch_element(element);
829 }
830 
831 /* remove single watch (i.e. manual unset) or implicit removed */
832 void phpdbg_remove_watch_element(phpdbg_watch_element *element) {
833 	phpdbg_watch_element *parent = element->parent, *child = element->child;
834 	while (parent) {
835 		phpdbg_watch_element *cur = parent;
836 		parent = parent->parent;
837 		phpdbg_clean_watch_element(cur);
838 		phpdbg_free_watch_element(cur);
839 	}
840 	while (child) {
841 		phpdbg_watch_element *cur = child;
842 		child = child->child;
843 		if (cur->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
844 			phpdbg_remove_watch_element_recursively(cur);
845 			child = NULL;
846 		} else {
847 			phpdbg_clean_watch_element(cur);
848 		}
849 		phpdbg_free_watch_element(cur);
850 	}
851 	if (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
852 		phpdbg_remove_watch_element_recursively(element);
853 	} else {
854 		phpdbg_clean_watch_element(element);
855 	}
856 	zend_hash_index_del(&PHPDBG_G(watch_elements), element->id);
857 	phpdbg_free_watch_element(element);
858 }
859 
860 void phpdbg_backup_watch_element(phpdbg_watch_element *element) {
861 	memcpy(&element->backup, &element->watch->backup, /* element->watch->size */ sizeof(element->backup));
862 }
863 
864 /* until argument to prevent double remove of children elements */
865 void phpdbg_dissociate_watch_element(phpdbg_watch_element *element, phpdbg_watch_element *until) {
866 	phpdbg_watch_element *child = element;
867 	ZEND_ASSERT((element->flags & (PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_RECURSIVE)) != PHPDBG_WATCH_RECURSIVE);
868 
869 	if (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
870 		phpdbg_backup_watch_element(element);
871 		phpdbg_remove_watch_element_recursively(element);
872 		return;
873 	}
874 
875 	while (child->child != until) {
876 		child = child->child;
877 		if (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
878 			phpdbg_backup_watch_element(child);
879 			phpdbg_remove_watch_element_recursively(child);
880 			child->child = NULL;
881 			break;
882 		}
883 		if (child->child == NULL || (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) {
884 			phpdbg_backup_watch_element(child);
885 		}
886 		phpdbg_clean_watch_element(child);
887 	}
888 	/* element needs to be removed last! */
889 	if (element->child == NULL) {
890 		phpdbg_backup_watch_element(element);
891 	}
892 	phpdbg_clean_watch_element(element);
893 }
894 
895 /* unlike phpdbg_remove_watch_element this *only* frees and does not clean up element + children! Only use after previous cleanup (e.g. phpdbg_dissociate_watch_element) */
896 void phpdbg_free_watch_element_tree(phpdbg_watch_element *element) {
897 	phpdbg_watch_element *parent = element->parent, *child = element->child;
898 	while (parent) {
899 		phpdbg_watch_element *cur = parent;
900 		parent = parent->parent;
901 		phpdbg_clean_watch_element(cur);
902 		phpdbg_free_watch_element(cur);
903 	}
904 	while (child) {
905 		phpdbg_watch_element *cur = child;
906 		child = child->child;
907 		phpdbg_free_watch_element(cur);
908 	}
909 	phpdbg_free_watch_element(element);
910 }
911 
912 void phpdbg_update_watch_element_watch(phpdbg_watch_element *element) {
913 	if (element->flags & PHPDBG_WATCH_IMPLICIT) {
914 		phpdbg_watch_element *child = element->child;
915 		while (child->flags & PHPDBG_WATCH_IMPLICIT) {
916 			child = child->child;
917 		}
918 
919 		ZEND_ASSERT(element->watch->type == WATCH_ON_ZVAL || element->watch->type == WATCH_ON_BUCKET);
920 		phpdbg_queue_element_for_recreation(element);
921 	} else if (element->flags & (PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_SIMPLE)) {
922 		phpdbg_queue_element_for_recreation(element);
923 	} else if (element->flags & PHPDBG_WATCH_RECURSIVE) {
924 		phpdbg_remove_watch_element_recursively(element);
925 		if (element->parent->flags & (PHPDBG_WATCH_OBJECT | PHPDBG_WATCH_ARRAY)) {
926 			zend_hash_del(&element->parent->child_container, element->str);
927 		} else {
928 			element->parent->child = NULL;
929 		}
930 		phpdbg_free_watch_element(element);
931 	}
932 }
933 
934 void phpdbg_update_watch_collision_elements(phpdbg_watchpoint_t *watch) {
935 	phpdbg_watchpoint_t *parent;
936 	phpdbg_watch_element *element;
937 
938 	ZEND_HASH_MAP_FOREACH_PTR(&watch->coll->parents, parent) {
939 		if (parent->coll) {
940 			phpdbg_update_watch_collision_elements(parent);
941 		} else {
942 			ZEND_HASH_MAP_FOREACH_PTR(&parent->elements, element) {
943 				phpdbg_update_watch_element_watch(element);
944 			} ZEND_HASH_FOREACH_END();
945 		}
946 	} ZEND_HASH_FOREACH_END();
947 }
948 
949 void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
950 	phpdbg_watch_element *element;
951 
952 	phpdbg_remove_watchpoint_btree(watch);
953 	phpdbg_deactivate_watchpoint(watch);
954 	phpdbg_delete_watch_collision(watch);
955 
956 	if (watch->coll) {
957 		phpdbg_update_watch_collision_elements(watch);
958 		return;
959 	}
960 
961 	watch->elements.nNumOfElements++; /* dirty hack to avoid double free */
962 	ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
963 		phpdbg_update_watch_element_watch(element);
964 	} ZEND_HASH_FOREACH_END();
965 	zend_hash_destroy(&watch->elements);
966 
967 	efree(watch);
968 }
969 
970 void phpdbg_clean_watch_element(phpdbg_watch_element *element) {
971 	phpdbg_unwatch_parent_ht(element);
972 
973 	if (element->watch) {
974 		HashTable *elements = &element->watch->elements;
975 		zend_hash_del(elements, element->str);
976 		if (zend_hash_num_elements(elements) == 0) {
977 			phpdbg_remove_watchpoint(element->watch);
978 		}
979 	}
980 }
981 
982 /* TODO: compile a name of all hit watchpoints (ids ??) */
983 zend_string *phpdbg_watchpoint_change_collision_name(phpdbg_watchpoint_t *watch) {
984 	phpdbg_watchpoint_t *parent;
985 	phpdbg_watch_element *element;
986 	zend_string *name = NULL;
987 	if (watch->coll) {
988 		ZEND_HASH_MAP_FOREACH_PTR(&watch->coll->parents, parent) {
989 			if (name) {
990 				zend_string_release(name);
991 			}
992 			name = phpdbg_watchpoint_change_collision_name(parent);
993 		} ZEND_HASH_FOREACH_END();
994 		return name;
995 	}
996 	ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
997 		if (element->flags & PHPDBG_WATCH_IMPLICIT) {
998 			if ((watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) && Z_TYPE(watch->backup.zv) > IS_STRING) {
999 				phpdbg_update_watch_element_watch(element->child);
1000 			}
1001 			continue;
1002 		}
1003 		name = element->str;
1004 	} ZEND_HASH_FOREACH_END();
1005 
1006 	return name ? zend_string_copy(name) : NULL;
1007 }
1008 
1009 /* ### WATCHING FOR CHANGES ### */
1010 /* TODO: enforce order: first parents, then children, in order to avoid false positives */
1011 void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
1012 	zend_string *name = NULL;
1013 	void *comparePtr;
1014 
1015 	if (watch->type == WATCH_ON_HASHTABLE) {
1016 		phpdbg_watch_element *element;
1017 		zend_string *str;
1018 		zend_long idx;
1019 		zval *zv;
1020 		ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
1021 			if (element->flags & PHPDBG_WATCH_RECURSIVE) {
1022 				phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) HT_WATCH_HT(watch));
1023 				phpdbg_watch_ht_info *hti = res ? res->ptr : NULL;
1024 
1025 				ZEND_HASH_REVERSE_FOREACH_KEY_VAL(HT_WATCH_HT(watch), idx, str, zv) {
1026 					if (!str) {
1027 						str = zend_long_to_str(idx); // TODO: hack, use proper int handling for name in parent
1028 					} else {
1029 						str = zend_string_copy(str);
1030 					}
1031 					if (hti && zend_hash_find(&hti->watches, str)) {
1032 						zend_string_release(str);
1033 						break;
1034 					}
1035 					ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
1036 						if (element->flags & PHPDBG_WATCH_RECURSIVE) {
1037 							phpdbg_add_recursive_watch_from_ht(element, idx, str, zv);
1038 						}
1039 					} ZEND_HASH_FOREACH_END();
1040 					phpdbg_notice("Element %.*s has been added to watchpoint", (int) ZSTR_LEN(str), ZSTR_VAL(str));
1041 					zend_string_release(str);
1042 					PHPDBG_G(watchpoint_hit) = 1;
1043 				} ZEND_HASH_FOREACH_END();
1044 
1045 				break;
1046 			}
1047 		} ZEND_HASH_FOREACH_END();
1048 	}
1049 	if (watch->type == WATCH_ON_HASHDATA) {
1050 		return;
1051 	}
1052 
1053 	switch (watch->type) {
1054 		case WATCH_ON_STR:
1055 			comparePtr = &ZSTR_LEN(watch->backup.str);
1056 			break;
1057 		case WATCH_ON_HASHTABLE:
1058 			comparePtr = (char *) &watch->backup.ht + HT_WATCH_OFFSET;
1059 			break;
1060 		default:
1061 			comparePtr = &watch->backup;
1062 	}
1063 	if (!phpdbg_check_watch_diff(watch->type, comparePtr, watch->addr.ptr)) {
1064 		return;
1065 	}
1066 	if (watch->type == WATCH_ON_REFCOUNTED && !(PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
1067 		phpdbg_watch_backup_data(watch);
1068 		return;
1069 	}
1070 	if (watch->type == WATCH_ON_BUCKET) {
1071 		if (watch->backup.bucket.key != watch->addr.bucket->key || (watch->backup.bucket.key != NULL && watch->backup.bucket.h != watch->addr.bucket->h)) {
1072 			phpdbg_watch_element *element = NULL;
1073 			zval *new;
1074 
1075 			ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
1076 				break;
1077 			} ZEND_HASH_FOREACH_END();
1078 
1079 			ZEND_ASSERT(element); /* elements must be non-empty */
1080 			new = zend_symtable_find(element->parent_container, element->name_in_parent);
1081 
1082 			if (!new) {
1083 				/* dequeuing will take care of appropriate notification about removal */
1084 				phpdbg_remove_watchpoint(watch);
1085 				return;
1086 			}
1087 
1088 			phpdbg_remove_watchpoint_btree(watch);
1089 			phpdbg_deactivate_watchpoint(watch);
1090 			watch->addr.zv = new;
1091 			phpdbg_store_watchpoint_btree(watch);
1092 			phpdbg_activate_watchpoint(watch);
1093 
1094 			if (!phpdbg_check_watch_diff(WATCH_ON_ZVAL, &watch->backup.bucket.val, watch->addr.ptr)) {
1095 				phpdbg_watch_backup_data(watch);
1096 				return;
1097 			}
1098 		} else if (Z_TYPE_P(watch->addr.zv) == IS_UNDEF) {
1099 			/* dequeuing will take care of appropriate notification about removal */
1100 			phpdbg_remove_watchpoint(watch);
1101 			return;
1102 		}
1103 	}
1104 
1105 	name = phpdbg_watchpoint_change_collision_name(watch);
1106 
1107 	if (name) {
1108 		phpdbg_print_watch_diff(watch->type, name, comparePtr, watch->addr.ptr);
1109 		zend_string_release(name);
1110 	}
1111 
1112 	if (watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) {
1113 		phpdbg_watch_element *element;
1114 		phpdbg_update_watch_ref(watch);
1115 		ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, element) {
1116 			if (element->flags & PHPDBG_WATCH_RECURSIVE) {
1117 				phpdbg_recurse_watch_element(element);
1118 			}
1119 		} ZEND_HASH_FOREACH_END();
1120 	}
1121 
1122 	phpdbg_watch_backup_data(watch);
1123 }
1124 
1125 void phpdbg_reenable_memory_watches(void) {
1126 	zend_ulong page;
1127 	phpdbg_btree_result *res;
1128 	phpdbg_watchpoint_t *watch;
1129 
1130 	ZEND_HASH_MAP_FOREACH_NUM_KEY(PHPDBG_G(watchlist_mem), page) {
1131 		/* Disable writing again if there are any watchers on that page */
1132 		res = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), page + phpdbg_pagesize - 1);
1133 		if (res) {
1134 			watch = res->ptr;
1135 			if ((char *) page < (char *) watch->addr.ptr + watch->size) {
1136 #ifdef HAVE_USERFAULTFD_WRITEFAULT
1137 				if (PHPDBG_G(watch_userfaultfd)) {
1138 					struct uffdio_writeprotect protect = {
1139 						.mode = UFFDIO_WRITEPROTECT_MODE_WP,
1140 						.range = {
1141 							.start = (__u64) page,
1142 							.len = phpdbg_pagesize
1143 						}
1144 					};
1145 					ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_WRITEPROTECT,  &protect);
1146 				} else
1147 #endif
1148 				{
1149 					mprotect((void *) page, phpdbg_pagesize, PROT_READ);
1150 				}
1151 			}
1152 		}
1153 	} ZEND_HASH_FOREACH_END();
1154 	zend_hash_clean(PHPDBG_G(watchlist_mem));
1155 }
1156 
1157 int phpdbg_print_changed_zvals(void) {
1158 	int ret;
1159 	zend_ulong page;
1160 	phpdbg_watchpoint_t *watch;
1161 	phpdbg_btree_result *res;
1162 	HashTable *mem_list = NULL;
1163 
1164 	if (zend_hash_num_elements(&PHPDBG_G(watch_elements)) == 0) {
1165 		return FAILURE;
1166 	}
1167 
1168 	if (zend_hash_num_elements(PHPDBG_G(watchlist_mem)) > 0) {
1169 		/* we must not add elements to the hashtable while iterating over it (resize => read into freed memory) */
1170 		mem_list = PHPDBG_G(watchlist_mem);
1171 		PHPDBG_G(watchlist_mem) = PHPDBG_G(watchlist_mem_backup);
1172 
1173 		ZEND_HASH_MAP_FOREACH_NUM_KEY(mem_list, page) {
1174 			phpdbg_btree_position pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), page, page + phpdbg_pagesize);
1175 
1176 			while ((res = phpdbg_btree_next(&pos))) {
1177 				watch = res->ptr;
1178 				phpdbg_check_watchpoint(watch);
1179 			}
1180 			if ((res = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), page - 1))) {
1181 				watch = res->ptr;
1182 				if ((char *) page < (char *) watch->addr.ptr + watch->size) {
1183 					phpdbg_check_watchpoint(watch);
1184 				}
1185 			}
1186 		} ZEND_HASH_FOREACH_END();
1187 	}
1188 
1189 	phpdbg_dequeue_elements_for_recreation();
1190 
1191 	phpdbg_reenable_memory_watches();
1192 
1193 	if (mem_list) {
1194 		PHPDBG_G(watchlist_mem) = mem_list;
1195 		phpdbg_reenable_memory_watches();
1196 	}
1197 
1198 	ret = PHPDBG_G(watchpoint_hit) ? SUCCESS : FAILURE;
1199 	PHPDBG_G(watchpoint_hit) = 0;
1200 
1201 	return ret;
1202 }
1203 
1204 void phpdbg_watch_efree(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) {
1205 	phpdbg_btree_result *result;
1206 
1207 	/* only do expensive checks if there are any watches at all */
1208 	if (zend_hash_num_elements(&PHPDBG_G(watch_elements))) {
1209 		if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ptr))) {
1210 			phpdbg_watchpoint_t *watch = result->ptr;
1211 			if (watch->type != WATCH_ON_HASHDATA) {
1212 				phpdbg_remove_watchpoint(watch);
1213 			} else {
1214 				/* remove all linked watchpoints, they will be dissociated from their elements */
1215 				phpdbg_watch_element *element;
1216 				phpdbg_watch_ht_info *hti = (phpdbg_watch_ht_info *) watch;
1217 
1218 				ZEND_HASH_MAP_FOREACH_PTR(&hti->watches, element) {
1219 					zend_ulong num = zend_hash_num_elements(&hti->watches);
1220 					phpdbg_remove_watchpoint(element->watch);
1221 					if (num == 1) { /* prevent access into freed memory */
1222 						break;
1223 					}
1224 				} ZEND_HASH_FOREACH_END();
1225 			}
1226 		}
1227 
1228 		/* special case watchpoints as they aren't on ptr but on ptr + HT_WATCH_OFFSET */
1229 		if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), HT_WATCH_OFFSET + (zend_ulong) ptr))) {
1230 			phpdbg_watchpoint_t *watch = result->ptr;
1231 			if (watch->type == WATCH_ON_HASHTABLE) {
1232 				phpdbg_remove_watchpoint(watch);
1233 			}
1234 		}
1235 
1236 		zend_hash_index_del(&PHPDBG_G(watch_free), (zend_ulong) ptr);
1237 	}
1238 
1239 	if (PHPDBG_G(original_free_function)) {
1240 		PHPDBG_G(original_free_function)(ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
1241 	}
1242 }
1243 
1244 /* ### USER API ### */
1245 void phpdbg_list_watchpoints(void) {
1246 	phpdbg_watch_element *element;
1247 
1248 	ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_elements), element) {
1249 		phpdbg_writeln("%.*s (%s, %s)", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str), (element->flags & (PHPDBG_WATCH_ARRAY|PHPDBG_WATCH_OBJECT)) ? "array" : "variable", (element->flags & PHPDBG_WATCH_RECURSIVE) ? "recursive" : "simple");
1250 	} ZEND_HASH_FOREACH_END();
1251 }
1252 
1253 static int phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element *element) {
1254 	element->flags = PHPDBG_WATCH_SIMPLE;
1255 	phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1256 	return SUCCESS;
1257 }
1258 
1259 static int phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element *element) {
1260 	phpdbg_watch_element *new;
1261 	zend_string *str;
1262 	zval *orig_zv = zv;
1263 
1264 	ZVAL_DEREF(zv);
1265 	if (Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT) {
1266 		return FAILURE;
1267 	}
1268 
1269 	new = ecalloc(1, sizeof(phpdbg_watch_element));
1270 
1271 	str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
1272 	zend_string_release(element->str);
1273 	element->str = str;
1274 	element->flags = PHPDBG_WATCH_IMPLICIT;
1275 	phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element);
1276 	element->child = new;
1277 
1278 	new->flags = PHPDBG_WATCH_SIMPLE;
1279 	new->str = zend_string_copy(str);
1280 	new->parent = element;
1281 	phpdbg_add_ht_watch_element(zv, new);
1282 	return SUCCESS;
1283 }
1284 
1285 static int phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element *element) {
1286 	element->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT;
1287 	element->child = NULL;
1288 	phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1289 	return SUCCESS;
1290 }
1291 
1292 typedef struct { int (*callback)(zval *zv, phpdbg_watch_element *); zend_string *str; } phpdbg_watch_parse_struct;
1293 
1294 static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, phpdbg_watch_parse_struct *info) {
1295 	int ret;
1296 	phpdbg_watch_element *element = ecalloc(1, sizeof(phpdbg_watch_element));
1297 	element->str = zend_string_init(name, namelen, 0);
1298 	element->name_in_parent = zend_string_init(key, keylen, 0);
1299 	element->parent_container = parent;
1300 	element->parent = PHPDBG_G(watch_tmp);
1301 	element->child = NULL;
1302 
1303 	ret = info->callback(zv, element);
1304 
1305 	efree(name);
1306 	efree(key);
1307 
1308 	if (ret != SUCCESS) {
1309 		phpdbg_remove_watch_element(element);
1310 	} else {
1311 		if (PHPDBG_G(watch_tmp)) {
1312 			PHPDBG_G(watch_tmp)->child = element;
1313 		}
1314 
1315 		if (element->child) {
1316 			element = element->child;
1317 		}
1318 
1319 		/* work around missing API for extending an array with a new element, and getting its index */
1320 		zend_hash_next_index_insert_ptr(&PHPDBG_G(watch_elements), element);
1321 		element->id = PHPDBG_G(watch_elements).nNextFreeElement - 1;
1322 
1323 		phpdbg_notice("Added%s watchpoint #%u for %.*s", (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) ? " recursive" : "", element->id, (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
1324 	}
1325 
1326 	PHPDBG_G(watch_tmp) = NULL;
1327 
1328 	return ret;
1329 }
1330 
1331 PHPDBG_API int phpdbg_watchpoint_parse_input(char *input, size_t len, HashTable *parent, size_t i, phpdbg_watch_parse_struct *info, bool silent) {
1332 	return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, NULL, 0, info);
1333 }
1334 
1335 static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, phpdbg_watch_parse_struct *info) {
1336 	phpdbg_watch_element *element;
1337 
1338 	/* do not install watch elements for references */
1339 	if (PHPDBG_G(watch_tmp) && Z_ISREF_P(PHPDBG_G(watch_tmp)->watch->addr.zv) && Z_REFVAL_P(PHPDBG_G(watch_tmp)->watch->addr.zv) == zv) {
1340 		efree(name);
1341 		efree(key);
1342 		return SUCCESS;
1343 	}
1344 
1345 	element = ecalloc(1, sizeof(phpdbg_watch_element));
1346 	element->flags = PHPDBG_WATCH_IMPLICIT;
1347 	element->str = zend_string_copy(info->str);
1348 	element->name_in_parent = zend_string_init(key, keylen, 0);
1349 	element->parent_container = parent;
1350 	element->parent = PHPDBG_G(watch_tmp);
1351 	element = phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1352 
1353 	efree(name);
1354 	efree(key);
1355 
1356 	if (PHPDBG_G(watch_tmp)) {
1357 		PHPDBG_G(watch_tmp)->child = element;
1358 	}
1359 	PHPDBG_G(watch_tmp) = element;
1360 
1361 	return SUCCESS;
1362 }
1363 
1364 static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(zval *, phpdbg_watch_element *)) {
1365 	zend_class_entry *scope = zend_get_executed_scope();
1366 	phpdbg_watch_parse_struct info;
1367 	int ret;
1368 
1369 	if (scope && len >= 5 && !memcmp("$this", input, 5)) {
1370 		zend_hash_add(EG(current_execute_data)->symbol_table, ZSTR_KNOWN(ZEND_STR_THIS), &EG(current_execute_data)->This);
1371 	}
1372 
1373 	if (callback == phpdbg_create_array_watchpoint) {
1374 		info.str = strpprintf(0, "%.*s[]", (int) len, input);
1375 	} else {
1376 		info.str = zend_string_init(input, len, 0);
1377 	}
1378 	info.callback = callback;
1379 
1380 	if (phpdbg_is_auto_global(input, len) && phpdbg_watchpoint_parse_input(input, len, &EG(symbol_table), 0, &info, 1) != FAILURE) {
1381 		zend_string_release(info.str);
1382 		return SUCCESS;
1383 	}
1384 
1385 	ret = phpdbg_parse_variable_with_arg(input, len, EG(current_execute_data)->symbol_table, 0, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_step, 0, &info);
1386 
1387 	zend_string_release(info.str);
1388 	return ret;
1389 }
1390 
1391 PHPDBG_WATCH(delete) /* {{{ */
1392 {
1393 	phpdbg_watch_element *element;
1394 	switch (param->type) {
1395 		case NUMERIC_PARAM:
1396 			if ((element = zend_hash_index_find_ptr(&PHPDBG_G(watch_elements), param->num))) {
1397 				phpdbg_remove_watch_element(element);
1398 				phpdbg_notice("Removed watchpoint %d", (int) param->num);
1399 			} else {
1400 				phpdbg_error("Nothing was deleted, no corresponding watchpoint found");
1401 			}
1402 			break;
1403 
1404 		phpdbg_default_switch_case();
1405 	}
1406 
1407 	return SUCCESS;
1408 } /* }}} */
1409 
1410 int phpdbg_create_var_watchpoint(char *input, size_t len) {
1411 	if (phpdbg_rebuild_symtable() == FAILURE) {
1412 		return FAILURE;
1413 	}
1414 
1415 	return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_create_simple_watchpoint);
1416 }
1417 
1418 PHPDBG_WATCH(recursive) /* {{{ */
1419 {
1420 	if (phpdbg_rebuild_symtable() == FAILURE) {
1421 		return SUCCESS;
1422 	}
1423 
1424 	switch (param->type) {
1425 		case STR_PARAM:
1426 			phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_recursive_watchpoint);
1427 			break;
1428 
1429 		phpdbg_default_switch_case();
1430 	}
1431 
1432 	return SUCCESS;
1433 } /* }}} */
1434 
1435 PHPDBG_WATCH(array) /* {{{ */
1436 {
1437 	if (phpdbg_rebuild_symtable() == FAILURE) {
1438 		return SUCCESS;
1439 	}
1440 
1441 	switch (param->type) {
1442 		case STR_PARAM:
1443 			phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_array_watchpoint);
1444 			break;
1445 
1446 		phpdbg_default_switch_case();
1447 	}
1448 
1449 	return SUCCESS;
1450 } /* }}} */
1451 
1452 
1453 void phpdbg_setup_watchpoints(void) {
1454 #if defined(_SC_PAGE_SIZE)
1455 	phpdbg_pagesize = sysconf(_SC_PAGE_SIZE);
1456 #elif defined(_SC_PAGESIZE)
1457 	phpdbg_pagesize = sysconf(_SC_PAGESIZE);
1458 #elif defined(_SC_NUTC_OS_PAGESIZE)
1459 	phpdbg_pagesize = sysconf(_SC_NUTC_OS_PAGESIZE);
1460 #else
1461 	phpdbg_pagesize = 4096; /* common pagesize */
1462 #endif
1463 
1464 	phpdbg_btree_init(&PHPDBG_G(watchpoint_tree), sizeof(void *) * 8);
1465 	phpdbg_btree_init(&PHPDBG_G(watch_HashTables), sizeof(void *) * 8);
1466 	zend_hash_init(&PHPDBG_G(watch_elements), 8, NULL, NULL, 0);
1467 	zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0);
1468 	zend_hash_init(&PHPDBG_G(watch_recreation), 8, NULL, NULL, 0);
1469 	zend_hash_init(&PHPDBG_G(watch_free), 8, NULL, NULL, 0);
1470 
1471 	/* put these on a separate page, to avoid conflicts with other memory */
1472 	PHPDBG_G(watchlist_mem) = malloc(phpdbg_pagesize > sizeof(HashTable) ? phpdbg_pagesize : sizeof(HashTable));
1473 	PHPDBG_G(original_watchlist_mem) = PHPDBG_G(watchlist_mem);
1474 	zend_hash_init(PHPDBG_G(watchlist_mem), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);
1475 	PHPDBG_G(watchlist_mem_backup) = malloc(phpdbg_pagesize > sizeof(HashTable) ? phpdbg_pagesize : sizeof(HashTable));
1476 	zend_hash_init(PHPDBG_G(watchlist_mem_backup), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);
1477 
1478 	PHPDBG_G(watch_tmp) = NULL;
1479 	PHPDBG_G(watchpoint_hit) = false;
1480 
1481 #ifdef HAVE_USERFAULTFD_WRITEFAULT
1482 	int flags = O_CLOEXEC;
1483 #ifdef UFFD_USER_MODE_ONLY
1484 	// unpriviliged userfaultfd are disabled by default,
1485 	// with this flag it allows ranges from the user space
1486 	// being reported.
1487 	flags |= UFFD_USER_MODE_ONLY;
1488 #endif
1489 	PHPDBG_G(watch_userfaultfd) = syscall(SYS_userfaultfd, flags);
1490 	if (PHPDBG_G(watch_userfaultfd) < 0) {
1491 		PHPDBG_G(watch_userfaultfd) = 0;
1492 	} else {
1493 		struct uffdio_api userfaultfd_features = {0};
1494 		userfaultfd_features.api = UFFD_API;
1495 		userfaultfd_features.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP;
1496 		ioctl(PHPDBG_G(watch_userfaultfd), UFFDIO_API, &userfaultfd_features);
1497 		if (userfaultfd_features.features & UFFD_FEATURE_PAGEFAULT_FLAG_WP) {
1498 			pthread_create(&PHPDBG_G(watch_userfault_thread), NULL, phpdbg_watchpoint_userfaultfd_thread, ZEND_MODULE_GLOBALS_BULK(phpdbg));
1499 		} else {
1500 			PHPDBG_G(watch_userfaultfd) = 0;
1501 		}
1502 	}
1503 #endif
1504 }
1505 
1506 void phpdbg_destroy_watchpoints(void) {
1507 	phpdbg_watch_element *element;
1508 
1509 	/* unconditionally free all remaining elements to avoid memory leaks */
1510 	ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
1511 		phpdbg_automatic_dequeue_free(element);
1512 	} ZEND_HASH_FOREACH_END();
1513 
1514 	/* 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. */
1515     phpdbg_purge_watchpoint_tree();
1516 
1517 #ifdef HAVE_USERFAULTFD_WRITEFAULT
1518 	if (PHPDBG_G(watch_userfaultfd)) {
1519 		pthread_cancel(PHPDBG_G(watch_userfault_thread));
1520 		close(PHPDBG_G(watch_userfaultfd));
1521 	}
1522 #endif
1523 
1524 	zend_hash_destroy(&PHPDBG_G(watch_elements)); PHPDBG_G(watch_elements).nNumOfElements = 0; /* phpdbg_watch_efree() is checking against this arrays size */
1525 	zend_hash_destroy(&PHPDBG_G(watch_recreation));
1526 	zend_hash_destroy(&PHPDBG_G(watch_free));
1527 	zend_hash_destroy(&PHPDBG_G(watch_collisions));
1528 	zend_hash_destroy(PHPDBG_G(original_watchlist_mem));
1529 	free(PHPDBG_G(original_watchlist_mem));
1530 	zend_hash_destroy(PHPDBG_G(watchlist_mem_backup));
1531 	free(PHPDBG_G(watchlist_mem_backup));
1532 }
1533 
1534 void phpdbg_purge_watchpoint_tree(void) {
1535 	phpdbg_btree_position pos;
1536 	phpdbg_btree_result *res;
1537 
1538 	pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), 0, -1);
1539 	while ((res = phpdbg_btree_next(&pos))) {
1540 		phpdbg_deactivate_watchpoint(res->ptr);
1541 	}
1542 }
1543