Skip to content

Check open integer-range bounds for shift-left overflow - #6647

Merged
VincentLanglet merged 2 commits into
phpstan:2.3.xfrom
JanTvrdik:jt-fix-bitshift-overflow
Oct 14, 2026
Merged

VincentLanglet merged 2 commits into
phpstan:2.3.xfrom
JanTvrdik:jt-fix-bitshift-overflow

Conversation

@JanTvrdik

@JanTvrdik JanTvrdik commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Check open integer-range bounds for shift-left overflow, using PHP_INT_MIN and PHP_INT_MAX when a bound is null.

Rebased onto 2.3.x after 2.2.x maintenance ended. The PHP resolver and its native Turbo implementation contain the same fix, with differential coverage and the required Turbo version bump.

The existing overflow check only checks finite bounds. For int<0, max> << 8, it checks zero but skips the maximum, so the result incorrectly remains non-negative. The same problem affects open lower bounds.

This causes a false smaller.alwaysFalse error in a big-endian u64() reader:

$value = 0;
for ($i = 0; $i < 8; $i++) {
    $value = ($value << 8) | ord($bytes[$i]);
}
if ($value < 0) {
    throw new RuntimeException('Value is too large for 64-bit signed integer');
}

The false positive first appears in 2.2.9. Git bisect identifies 1081ec4, whose bitwise OR range inference exposes the shift-left problem. The more recent finite-bound overflow check does not cover this case.

Tests

  • Added type-inference tests for the original reader and six open-bound variants.
  • Added a comparison-rule test that rejects the false positive.
  • Confirmed the new tests fail before the source fix and pass after it.
  • Updated two existing shift-left expectations that also assumed overflow was impossible.
  • Zero shifts and right shifts retain their existing range inference.

Initial validation on PHP 8.5, before retargeting from 2.2.x:

  • Full ParaTest suite: 21,765 tests, 97,025 assertions, 74 skipped, no failures.
  • PHPStan self-analysis: no errors.
  • Code style for the changed source and test class: passes.
  • The PHAR built by this PR's CI also passes the full passkeys analysis with its original configuration.

Validation after rebasing onto 2.3.x (PHP 8.5):

  • Full PHP ParaTest suite: 22,526 tests, 96,997 assertions, 75 skipped, no failures.
  • PHPStan self-analysis: no errors, with and without Turbo.
  • Changed-source code style and PHP/C++ method/signature parity: pass.
  • Strict-warning native compilation: passes (local linking uses the shared C++ runtime because the static library is not installed).
  • Focused whole-walk differential: identical (2,768 trace lines); reproducer analysis output is also identical with and without Turbo.
  • Full native ParaTest suite: 22,526 tests, 96,997 assertions, 75 skipped, no failures (same counts as PHP).
  • Turbo smoke differential: ALL OK.
  • Local C++ lint is unavailable because the required clang-tidy 21 is not installed; upstream CI runs it.

Original failure: https://github.com/shipmonk-rnd/passkeys/actions/runs/36873090362/job/110405538537

Task: https://app.asana.com/0/0/1219068476946134

Copilot AI balanced review requested due to automatic review settings October 1, 2026 14:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The focused fix correctly handles both open bounds and includes comprehensive regression coverage.

Review effort: Balanced
Findings: None

What changed in this PR

Fixes shift-left inference for open integer ranges by accounting for platform limits during overflow detection.

Changes:

  • Checks unbounded range endpoints using PHP_INT_MIN and PHP_INT_MAX.
  • Updates affected shift-left expectations.
  • Adds regression coverage for open ranges and the u64() reader false positive.
File Description
src/​Reflection/​InitializerExprTypeResolver.php Detects overflow at open integer-range bounds.
tests/​PHPStan/​Analyser/​nsrt/​shift-left-unbounded-range.php Covers open bounds, zero/right shifts, and the reader regression.
tests/​PHPStan/​Analyser/​nsrt/​integer-range-types.php Updates overflow-sensitive inferred types.
tests/​PHPStan/​Rules/​Comparison/​NumberComparisonOperatorsConstantConditionRuleTest.php Verifies the false comparison warning is eliminated.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@JanTvrdik

Copy link
Copy Markdown
Contributor Author

The core unit-test matrix, self-analysis matrix, generated baselines, and coding standard have passed. I also downloaded the PHAR built by this PR and ran the full passkeys analysis with its original configuration: no errors.

The broader PHAR integration matrix is not green, but its current failures also occur in the earlier run https://github.com/phpstan/phpstan-src/actions/runs/36867554483, before this PR:

I have not changed those unrelated checks or their baselines. This account has read-only access to the upstream repository, so I cannot rerun its jobs.

@JanTvrdik

Copy link
Copy Markdown
Contributor Author

Final CI update: the core unit tests, self-analysis, coding standard, and generated baselines pass, but not all remaining failures are pre-existing.

PocketMine is affected by this fix. At src/world/format/io/region/RegionLoader.php:153–154, it passes getSectorCount() << 12 to fread(). getSectorCount() is annotated only as positive-int, with no finite upper bound. The old inference assumed this shift stayed positive; the corrected inference returns int because it can overflow. Consequently, fread() now reports that its length must be int<1, max>. A finite upper bound on the sector count would allow the existing bounded-range inference to retain positivity. I have not suppressed this diagnostic or changed the integration baseline.

The other newly completed failures are:

  • Both mutation-testing jobs were interrupted by runner shutdown/cancellation while generating mutants; they did not report mutation-score failures.
  • The PHP 8.5 benchmark exceeded its timing thresholds for bug-14972.php and bug-14996.php. I have not established whether those timing failures are related to this change.

The earlier comment's pre-existing-failure evidence applies to the specific jobs listed there, not to every failure in the final matrix.

@VincentLanglet

Copy link
Copy Markdown
Contributor

Hi, 2.2.x branch is not maintained anymore.
Could you target 2.3.x instead. Thanks

@JanTvrdik
JanTvrdik changed the base branch from 2.2.x to 2.3.x October 14, 2026 16:04
@JanTvrdik
JanTvrdik force-pushed the jt-fix-bitshift-overflow branch from a073615 to 40c807b Compare October 14, 2026 16:07
@JanTvrdik

JanTvrdik commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks! Retargeted to 2.3.x and rebased onto its current tip without conflicts.

I also applied the same fix to the native Turbo implementation added on 2.3.x, added open-range differential coverage, and bumped the expected extension version.

The full PHP and native suites both pass (22,526 tests, 96,997 assertions, 75 skipped each), as do self-analysis in both modes, method/signature parity, the focused PHP/Turbo whole-walk differential, and the Turbo smoke differential (ALL OK). The updated branch has been pushed.

@VincentLanglet
VincentLanglet requested a review from staabm October 14, 2026 16:37
@VincentLanglet
VincentLanglet merged commit 55df31e into phpstan:2.3.x Oct 14, 2026
973 of 975 checks passed
pawell67 added a commit to pawell67/phpstan that referenced this pull request Oct 15, 2026
…ow check

phpstan/phpstan-src#6647 made an open positive-int << 12 return int, since it
can overflow. RegionLoader passes getSectorCount() << 12 to fread(), and
getSectorCount() is only positive-int, so the length is no longer int<1, max>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ondrejmirtes pushed a commit to phpstan/phpstan that referenced this pull request Oct 17, 2026
…ow check

phpstan/phpstan-src#6647 made an open positive-int << 12 return int, since it
can overflow. RegionLoader passes getSectorCount() << 12 to fread(), and
getSectorCount() is only positive-int, so the length is no longer int<1, max>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

4 participants