Skip to content

Make exit() unwind properly #5243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Make exit() unwind properly #5243

wants to merge 1 commit into from

Conversation

nikic
Copy link
Member

@nikic nikic commented Mar 6, 2020

Exit() is not internally modeled as an exception, but not exposed to userland. This means that the stack will be properly unwound and there will be no memory leaks.

Additionally, this enables running of finally blocks on exit:

try {
    exit("Exit\n");
} finally {
    echo "Finally\n";
}

// Prints:
Finally
Exit

Note that the exit message is not printed immediately, but only once the exit reaches the top-most frame. I believe it has to work this way if we ever want to make exits catchable in some form (e.g. via a special catch_exit function).

/* Restore bailout based exit. */
zend_bailout();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does phpdbg still need the bailout?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I have no idea how to handle it properly ... maybe @bwoebi knows?

Copy link
Member

@bwoebi bwoebi Mar 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need special handling at all here?

at least you've removed EG(exit_status) - so have a look at phpdbg.c:1963 and retrieve the exit state from the exception?

}
} while (0);
FREE_OP1();
}
zend_bailout();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\0/ Burn it with 🔥! 😄

@cmb69 cmb69 added the Feature label Mar 9, 2020
@ghost
Copy link

ghost commented Mar 13, 2020

Do we actually need/want to run finally blocks? My understanding is that when calling exit no further PHP code is executed and the PHP engine goes into shutdown, which leads to internal cleanup, stack unwind and then a clean process exit.

Edit: To expand my comment a little...

I am talking in the context of the CLI SAPI.

If the finally blocks were executed it could have unintended side effects. exit could suddenly not mean anymore exit now. Since code will be executed, the shutdown could be delayed massively.

If I'm 10 stack frames deep down in a function call and each frame has a finally blocks that access some blocking part (NFS, database, etc.), then it wont exit now, but later when all finally blocks have been executed.

Now imagine this piece of code:

try {
    exit;
} finally {
    while(true);
}

What will happen now if finally blocks were executed? I'd imagine an exit will never happen, because we have an infinite loop. Now even an once guaranteed exit will never happen anymore.

I know the example is ridiculous, but it should visualize that if I want to exit but run all finally blocks, there are ways to do that, exit's behaviour should not be changed. It's by any means "a last effort to exit at all".

@nikic
Copy link
Member Author

nikic commented Mar 13, 2020

@CharlotteDunois Nothing will materially change, because exit currently already results in the execution of shutdown handlers and destructors. "Finally" is basically a different form of writing RAII destructors.

@nikic
Copy link
Member Author

nikic commented Mar 13, 2020

To make it clearer what I mean:

<?php

class OnScopeExit {
    private Closure $callback;
    public function __construct(Closure $callback) {
        $this->callback = $callback;
    }
    public function __destruct() {
        ($this->callback)();
    }
}

function foo() {
    try {
        // Try
    } finally {
        // Finally
    }
}

function bar() {
    $_ = new OnScopeExit(function() {
        // Finally
    });
    // Try
}

foo() and bar() are basically two ways to write the same thing (glossing over a lot of details). Right now for exit the destructor version does run, while the finally version does not run. This is of course quite bad if your finally block contains something like unlock_shm();.

@ghost
Copy link

ghost commented Mar 16, 2020

@nikic Do I interpret the Zend/tests/exit_finally_2.phpt test correctly that with this change an exit instruction can be "caught"? Looking at the expected output shows that the exit output is missing, thus the exit got actually caught/overwritten by the throw new Exception instruction?

@nikic
Copy link
Member Author

nikic commented Jun 25, 2020

I've opened #5768 as a version of this with reduced scope. Let's get the basic unwinding in first and discuss finally blocks separately.

@ramsey
Copy link
Member

ramsey commented May 31, 2022

It looks like the version of this with reduced scope (#5768) was merged. Is it time to revisit this, or are the finally blocks being dealt with elsewhere?

@github-actions
Copy link

There has not been any recent activity in this PR. It will automatically be closed in 7 days if no further action is taken.

@github-actions github-actions bot added the Stale label Jul 31, 2022
@github-actions github-actions bot closed this Aug 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants