Skip to content

ext/session: Close session without retrying write() after handler bailout - #23846

Open
iliaal wants to merge 1 commit into
php:PHP-8.6from
iliaal:promote/session-bailout-guard
Open

iliaal wants to merge 1 commit into
php:PHP-8.6from
iliaal:promote/session-bailout-guard

Conversation

@iliaal

@iliaal iliaal commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

A bailout from inside a userland save handler, a fatal error inside write() for instance, left PS(in_save_handler) set, so the next handler call was refused with "Cannot call session save handler in a recursive manner": request shutdown retried the failed write(), or, when the bailout came from the shutdown flush itself, close() never ran. The flag is now cleared once the handler returns or bails out. The bailout is recorded, as is one from the flush itself, and while it is, a flush only closes the handler and session_regenerate_id() refuses, so a failed write() is not retried and the empty data left by a bailed-out read() is never written over the stored session. A bailout in open() during session_reset() keeps the already open session active so it is still closed. bug60634_error_3.phpt and bug60634_error_4.phpt asserted the old warning and now assert the close handler running.

Comment thread ext/session/mod_user.c Outdated
@iliaal
iliaal force-pushed the promote/session-bailout-guard branch from c651024 to 2b96d11 Compare September 22, 2026 15:56

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay but this is a behaviour change though, so deferring the final approval to an ext/session codeowner

@iliaal

iliaal commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

Agreed, may even need to rebase to just 8.6+, but it is a bug so I'll let the code owner make the final call here.

@jorgsowa

Copy link
Copy Markdown
Contributor

I am not sure this should be merged now. It changes the semantic of the failed write. If first session write failed, why it should be retried? There is bigger chance that it will fail again than succeed. Moreover, the flow does not know whether nothing was written, the write partially completed, or the external operation already succeeded before the failure.

I think firstly we should explicitly decide the semantics of a bailed-out save-handler invocation, especially whether write() may be retried, because this fix exposes this behavior. I will try investigating this issue.

but it is a bug so I'll let the code owner make the final call here.

You have basically co-created this module, so you are code owner too ;)

@Girgias Girgias left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm struggling to see why we need to wrap all the calls in a try/catch to catch a bailout for something happening at shutdown.

Comment on lines +15 to +20
self::$writes++;
if (self::$writes === 1) {
/* Force a real bailout (fatal OOM), not an unwind-exit */
$GLOBALS['oom'] = str_repeat('a', 64 * 1024 * 1024);
}
echo "write ", self::$writes, "\n";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just use zend_trigger_bailout from the zend_test extension.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. The PR now targets PHP-8.6.

Comment thread ext/session/mod_user.c
{
int i;
bool bailout = false;
if (PS(in_save_handler)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't you just check for if (UNEXPECTED(EG(bailout)) and return with an UNDEF retval?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or even EG(active) maybe? But I guess we're still active while calling it during shutdown.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

EG(bailout) is set by every zend_try, so it is non-NULL during the shutdown flush regardless. CG(unclean_shutdown) is set by any fatal, including one outside the handler, where the session is still written at shutdown today; keying on it would drop that write.

@iliaal
iliaal force-pushed the promote/session-bailout-guard branch from 2b96d11 to 5f717e2 Compare October 12, 2026 11:56
@iliaal
iliaal changed the base branch from PHP-8.4 to PHP-8.6 October 12, 2026 11:56
@iliaal iliaal changed the title ext/session: Clear in_save_handler recursion guard on bailout ext/session: Close session without retrying write() after handler bailout Oct 12, 2026
@iliaal

iliaal commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Agreed. Reworked so that after a bailout in a handler or the flush, later flushes only call close(): nothing is written again and session_regenerate_id() refuses, but the handler can still release its lock. Retargeted to PHP-8.6 since this changes behavior.

@iliaal
iliaal requested a review from Girgias October 16, 2026 08:19
@ndossche

Copy link
Copy Markdown
Member

Is this still PR (and linked issue) relevant now that we have the general graceful OOM handling?

@iliaal
iliaal force-pushed the promote/session-bailout-guard branch from 5f717e2 to 628f127 Compare October 26, 2026 12:40
@iliaal

iliaal commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

The OOM case from the issue is gone on PHP-8.6. Any other fatal inside write() still hits it, e.g. max_execution_time or a "Cannot redeclare function" from eval(): the shutdown flush warns "Cannot call session save handler in a recursive manner" and reports the write as failed. So the PR still applies, but GH-15529 itself is fixed.

…lout

A zend_bailout() from inside a userland save handler, such as a fatal error
in write(), left PS(in_save_handler) set, so the next handler call was
refused with "Cannot call session save handler in a recursive manner":
request shutdown retried the failed write(), or, when the bailout came from
the shutdown flush itself, close() never ran. Clear the flag once the
handler returns or bails out, and record the bailout, as well as one from
php_session_flush() itself. While it is recorded, a flush only closes the
handler, so neither a failed encode or write() nor the empty data left by a
bailed-out read() is written, and session_regenerate_id() refuses. A
bailout in open() keeps an already open session active so it can still be
closed. Initializing the session clears the record.
@iliaal
iliaal force-pushed the promote/session-bailout-guard branch from 628f127 to cd92546 Compare October 26, 2026 12:42
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.

4 participants