Skip to content

Commit fd289bc

Browse files
committed
Fix GH-13230: phpdbg use-after-free at shutdown
phpdbg_watch_element back-pointers to phpdbg_watchpoint_t went stale when the watchpoint was freed, defeating the GH-13681 NULL guards. phpdbg_destroy_watchpoints also iterated its hashes in MSHUTDOWN, after zend_mm_shutdown freed their emalloc backings: non-ASAN tolerated the read, ZTS ASAN aborted. NULL the back-pointer in phpdbg_clean_watch_element, tolerate NULL in phpdbg_backup_watch_element, unregister the freed element from watch_recreation in phpdbg_free_watch_element, and move the recreation drain and the btree plus hash reset into RSHUTDOWN so the work runs while emalloc memory is alive. Drop the late notices from the existing watch_*, gh15210_*, and bug73927 expected outputs since they were artifacts of reading freed memory. Fixes GH-13230
1 parent 755f4e6 commit fd289bc

12 files changed

Lines changed: 39 additions & 29 deletions

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ PHP NEWS
138138
. Mark Phar::buildFromIterator() base directory argument as a path.
139139
(ndossche)
140140

141+
- phpdbg:
142+
. Fixed bug GH-13230 (use-after-free at phpdbg shutdown). (iliaal)
143+
141144
- Posix:
142145
. Added validity check to the flags argument for posix_access(). (arshidkv12)
143146

sapi/phpdbg/phpdbg.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ static PHP_RINIT_FUNCTION(phpdbg) /* {{{ */
231231

232232
static PHP_RSHUTDOWN_FUNCTION(phpdbg) /* {{{ */
233233
{
234+
phpdbg_destroy_watch_request_state();
235+
234236
if (PHPDBG_G(stdin_file)) {
235237
fclose(PHPDBG_G(stdin_file));
236238
PHPDBG_G(stdin_file) = NULL;

sapi/phpdbg/phpdbg_watch.c

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ void phpdbg_dequeue_elements_for_recreation(void) {
801801
void phpdbg_clean_watch_element(phpdbg_watch_element *element);
802802

803803
void phpdbg_free_watch_element(phpdbg_watch_element *element) {
804+
zend_hash_del(&PHPDBG_G(watch_recreation), element->str);
804805
zend_string_release(element->str);
805806
if (element->name_in_parent) {
806807
zend_string_release(element->name_in_parent);
@@ -856,6 +857,9 @@ void phpdbg_remove_watch_element(phpdbg_watch_element *element) {
856857
}
857858

858859
void phpdbg_backup_watch_element(phpdbg_watch_element *element) {
860+
if (!element->watch) {
861+
return;
862+
}
859863
memcpy(&element->backup, &element->watch->backup, /* element->watch->size */ sizeof(element->backup));
860864
}
861865

@@ -974,6 +978,7 @@ void phpdbg_clean_watch_element(phpdbg_watch_element *element) {
974978
if (zend_hash_num_elements(elements) == 0) {
975979
phpdbg_remove_watchpoint(element->watch);
976980
}
981+
element->watch = NULL;
977982
}
978983
}
979984

@@ -1501,15 +1506,27 @@ void phpdbg_setup_watchpoints(void) {
15011506
#endif
15021507
}
15031508

1504-
void phpdbg_destroy_watchpoints(void) {
1509+
void phpdbg_destroy_watch_request_state(void) {
15051510
phpdbg_watch_element *element;
15061511

1507-
/* unconditionally free all remaining elements to avoid memory leaks */
15081512
ZEND_HASH_MAP_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
15091513
phpdbg_automatic_dequeue_free(element);
15101514
} ZEND_HASH_FOREACH_END();
15111515

1512-
/* 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. */
1516+
phpdbg_btree_init(&PHPDBG_G(watchpoint_tree), sizeof(void *) * 8);
1517+
phpdbg_btree_init(&PHPDBG_G(watch_HashTables), sizeof(void *) * 8);
1518+
1519+
zend_hash_destroy(&PHPDBG_G(watch_elements));
1520+
zend_hash_init(&PHPDBG_G(watch_elements), 8, NULL, NULL, 0);
1521+
zend_hash_destroy(&PHPDBG_G(watch_recreation));
1522+
zend_hash_init(&PHPDBG_G(watch_recreation), 8, NULL, NULL, 0);
1523+
zend_hash_destroy(&PHPDBG_G(watch_free));
1524+
zend_hash_init(&PHPDBG_G(watch_free), 8, NULL, NULL, 0);
1525+
zend_hash_destroy(&PHPDBG_G(watch_collisions));
1526+
zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0);
1527+
}
1528+
1529+
void phpdbg_destroy_watchpoints(void) {
15131530
phpdbg_purge_watchpoint_tree();
15141531

15151532
#ifdef HAVE_USERFAULTFD_WRITEFAULT
@@ -1519,7 +1536,7 @@ void phpdbg_destroy_watchpoints(void) {
15191536
}
15201537
#endif
15211538

1522-
zend_hash_destroy(&PHPDBG_G(watch_elements)); PHPDBG_G(watch_elements).nNumOfElements = 0; /* phpdbg_watch_efree() is checking against this arrays size */
1539+
zend_hash_destroy(&PHPDBG_G(watch_elements));
15231540
zend_hash_destroy(&PHPDBG_G(watch_recreation));
15241541
zend_hash_destroy(&PHPDBG_G(watch_free));
15251542
zend_hash_destroy(&PHPDBG_G(watch_collisions));

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_destroy_watch_request_state(void);
113114
void phpdbg_purge_watchpoint_tree(void);
114115

115116
#ifndef _WIN32

sapi/phpdbg/tests/bug73927.phpt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
--TEST--
22
Bug #73927 (phpdbg fails with windows error prompt at "watch array")
3-
--SKIPIF--
4-
<?php
5-
if (getenv('SKIP_ASAN')) {
6-
die("skip intentionally causes segfaults");
7-
}
8-
?>
93
--PHPDBG--
104
b 19
115
r
@@ -26,8 +20,7 @@ prompt> [Breakpoint #0 at %s:%d, hits: 2]
2620
00021: } else {
2721
prompt> [Added watchpoint #0 for $value]
2822
prompt> [Added watchpoint #1 for $lower[0]]
29-
prompt> [$lower[0] has been removed, removing watchpoint]
30-
[$value has been removed, removing watchpoint]
23+
prompt>
3124
--FILE--
3225
<?php
3326

sapi/phpdbg/tests/gh15210_001.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ New value: 1
3333
>00002: header_register_callback(function() { echo "sent";});
3434
00003: $a = [0];
3535
00004: $a[0] = 1;
36-
prompt> [$a[0] has been removed, removing watchpoint]
36+
prompt>

sapi/phpdbg/tests/gh15210_002.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,5 @@ New value: 1
3737
prompt> sent0
3838
New value: 1
3939

40-
[$a[0] has been removed, removing watchpoint]
4140
[Script ended normally]
4241
prompt>

sapi/phpdbg/tests/watch_001.phpt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ prompt> [Breaking on watchpoint $b]
4040
Old value:
4141
New value: 2
4242
>00008:
43-
prompt> [$b has been removed, removing watchpoint recursively]
44-
[Script ended normally]
45-
prompt>
43+
prompt> [Script ended normally]
44+
prompt>
4645
--FILE--
4746
<?php
4847

sapi/phpdbg/tests/watch_002.phpt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ prompt> [Added watchpoint #0 for $a[]]
2424
prompt> [Breaking on watchpoint $a[]]
2525
1 elements were added to the array
2626
>00009:
27-
prompt> [$a[] has been removed, removing watchpoint]
28-
[Script ended normally]
29-
prompt>
27+
prompt> [Script ended normally]
28+
prompt>
3029
--FILE--
3130
<?php
3231

sapi/phpdbg/tests/watch_003.phpt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ prompt> [Breaking on watchpoint $a[0]]
3131
Old value: 2
3232
New value: 3
3333
>00009:
34-
prompt> [$a[0] has been removed, removing watchpoint]
35-
[Script ended normally]
36-
prompt>
34+
prompt> [Script ended normally]
35+
prompt>
3736
--FILE--
3837
<?php
3938

0 commit comments

Comments
 (0)