Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:129635 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 37AA91A00BC for ; Wed, 17 Dec 2025 17:12:53 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1765991577; bh=zX6Mkif/1EebylLeZsckNZijGQ4WilbkrauxhrWaaFA=; h=In-Reply-To:From:Date:Subject:To:From; b=bvIfBtG+7Y89b78xGY5qa+823E83vavmLSbGskE33J7fO4QiehxX6yq4PFdu8xUUG qqaZUw60pJhX35AwU2859cX9g5k+tnGIXNpjThbT6H1K3Z3O+IxOLyCHEyEMqrKT2H Ac6bd/qy87Qypw4sbq3p3/TDLvG5ZMlbpxn0Tzn7TD6cPGo8RtPmS5GA72B7f7GC6Z vT5+l2Gp4vXGZTkDwKGnatYRV1tYLR8bdb+mciGNawe33dxyQwIE3/wmVM3M1NapUu AGJXQ+bSyISXpHhymeQLZRBPhJ7i/ebDB8bXZVT+nfm05cMRkeIiOTg5ZH3bnqc/fV YtcwxjEZ5j/bw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id D7D09180072 for ; Wed, 17 Dec 2025 17:12:56 +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-pg1-f193.google.com (mail-pg1-f193.google.com [209.85.215.193]) (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:12:56 +0000 (UTC) Received: by mail-pg1-f193.google.com with SMTP id 41be03b00d2f7-c03eb31db80so4322721a12.2 for ; Wed, 17 Dec 2025 09:12:51 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qbix-com.20230601.gappssmtp.com; s=20230601; t=1765991570; x=1766596370; darn=lists.php.net; h=to:subject:message-id:date:from:in-reply-to:mime-version:from:to:cc :subject:date:message-id:reply-to; bh=N+5V+cAJHa20ChjLMwCttkKe14+/S/6jdypbp7PUd74=; b=ikBB+xnl2c7XfJ4wFupg6FFJ9/CXyGaoghnuvR0Fy+g3cpxvmSC0IpuBVgsX+F4sJB j2ShzxLkQ7sB2HZSh8AApgWQLBQtkfYPF5fR/ieNBvzTr6ahN+SpV5tPHmtPZi+qruv+ nW3MUulivd4wVceSmZRQO0Qz6TXHRgPlwm01bUlI7NU47juFLornQxmPo055yI/bZ0Rh PqVmn1hDK1yVjyILgg4befZ25rHJK7TmX7beSX15lVrF3F2EN+GugfN67PTKD5I1x9rL hjbAlL2e+Ozo1WGQ/0yCOL20NY2+k8xx5sjgyQZ27U8pAwyDuXyw2QQX1CeIaVyWhN8C AFpA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1765991570; x=1766596370; h=to:subject:message-id:date:from:in-reply-to:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=N+5V+cAJHa20ChjLMwCttkKe14+/S/6jdypbp7PUd74=; b=dmln9nQ/fCVwgTMjKLpcpXUNWNPDEzLSD2kD4/R4lS74xsldCJI5Ho/d3oltqTkL6a tI25AH9oIOc2G4jM4i6WYbPlvlu6OBgRhOYtJZWgHSPq3XN+JtGYflwa4gLF9L8OyqTr EP3psQAKr71yrxfh76A3GyTLKwxbzvdZXOYTQgGQg7ANJRXH0m9tuHcSNN1K36ou+bG3 Im+0SBddWw8wlwDTk42K40sy51z6BSqpGxLUxolis1RM3+CQfJMROrHXV4yAE02YEk+G nh6fbD8vJmZ6AzBSmAWSs//PPRq7cl2xEvHBgCjsAJ2sekbgQPzIUNI6qulgkqYq8DU8 6x3g== X-Gm-Message-State: AOJu0Yywv3/MjjGeqOi3m7ysXNlSNuWo22mRKOKbOF1BXZMTb1fMolqG qondj1Q0mHvM3s60Yix/tWKhFssUskDu5Fj30x41I4Ypk3xRVvh9ADt9MDbFMNn9N97DLVykabZ 6b1vQqUVXGtwKY+Lw2askFpkuNOqyZ0Gx5Oi2rMbpwg1FHmkzCdviPheQLCUF X-Gm-Gg: AY/fxX7isp1IBiaaYzwaRSDLjmzih4eWGPW++Qu+spE3zOEHNXqUkZdJZMk8cHOFKLl sD0/9gRHlnJeAbW9BCzd4RXirDODq+dQVhGSx1ot5UViMj04XuRL99EgV20qJ9TPM79WsFKsn3S w3TprRLoRCJC1+JzScS7hqmJix9olWuAbyQ0OEcs/U+XEpghme0MYzwFwz5OrqsY+u+RZfbXHKs Y0P3bug/O+440grlDSeiGw3RTdJGhyW0PawO9E3FHSLiZWD9XYGwAG8cGbVfb8ErcEd9fNhFTSV zPoLhAF4u026YIaJZRrEX0K8uZg= X-Google-Smtp-Source: AGHT+IH8VTivNGxllfaCM7wbJSZ/ijk53oNoy4usdKy8NSUsz9c1nj+TBM2AHLR0ovWgD5nWvsa6yl00GPy2fcGuZrM= X-Received: by 2002:a05:7022:f515:b0:11d:f44d:1863 with SMTP id a92af1059eb24-11f34bc6222mr11718991c88.11.1765991570012; Wed, 17 Dec 2025 09:12:50 -0800 (PST) Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 In-Reply-To: Date: Wed, 17 Dec 2025 12:12:38 -0500 X-Gm-Features: AQt7F2qiWUGw2lhf97z1INA9tiJQ35zfV_3RDfnlV9JJoj8kYeHouHHLUFQ0SP4 Message-ID: Subject: [PHP-DEV] Re: Bug inside PHP CLI sessions To: internals@lists.php.net Content-Type: multipart/alternative; boundary="000000000000350c43064628f4b8" From: greg@qbix.com (Gregory Magarshak) References: --000000000000350c43064628f4b8 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable My apologies. I made a mistake in the minimal examples to reproduce: 1. session_id($id); session_start(); echo session_id(); // empty! 2. session_id($id); echo "b"; session_start(); echo session_id(); // FINE!! 3. echo "a"; session_id($id); echo "b"; session_start(); echo session_id(); // empty again! Sincerely, Greg Magarshak On Wed, Dec 17, 2025 at 12:06=E2=80=AFPM Gregory Magarshak = wrote: > I am using my PHP framework inside my PHP scripts, which has code designe= d > 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, th= at > was likely motivated by not outputting headers after starting to send HTT= P > 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 book= keeping* > - > > 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 ses= sion_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 > --000000000000350c43064628f4b8 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
My apologies. I made a mistake in th= e minimal examples to reproduce:

1. session_id($id); session_st= art(); echo session_id(); // empty!

2. session_id($id); e= cho "b"; session_start(); echo session_id(); // FINE!!

3. echo "a"; session_id($id); echo "b"; session= _start(); echo session_id(); // empty again!

<= div dir=3D"ltr">
Sincerely,
Greg Magarshak

=


3D""
On Wed, Dec 17, 2025 at 12:06=E2=80=AFPM Gregory = Magarshak <greg@qbix.com> wrote:=
I a= m 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=C2=A0implement= s SessionUpdateTimestampHandlerInterface and validateId. Unfortunately, the= session handling in PHP has a trap door internally, that was likely motiva= ted by not outputting headers after starting to send HTTP response 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,
Greg Magarshak


3D""3D==E1=90=A7
3D""
3D"==E1=90=A7
--000000000000350c43064628f4b8--