Skip to content

Commit bea0b2d

Browse files
committed
fix: prevent int overflow crash when comparing against PHP_INT_MAX/MIN
When an int is compared against PHP_INT_MAX (e.g. $v <= PHP_INT_MAX) and psalm evaluates the negated assertion (> PHP_INT_MAX) in the else branch, reconcileIsGreaterThan computes PHP_INT_MAX + 1, which silently overflows from int to float. Passing that float to TIntRange::contains(int $i) under strict_types=1 raises a fatal TypeError. The same underflow occurs in reconcileIsLessThan when the assertion value is PHP_INT_MIN. Add !is_int() guards after each arithmetic step. When the guard fires, no integer can satisfy the comparison (> PHP_INT_MAX or < PHP_INT_MIN), so all TIntRange, TInt, and TLiteralInt types are removed from the union. Fixes #11209.
1 parent 551172c commit bea0b2d

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

src/Psalm/Internal/Type/SimpleAssertionReconciler.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,25 @@ private static function reconcileIsGreaterThan(
19641964
//we add 1 from the assertion value because we're on a strict operator
19651965
$assertion_value = $assertion->value + 1;
19661966

1967+
if (!is_int($assertion_value)) {
1968+
// PHP_INT_MAX + 1 overflows to float; no integer can satisfy > PHP_INT_MAX.
1969+
if ($assertion->doesFilterNullOrFalse() &&
1970+
($existing_var_type->hasType('null') || $existing_var_type->hasType('false'))
1971+
) {
1972+
$existing_var_type->removeType('null');
1973+
$existing_var_type->removeType('false');
1974+
}
1975+
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
1976+
if ($atomic_type instanceof TIntRange
1977+
|| $atomic_type instanceof TInt
1978+
|| $atomic_type instanceof TLiteralInt
1979+
) {
1980+
$existing_var_type->removeType($atomic_type->getKey());
1981+
}
1982+
}
1983+
return $existing_var_type->freeze();
1984+
}
1985+
19671986
$redundant = true;
19681987

19691988
if ($assertion->doesFilterNullOrFalse() &&
@@ -2013,7 +2032,7 @@ private static function reconcileIsGreaterThan(
20132032
$existing_var_type->addType(new TIntRange($assertion_value, $atomic_type->value));
20142033
}
20152034
}*/
2016-
} elseif ($atomic_type instanceof TInt && is_int($assertion_value)) {
2035+
} elseif ($atomic_type instanceof TInt) {
20172036
$redundant = false;
20182037
$existing_var_type->removeType($atomic_type->getKey());
20192038
$existing_var_type->addType(new TIntRange($assertion_value, null));
@@ -2073,6 +2092,25 @@ private static function reconcileIsLessThan(
20732092
$assertion_value = $assertion->value - 1;
20742093
$existing_var_type = $existing_var_type->getBuilder();
20752094

2095+
if (!is_int($assertion_value)) {
2096+
// PHP_INT_MIN - 1 underflows to float; no integer can satisfy < PHP_INT_MIN.
2097+
if ($assertion->doesFilterNullOrFalse() &&
2098+
($existing_var_type->hasType('null') || $existing_var_type->hasType('false'))
2099+
) {
2100+
$existing_var_type->removeType('null');
2101+
$existing_var_type->removeType('false');
2102+
}
2103+
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
2104+
if ($atomic_type instanceof TIntRange
2105+
|| $atomic_type instanceof TInt
2106+
|| $atomic_type instanceof TLiteralInt
2107+
) {
2108+
$existing_var_type->removeType($atomic_type->getKey());
2109+
}
2110+
}
2111+
return $existing_var_type->freeze();
2112+
}
2113+
20762114
$redundant = true;
20772115

20782116
if ($assertion->doesFilterNullOrFalse() &&

tests/IntRangeTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,29 @@ function bar(int $_a, int $_b): void {}
10341034
'$z' => 'int<min, 9223372036854775807>|null',
10351035
],
10361036
],
1037+
'noInternalErrorOnGreaterThanPhpIntMax' => [
1038+
'code' => '<?php
1039+
function packInteger(int $value): void {
1040+
if ($value >= 0) {
1041+
} elseif ($value <= 9223372036854775807) {
1042+
}
1043+
}
1044+
',
1045+
'assertions' => [],
1046+
'ignored_issues' => ['RedundantCondition'],
1047+
],
1048+
// Psalm does not currently emit RedundantCondition for the >= direction at PHP_INT_MIN,
1049+
// so no ignored_issues entry is needed here (unlike the <= PHP_INT_MAX case above).
1050+
'noInternalErrorOnLessThanPhpIntMin' => [
1051+
'code' => '<?php
1052+
function checkBound(int $value): void {
1053+
if ($value <= 0) {
1054+
} elseif ($value >= -9223372036854775808) {
1055+
}
1056+
}
1057+
',
1058+
'assertions' => [],
1059+
],
10371060
];
10381061
}
10391062

0 commit comments

Comments
 (0)