From 681b114e33131437f605fa5473f2b126fd0ea5e2 Mon Sep 17 00:00:00 2001 From: "Hans Krentel (hakre)" Date: Sun, 3 May 2026 20:02:50 +0200 Subject: [PATCH 1/4] Testdata *scanf(format) bad scan conversion character '.' cases --- .../PHPStan/Rules/Functions/PrintfParametersRuleTest.php | 8 ++++++++ tests/PHPStan/Rules/Functions/data/printf.php | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 1e3ab9ddd78..15e8c997dbd 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -84,6 +84,14 @@ public function testFile(): void 'Call to sprintf contains 2 placeholders, 1 value given.', 29, ], + [ + 'Call to sscanf contains an invalid placeholder.', + 38, + ], + [ + 'Call to fscanf contains an invalid placeholder.', + 39, + ], [ 'Call to sprintf contains 2 placeholders, 1 value given.', 45, diff --git a/tests/PHPStan/Rules/Functions/data/printf.php b/tests/PHPStan/Rules/Functions/data/printf.php index b4236303978..c7b2de803ce 100644 --- a/tests/PHPStan/Rules/Functions/data/printf.php +++ b/tests/PHPStan/Rules/Functions/data/printf.php @@ -35,8 +35,8 @@ sscanf($str, "%20[^abcde]a%d", $string, $number); // ok printf("%.E", 3.14159); // ok sprintf("%.E", 3.14159); // ok -sscanf($str, '%.E', $number); // ok -fscanf($str, '%.E', $number); // ok +sscanf($str, '%.E', $number); // bad scan conversion character '.' +fscanf($str, '%.E', $number); // bad scan conversion character '.' sscanf($str, '%[A-Z]%d', $char, $number); // ok sprintf('%s %s %s', ...[1]); // do not detect unpacked arguments sprintf('%s %s %s', ...[1, 2, 3]); // ok From 6c8eddf814b183fc6943fce38ab115d402eb1fca Mon Sep 17 00:00:00 2001 From: hakre Date: Sun, 3 May 2026 17:59:41 +0200 Subject: [PATCH 2/4] Update PrintfHelper.php Make `public function getScanfPlaceholdersCount(string $format): ?int` returning the sscanf() vetted number of placeholders that give/return/assign conversions. refs: - https://github.com/phpstan/phpstan-src/pull/5591 - https://github.com/phpstan/phpstan/issues/14567 --- src/Rules/Functions/PrintfHelper.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php index 411972885d3..ae8a8f2f771 100644 --- a/src/Rules/Functions/PrintfHelper.php +++ b/src/Rules/Functions/PrintfHelper.php @@ -2,15 +2,21 @@ namespace PHPStan\Rules\Functions; +use ErrorException; use Nette\Utils\Strings; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; +use Throwable; use function array_filter; use function array_keys; +use function array_slice; use function count; use function in_array; use function max; +use function restore_error_handler; +use function set_error_handler; use function sprintf; +use function sscanf; use function strlen; use const PREG_SET_ORDER; @@ -37,7 +43,24 @@ public function getPrintfPlaceholders(string $format): ?array public function getScanfPlaceholdersCount(string $format): ?int { - return $this->getPlaceholdersCount('(?[cdDeEfinosuxX%s]|\[[^\]]+\])', $format, true); + try { + set_error_handler( + static function ($s, $m, ...$vv) { + $vv = array_slice($vv, 0, 2); + throw new ErrorException($m, 0, $s, ...$vv); + }, + ); + $nFormat = '%n' . $format; + $result = sscanf('', $nFormat); + } catch (Throwable) { + return null; + } finally { + restore_error_handler(); + } + if ($result === null) { + return null; + } + return count($result) + -1; } /** From 5742f2bc4cfe18baf478c9c7dd7f7487e2f43fa1 Mon Sep 17 00:00:00 2001 From: "Hans Krentel (hakre)" Date: Sun, 3 May 2026 21:13:36 +0200 Subject: [PATCH 3/4] Cherrypick: Inferred test-cases from Truncate `sscanf`/`fscanf` format string at NUL byte before counting placeholders #5591 --- .../Functions/PrintfParametersRuleTest.php | 5 +++++ .../PHPStan/Rules/Functions/data/bug-14567.php | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-14567.php diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 15e8c997dbd..d31344d8cdb 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -155,4 +155,9 @@ public function testBug10260(): void $this->analyse([__DIR__ . '/data/bug-10260.php'], []); } + public function testBug14567(): void + { + $this->analyse([__DIR__ . '/data/bug-14567.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-14567.php b/tests/PHPStan/Rules/Functions/data/bug-14567.php new file mode 100644 index 00000000000..127d3c577f9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-14567.php @@ -0,0 +1,18 @@ + Date: Mon, 4 May 2026 10:46:44 +0200 Subject: [PATCH 4/4] #hakre memo: fixup! Update PrintfHelper.php @phpstan-bot: perhaps you can spend some LLM-inference on the two comments, the earlier for feasibility, the later with your bets when this could happen. --- src/Rules/Functions/PrintfHelper.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php index ae8a8f2f771..4c72899fe83 100644 --- a/src/Rules/Functions/PrintfHelper.php +++ b/src/Rules/Functions/PrintfHelper.php @@ -44,23 +44,29 @@ public function getPrintfPlaceholders(string $format): ?array public function getScanfPlaceholdersCount(string $format): ?int { try { + // if we would *know* that simple downgrader can handle + // Throwable -> ErrorException then 7.4 required error + // handler could be injected this way into the try/catch + // & appending finally as an extension. *dreaming* set_error_handler( static function ($s, $m, ...$vv) { $vv = array_slice($vv, 0, 2); throw new ErrorException($m, 0, $s, ...$vv); }, ); - $nFormat = '%n' . $format; + $nFormat = '%*n' . $format; $result = sscanf('', $nFormat); } catch (Throwable) { return null; } finally { restore_error_handler(); } + // one day phpstan may report here that $result can never be null for sscanf('', $nFormat), + // which is actually correct: https://3v4l.org/rO7Ni if ($result === null) { return null; } - return count($result) + -1; + return count($result); } /**