Page MenuHomePhabricator

Figure out why php-ast sometimes loses MAGIC_* flags in PHP 8.3
Closed, ResolvedPublic

Description

Seen several times over the last year in SecurityCheckPlugin, where certain tests start failing with PHP 8.3. At the time, I eventually found that it's because the native AST generated by php-ast no longer has the MAGIC_CLASS flag on __CLASS__ nodes, so I reported it for phan in https://github.com/phan/phan/issues/4841. Upon digging further, I found an issue in php-ast that seems at least related: https://github.com/nikic/php-ast/issues/239. There, the problem was caused by PHP 7 and PHP 8 coexisting on the host, and values from the former being used when compiling php-ast for the latter.

The issue can be reproduced locally, too. A while back I fixed it by switching from sury's php-ast to PECL, but because it worked immediately, I didn't do any research as to why it worked. Just 5 days ago, on June 3rd, the issue came back with PECL php-ast. I switched to sury, and this also fixed it. Once again though, I haven't tried tracking this down, nor am I sure where to look next.

Event Timeline

Daimona renamed this task from Figure out why php-ast sometimes loses MAGIC_* flags to Figure out why php-ast sometimes loses MAGIC_* flags in PHP 8.3.Jun 8 2025, 8:29 PM
Daimona added a project: PHP 8.3 support.

Example failed build: https://integration.wikimedia.org/ci/job/composer-package-php83/3901/console

I don't think it's worth saving it forever, as I hope the issue will be fixed soon. Things worth noting:

  • Uses composer-package-php83:8.3.21 (downloaded)
  • Tests erroring: "hookregistration" and "nonexistinghooks" ("Can't figure out method call for __construct")
  • Test failures: "taghook", "callbackhook", "parser-namespace", "stripitem"
  • Native AST only (not polyfill parser)

I had posted some code upstream to reproduce it:

$ php8.2 internal/dump_fallback_ast.php --php-ast-native '__CLASS__;'
AST_STMT_LIST [] #1
	0 => AST_MAGIC_CONST [MAGIC_CLASS] #1

$ php8.3 internal/dump_fallback_ast.php --php-ast-native '__CLASS__;'
AST_STMT_LIST [] #1
	0 => AST_MAGIC_CONST [] #1

The phan dependency (dump_fallback_ast) can be avoided by running the following:

$ php8.3 -r "print_r( ast\parse_code( '<?php __CLASS__;', 85 ) );"
ast\Node Object
(
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array
        (
            [0] => ast\Node Object
                (
                    [kind] => 0
                    [flags] => 346
                    [lineno] => 1
                    [children] => Array
                        (
                        )

                )

        )

)

The above output is correct, and because things are currently working for me locally, I don't have the incorrect output available. I briefly tried running the above using the releng/php83:8.3.21 image, but the output apparently matches my local.

I can reproduce this locally with composer-package-php83. Indeed, PHP is using the incorrect value for the \ast\flags\MAGIC_CLASS constant: 382 instead of 346. Trying with the MAGIC_LINE constant yields 379, which matches the report in https://github.com/nikic/php-ast/issues/239. Next is figuring out why the values from the stub are used.

The above output is correct, and because things are currently working for me locally, I don't have the incorrect output available. I briefly tried running the above using the releng/php83:8.3.21 image, but the output apparently matches my local.

Of course it matches, that's the bug! The output of parse_code() is always the same, what changes is the value of the MAGIC_* constants. So, the generated AST will always use 346, but if, at the same time, PHP thinks that MAGIC_CLASS === 382 (and not 346), we get a mismatch and the bug. In fact, it can be easily reproduced with just:

$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.21 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)

Trying a few more images:

$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.17 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12-s1 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(346)

It can't be specific to our docker stuff though, as I had the same issue locally.

$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.17 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12-s1 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(346)

So the breakage happened between the 8.3.12 and 8.3.12-s1 images?

https://gerrit.wikimedia.org/r/plugins/gitiles/integration/config/+/refs/heads/master/dockerfiles/php83/changelog#13

8.3.12-s1 is when we switched to the bookworm image for sury; is it likely that that caused something odd?

$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.17 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12-s1 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)
$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.12 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(346)

So the breakage happened between the 8.3.12 and 8.3.12-s1 images?

I checked quickly and it didn't:

$ docker run --rm docker-registry.wikimedia.org/releng/php83:0.0.2-s1 -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(382)

Which seems to match what I observed locally with the issue coming and going. Also, as mentioned above, I could reproduce this with php-ast from both sury and PECL.

I also tried playing a bit inside the container, including installing php-ast via apt, and fresh-compiling it from source. It keeps insisting that MAGIC_CLASS is 382. On the bright side, it seems really simple to reproduce, so this is probably good news for people who actually know how to debug this.

Trying to get rid of some dependencies, I could also reproduce this in the base sury-php image, installing php8.3-cli; I tried both compiling php-ast and getting it from sury, it reproduced the bug either way.

I went further up the chain, to ci-bookworm. Bookworm ships with PHP 8.2, and I didn't want to use sury, so I built PHP from source. Then I also built php-ast from source. This worked correctly:

root@3cd26ddd4a67:/srv/php-ast# php -v
PHP 8.3.21 (cli) (built: Jun  9 2025 21:17:05) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.3.21, Copyright (c) Zend Technologies
root@3cd26ddd4a67:/srv/php-ast# php -r 'var_dump(\ast\flags\MAGIC_CLASS);'
int(346)

One possibility is that there's something going on with the PHP 8.3 package from sury. I mentioned experiencing the bug locally with php-ast from PECL, but I, like CI, use PHP 8.3 from sury locally.

I repeated the test above (PHP from source + php-ast from source) using the sury-php image, and there too it works correctly (as I expected). Surprisingly though, I then installed php8.3-cli (from sury), and this time it worked! I tried again from scratch, php8.3-cli + ast from source, broken. Next, again from scratch, php8.3-cli + php8.3-ast, still broken. I don't know what to do next, though.

From what I gather the bug happens due to the php 8.3 package from sury.org? It is a bit hard to follow since are involved:

  • two softwares involved (php and the AST extension)
  • two sources of code (build from source and code from Sury)

The Docker images can have an entirely different php version than the Docker tag implies since the version is not pinned when doing the apt install.

The releng/php83:8.3.12-s1 image implies it has 8.3.12 but it has 8.3.15.

$ docker run --entrypoint=apt-cache --rm docker-registry.wikimedia.org/releng/php83:8.3.12-s1 policy php8.3-cli
                                                                                    ^^^^^^
php8.3-cli:
  Installed: 8.3.15-1+0~20241224.50+debian12~1.gbp36719a
  Candidate: 8.3.15-1+0~20241224.50+debian12~1.gbp36719a
  Version table:
     vvvvvv
 *** 8.3.15-1+0~20241224.50+debian12~1.gbp36719a 100
     ^^^^^^
        100 /var/lib/dpkg/status

For MAGIC_CLASS, the flag value comes from Zend T_CLASS_C:

ast_register_flag_constant("MAGIC_CLASS", T_CLASS_C);

Its value comes from PHP itself and thus differs between PHP versions. We do build the AST extension with PHP from sury:

FROM {{ "sury-php" | image_tag }}

USER root
RUN {{ "build-essential php7.4-dev php8.0-dev php8.1-dev php8.2-dev php8.3-dev php8.4-dev" | apt_install }}
...
RUN git clone https://github.com/nikic/php-ast /srv/php-ast && \
    cd /srv/php-ast && \
    # v1.1.2, use sha1 for immutability
    git checkout --quiet 152b420ed6ca9029b47e52362916af0b25b2c7b3
...
    git clean -fdx && \
    phpize8.3 && ./configure --with-php-config=php-config8.3 && make && \
    cp modules/ast.so /srv/modules/ast_83.so && \
...
    cp /srv/modules/ast_83.so /usr/lib/php/20230831/ast.so && \

The releng/php83 image apt install packages from sury.org, it is not unlikely that the resulting installed version is different than the one that was used to build the AST extension which would result in mismatching tokens?

$ for tag in 8.3.12 8.3.12-s1 8.3.17; do
  docker run --rm docker-registry.wikimedia.org/releng/php83:$tag -r \
    'printf("AST: %s PHP %s: %s\n", \ast\flags\MAGIC_CLASS, PHP_VERSION, T_CLASS_C);';
done;
AST: 346 PHP 8.3.12: 346
AST: 382 PHP 8.3.15: 382
AST: 382 PHP 8.3.17: 382

Well hmm... those are matching each other :/

From what I gather the bug happens due to the php 8.3 package from sury.org? It is a bit hard to follow since are involved:

That's my current theory, but I haven't proven it, nor have I determined if that package is the ultimate culprit (and not, say, something wrong in php-ast that the sury package just so happens to trigger).

Well hmm... those are matching each other :/

However:

$ docker run --rm docker-registry.wikimedia.org/releng/php83:8.3.21 -r 'printf("AST: %s PHP %s: %s\n", \ast\flags\MAGIC_CLASS, PHP_VERSION, T_CLASS_C);'
AST: 382 PHP 8.3.21: 346

While locally:

$ php8.3 -r 'printf("AST: %s PHP %s: %s\n", \ast\flags\MAGIC_CLASS, PHP_VERSION, T_CLASS_C);'
AST: 346 PHP 8.3.21: 346

I also made https://3v4l.org/uBBvm, and sure enough, it does look like the value of T_CLASS_C keeps alternating between 346 and 382 every ~2 patch versions of PHP 8.3. This looks odd (and worth checking upstream in php-src to see if it's intentional), but on its own it should cause issues: php-ast should work regardless of the underlying integer value used by PHP. Unless, as you say, php-ast is built using a different version of PHP. Then yeah, I guess we would see the exact mismatch I observed! So, next steps (I might take a look later today or tomorrow):

  • Confirm the PHP version mismatch theory
  • Check if the alternation is known to upstream, or if it's worth reporting (for php-src)
  • If the PHP version mismatch theory is confirmed, check with upstream php-ast if it would be worth having some workaround in the extension

As a first step, I spun up a container again using the ci-bookworm image. I started by compiling PHP 8.3.21 to confirm the value of the constant.

root@ccfe237fbf27:/srv/php-src# php -r 'printf("PHP %s: %s\n", PHP_VERSION, T_CLASS_C);'
PHP 8.3.21: 346

Next I tried switching to the 8.3.22 branch and recompiled to see the value there:

root@ccfe237fbf27:/srv/php-src# php -r 'printf("PHP %s: %s\n", PHP_VERSION, T_CLASS_C);'
PHP 8.3.22: 346

That's already kind of unexpected: according to https://3v4l.org/uBBvm, I should've seen 382 here. I tried again in a new container, but got the same result. I also tried PHP 8.3.17 (which had 382 in our docker image as well as in 3v4l), but still got 346. I'm thus going to assume that when compiling from source, that constant is always 346 for every PHP 8.3 patch version. (Such a shame, because I had already written a revolutionary kg to pound converter: P77589. My disappointment is immeasurable and my day is ruined.)

So, this already explains why I could not reproduce the bug when compiling PHP from source: if the T_CLASS_C is 346, php-ast works as expected! But it also means that there is something which, under certain conditions, causes the constant's value to be 382 in certain PHP versions. So we need to figure out what those conditions are.

Both our CI images and my local use PHP from sury, so that's a starting point. I reconfirmed that the sury package has the wrong value, by spinning up a container using the sury-php base image and installing php8.3-cli only:

root@1bc5f2578167:/# php -r 'printf("PHP %s: %s\n", PHP_VERSION, T_CLASS_C);'
PHP 8.3.22: 382

Then I don't know about 3v4l, but I suppose it's using custom builds. Which would also mean that this isn't specific to sury, but rather caused by something that sury and 3v4l builds have in common. What that is, I don't know. I also tried switching the docker image to use debian unstable, but still:

root@ca041e8b3c3b:/# php -r 'printf("PHP %s: %s\n", PHP_VERSION, T_CLASS_C);'
PHP 8.3.22: 346

Comparing the output of var_dump(T_CLASS_C); on different PHP versions is an excellent idea:

Output for 8.4.1 - 8.4.8
    int(349)

Output for 8.3.0, 8.3.2, 8.3.4, 8.3.7, 8.3.9, 8.3.11, 8.3.13, 8.3.15, 8.3.17, 8.3.20, 8.3.22
    int(382)

Output for 8.1.0 - 8.1.32, 8.2.0 - 8.2.28, 8.3.1, 8.3.3, 8.3.5 - 8.3.6, 8.3.8, 8.3.10, 8.3.12, 8.3.14, 8.3.16, 8.3.18 - 8.3.19, 8.3.21
    int(346)

As a generalization when comparing all tokens using:

<?php

for ( $i = 0; $i < 1024; $i++ ) {
    $name = token_name($i);
    if ( $name == 'UNKNOWN' ) continue;
    printf("%s => %s\n", $i, token_name($i));
}

That gives pretty much the same list of version mismatches: https://3v4l.org/bv2dt

When I compile 8.3.21 from source I get:

Zend/zend_language_parser.h
/* Token kinds.  */
#ifndef ZENDTOKENTYPE
# define ZENDTOKENTYPE
  enum zendtokentype
  {
    ZENDEMPTY = -2,
    END = 0,                       /* "end of file"  */
    ZENDerror = 256,               /* error  */
    ZENDUNDEF = 257,               /* "invalid token"  */
...
    T_CLASS_C = 346,               /* "'__CLASS__'"  */
...

That is a constant defined in a grammar file for YACC:

Zend/zend_language_parser.y`
constant:
        name        { $$ = zend_ast_create(ZEND_AST_CONST, $1); }
    |   T_LINE      { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_LINE); }
    |   T_FILE      { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_FILE); }
    |   T_DIR       { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_DIR); }
    |   T_TRAIT_C   { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_TRAIT_C); }
    |   T_METHOD_C  { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_METHOD_C); }
    |   T_FUNC_C    { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_FUNC_C); }
    |   T_NS_C      { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_NS_C); }
    |   T_CLASS_C   { $$ = zend_ast_create_ex(ZEND_AST_MAGIC_CONST, T_CLASS_C); }
;

YACC / Bison leads me to Upstream issue #14937 - Bison parser generates different zendtokentype constants for each patch version since php 8.3:

I noticed that since PHP 8.3 the value of T_* constants changes for each patch version. For example, if I'll use token_get_all and cache or serialize returning result I can got unexpected behavior.
...
For example
T_STRING constant is 313 for 8.3.0, 8.3.2, 8.3.4, 8.3.7, 8.3.9 versions.
T_STRING constant is 262 for 8.3.1, 8.3.3, 8.3.5 - 8.3.6, 8.3.8 versions.

That issue got declined due to the doc at https://www.php.net/manual/en/tokens.php stating (emphasis is mine):

T_* constants values are automatically generated based on PHP's underlying parser infrastructure.

This means that the concrete value of a token may change between two PHP versions. This means that your code should never rely directly on the original T_* values taken from PHP version X.Y.Z, to provide some compatibility across multiple PHP versions.

Why does it varies? Well it most probably depends on the version of Bison/Yacc being used, the answer might depend on who is compiling the official release?

If I get the list of tagger for the 8.3.* releases using git tag -l php-8.3.? php-8.3.?? --format=" '%(refname:strip=2)' => '%(taggername)',")" and compare them with the output from 3v4l.org:

<?php
$authors = [
    'php-8.3.0' => 'Jakub Zelenka',
    'php-8.3.1' => 'Pierrick Charron',
    'php-8.3.2' => 'Jakub Zelenka',
    'php-8.3.3' => 'Eric Mann',
    'php-8.3.4' => 'Jakub Zelenka',
    'php-8.3.5' => 'Eric Mann',
    'php-8.3.6' => 'Eric Mann',
    'php-8.3.7' => 'Jakub Zelenka',
    'php-8.3.8' => 'Eric Mann',
    'php-8.3.9' => 'Jakub Zelenka',
    'php-8.3.10' => 'Eric Mann',
    'php-8.3.11' => 'Jakub Zelenka',
    'php-8.3.12' => 'Eric Mann',
    'php-8.3.13' => 'Jakub Zelenka',
    'php-8.3.14' => 'Eric Mann',
    'php-8.3.15' => 'Jakub Zelenka',
    'php-8.3.16' => 'Eric Mann',
    'php-8.3.17' => 'Jakub Zelenka',
    'php-8.3.18' => 'Eric Mann',
    'php-8.3.19' => 'Eric Mann',
    'php-8.3.20' => 'Jakub Zelenka',
    'php-8.3.21' => 'Eric Mann',
    'php-8.3.22' => 'Jakub Zelenka',
];
$token_versions = [
    382 => ['8.3.0', '8.3.2', '8.3.4', '8.3.7', '8.3.9', '8.3.11', '8.3.13', '8.3.15', '8.3.17', '8.3.20', '8.3.22',],
    346 => ['8.3.1', '8.3.3', '8.3.5', '8.3.6', '8.3.8', '8.3.10', '8.3.12', '8.3.14', '8.3.16', '8.3.18', '8.3.19', '8.3.21',],
];
$freq = [];
foreach ( $authors as $tag => $author ) {
    foreach ( $token_versions as $token_value => $php_versions ) {
        if (
            in_array( substr( $tag, 4 ), $php_versions )
        )  {
            $freq[$token_value][$author] ++;
        }
    }
}
var_dump( $freq );

Surely I get a correlation:

array(2) {
  [382]=>
  array(1) {
    ["Jakub Zelenka"]=>
    int(11)
  }
  [346]=>
  array(2) {
    ["Pierrick Charron"]=>
    int(1)
    ["Eric Mann"]=>
    int(11)
  }
}

Thus the PHP versions tagged by Jakub Zelenka have T_CLASS_C value at 382 while others have 346. Why does it varies when the value is generated at compile time? I have no idea :/

Lets find out the diff between 8.3.20 and 8.3.21 release tarball as used by the Debian project. The deltas are stored in the git repo using pristine-tar:

$ git clone --reference=/home/hashar/projects/php-src https://salsa.debian.org/php-team/php
$ pristine-tar list |grep 'php8.3_8.3.2[01]'
php8.3_8.3.21.orig.tar.xz
php8.3_8.3.20.orig.tar.xz
$ pristine-tar --verbose checkout php8.3_8.3.20.orig.tar.xz
$ pristine-tar --verbose checkout php8.3_8.3.21.orig.tar.xz
$ tar xf php8.3_8.3.20.orig.tar.xz
$ tar xf php8.3_8.3.21.orig.tar.xz

Since from T396312#10902917 we already know the token is in Zend/zend_language_parser.h and Zend/zned/language_parser.c:

$ grep 'T_CLASS_C = ' php-8.3.{20,21}/Zend/zend_language_parser.*
php-8.3.20/Zend/zend_language_parser.c:    T_CLASS_C = 382,
php-8.3.20/Zend/zend_language_parser.h:    T_CLASS_C = 382,
php-8.3.21/Zend/zend_language_parser.c:  YYSYMBOL_T_CLASS_C = 107,                /* "'__CLASS__'"  */
php-8.3.21/Zend/zend_language_parser.h:    T_CLASS_C = 346,               /* "'__CLASS__'"  */

The 8.3.20 release tarball was made using Bison 3.0.4, the 8.3.21 was made with Bison 3.8.2. Diffing the .h file shows extra tokens are available in the later:

Bison 3.0.4 is quite old, it was released back in 2015 :)

TLDR: the token values are hardcoded in the PHP upstream tarballs and vary depending on the version of Bison used by whoever happens to have created the tarball.

When compiling PHP from git sources, assuming your local Bison version is the same, you would get the same token values.

When using a Debian package, which itself is build from the tarball rather than source, you get exposed to the token versions varying.

The issue is in the AST PHP extension: when compiled, it includes the token values from whichever version of PHP that is used. Those "constants" then mismatches when used with a PHP that got compiled with a different version of Bison. The token values are not part of the Zend API, only the names are and the extension should use names rather than values. How that can be done? I have no idea.

Thanks for getting to the bottom of this! I see the upstream issue https://github.com/php/php-src/issues/14937 just got reopened and is receiving some activity. Let's see how this is going to be mitigated in php-src and perhaps php-ast. And then I'll see if anything can be done in phan instead.

As for our CI environments (where the build is failing for mediawiki/tools/phan* repos in PHP 8.3): I suppose one option is waiting ~1 month until the PHP 8.3.23 release, but I wouldn't want to do this for obvious reasons. So, I guess we could switch back to our previous 8.3.x image; or disable 8.3 tests for those repos. I'm obviously leaving the call here to you.

Sorry, I got slightly confused. Our current PHP 8.3 image uses 8.3.21, which is "good" (value=346). Our previous image used 8.3.17, which is bad (value=382). Our php-ast was last built last November using PHP 8.3.15, which is also bad. So, the version numbers were both wrong but they matched, so that's OK. But now, we have good PHP and bad php-ast, so we need to fix php-ast. Rebuilding with the latest PHP (James attempted this) won't help because 8.3.22 is also bad. We need to rebuild php-ast using PHP 8.3.21 (or any other "good" version).

Change #1155742 had a related patch set uploaded (by Hashar; author: Hashar):

[integration/config@master] dockerfiles: assert AST tokens match PHP tokens

https://gerrit.wikimedia.org/r/1155742

Forcing a working version of https://packages.debian.org/experimental/php8.3-cli seems beyond apt's ability right now. Or at least, beyond my apt-foo.

sury only provides the latest version (e.g., https://github.com/oerdnj/deb.sury.org/issues/1191), and it seems that we're out of luck:

root@770dfe897a40:/# apt-cache policy php8.3-dev
php8.3-dev:
  Installed: 8.3.15-1+0~20241224.50+debian12~1.gbp36719a
  Candidate: 8.3.22-1+0~20250609.63+debian12~1.gbp61bc56
  Version table:
     8.3.22-1+0~20250609.63+debian12~1.gbp61bc56 500
        500 https://packages.sury.org/php bookworm/main amd64 Packages
 *** 8.3.15-1+0~20241224.50+debian12~1.gbp36719a 100
        100 /var/lib/dpkg/status
root@770dfe897a40:/# apt-cache policy php-dev
php-dev:
  Installed: (none)
  Candidate: 2:8.4+96+0~20250402.56+debian12~1.gbp84a5b7
  Version table:
     2:8.4+96+0~20250402.56+debian12~1.gbp84a5b7 500
        500 https://packages.sury.org/php bookworm/main amd64 Packages
     2:8.2+93 500
        500 http://mirrors.wikimedia.org/debian bookworm/main amd64 Packages

In all this, I realized that I've also forgotten one key detail: why are we using custom php-ast builds in the first place? Can't we just switch it for the sury version? That would be consistent with the php-cli used, and it wouldn't be affected by the mismatch.

I know we were previously stuck on older versions of php-ast due to taint-check using an old version of phan, but that's been resolved like 6 years ago. Nowadays we're fine just using the latest version. Sure, there might be "what if"s, but as it stands, I'm not sure if I see the advantages of having a standalone php-ast image.

In all this, I realized that I've also forgotten one key detail: why are we using custom php-ast builds in the first place? Can't we just switch it for the sury version? That would be consistent with the php-cli used, and it wouldn't be affected by the mismatch.

The comment in the Dockerfiles is "Set a standard, new version of php-ast rather than using Debian's older one", but indeed, I think that's no longer an issue now? Let me try.

Aha, right, well. Wikimedia's custom PHP build doesn't have an ast module, so that needs to still have our custom build, and so for consistency we probably want to use the same version of ast in all PHP versions?

Aha, right, well. Wikimedia's custom PHP build doesn't have an ast module, so that needs to still have our custom build, and so for consistency we probably want to use the same version of ast in all PHP versions?

Welp. Still, could we temporarily swap the 8.3 version for sury, and switch back after the next PHP release?

Sorry, I got slightly confused. Our current PHP 8.3 image uses 8.3.21, which is "good" (value=346).

It is "good" cause the upstream tarball for PHP 8.3.21 was not released by Jakub:

$ git -C ~/projects/php-src tag -l php-8.3.21 --format='%(taggername)'
Eric Mann

Our previous image used 8.3.17, which is bad (value=382).

And indeed it was tagged by Jakub:

$ git -C ~/projects/php-src tag -l php-8.3.17 --format='%(taggername)'
Jakub Zelenka

Who ended up having an archaic (per their own terms) version of Bison. It is definitely not his fault, the issue is more about not pinning versions of critical dependencies (GNU Bison, re2c) and not controlling the release environment. We had a similar issue when different people updated mediawiki/vendor with different versions of composer. Anyway that is an entirely different story.

Our php-ast was last built last November using PHP 8.3.15, which is also bad.

Correct:

$ git -C ~/projects/php-src tag -l php-8.3.15 --format='%(taggername)'
Jakub Zelenka

I find it funny we can reliably find out a PHP version is "broken" simply by looking at the git tag :]

So, the version numbers were both wrong but they matched, so that's OK.

But now, we have good PHP and bad php-ast, so we need to fix php-ast. Rebuilding with the latest PHP (James attempted this) won't help because 8.3.22 is also bad. We need to rebuild php-ast using PHP 8.3.21 (or any other "good" version).

Yes that would be the case when the the ast.so is imported in a php image and that image happens to have the same version of php that was used in the releng/php-ast. But that is not always the case!! If the php images are rebuild and PHP/sury released a new version, the php image would have a different one than the one used originally to build ast.so.

An example timeline would be:

  • Rebuild php-ast to bump the extension
    • the image does an apt install so we get the latest versions patchset versions of each PHP from sury.org (good).
    • since all php?? and node??-test-browser-php??-composer depends on php-ast (per their control file having Depends: php-ast)
    • all those php images are rebuild at the same time as the php-ast and thus all get the latest versions from sury.org

Everything is in sync. There is a time based raced condition which is that a new package can be uploaded between the time the php-ast image is build and the other php images are build, but I am willing to ignore it :]

Next, we rebuild the php image for whatever reason. An example is 27fd2e481f965a20e0ebdd36082f73a0da2902a6 which rebuilt them to included php-uuid.

  • the php images are build and potentially get the latest version from sury.org
  • the ast image is NOT REBUILT and thus the ast.so

Thus if:

  • ast image got built with eg 8.3.17 (tagged by Jakub, emitting value 382)
  • the new php image get 8.3.21 (tagged by Eric Mann, emitting value 346)

You get the mismatch :]

One way would be to change the dependency order and build the ast extension using the php image that will end up using it. Then there is a bit of a chicken and eg between the installation of the packages and the compilation.

Given building ast is quite straightforward, we should phase out that releng/php-ast image and use a multistage build:

  • Install the php package
  • add a stage that adds the -dev package and build ast
  • copy from the build stage

This way anytime the php image is rebuild, it get an ast extension compiled with the same php version.

It is "good" cause the upstream tarball for PHP 8.3.21 was not released by Jakub:

Just to clarify, I based my "good" and "bad" on your list in T396312#10903133, so it makes sense that we're in agreement ;)

I find it funny we can reliably find out a PHP version is "broken" simply by looking at the git tag :]

You can do much more than that ;) P77589#312229

So, the version numbers were both wrong but they matched, so that's OK.

But now, we have good PHP and bad php-ast, so we need to fix php-ast. Rebuilding with the latest PHP (James attempted this) won't help because 8.3.22 is also bad. We need to rebuild php-ast using PHP 8.3.21 (or any other "good" version).

Yes that would be the case when the the ast.so is imported in a php image and that image happens to have the same version of php that was used in the releng/php-ast.

Well, not necessarily "the same version". It only needs to have the same status of good or bad.

But that is not always the case!! If the php images are rebuild and PHP/sury released a new version, the php image would have a different one than the one used originally to build ast.so.

True, but the key assumption is that every new version of PHP going forwards will be good, so we shouldn't be affected by rebuilds. So, if the PHP images are rebuilt for whatever reason, and we get a more recent version of PHP, that should be fine because the new version is going to be good, just like the one used to build php-ast.

I should also point out that another solution would be to update PHP from 8.3.21 to 8.3.22. Ironically, updating to a bad version would fix the issue because php-ast is also bad. However, this is prone to breaking the next time the PHP version gets updated, as you mention. Which is why I'd rather rebuild php-ast using a good 8.3 version, and forget about it.


I suppose the multistage approach might work too. It seems more work than temporarily installing php-ast from sury, though (and then rebuilding everything next month with 8.3.23). But you surely know best ;)

Change #1156350 had a related patch set uploaded (by Hashar; author: Hashar):

[integration/config@master] dockerfiles: node-test-brower-php-composer: build ast inline

https://gerrit.wikimedia.org/r/1156350

Change #1156363 had a related patch set uploaded (by Hashar; author: Hashar):

[integration/config@master] dockerfiles: remove php-ast image

https://gerrit.wikimedia.org/r/1156363

Change #1155742 merged by jenkins-bot:

[integration/config@master] Docker: [php*] Build php-ast with the exact same PHP version

https://gerrit.wikimedia.org/r/1155742

Mentioned in SAL (#wikimedia-releng) [2025-06-12T14:50:35Z] <James_F> Docker: [php*] Build php-ast with the exact same PHP version, for T396312

Change #1156350 merged by jenkins-bot:

[integration/config@master] Docker: [node-test-brower-php*-composer] Build php-ast inline

https://gerrit.wikimedia.org/r/1156350

Mentioned in SAL (#wikimedia-releng) [2025-06-12T15:04:07Z] <James_F> Docker: [node-test-brower-php*-composer] Build php-ast inline, for T396312

Change #1156383 had a related patch set uploaded (by Jforrester; author: Jforrester):

[integration/config@master] Docker: Cascade uses of php* with new php-ast inline build

https://gerrit.wikimedia.org/r/1156383

Change #1156383 merged by jenkins-bot:

[integration/config@master] Docker: Cascade uses of php* with new php-ast inline build

https://gerrit.wikimedia.org/r/1156383

Mentioned in SAL (#wikimedia-releng) [2025-06-12T16:24:43Z] <James_F> Docker: Cascade uses of php* with new php-ast inline build, for T396312

Change #1156846 had a related patch set uploaded (by Jforrester; author: Jforrester):

[integration/config@master] jjb: Switch regular PHP image users over to inline-ast-built ones

https://gerrit.wikimedia.org/r/1156846

Change #1156846 merged by jenkins-bot:

[integration/config@master] jjb: Switch regular PHP image users over to inline-ast-built ones

https://gerrit.wikimedia.org/r/1156846

SitRep, 2025-06-13:

  • This is worked around for Wikimedia CI.
  • Upstream are aware and might fix this more generally.

Shall we keep this open until it's fully fixed, or just call it Resolved?

Maybe wait a bit to see if upstream are doing anything in the short term? (Also, it would be nice to include https://gerrit.wikimedia.org/r/c/integration/config/+/1156363 as part of this task)

Change #1156363 merged by jenkins-bot:

[integration/config@master] Docker: Drop php-ast image, now unused

https://gerrit.wikimedia.org/r/1156363

Mentioned in SAL (#wikimedia-releng) [2025-06-13T15:50:07Z] <James_F> Docker: Drop php-ast image, now unused, for T396312

The issue is upstream releases breaks ABI by changing the token values.

That was highlighted by our CI image which builds php-ast with php 8.1 to 8.4 and other images COPY the ast.so but can (and do) have different versions of PHP since the images are build independently from the first.

I have fixed that by inlining the build in each of the php image, thanks to Docker multi stage build. I think the ast compilation might have been introduced before Docker supported multistage build.

So essentially:

  • we found a bug in PHP 8.3
  • got rid of an image

Success!