Skip to content

Let OpenSslCipherMethodsProvider contribute its own result cache meta - #6338

Merged
staabm merged 2 commits into
phpstan:2.2.xfrom
SanderMuller:runtime-list-cache-key
Sep 2, 2026
Merged

Let OpenSslCipherMethodsProvider contribute its own result cache meta#6338
staabm merged 2 commits into
phpstan:2.2.xfrom
SanderMuller:runtime-list-cache-key

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

OpenSslCipherMethodsProvider builds its list of supported ciphers by asking openssl_get_cipher_methods() and then probing every algorithm with openssl_cipher_iv_length(), and the inferred type follows it: openssl_cipher_iv_length() is int for a supported algorithm and false for an unsupported one.

That list is a property of the PHP build rather than of the PHP version, so two hosts on the same PHP_VERSION_ID disagree. Measured across CI runners and docker images:

PHP_VERSION_ID macos-latest ubuntu-latest docker / windows
80333 208 212 212
80425 208 212 212
80510 248 234 -

At PHP 8.4.25 on both sides, ubuntu-latest has four ciphers macos-latest does not: aes-128-cbc-hmac-sha1, aes-128-cbc-hmac-sha256, aes-256-cbc-hmac-sha1, aes-256-cbc-hmac-sha256. So openssl_cipher_iv_length('aes-256-cbc-hmac-sha1') is int on one and false on the other, at the same patch version. phpExtensions records extension names only, so the metadata is identical and the cache is reused across the difference.

Changed since the first version

Both review points, thank you.

  • @ondrejmirtes, the provider now implements ResultCacheMetaExtension instead of ResultCacheManager reaching out to it. That is clearly the better home: the fingerprint sits where the knowledge already lives, ResultCacheManager is untouched, and the build/baseline-8.0.neon entry the first version needed is gone. The diff went from two files and 60 lines to one file and 25.
  • @staabm, there is a test now: OpenSslCipherMethodsProviderTest covers that the key is stable, that a different set of ciphers produces a different hash, and that the order the runtime reports them in does not. It errors on all four cases without the change.

I also dropped the mbstring half of the first version. I could not find any environment where mb_list_encodings() differs at a given PHP version, so it was unmotivated; only the openssl list is evidenced.

Checks

make tests (21266 tests, 96360 assertions), make phpstan and phpcs on both touched files pass. Also verified end to end by patching the provider to drop one cipher: without this change the restored cache still reported int, with it the metadata no longer matches and the file is re-analysed to false.

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a test

@ondrejmirtes

Copy link
Copy Markdown
Member

I don't love this solution. It'd make more sense to me if the extension implemented ResultCacheMetaExtension too.

@SanderMuller
SanderMuller force-pushed the runtime-list-cache-key branch from b9962c2 to 40d9ba6 Compare September 2, 2026 10:18
@SanderMuller SanderMuller changed the title Include runtime-provided list fingerprints in the result cache metadata Let OpenSslCipherMethodsProvider contribute its own result cache meta Sep 2, 2026
@SanderMuller

Copy link
Copy Markdown
Contributor Author

Both addressed, thanks.

The provider implements ResultCacheMetaExtension now, which is a better fit than what I had: the fingerprint lives where the knowledge already is, ResultCacheManager is untouched, and the baseline-8.0.neon entry my first version needed has gone with it. One file, 25 lines, down from two files and 60.

There is a test as well, OpenSslCipherMethodsProviderTest: the key is stable, a different set of ciphers gives a different hash, and the order the runtime reports them in does not change it. All four cases error without the change.

I also dropped the mbstring fingerprint from the first version. I could not find an environment where mb_list_encodings() differs at a given PHP version, so only the openssl list is actually evidenced.

Comment thread tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php Outdated
Comment thread tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php Outdated
@staabm

staabm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

OpenSslCipherMethodsProvider builds its list of supported ciphers by asking openssl_get_cipher_methods() and then probing every algorithm with openssl_cipher_iv_length()

just curios: how did you find out this is a problem? which problem you experienced triggered the PR?

@SanderMuller
SanderMuller force-pushed the runtime-list-cache-key branch from 40d9ba6 to 489fcc1 Compare September 2, 2026 10:30
@SanderMuller

Copy link
Copy Markdown
Contributor Author

Agreed on both, removed. The sha1 one was re-implementing the production code in the assertion, and the key one only asserted a literal that a deliberate rename would update anyway. What is left is the two that pin behaviour: a different set of ciphers has to produce a different hash, and the order the runtime reports them in must not. Both still error without the change.

how did you find out this is a problem? which problem you experienced triggered the PR?

Nothing in production, so worth being straight about that. It came out of the result cache sharing thread you started. I was going through what actually ends up in the cache metadata to understand why a CI-built cache cannot be reused on a developer machine, and noticed that phpExtensions records extension names only, while two type extensions read a runtime list and infer types from its contents.

That could have been theoretical, so I measured it before writing anything: a probe across the runners and docker images, printing the cipher and encoding hashes per environment. The ciphers differ at the same PHP_VERSION_ID (212 on ubuntu-latest against 208 on macos-latest at PHP 8.4.25), which makes CI to laptop a real case. The encodings did not differ anywhere, which is why the mbstring half is gone.

So: found by reading, confirmed by measuring, not by being bitten. If you would rather wait until someone actually reports a stale type, that is a fair call.

@SanderMuller
SanderMuller requested a review from staabm September 2, 2026 10:33
@SanderMuller
SanderMuller force-pushed the runtime-list-cache-key branch from 489fcc1 to 1585c96 Compare September 2, 2026 10:44
Comment thread src/Type/Php/OpenSslCipherMethodsProvider.php
@staabm

staabm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

do we have the same problem with e.g. hash_algos in HashFunctionsReturnTypeExtension?

@SanderMuller
SanderMuller force-pushed the runtime-list-cache-key branch from 1585c96 to 31d485d Compare September 2, 2026 11:19
@SanderMuller

Copy link
Copy Markdown
Contributor Author

Good question, and it turned out to be worth measuring rather than reasoning about.

Same shape, but not reachable. HashFunctionsReturnTypeExtension does read the runtime the same way ($this->hashAlgorithms = hash_algos() in the constructor) and the inferred type does follow it: hash('sha256', $s) gives lowercase-string&non-falsy-string while hash('not-a-real-algo', $s) gives *NEVER*. So structurally it is the same as the ciphers.

The difference is that the algorithm list does not actually vary. I re-ran the environment probe with hash_algos() added, across the same 14 environments:

distinct hash_algos() fingerprints
PHP 8.3.33 (ubuntu, macos, windows, bookworm, alpine) 1
PHP 8.4.25 (ubuntu, macos, windows, bookworm, alpine, trixie) 1
PHP 8.5.9 / 8.5.10 1

60 algorithms with the same fingerprint everywhere, not just at a given PHP_VERSION_ID but across all three minor versions. The cipher list, on the same runs, goes 208 / 212 / 234 / 243 / 248. So hash is compiled in with a fixed set and openssl is the odd one out because its list depends on the linked library and gets filtered by probing each algorithm.

While I was in there I swept for the same pattern across src/Type, and there are exactly three runtime-enumeration readers: hash_algos(), openssl_get_cipher_methods() and mb_list_encodings(). The mbstring one showed no variance either, which is why I dropped its fingerprint from this PR. So the set is closed, and only openssl needs the meta entry on the evidence I have.

Also applied your other note: getSupportedCipherMethods() is private now. You were right that nothing outside the class used it any more, the two consumers go through isSupportedCipherMethod().

@SanderMuller
SanderMuller requested a review from staabm September 2, 2026 11:27
@staabm

staabm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The difference is that the algorithm list does not actually vary. I re-ran the environment probe with hash_algos() added, across the same 14 environments:

it seems there is a api to register new hash algos from a php extension.

I think depending on php-extensions implementation details algos might show up and disappear.
might be worth looking into in a separate PR

@staabm
staabm force-pushed the runtime-list-cache-key branch from 31d485d to ed57f30 Compare September 2, 2026 11:29
Comment thread src/Type/Php/OpenSslCipherMethodsProvider.php Outdated
@staabm
staabm force-pushed the runtime-list-cache-key branch from fb1cf35 to a22cb36 Compare September 2, 2026 12:09
Sander Muller and others added 2 commits September 2, 2026 14:09
The provider builds its list of supported ciphers by asking
openssl_get_cipher_methods() and then probing every algorithm with
openssl_cipher_iv_length(), and the inferred type follows it:
openssl_cipher_iv_length() is int for a supported algorithm and false
for an unsupported one.

That list is a property of the PHP build rather than of the PHP version,
so two hosts on the same PHP_VERSION_ID disagree. Measured across CI
runners and docker images:

  PHP 8.4.25, ubuntu-latest, OpenSSL 3.0.13 -> 212 ciphers
  PHP 8.4.25, macos-latest,  OpenSSL 3.6.3  -> 208 ciphers

The four missing on macOS are aes-128-cbc-hmac-sha1,
aes-128-cbc-hmac-sha256, aes-256-cbc-hmac-sha1 and
aes-256-cbc-hmac-sha256, so openssl_cipher_iv_length() for one of them
is int on the first and false on the second. phpExtensions records
extension names only, so the metadata is identical and the cache is
reused across the difference.

Implementing ResultCacheMetaExtension on the provider puts the
fingerprint where the knowledge already lives, and it costs nothing for
projects that never touch openssl, since the list is only computed when
something asks for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@staabm
staabm force-pushed the runtime-list-cache-key branch from a22cb36 to 0e6182a Compare September 2, 2026 12:09
@SanderMuller

Copy link
Copy Markdown
Contributor Author

The API is exported, so you're right that an extension can add algos: PHP_HASH_API void php_hash_register_algo(const char *algo, const php_hash_ops *ops) in ext/hash/php_hash.h.

Two things narrow it though:

  • ext/hash's own set is registered unconditionally in PHP_MINIT_FUNCTION(hash), with no build flags around it. PHP_MHASH_BC only registers the MHASH_* constants, not algos. So the core list cannot vary.
  • An extension that registers an algo is itself in get_loaded_extensions(), and that already feeds the phpExtensions meta key, so loading or unloading it invalidates the whole cache today.

What is left is narrower than the openssl case: an extension that keeps its name and changes its algo set across its own versions or ini settings, or a patched build of ext/hash. The meta key holds extension names, not versions, so that would slip through. I couldn't find a published extension that calls the API, which is also why the probe saw one fingerprint across all 14 environments.

Agreed on leaving it out of this PR. If a real case turns up, the fix is the same ResultCacheMetaExtension shape.

@staabm
staabm merged commit 1f31805 into phpstan:2.2.x Sep 2, 2026
783 of 790 checks passed
@staabm

staabm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

thank you

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.

3 participants