Find Segfaults in PHP like a boss

A bit of history

Sometimes, a segfault happens, but you don’t know where, and your PHP installation does not have tools to find it. Or sometime, you think PHP is hanging, but you don’t know where. You may use xdebug, but you don’t want to click so many times on the « next call » button.

To address theses issues, I used to use this hack.

register_tick_function(function() {
    $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
    $last = reset($bt);
    $info = sprintf("%s +%d\n", $last['file'], $last['line']);
    file_put_contents('/tmp/segfault.txt', $info, FILE_APPEND);
    // or
    // file_put_contents('php://output', $info, FILE_APPEND);
});
declare(ticks=1);

The code is pretty small but it could appear really weird. Don’t worry, I will explain it.

  1. We register a tick function. A tick is an event emitted by PHP when a very low-level (tickable) statements is executed. This function will be executed on each PHP Tick and will print the last executed line.

  2. We tell PHP to fire an event for all possible tick.

  3. Profit… Thanks to that, it’s possible to find the last successfully executed line.

But, what I did not know, is that it worked because of a PHP Bug: declare(ticks=1) is not supposed to leak to other files. This has been fixed in PHP 7.0 and so my hack does not work anymore.

Let’s use a bigger cleaner hack

So If I want to continue to use this debug method, I need to put by hand declare(ticks=1) on every PHP files… Boring! I could write a simple tool that will do that for me but I don’t want to modify all my vendors.

So I decided to use PHP Stream Wrapper and Stream Filter. Theses PHP features are not really well known, but they are very powerful. I encourage you to read more about it.

This new implementation replaces the default file and phar stream wrapper implementation of PHP to be able to automatically add declare(ticks=1) on each PHP file. But this is done only in memory, not physically on the disk.

Usage

To use it, copy the HardCoreDebugLogger.php file somewhere on your disk and then add the following line in your code:

require '/path/to/HardCoreDebugLogger.php'

HardCoreDebugLogger::register();

By default, the traces will be displayed on STDOUT, but you can change it to save it a file:

require '/path/to/HardCoreDebugLogger.php'

HardCoreDebugLogger::register('/tmp/trace.txt');

Demo?

First, you will learn how to generate a segfault with PHP. You should not try to reproduce it at home!

// require __DIR__.'/HardCoreDebugLogger.php';
// declare(ticks=1); // We need tick in this file
// HardCoreDebugLogger::register();

function a()
{
    b();
}

function b()
{
     c();
}

function c()
{
    $a = 1 + 2;
    "".(new Crash());
}

class Crash
{
    public function __tostring()
    {
        return "".$this;
    }
}

a();

If you try to execute this code, you will get the following:

$ php segfault.php 
Segmentation fault (core dumped)

Not easy to find the segfault, isn’t it? And now, imagine you have 100 000 lines of codes 😱

We need to uncomment the 3 first lines to get the following:

$ php segfault.php 
1555494674.7385 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +5
1555494674.7385 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +10
1555494674.7385 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +16
1555494674.7385 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +23
1555494674.7385 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +25
1555494674.7386 /tmp/56dfc48fb7e807dd2a229813da89a0dc/segfault.php +20
Segmentation fault (core dumped)

And now, we know the last successful executed line is the line 20. So the segfault should be triggered by:

"".(new Crash());

Credit

Writing a Stream Wrapper is boring, so I would like to credit Anthony Ferrara for his work on php-preprocessor.

blog comments powered by Disqus