Skip to content

ext/session: hold the save handler recursion guard for the whole call (stacked on GH-22926) - #200

Open
iliaal wants to merge 2 commits into
PHP-8.4from
fix/session-recursion-guard-ownership
Open

ext/session: hold the save handler recursion guard for the whole call (stacked on GH-22926)#200
iliaal wants to merge 2 commits into
PHP-8.4from
fix/session-recursion-guard-ownership

Conversation

@iliaal

@iliaal iliaal commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Stacked on php#22926, which is the first of the two commits here. Its test expectations change once the guard actually holds, so the two cannot land separately.

ps_call_handler() cleared PS(in_save_handler) on the branch that rejects a recursive call, so the guard was released by the frame that was refused rather than by the frame that set it. One rejected nested call disarms it and the next one re-enters userland. Two session_gc() calls from inside a write() handler show it, the first correctly refused, the second running gc(). phpGH-22382 moved the assignment but kept it, so the defect predates that commit.

Holding the guard stops php_rshutdown_session_globals() from running SessionHandler::close(), which orphans PS(default_mod)'s data once PS(mod_data) is cleared, so the release moves there. That also covers a leak needing no recursion at all: a SessionHandler subclass overriding close() without delegating to the parent leaks 3 allocations per request on stock PHP-8.4, including the ps_files struct.

Three existing expectations gain a "Cannot call session save handler in a recursive manner" line where the nested s_close used to run silently. Their assertions are unchanged.

iliaal added 2 commits July 29, 2026 14:14
session_regenerate_id() runs the userland write or destroy handler and
then the close handler, and keeps operating on PS(id) afterwards. Any of
them may call session_destroy(): php_session_destroy() runs
php_rshutdown_session_globals() even when the recursive call was
rejected, releasing PS(id) and setting it to NULL, so the following
zend_string_release_ex() dereferences NULL. Check that the session is
still active once every handler has run.

Closes phpGH-22926
ps_call_handler() cleared PS(in_save_handler) on the branch that rejects a
recursive call, so the guard was released by the frame that was refused
rather than by the frame that set it. One rejected nested call disarmed it
and the next nested call from the same handler re-entered userland.
Holding it exposes a second defect: php_rshutdown_session_globals() can no
longer run SessionHandler::close(), and PS(default_mod)'s data is orphaned
once PS(mod_data) is cleared. Release it there, which also covers a
handler that never delegates close() to its parent.
@iliaal
iliaal force-pushed the fix/session-recursion-guard-ownership branch from 634d86e to f967d5e Compare July 29, 2026 18:14
@iliaal iliaal changed the title ext/session: hold the save handler recursion guard for the whole call (stacked on #196) ext/session: hold the save handler recursion guard for the whole call (stacked on GH-22926) Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant