Skip to content

Commit 2e970c8

Browse files
committed
Fix GH-22480: phpdbg use-after-free re-watching a watched variable
phpdbg_add_watch_element() frees the passed element and returns the existing one when the variable is already watched, but several callers kept using the freed pointer, so re-watching a variable was a use-after-free in more places than the two reported. Report the duplicate back through the parse callback and register nothing, and reject a recursive watch's hashtable element that collides with an existing watch, at creation and recreation, rather than freeing a still-referenced one. A recursive watch over an already-array-watched variable now watches only the root and skips the shared hashtable. Running the tests under ASAN exposed a second, latent use-after-free at shutdown: the watch tables are module-scoped but hold request-pool memory, so under the tracked allocator freeing a watched variable during executor teardown re-queues an element after the last drain, tracked_free_all frees the pool, and phpdbg_destroy_watchpoints reads it at module shutdown. Tear the watch state down in a post_deactivate handler, which runs after the executor frees values but before the memory manager is torn down. Dropping watch_006's xfail surfaced a separate pre-existing bug on large-page platforms: the fault handler rechecks every watchpoint on the faulting page, so on 16 KB pages a value-only write to an element can fault the page holding a watched hashtable's own struct and run the recursive rescan, which re-reported existing elements as freshly added. Only run that rescan when the element count actually changed. Fixes GH-22480
1 parent 8b39b01 commit 2e970c8

7 files changed

Lines changed: 253 additions & 46 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ PHP NEWS
184184
. Mark Phar::buildFromIterator() base directory argument as a path.
185185
(ndossche)
186186

187+
- phpdbg:
188+
. Fixed GH-22480 (Use-after-free when re-watching an already-watched
189+
variable). (iliaal)
190+
187191
- Posix:
188192
. Added validity check to the flags argument for posix_access(). (arshidkv12)
189193

sapi/phpdbg/phpdbg.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ static PHP_RSHUTDOWN_FUNCTION(phpdbg) /* {{{ */
239239
return SUCCESS;
240240
} /* }}} */
241241

242+
static ZEND_MODULE_POST_ZEND_DEACTIVATE_D(phpdbg) /* {{{ */
243+
{
244+
phpdbg_release_watch_elements();
245+
246+
return SUCCESS;
247+
} /* }}} */
248+
242249
/* {{{ Attempt to set the execution context for phpdbg
243250
If the execution context was set previously it is returned
244251
If the execution context was not set previously boolean true is returned
@@ -681,7 +688,9 @@ static zend_module_entry sapi_phpdbg_module_entry = {
681688
PHP_RSHUTDOWN(phpdbg),
682689
NULL,
683690
PHPDBG_VERSION,
684-
STANDARD_MODULE_PROPERTIES
691+
NO_MODULE_GLOBALS,
692+
ZEND_MODULE_POST_ZEND_DEACTIVATE_N(phpdbg),
693+
STANDARD_MODULE_PROPERTIES_EX
685694
};
686695

687696
static inline int php_sapi_phpdbg_module_startup(sapi_module_struct *module) /* {{{ */

sapi/phpdbg/phpdbg_watch.c

Lines changed: 174 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,39 @@ void phpdbg_free_watch_element(phpdbg_watch_element *element);
501501
void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch);
502502
void phpdbg_watch_parent_ht(phpdbg_watch_element *element);
503503

504-
phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element) {
504+
static bool phpdbg_watch_element_collides(void *addr, phpdbg_watchtype type, phpdbg_watch_element *element) {
505+
phpdbg_watch_element *old;
506+
phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) addr);
507+
508+
if (res) {
509+
phpdbg_watchpoint_t *watch = res->ptr;
510+
if (watch->type == type && (old = zend_hash_find_ptr(&watch->elements, element->str)) && old != element) {
511+
return true;
512+
}
513+
}
514+
515+
return (old = zend_hash_find_ptr(&PHPDBG_G(watch_recreation), element->str)) && old != element;
516+
}
517+
518+
static bool phpdbg_ht_watch_element_collides(zval *zv, phpdbg_watch_element *element) {
519+
HashTable *ht = HT_FROM_ZVP(zv);
520+
521+
if (!ht) {
522+
return false;
523+
}
524+
525+
return phpdbg_watch_element_collides(HT_WATCH_OFFSET + (char *) ht, WATCH_ON_HASHTABLE, element);
526+
}
527+
528+
static bool phpdbg_bucket_watch_element_collides(zval *zv, phpdbg_watch_element *element) {
529+
return phpdbg_watch_element_collides((Bucket *) zv, WATCH_ON_BUCKET, element);
530+
}
531+
532+
phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element, bool *is_new) {
505533
phpdbg_btree_result *res;
534+
if (is_new) {
535+
*is_new = true;
536+
}
506537
if ((res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr)) == NULL) {
507538
phpdbg_watchpoint_t *mem = emalloc(sizeof(*mem));
508539
*mem = *watch;
@@ -520,6 +551,9 @@ phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdb
520551
if (element != old_element) {
521552
phpdbg_free_watch_element(element);
522553
}
554+
if (is_new) {
555+
*is_new = false;
556+
}
523557
return old_element;
524558
}
525559
}
@@ -534,25 +568,35 @@ phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdb
534568
return element;
535569
}
536570

537-
phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element) {
571+
phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element, bool *is_new) {
538572
phpdbg_watchpoint_t watch;
539573
phpdbg_set_bucket_watchpoint(bucket, &watch);
540-
element = phpdbg_add_watch_element(&watch, element);
541-
phpdbg_watch_parent_ht(element);
542-
return element;
574+
bool added_new;
575+
phpdbg_watch_element *added = phpdbg_add_watch_element(&watch, element, &added_new);
576+
if (added_new) {
577+
phpdbg_watch_parent_ht(added);
578+
}
579+
if (is_new) {
580+
*is_new = added_new;
581+
}
582+
return added;
543583
}
544584

545-
phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element) {
585+
phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element, bool *is_new) {
546586
phpdbg_watchpoint_t watch;
547587
HashTable *ht = HT_FROM_ZVP(zv);
548588

589+
if (is_new) {
590+
*is_new = false;
591+
}
592+
549593
if (!ht) {
550594
return NULL;
551595
}
552596

553597
element->flags |= Z_TYPE_P(zv) == IS_ARRAY ? PHPDBG_WATCH_ARRAY : PHPDBG_WATCH_OBJECT;
554598
phpdbg_set_ht_watchpoint(ht, &watch);
555-
return phpdbg_add_watch_element(&watch, element);
599+
return phpdbg_add_watch_element(&watch, element, is_new);
556600
}
557601

558602
bool phpdbg_is_recursively_watched(void *ptr, phpdbg_watch_element *element) {
@@ -588,8 +632,15 @@ void phpdbg_add_recursive_watch_from_ht(phpdbg_watch_element *element, zend_long
588632
child->parent = element;
589633
child->child = NULL;
590634
child->parent_container = HT_WATCH_HT(element->watch);
591-
zend_hash_add_ptr(&element->child_container, child->str, child);
592-
phpdbg_add_bucket_watch_element((Bucket *) zv, child);
635+
if (phpdbg_bucket_watch_element_collides(zv, child)) {
636+
phpdbg_free_watch_element(child);
637+
return;
638+
}
639+
bool added_new;
640+
phpdbg_add_bucket_watch_element((Bucket *) zv, child, &added_new);
641+
if (added_new) {
642+
zend_hash_add_ptr(&element->child_container, child->str, child);
643+
}
593644
}
594645

595646
void phpdbg_recurse_watch_element(phpdbg_watch_element *element) {
@@ -627,8 +678,13 @@ void phpdbg_recurse_watch_element(phpdbg_watch_element *element) {
627678
child->child = NULL;
628679
element->child = child;
629680
}
681+
if (phpdbg_ht_watch_element_collides(zv, child)) {
682+
phpdbg_free_watch_element(child);
683+
element->child = NULL;
684+
return;
685+
}
630686
zend_hash_init(&child->child_container, 8, NULL, NULL, 0);
631-
phpdbg_add_ht_watch_element(zv, child);
687+
phpdbg_add_ht_watch_element(zv, child, NULL);
632688
} else if (zend_hash_num_elements(&element->child_container) == 0) {
633689
zend_string *str;
634690
zend_long idx;
@@ -727,7 +783,10 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem
727783
phpdbg_print_watch_diff(WATCH_ON_HASHTABLE, element->str, oldPtr, htPtr);
728784
}
729785

730-
phpdbg_add_ht_watch_element(parent, element);
786+
if ((element->flags & PHPDBG_WATCH_RECURSIVE) && phpdbg_ht_watch_element_collides(parent, element)) {
787+
return false;
788+
}
789+
element = phpdbg_add_ht_watch_element(parent, element, NULL);
731790
} else if ((zv = zend_symtable_find(ht, element->name_in_parent))) {
732791
if (element->flags & PHPDBG_WATCH_IMPLICIT) {
733792
zval *next = zv;
@@ -746,9 +805,11 @@ bool phpdbg_try_re_adding_watch_element(zval *parent, phpdbg_watch_element *elem
746805
phpdbg_print_watch_diff(WATCH_ON_ZVAL, element->str, &element->backup.zv, zv);
747806
}
748807

808+
if ((element->flags & PHPDBG_WATCH_RECURSIVE) && phpdbg_bucket_watch_element_collides(zv, element)) {
809+
return false;
810+
}
749811
element->parent_container = ht;
750-
phpdbg_add_bucket_watch_element((Bucket *) zv, element);
751-
phpdbg_watch_parent_ht(element);
812+
element = phpdbg_add_bucket_watch_element((Bucket *) zv, element, NULL);
752813
} else {
753814
return false;
754815
}
@@ -1010,7 +1071,8 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
10101071
zend_string *name = NULL;
10111072
void *comparePtr;
10121073

1013-
if (watch->type == WATCH_ON_HASHTABLE) {
1074+
if (watch->type == WATCH_ON_HASHTABLE
1075+
&& phpdbg_check_watch_diff(WATCH_ON_HASHTABLE, (char *) &watch->backup.ht + HT_WATCH_OFFSET, watch->addr.ptr)) {
10141076
phpdbg_watch_element *element;
10151077
zend_string *str;
10161078
zend_long idx;
@@ -1248,63 +1310,106 @@ void phpdbg_list_watchpoints(void) {
12481310
} ZEND_HASH_FOREACH_END();
12491311
}
12501312

1251-
static int phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element *element) {
1252-
element->flags = PHPDBG_WATCH_SIMPLE;
1253-
phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1254-
return SUCCESS;
1313+
typedef enum {
1314+
PHPDBG_WATCH_ADD_OK,
1315+
PHPDBG_WATCH_ADD_FAILED,
1316+
PHPDBG_WATCH_ADD_DUPLICATE,
1317+
} phpdbg_watch_add_result;
1318+
1319+
static phpdbg_watch_add_result phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element **element) {
1320+
phpdbg_watch_element *added;
1321+
(*element)->flags = PHPDBG_WATCH_SIMPLE;
1322+
bool added_new;
1323+
added = phpdbg_add_bucket_watch_element((Bucket *) zv, *element, &added_new);
1324+
if (!added_new) {
1325+
*element = added;
1326+
return PHPDBG_WATCH_ADD_DUPLICATE;
1327+
}
1328+
return PHPDBG_WATCH_ADD_OK;
12551329
}
12561330

1257-
static int phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element *element) {
1258-
phpdbg_watch_element *new;
1331+
static phpdbg_watch_add_result phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element **element_ptr) {
1332+
phpdbg_watch_element *element = *element_ptr;
1333+
phpdbg_watch_element *new, *added;
12591334
zend_string *str;
12601335
zval *orig_zv = zv;
12611336

12621337
ZVAL_DEREF(zv);
12631338
if (Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT) {
1264-
return FAILURE;
1339+
return PHPDBG_WATCH_ADD_FAILED;
12651340
}
12661341

1267-
new = ecalloc(1, sizeof(phpdbg_watch_element));
1268-
12691342
str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
12701343
zend_string_release(element->str);
12711344
element->str = str;
12721345
element->flags = PHPDBG_WATCH_IMPLICIT;
1273-
phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element);
1274-
element->child = new;
1346+
bool added_new;
1347+
added = phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element, &added_new);
1348+
if (!added_new) {
1349+
*element_ptr = added;
1350+
return PHPDBG_WATCH_ADD_DUPLICATE;
1351+
}
1352+
element = added;
12751353

1354+
new = ecalloc(1, sizeof(phpdbg_watch_element));
12761355
new->flags = PHPDBG_WATCH_SIMPLE;
12771356
new->str = zend_string_copy(str);
12781357
new->parent = element;
1279-
phpdbg_add_ht_watch_element(zv, new);
1280-
return SUCCESS;
1358+
added = phpdbg_add_ht_watch_element(zv, new, &added_new);
1359+
if (added != NULL && !added_new) {
1360+
phpdbg_clean_watch_element(element);
1361+
phpdbg_free_watch_element(element);
1362+
*element_ptr = added;
1363+
return PHPDBG_WATCH_ADD_DUPLICATE;
1364+
}
1365+
element->child = new;
1366+
*element_ptr = element;
1367+
return PHPDBG_WATCH_ADD_OK;
12811368
}
12821369

1283-
static int phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element *element) {
1284-
element->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT;
1285-
element->child = NULL;
1286-
phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1287-
return SUCCESS;
1370+
static phpdbg_watch_add_result phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element **element) {
1371+
phpdbg_watch_element *added;
1372+
(*element)->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT;
1373+
(*element)->child = NULL;
1374+
bool added_new;
1375+
added = phpdbg_add_bucket_watch_element((Bucket *) zv, *element, &added_new);
1376+
if (!added_new) {
1377+
*element = added;
1378+
return PHPDBG_WATCH_ADD_DUPLICATE;
1379+
}
1380+
return PHPDBG_WATCH_ADD_OK;
12881381
}
12891382

1290-
typedef struct { int (*callback)(zval *zv, phpdbg_watch_element *); zend_string *str; } phpdbg_watch_parse_struct;
1383+
typedef struct { phpdbg_watch_add_result (*callback)(zval *zv, phpdbg_watch_element **); zend_string *str; } phpdbg_watch_parse_struct;
12911384

12921385
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) {
1293-
int ret;
1386+
int ret = SUCCESS;
12941387
phpdbg_watch_element *element = ecalloc(1, sizeof(phpdbg_watch_element));
12951388
element->str = zend_string_init(name, namelen, 0);
12961389
element->name_in_parent = zend_string_init(key, keylen, 0);
12971390
element->parent_container = parent;
12981391
element->parent = PHPDBG_G(watch_tmp);
12991392
element->child = NULL;
13001393

1301-
ret = info->callback(zv, element);
1394+
phpdbg_watch_add_result result = info->callback(zv, &element);
13021395

13031396
efree(name);
13041397
efree(key);
13051398

1306-
if (ret != SUCCESS) {
1399+
if (result == PHPDBG_WATCH_ADD_FAILED) {
13071400
phpdbg_remove_watch_element(element);
1401+
ret = FAILURE;
1402+
} else if (result == PHPDBG_WATCH_ADD_DUPLICATE) {
1403+
phpdbg_notice("%.*s is already being watched", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
1404+
while (PHPDBG_G(watch_tmp) && PHPDBG_G(watch_tmp)->child == NULL) {
1405+
phpdbg_watch_element *parent = PHPDBG_G(watch_tmp)->parent;
1406+
phpdbg_clean_watch_element(PHPDBG_G(watch_tmp));
1407+
phpdbg_free_watch_element(PHPDBG_G(watch_tmp));
1408+
if (parent) {
1409+
parent->child = NULL;
1410+
}
1411+
PHPDBG_G(watch_tmp) = parent;
1412+
}
13081413
} else {
13091414
if (PHPDBG_G(watch_tmp)) {
13101415
PHPDBG_G(watch_tmp)->child = element;
@@ -1346,7 +1451,7 @@ static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, s
13461451
element->name_in_parent = zend_string_init(key, keylen, 0);
13471452
element->parent_container = parent;
13481453
element->parent = PHPDBG_G(watch_tmp);
1349-
element = phpdbg_add_bucket_watch_element((Bucket *) zv, element);
1454+
element = phpdbg_add_bucket_watch_element((Bucket *) zv, element, NULL);
13501455

13511456
efree(name);
13521457
efree(key);
@@ -1359,7 +1464,7 @@ static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, s
13591464
return SUCCESS;
13601465
}
13611466

1362-
static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(zval *, phpdbg_watch_element *)) {
1467+
static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, phpdbg_watch_add_result (*callback)(zval *, phpdbg_watch_element **)) {
13631468
zend_class_entry *scope = zend_get_executed_scope();
13641469
phpdbg_watch_parse_struct info;
13651470
int ret;
@@ -1529,6 +1634,37 @@ void phpdbg_destroy_watchpoints(void) {
15291634
free(PHPDBG_G(watchlist_mem_backup));
15301635
}
15311636

1637+
void phpdbg_release_watch_elements(void) {
1638+
phpdbg_watch_element *element;
1639+
uint32_t guard;
1640+
1641+
ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
1642+
phpdbg_automatic_dequeue_free(element);
1643+
} ZEND_HASH_FOREACH_END();
1644+
zend_hash_clean(&PHPDBG_G(watch_recreation));
1645+
1646+
guard = zend_hash_num_elements(&PHPDBG_G(watch_elements)) + 1;
1647+
while (zend_hash_num_elements(&PHPDBG_G(watch_elements)) > 0 && guard-- > 0) {
1648+
ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_elements), element) {
1649+
phpdbg_remove_watch_element(element);
1650+
break;
1651+
} ZEND_HASH_FOREACH_END();
1652+
}
1653+
zend_hash_clean(&PHPDBG_G(watch_recreation));
1654+
1655+
phpdbg_btree_clean(&PHPDBG_G(watchpoint_tree));
1656+
phpdbg_btree_clean(&PHPDBG_G(watch_HashTables));
1657+
1658+
zend_hash_destroy(&PHPDBG_G(watch_elements));
1659+
zend_hash_init(&PHPDBG_G(watch_elements), 8, NULL, NULL, 0);
1660+
zend_hash_destroy(&PHPDBG_G(watch_recreation));
1661+
zend_hash_init(&PHPDBG_G(watch_recreation), 8, NULL, NULL, 0);
1662+
zend_hash_destroy(&PHPDBG_G(watch_free));
1663+
zend_hash_init(&PHPDBG_G(watch_free), 8, NULL, NULL, 0);
1664+
zend_hash_destroy(&PHPDBG_G(watch_collisions));
1665+
zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0);
1666+
}
1667+
15321668
void phpdbg_purge_watchpoint_tree(void) {
15331669
phpdbg_btree_position pos;
15341670
phpdbg_btree_result *res;

sapi/phpdbg/phpdbg_watch.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ typedef struct {
110110

111111
void phpdbg_setup_watchpoints(void);
112112
void phpdbg_destroy_watchpoints(void);
113+
void phpdbg_release_watch_elements(void);
113114
void phpdbg_purge_watchpoint_tree(void);
114115

115116
#ifndef _WIN32

0 commit comments

Comments
 (0)