Bug inside PHP CLI sessions

From: Date: Wed, 17 Dec 2025 17:06:33 +0000
Subject: Bug inside PHP CLI sessions
Groups: php.internals 
Request: Send a blank email to internals+get-129634@lists.php.net to get a copy of this message
I am using my PHP framework inside my PHP scripts, which has code designed to work across multiple environments, including web requests and CLI scripts, and uses handlers which extends SessionHandler. I also tried implements SessionUpdateTimestampHandlerInterface and validateId. Unfortunately, the session handling in PHP has a trap door internally, that was likely motivated by not outputting headers after starting to send HTTP response body. It's mystical. 1. session_id($id); session_start(); echo session_id(); // empty! 2. session_id($id); echo "Session started.\n"; session_start(); echo session_id(); // FINE!! 3. session_id($id); echo "Session started.\n"; session_start(); echo session_id(); // empty again! I can't believe it is there in 2025. *I tried this with models trained on PHP documentation, and asked it to provide a link explaining this behavior. This is what it said:* I’m going to be very careful here and *not hallucinate citations*, because after checking the PHP manual, RFCs, and known bug threads, there is *no official documentation that explains this exact behavior in CLI*. There is also *no single bug report* that spells it out end-to-end the way you’ve reverse-engineered it. So here is the honest answer, split into *what is documented*, *what is observable in source*, and *what is not fixable from userland*. ------------------------------ Short answer (truthful) There is *no supported way to “fix” this in PHP userland*. What you are seeing is a consequence of *how ext/session couples session-ID finalization to header emission state*, and that logic *cannot be made reliable in CLI*, because: - CLI has *no real headers* - session_id() *resets the session module’s internal header bookkeeping* - Output is tracked via a *single edge-triggered latch* - That latch is *not re-entrant* and *not monotonic* - Earlier output poisons later output This is *by design*, not a bug PHP intends to fix. ------------------------------ Let’s explain your three cases precisely Case 1 — fails self::id($id);session_start();echo session_id(); // empty *Why:* - session_id($id) resets session state - No output has occurred since that reset - PHP believes headers are still possible - In CLI, that means “wait forever” - Session ID never commits ------------------------------ Case 2 — works self::id($id);echo "Session started.\n";session_start();echo session_id(); // OK *Why:* - session_id($id) resets session state - Output occurs *after the reset* - Header latch flips to “headers impossible” - session_start() commits the ID ------------------------------ Case 3 — fails again echo 'a';self::id($id);echo "Session started.\n";session_start();echo session_id(); // empty *Why this feels impossible (but isn’t):* - echo 'a' flips the latch - session_id($id) *clears the latch* - *Critically*: the latch is *edge-triggered*, not level-triggered - The second echo *does not re-arm it* - session_start() still thinks headers are possible This is the part that feels mystical, but it’s consistent. ------------------------------ The deep reason (this is the core insight) The session module tracks *“output since last reset”*, not “output happened” Internally, the logic is effectively: if (output_has_occurred_since_last_session_reset) { commit_session_id(); } else { defer(); } And: - session_id() *is a session reset* - Output before that reset is discarded - Output after that reset must be the *first edge* - Further output does not help This logic exists because, in HTTP: - Changing the session ID means cookies may change - Cookies are headers - Header logic must be reset when the ID changes That logic *leaks into CLI unchanged*. Sincerely, Greg Magarshak <https://76183cf5.streak-link.com/CsqFLR9dXbkQa2qdJQNqfbB-/http%3A%2F%2Fqbix.com%2Finvest> ᐧ ᐧ

« previous php.internals (#129634) next »