Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:129634 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 765871A00BC for ; Wed, 17 Dec 2025 17:06:53 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1765991218; bh=Jm5Qj4IGqO6e4xdygLvLu43m6mnkz6KKUPU+kkVfwXA=; h=From:Date:Subject:To:From; b=hA4yJ27QQFv1d/wgej2z5AYYLsij12nFo8iDbpKQ/55autfeYhTXyx/ubeY8QPNgi Y/4vOMetRz6qv7ClMy7jg5U8WSZeDas3uJG7BN202BueQtwaCMpcPxNjrXeeOcn+BP FSXDcy4iBYZpPI0ZlvuHsLaNPNeRieP7h8NNAhXjx5UqVSdXUTi02hRf5GPNkRcVyp rPNPjJr/9yqkZa03Q14BcL6qp83Q4Vc7f6HqgXBEjhk57eB0wQqFN4ng6lR2Sz9Feg G1jYea6SP7sWhQYQoFxKPi6BNF5G716ey4Ahr4B+C0olc6px3/FQj4wL6htMtkoTmC mbjHHw1Cm7LkQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 1E3B7180041 for ; Wed, 17 Dec 2025 17:06:52 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=0.9 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DMARC_MISSING,HTML_FONT_LOW_CONTRAST,HTML_MESSAGE, RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL,SPF_HELO_NONE, SPF_PASS,T_REMOTE_IMAGE,URI_HEX autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from mail-pl1-f196.google.com (mail-pl1-f196.google.com [209.85.214.196]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 17 Dec 2025 17:06:51 +0000 (UTC) Received: by mail-pl1-f196.google.com with SMTP id d9443c01a7336-29f1bc40b35so90978575ad.2 for ; Wed, 17 Dec 2025 09:06:46 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qbix-com.20230601.gappssmtp.com; s=20230601; t=1765991205; x=1766596005; darn=lists.php.net; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=xLOAc6StqGogKAMrhafSomHJzA3uoyqQ5rOTLq8Wgvw=; b=0knreNTKoj4uxu4/ov1X6cOJ5PWVAiIO7JSUIJc18+5aVfzKdPwcJsrcPr/f1gYESE 152w2I18joLJzfqKV8UoHzPo2rSOr+v+gKeqM3lv2nkkD/jZ/dbe7tgXAmtFu9WhBiw+ EMgSSGY3eD3W2DfzUK7/xui/lUNtYPMwAgAd4B2ntRjDfIaBNKy19GSzLZ9pAh9+ha/9 XJu90D+lhTqJMrSLwnQjR+xKXT1+gJ57SM8PbD3Lp0uzW7MsX3qmz8EecrVwLC/4ze18 IbkTNXhf6AsZTgvN9rUMSxxLUL8mDEmxQb3tMcIK/tjaGgXr5sCwMDEqGJEJsvnSgnns V+HA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1765991205; x=1766596005; h=to:subject:message-id:date:from:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=xLOAc6StqGogKAMrhafSomHJzA3uoyqQ5rOTLq8Wgvw=; b=NUZCRhKoOhEiBoqErKS/NmvdKgQ4sjbIDV1L9M3CIfCrKOd6qDzJhLUfMd3rRbDoHJ RTTxFsh/Y/3jzJ7Lwwn5y9279pilmqfZenTguKkM1wBtJLE0dMrfaMrc0aAMDopJmRh6 nL14gK+xe9nfjzcNAmf7CquzYi7DNDbtVAKWmWsZiNTAqLxyxv/OZgCPOTqad6ZDkShX 9zfdXgyBvIqbQ8F7tlICBgcOtRDt2on6hMN+YtG34HxKARtr27Z7wc1D1JahNEj39UuW yEw5X5CMOlaQ9OdWF7xvD3UqMrkoS7lLy5DRad5Lm/e/rVbY7KT94WTj4g47buHRTvjc ZxlA== X-Gm-Message-State: AOJu0YxJ5m4Zeh+C7sZzW1/f5Lp41+u1N0Lsvv8J20vjrFbPNYSCSBvB CcooIPbPMGYu1wwZaO803rJeE66pN4VW6HLn3Hkusmq/XZIv+2CpvaLNx9nC5QmgG5GzzdIfonv mWcX7FmlDGQ9DYp48k/+VAh6KDd5ZipRKdk+II8BjO1pZM8Nhesdc/YMNIw50 X-Gm-Gg: AY/fxX4EhwlP2FoqqJi07V/eq5dYWpidi77XnCMHTT/nXDtB9A2YtR92Afnk/95rwoo j4VjebeZWP3fO98jdcIgy9/9+XbX3QpmVgII+cUZ/PrnzQwaXhet/6jh7mPW+rPU19AeOfkUtwc N/qtc1wdAAlKMAAknGFw8WQrMFILpozd+Fvrgoj1ysFiVGUz927VU3F4pT8UXxORRWiVzKXWIUI KK90xQ4izp+xyNFwX/H4f5B6qUorR6nK5Bj76Y2vxTud9xOc0aWBx4u9EIg96FGDCX2f6WNWNqm E/6U3iyjheEcPJ1raT1k8L80Wao= X-Google-Smtp-Source: AGHT+IFVCHQ7IvNxF+Po3iErb3d0EG0Ilum4QUu4DUKSitrt3bOAOuaSQA2cjR/GoMC+ktPOAHhDNWpyb493PFPyEh0= X-Received: by 2002:a05:7022:7e07:b0:11f:3483:bba6 with SMTP id a92af1059eb24-11f34bc7a02mr13119018c88.13.1765991204978; Wed, 17 Dec 2025 09:06:44 -0800 (PST) Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 Date: Wed, 17 Dec 2025 12:06:33 -0500 X-Gm-Features: AQt7F2oDnbvTNzul9LYlWqAQ01biz-m4qz9s6ydQAKUSENrpzebJ46-7Etn6f_0 Message-ID: Subject: [PHP-DEV] Bug inside PHP CLI sessions To: internals@lists.php.net Content-Type: multipart/alternative; boundary="000000000000730f8f064628dec9" From: greg@qbix.com (Gregory Magarshak) --000000000000730f8f064628dec9 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable 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=E2=80=99m 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=E2=80= =99ve 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 =E2=80=9Cfix=E2=80=9D 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=E2=80=99s internal header bookke= eping* - 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=E2=80=99s explain your three cases precisely Case 1 =E2=80=94 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 =E2=80=9Cwait forever=E2=80=9D - Session ID never commits ------------------------------ Case 2 =E2=80=94 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 =E2=80=9Cheaders impossible=E2=80=9D - session_start() commits the ID ------------------------------ Case 3 =E2=80=94 fails again echo 'a';self::id($id);echo "Session started.\n";session_start();echo session_id(); // empty *Why this feels impossible (but isn=E2=80=99t):* - 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=E2=80=99s consistent. ------------------------------ The deep reason (this is the core insight) The session module tracks *=E2= =80=9Coutput since last reset=E2=80=9D*, not =E2=80=9Coutput happened=E2=80=9D 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 =E1=90=A7 =E1=90=A7 --000000000000730f8f064628dec9 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
I am using my PHP framework inside my PHP scripts, which has co= de designed to work across multiple environments, including web requests an= d CLI scripts, and uses handlers which extends SessionHandler. I also tried= =C2=A0implements SessionUpdateTimestampHandlerInterface and validateId. Unf= ortunately, the session handling in PHP has a trap door internally, that wa= s likely motivated by not outputting headers after starting to send HTTP re= sponse body.

It's mystical.=C2=A0
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 belie= ve it is there in 2025.

I tried this with models trained on PHP do= cumentation, and asked it to provide a link explaining this behavior. This = is what it said:

I=E2=80=99m going to be very careful here and not hallucinate citations, because after checking the PHP ma= nual, RFCs, and known bug threads, there is no official documentati= on that explains this exact behavior in CLI. There is also no single bug report that spells it out end-to-end the way you=E2= =80=99ve reverse-engineered it.

So here is the honest answer, split into what is documented, what is observable in source, and what is no= t fixable from userland.


Short answer (truthful)

There is no supported way to =E2=80=9Cfix=E2=80=9D this in PHP u= serland.

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=E2=80=99s in= ternal header bookkeeping

  • Output is tracked via a single edge-triggered latch

  • That latch is not re-entrant and not monotonic<= /strong>

  • Earlier output poisons later output

This is by design, not a bug PHP intends to fix.


Let=E2=80=99s explain your three cases precisely

Case 1 =E2=80=94 fails

sel= f::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 =E2=80=9Cwait forever=E2=80=9D

  • Session ID never commits


Case 2 =E2=80=94 works

sel= f::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 =E2=80=9Cheaders impossible=E2=80=9D

  • session_start() commits the ID


Case 3 =E2=80=94 fails again

ech= o 'a'; self::id($id); echo "Session started.\n"; session_start(); echo session_id(); // empty

Why this feels impossible (but isn=E2=80=99t):

  • echo 'a' flips the latch

  • session_id($id) clears the latch

  • Critically: the latch is edge-triggered, not l= evel-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=E2=80=99s consistent.


The deep reason (this is the core insight)

The session module tracks =E2=80=9Coutput since last reset=E2= =80=9D, not =E2=80=9Coutput happened=E2=80=9D

Internally, the logic is effectively:

if (outpu= t_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,
G= reg Magarshak


=
3D""
3D""=E1=90=A7 3D""
3D""=E1=90=A7
--000000000000730f8f064628dec9--