Skip to content

Commit b830e66

Browse files
authored
Keep the openssl cipher probe's warnings inside the probe (#6372)
1 parent 0e156a6 commit b830e66

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎src/Internal/Silencer.php‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Internal;
4+
5+
use Closure;
6+
use function restore_error_handler;
7+
use function set_error_handler;
8+
9+
/**
10+
* Runs something that predictably emits diagnostics which are none of the user's business, and keeps
11+
* them to itself.
12+
*
13+
* Not the `@` operator, and not Composer's `Silencer` either: both work by lowering what is reported,
14+
* and PHP calls a user error handler for a diagnostic regardless of `error_reporting()`. A handler that
15+
* does not consult it therefore still sees everything `@` was meant to hide, and Xdebug's `scream`
16+
* disables `@` outright. A handler of our own, on top of the stack for the duration of the call, is
17+
* called instead of theirs and reports nothing - and theirs is back in place afterwards.
18+
*
19+
* Use it only where the diagnostics are expected and meaningless, never to hide a failure that should
20+
* be handled: the callable's own return value still says whether it worked.
21+
*/
22+
final class Silencer
23+
{
24+
25+
/**
26+
* @template T
27+
* @param Closure(): T $callback
28+
* @return T
29+
*/
30+
public static function call(Closure $callback)
31+
{
32+
set_error_handler(static fn (): bool => true);
33+
34+
try {
35+
return $callback();
36+
} finally {
37+
restore_error_handler();
38+
}
39+
}
40+
41+
}

‎src/Type/Php/OpenSslCipherMethodsProvider.php‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Analyser\DependencyTracker;
66
use PHPStan\Analyser\ResultCache\ResultCacheValueExtension;
77
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Internal\Silencer;
89
use function array_filter;
910
use function array_map;
1011
use function array_values;
@@ -54,10 +55,14 @@ private function getSupportedCipherMethods(): array
5455
// openssl_get_cipher_methods() reports algorithms that are not actually
5556
// supported on PHP 8.0-8.4 due to https://github.com/php/php-src/issues/19994
5657
// Filter by actually testing each algorithm with openssl_cipher_iv_length().
57-
$methods = array_values(array_filter(
58+
//
59+
// Probing an unsupported algorithm warns, and @ is not enough on its own: a user error
60+
// handler that does not consult error_reporting() is still called for a suppressed
61+
// diagnostic, and whatever installs one is out of our hands. See Silencer.
62+
$methods = Silencer::call(static fn (): array => array_values(array_filter(
5863
openssl_get_cipher_methods(true),
59-
static fn (string $algorithm): bool => @openssl_cipher_iv_length($algorithm) !== false,
60-
));
64+
static fn (string $algorithm): bool => openssl_cipher_iv_length($algorithm) !== false,
65+
)));
6166
}
6267

6368
$this->supportedCipherMethods = array_map('strtolower', $methods);

‎tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use PHPStan\Analyser\ScopeFactory;
77
use PHPStan\Analyser\ValueDependencyCollector;
88
use PHPStan\Testing\PHPStanTestCase;
9+
use function restore_error_handler;
10+
use function set_error_handler;
911

1012
class OpenSslCipherMethodsProviderTest extends PHPStanTestCase
1113
{
@@ -44,6 +46,36 @@ public function testIsSupportedCipherMethodTracksTheCipher(): void
4446
], $dependencies['dependents']['/project/src/Analysed.php']['analysis']);
4547
}
4648

49+
/**
50+
* Reading the ciphers out of the runtime means probing each one, and on PHP 8.0-8.4
51+
* openssl_get_cipher_methods() reports algorithms openssl_cipher_iv_length() rejects with a
52+
* warning (php/php-src#19994) - 40 of 248 on PHP 8.4.23. `@` does not settle that: a user error
53+
* handler that does not consult error_reporting() is still called for a suppressed diagnostic.
54+
* See phpstan/phpstan#15176.
55+
*
56+
* Vacuous on a PHP where nothing is rejected, which is why the count is not asserted - only that
57+
* whatever the probe does stays inside it.
58+
*/
59+
public function testProbingTheRuntimeLeaksNoWarningThroughAnUnsuppressedHandler(): void
60+
{
61+
$leaked = [];
62+
set_error_handler(static function (int $errno, string $errstr) use (&$leaked): bool {
63+
// deliberately does not check error_reporting(), so the @ operator does not hide anything
64+
$leaked[] = $errstr;
65+
66+
return true;
67+
});
68+
69+
try {
70+
$value = (new OpenSslCipherMethodsProvider())->getValue('aes-128-cbc');
71+
} finally {
72+
restore_error_handler();
73+
}
74+
75+
$this->assertSame([], $leaked, 'Probing the runtime for supported ciphers must not emit warnings.');
76+
$this->assertContains($value, ['supported', 'unsupported']);
77+
}
78+
4779
/**
4880
* @param list<string> $supportedCipherMethods
4981
*/

0 commit comments

Comments
 (0)