Skip to content

Commit c92ca4b

Browse files
committed
Fix phpdbg over-read watching an element of a packed array
phpdbg watched every array element as a Bucket, but a packed array stores bare zvals, so reading Bucket.h/.key over-read the neighbouring element and a sibling write tripped a phantom break. De-indirected stack variables are not buckets either. Watch such elements as WATCH_ON_ZVAL, re-resolve them in the parent on relocation, drop the watch when the stack frame releases (IS_UNDEF), and compare the type info before the value so uninitialised CV bytes are never read. phpdbg_btree_insert_or_update published each freshly allocated node into the tree before initialising its child pointers. When the node landed on a watched page the write that initialised it faulted into the watchpoint handler, which walked the half-built node and dereferenced a wild pointer. Build the node fully, then link it in with a single store. This crashed only on 32-bit, where the compact heap places the node on the same page as the watched zval. Closes GH-22756
1 parent b6ebae1 commit c92ca4b

5 files changed

Lines changed: 79 additions & 24 deletions

File tree

sapi/phpdbg/phpdbg_btree.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,18 @@ int phpdbg_btree_insert_or_update(phpdbg_btree *tree, zend_ulong idx, void *ptr,
156156
}
157157

158158
{
159-
phpdbg_btree_branch *memory = *branch = pemalloc((i + 2) * sizeof(phpdbg_btree_branch), tree->persistent);
159+
phpdbg_btree_branch *memory = pemalloc((i + 2) * sizeof(phpdbg_btree_branch), tree->persistent);
160+
phpdbg_btree_branch *node = memory;
160161
do {
161-
(*branch)->branches[!((idx >> i) % 2)] = NULL;
162-
branch = &(*branch)->branches[(idx >> i) % 2];
163-
*branch = ++memory;
162+
node->branches[!((idx >> i) % 2)] = NULL;
163+
node->branches[(idx >> i) % 2] = node + 1;
164+
node = node + 1;
164165
} while (i--);
166+
node->result.idx = idx;
167+
node->result.ptr = ptr;
168+
*branch = memory;
165169
tree->count++;
170+
return SUCCESS;
166171
}
167172
} else if (!(flags & PHPDBG_BTREE_UPDATE)) {
168173
return FAILURE;

sapi/phpdbg/phpdbg_watch.c

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ const phpdbg_command_t phpdbg_watch_commands[] = {
133133
#define HT_WATCH_HT(watch) HT_PTR_HT((watch)->addr.ptr)
134134

135135
/* ### PRINTING POINTER DIFFERENCES ### */
136+
static bool phpdbg_check_zval_watch_diff(zval *oldPtr, zval *newPtr) {
137+
if (Z_TYPE_INFO_P(oldPtr) != Z_TYPE_INFO_P(newPtr)) {
138+
return true;
139+
}
140+
if (Z_TYPE_P(oldPtr) < IS_LONG) {
141+
return false;
142+
}
143+
return memcmp(oldPtr, newPtr, sizeof(zend_value)) != 0;
144+
}
145+
136146
bool phpdbg_check_watch_diff(phpdbg_watchtype type, void *oldPtr, void *newPtr) {
137147
switch (type) {
138148
case WATCH_ON_BUCKET:
@@ -142,7 +152,7 @@ bool phpdbg_check_watch_diff(phpdbg_watchtype type, void *oldPtr, void *newPtr)
142152
/* Fall through to also compare the value from the bucket. */
143153
ZEND_FALLTHROUGH;
144154
case WATCH_ON_ZVAL:
145-
return memcmp(oldPtr, newPtr, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */) != 0;
155+
return phpdbg_check_zval_watch_diff((zval *) oldPtr, (zval *) newPtr);
146156
case WATCH_ON_HASHTABLE:
147157
return zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_PTR_HT(newPtr));
148158
case WATCH_ON_REFCOUNTED:
@@ -568,9 +578,28 @@ phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdb
568578
return element;
569579
}
570580

581+
static bool phpdbg_zval_is_bucket_of(HashTable *ht, zval *zv) {
582+
uintptr_t off;
583+
584+
if (!ht || HT_IS_PACKED(ht)) {
585+
return false;
586+
}
587+
if ((uintptr_t) zv < (uintptr_t) ht->arData
588+
|| (uintptr_t) zv >= (uintptr_t) (ht->arData + ht->nNumUsed)) {
589+
return false;
590+
}
591+
off = (uintptr_t) zv - (uintptr_t) ht->arData;
592+
return off % sizeof(Bucket) == 0;
593+
}
594+
571595
phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element, bool *is_new) {
572596
phpdbg_watchpoint_t watch;
573-
phpdbg_set_bucket_watchpoint(bucket, &watch);
597+
598+
if (phpdbg_zval_is_bucket_of(element->parent_container, (zval *) bucket)) {
599+
phpdbg_set_bucket_watchpoint(bucket, &watch);
600+
} else {
601+
phpdbg_set_zval_watchpoint((zval *) bucket, &watch);
602+
}
574603
bool added_new;
575604
phpdbg_watch_element *added = phpdbg_add_watch_element(&watch, element, &added_new);
576605
if (added_new) {
@@ -697,7 +726,7 @@ void phpdbg_recurse_watch_element(phpdbg_watch_element *element) {
697726
}
698727

699728
void phpdbg_watch_parent_ht(phpdbg_watch_element *element) {
700-
if (element->watch->type == WATCH_ON_BUCKET) {
729+
if (element->watch->type == WATCH_ON_BUCKET || element->watch->type == WATCH_ON_ZVAL) {
701730
phpdbg_btree_result *res;
702731
phpdbg_watch_ht_info *hti;
703732
ZEND_ASSERT(element->parent_container);
@@ -716,14 +745,17 @@ void phpdbg_watch_parent_ht(phpdbg_watch_element *element) {
716745
hti = (phpdbg_watch_ht_info *) res->ptr;
717746
}
718747

719-
zend_hash_add_ptr(&hti->watches, element->name_in_parent, element);
748+
if (zend_hash_add_ptr(&hti->watches, element->name_in_parent, element)) {
749+
element->flags |= PHPDBG_WATCH_HT_REGISTERED;
750+
}
720751
}
721752
}
722753

723754
void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
724-
if (element->watch && element->watch->type == WATCH_ON_BUCKET) {
755+
if (element->flags & PHPDBG_WATCH_HT_REGISTERED) {
725756
phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) element->parent_container);
726757
ZEND_ASSERT(element->parent_container);
758+
element->flags &= ~PHPDBG_WATCH_HT_REGISTERED;
727759
if (res) {
728760
phpdbg_watch_ht_info *hti = res->ptr;
729761

@@ -1120,6 +1152,32 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
11201152
default:
11211153
comparePtr = &watch->backup;
11221154
}
1155+
if (watch->type == WATCH_ON_BUCKET || watch->type == WATCH_ON_ZVAL) {
1156+
phpdbg_watch_element *first = NULL;
1157+
ZEND_HASH_MAP_FOREACH_PTR(&watch->elements, first) {
1158+
break;
1159+
} ZEND_HASH_FOREACH_END();
1160+
if (first && first->parent_container && HT_IS_PACKED(first->parent_container)) {
1161+
zval *new = zend_symtable_find(first->parent_container, first->name_in_parent);
1162+
if (!new || Z_TYPE_P(new) == IS_UNDEF) {
1163+
phpdbg_remove_watchpoint(watch);
1164+
return;
1165+
}
1166+
if (new != watch->addr.zv) {
1167+
phpdbg_remove_watchpoint_btree(watch);
1168+
phpdbg_deactivate_watchpoint(watch);
1169+
watch->addr.zv = new;
1170+
phpdbg_store_watchpoint_btree(watch);
1171+
phpdbg_activate_watchpoint(watch);
1172+
}
1173+
if (!phpdbg_check_watch_diff(WATCH_ON_ZVAL, &watch->backup.bucket.val, watch->addr.ptr)) {
1174+
phpdbg_watch_backup_data(watch);
1175+
return;
1176+
}
1177+
goto changed;
1178+
}
1179+
}
1180+
11231181
if (!phpdbg_check_watch_diff(watch->type, comparePtr, watch->addr.ptr)) {
11241182
return;
11251183
}
@@ -1160,8 +1218,14 @@ void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
11601218
phpdbg_remove_watchpoint(watch);
11611219
return;
11621220
}
1221+
} else if (watch->type == WATCH_ON_ZVAL && !watch->coll
1222+
&& zend_hash_num_elements(&watch->elements) > 0
1223+
&& Z_TYPE_P(watch->addr.zv) == IS_UNDEF) {
1224+
phpdbg_remove_watchpoint(watch);
1225+
return;
11631226
}
11641227

1228+
changed:
11651229
name = phpdbg_watchpoint_change_collision_name(watch);
11661230

11671231
if (name) {

sapi/phpdbg/phpdbg_watch.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ typedef enum {
5454
#define PHPDBG_WATCH_NORMAL (PHPDBG_WATCH_SIMPLE | PHPDBG_WATCH_RECURSIVE)
5555
#define PHPDBG_WATCH_IMPLICIT 0x10
5656
#define PHPDBG_WATCH_RECURSIVE_ROOT 0x20
57+
#define PHPDBG_WATCH_HT_REGISTERED 0x40
5758

5859
typedef struct _phpdbg_watch_collision phpdbg_watch_collision;
5960

sapi/phpdbg/tests/watch_005.phpt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
--TEST--
22
Test proper watch comparisons when having multiple levels of indirection from a zval to its value
3-
--SKIPIF--
4-
<?php
5-
if (PHP_INT_SIZE == 4) {
6-
die("xfail There may be flaws in the implementation of watchpoints that cause failures");
7-
}
8-
if (getenv('SKIP_ASAN')) {
9-
die("skip intentionally causes segfaults");
10-
}
11-
?>
123
--PHPDBG--
134
b 3
145
r

sapi/phpdbg/tests/watch_006.phpt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ c
1111

1212

1313

14-
1514
q
1615
--EXPECTF--
1716
[Successful compilation of %s]
@@ -48,11 +47,6 @@ prompt> [Element 1 has been added to watchpoint]
4847
00010:
4948
prompt> [Breaking on watchpoint $b]
5049
Old value inaccessible or destroyed
51-
New value (reference): Array ([0] => 2,[1] => 3)
52-
>00009: $b = &$c;
53-
00010:
54-
prompt> [Breaking on watchpoint $b]
55-
Old value inaccessible or destroyed
5650
New value (reference): Array ([0] => 1)
5751
>00010:
5852
prompt> [$b has been removed, removing watchpoint recursively]

0 commit comments

Comments
 (0)