View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0002332 | Xdebug | Code Coverage | public | 2025-03-25 19:29 | 2025-03-27 13:26 |
Reporter | clue | Assigned To | derick | ||
Priority | normal | Severity | crash | Reproducibility | always |
Status | confirmed | Resolution | open | ||
Product Version | 3.4.2 | ||||
Summary | 0002332: Segmentation fault for code coverage with nested fibers | ||||
Description | A segmentation fault happens when getting code coverage information when using nested fibers (PHP 8.1+). In particular, this is currently noticeable when using PHPUnit with code coverage report for any project using ReactPHP v3+. Using async fibers is one of the core features of ReactPHP v3+, so this currently affects a larger number of projects (fortunately only) during development. We've started seeing these segfaults consistently a couple of weeks ago and could track this down to a segfault that only happens as of Xdebug 3.4.2 and none of the earlier versions. In other words, I don't think we've introduced any other new code that would trigger this behavior and these errors started showing up seemingly randomly in newer test runs. I can consistently reproduce the exact same segmentation fault on any PHP version that supports fibers (PHP 8.1, PHP 8.2, PHP 8.3 and PHP 8.4) across multiple operating systems (Linux x64 and Windows x64 at least). The segmentation fault only occurs with latest Xdebug 3.4.2, it does not occur in any earlier versions. Taking a look at the diff between 3.4.1 and 3.4.2 (https://github.com/xdebug/xdebug/compare/3.4.1...3.4.2), I believe this might be related to https://github.com/xdebug/xdebug/commit/97719d96600e29b98dbe3848685e300b03d35266 and earlier https://github.com/xdebug/xdebug/commit/d3a8a6b0d6feac962187825479238c00f71e48eb. Thank you for your excellent work on Xdebug! | ||||
Steps To Reproduce | The provided script crashes consistently on PHP 8.1+ when code coverage is enabled:
The provided script is the minimal reproducible example I could come up with and does indeed look quite contrived. However, it does not crash when Xdebug or code coverage is disabled or when using an earlier Xdebug 3.4.1. A more realistic example would be the test suite of reactphp/async v4 (or pretty much any project that depends on it):
| ||||
Additional Information | Here's the minimal Docker environment to reproduce this:
| ||||
Tags | No tags attached. | ||||
Attached Files | valgrind.log (7,841 bytes)
==8169== Memcheck, a memory error detector ==8169== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. ==8169== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info ==8169== Command: php -d zend_extension=xdebug.so -d xdebug.mode=coverage fibers.php ==8169== ==8169== Warning: client switching stacks? SP change: 0x1ffeffcd28 --> 0x8d12fb8 ==8169== to suppress, use: --max-stackframe=137274236272 or greater ==8169== Warning: client switching stacks? SP change: 0x8d12b88 --> 0x8f13fb8 ==8169== to suppress, use: --max-stackframe=2102320 or greater ==8169== Warning: client switching stacks? SP change: 0x8f13ee8 --> 0x8d12b88 ==8169== to suppress, use: --max-stackframe=2102112 or greater ==8169== further instances of this message will not be shown. bool(true) ==8169== Invalid read of size 8 ==8169== at 0x8633FBD: xdebug_code_coverage_end_of_function (code_coverage.c:729) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x87BE44F: ??? ==8169== Address 0x88301b8 is 8 bytes inside a block of size 24 free'd ==8169== at 0x484417B: free (vg_replace_malloc.c:872) ==8169== by 0x8633FF6: xdebug_code_coverage_end_of_function (code_coverage.c:737) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== Block was alloc'd at ==8169== at 0x48465EF: calloc (vg_replace_malloc.c:1328) ==8169== by 0x8631CF9: xdebug_path_new (branch_info.c:208) ==8169== by 0x8633EB3: xdebug_code_coverage_start_of_function (code_coverage.c:701) ==8169== by 0x863445D: xdebug_coverage_execute_ex (code_coverage.c:1051) ==8169== by 0x861F71A: xdebug_execute_user_code_begin (base.c:784) ==8169== by 0x6D05BA: ??? (in /usr/local/bin/php) ==8169== by 0x62D2B0: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== ==8169== Invalid read of size 8 ==8169== at 0x8631D74: xdebug_path_free (branch_info.c:222) ==8169== by 0x8633FF6: xdebug_code_coverage_end_of_function (code_coverage.c:737) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x87BE44F: ??? ==8169== Address 0x88301b8 is 8 bytes inside a block of size 24 free'd ==8169== at 0x484417B: free (vg_replace_malloc.c:872) ==8169== by 0x8633FF6: xdebug_code_coverage_end_of_function (code_coverage.c:737) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== Block was alloc'd at ==8169== at 0x48465EF: calloc (vg_replace_malloc.c:1328) ==8169== by 0x8631CF9: xdebug_path_new (branch_info.c:208) ==8169== by 0x8633EB3: xdebug_code_coverage_start_of_function (code_coverage.c:701) ==8169== by 0x863445D: xdebug_coverage_execute_ex (code_coverage.c:1051) ==8169== by 0x861F71A: xdebug_execute_user_code_begin (base.c:784) ==8169== by 0x6D05BA: ??? (in /usr/local/bin/php) ==8169== by 0x62D2B0: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== ==8169== Invalid free() / delete / delete[] / realloc() ==8169== at 0x484417B: free (vg_replace_malloc.c:872) ==8169== by 0x8633FF6: xdebug_code_coverage_end_of_function (code_coverage.c:737) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x87BE44F: ??? ==8169== Address 0x88301b0 is 0 bytes inside a block of size 24 free'd ==8169== at 0x484417B: free (vg_replace_malloc.c:872) ==8169== by 0x8633FF6: xdebug_code_coverage_end_of_function (code_coverage.c:737) ==8169== by 0x86345AB: xdebug_coverage_execute_ex_end (code_coverage.c:1072) ==8169== by 0x861E45A: xdebug_execute_user_code_end (base.c:814) ==8169== by 0x6D08E5: zend_observer_fcall_end (in /usr/local/bin/php) ==8169== by 0x34277B: ??? (in /usr/local/bin/php) ==8169== by 0x69FB62: execute_ex (in /usr/local/bin/php) ==8169== by 0x62D2BE: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== Block was alloc'd at ==8169== at 0x48465EF: calloc (vg_replace_malloc.c:1328) ==8169== by 0x8631CF9: xdebug_path_new (branch_info.c:208) ==8169== by 0x8633EB3: xdebug_code_coverage_start_of_function (code_coverage.c:701) ==8169== by 0x863445D: xdebug_coverage_execute_ex (code_coverage.c:1051) ==8169== by 0x861F71A: xdebug_execute_user_code_begin (base.c:784) ==8169== by 0x6D05BA: ??? (in /usr/local/bin/php) ==8169== by 0x62D2B0: zend_call_function (in /usr/local/bin/php) ==8169== by 0x6D253C: ??? (in /usr/local/bin/php) ==8169== by 0x6D34CD: ??? (in /usr/local/bin/php) ==8169== by 0x882130F: ??? ==8169== bool(true) ==8169== ==8169== HEAP SUMMARY: ==8169== in use at exit: 6,908 bytes in 18 blocks ==8169== total heap usage: 24,658 allocs, 24,641 frees, 3,685,051 bytes allocated ==8169== ==8169== LEAK SUMMARY: ==8169== definitely lost: 24 bytes in 1 blocks ==8169== indirectly lost: 0 bytes in 0 blocks ==8169== possibly lost: 0 bytes in 0 blocks ==8169== still reachable: 6,884 bytes in 17 blocks ==8169== suppressed: 0 bytes in 0 blocks ==8169== Rerun with --leak-check=full to see details of leaked memory ==8169== ==8169== For lists of detected and suppressed errors, rerun with: -s ==8169== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0) fibers.php (210 bytes)
<?php xdebug_start_code_coverage(XDEBUG_CC_UNUSED); $fiber = new \Fiber(function () { $fiber = new \Fiber(function () { }); var_dump($fiber->start() === null); }); var_dump($fiber->start() === null); | ||||
Operating System | |||||
PHP Version | 8.1-dev | ||||
|
Hi, I had a look at this, and I can indeed reproduce this with your minimal example. I also know what the problem is, I think. Each fiber has their own stack, and Xdebug switches its internal stack when execution switches to another fiber (or the main thread): https://github.com/xdebug/xdebug/blob/master/src/base/base.c#L1200 The code coverage functionality also uses a stack to keep track about how functions are called, but this stack is (incorrectly) shared with all fibers. In your case, when the execution changes to a different fiber, the code tries to find the path for the current function using the stack size of the current execution unit. But if this is the same level as in the previous fiber, this information and memory structure is already freed through The fix here is to also have separate stacks for code coverage, and this is not as trivial of a fix as I would like. It doesn't trigger with 3.4.1 and earlier, because of the memory leak, but the code coverage result would also still not be accurate. For other reasons, I have a new implementation for coverage (https://github.com/derickr/xdebug/tree/improve-code-coverage) where I would rather fix this. But this branch is currently quite a bit slower (0000023:0000030%) and I haven't figured out how to address that yet (nor do I understand why it is slower). In short, this is going to take a while I'm afraid. cheers, |