Skip to content

Commit 55df31e

Browse files
authored
Check open integer-range bounds for shift-left overflow (#6647)
1 parent 579a7f1 commit 55df31e

7 files changed

Lines changed: 76 additions & 7 deletions

File tree

‎src/Reflection/InitializerExprTypeResolver.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2618,9 +2618,10 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
26182618
return new ErrorType();
26192619
}
26202620
// an overflowing shift wraps around, which breaks the monotonicity the bounds rely on
2621+
// open bounds still include the platform's minimum or maximum integer
26212622
if (
2622-
($rangeMin !== null && self::shiftLeftOverflows(intval($rangeMin), $operand->getValue()))
2623-
|| ($rangeMax !== null && self::shiftLeftOverflows(intval($rangeMax), $operand->getValue()))
2623+
self::shiftLeftOverflows($rangeMin ?? PHP_INT_MIN, $operand->getValue())
2624+
|| self::shiftLeftOverflows($rangeMax ?? PHP_INT_MAX, $operand->getValue())
26242625
) {
26252626
return new IntegerType();
26262627
}

‎src/Turbo/TurboExtensionEnabler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
final class TurboExtensionEnabler
3434
{
3535

36-
public const EXPECTED_EXTENSION_VERSION = 'f46d060';
36+
public const EXPECTED_EXTENSION_VERSION = 'a0f7086';
3737

3838
private static bool $active = false;
3939

‎tests/PHPStan/Analyser/nsrt/integer-range-types.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ public function shiftLeft($a, $b, $c, $d, $e, $f): void
364364
assertType('*ERROR*', $f << 0);
365365

366366
assertType('int<-10, 10>', $a << 1);
367-
assertType('int<10, max>', $b << 1);
368-
assertType('int<min, -10>', $c << 1);
367+
assertType('int', $b << 1);
368+
assertType('int', $c << 1);
369369
assertType('2|50|int<10, 20>|int<60, 80>', $d << 1);
370370
assertType('2|6|10', $e << 1);
371371
assertType('*ERROR*', $f << 1);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShiftLeftUnboundedRange;
4+
5+
use RuntimeException;
6+
use function PHPStan\Testing\assertType;
7+
8+
/**
9+
* @param non-negative-int $nonNegative
10+
* @param positive-int $positive
11+
* @param int<min, 0> $nonPositive
12+
* @param negative-int $negative
13+
* @param int<min, 10> $unboundedMin
14+
* @param int<-10, max> $unboundedMax
15+
*/
16+
function shiftRanges(int $nonNegative, int $positive, int $nonPositive, int $negative, int $unboundedMin, int $unboundedMax): void
17+
{
18+
assertType('int', $nonNegative << 1);
19+
assertType('int', $positive << 1);
20+
assertType('int', $nonPositive << 1);
21+
assertType('int', $negative << 1);
22+
assertType('int', $unboundedMin << 1);
23+
assertType('int', $unboundedMax << 1);
24+
25+
assertType('int<0, max>', $nonNegative << 0);
26+
assertType('int<1, max>', $positive << 0);
27+
assertType('int<min, 0>', $nonPositive << 0);
28+
assertType('int<min, -1>', $negative << 0);
29+
assertType('int<min, 10>', $unboundedMin << 0);
30+
assertType('int<-10, max>', $unboundedMax << 0);
31+
32+
assertType('int<0, max>', $nonNegative >> 1);
33+
assertType('int<0, max>', $positive >> 1);
34+
assertType('int<min, 0>', $nonPositive >> 1);
35+
assertType('int<min, -1>', $negative >> 1);
36+
assertType('int<min, 5>', $unboundedMin >> 1);
37+
assertType('int<-5, max>', $unboundedMax >> 1);
38+
}
39+
40+
function readU64(string $bytes): int
41+
{
42+
$value = 0;
43+
for ($i = 0; $i < 8; $i++) {
44+
$value = ($value << 8) | ord($bytes[$i]);
45+
}
46+
47+
assertType('int', $value);
48+
assertType('bool', $value < 0);
49+
if ($value < 0) {
50+
throw new RuntimeException('Value is too large for 64-bit signed integer');
51+
}
52+
53+
assertType('int<0, max>', $value);
54+
return $value;
55+
}

‎tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,9 @@ public function testBug15243(): void
327327
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15243.php'], []);
328328
}
329329

330+
public function testShiftLeftUnboundedRange(): void
331+
{
332+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/shift-left-unbounded-range.php'], []);
333+
}
334+
330335
}

‎turbo-ext/src/InitializerExprTypeResolver.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3034,10 +3034,11 @@ class InitializerExprTypeResolver
30343034
}
30353035
if (kind == IETR_SHIFT_LEFT) {
30363036
// an overflowing shift wraps around, which breaks the monotonicity the bounds rely on
3037+
// open bounds still include the platform's minimum or maximum integer
30373038
bool minOverflows = false;
30383039
bool maxOverflows = false;
3039-
if (Z_TYPE(rangeMin) != IS_NULL && UNEXPECTED(!shiftLeftOverflows(zval_get_long(&rangeMin), operandValue, minOverflows))) return zv::Val();
3040-
if (!minOverflows && Z_TYPE(rangeMax) != IS_NULL && UNEXPECTED(!shiftLeftOverflows(zval_get_long(&rangeMax), operandValue, maxOverflows))) return zv::Val();
3040+
if (UNEXPECTED(!shiftLeftOverflows(Z_TYPE(rangeMin) == IS_NULL ? ZEND_LONG_MIN : zval_get_long(&rangeMin), operandValue, minOverflows))) return zv::Val();
3041+
if (!minOverflows && UNEXPECTED(!shiftLeftOverflows(Z_TYPE(rangeMax) == IS_NULL ? ZEND_LONG_MAX : zval_get_long(&rangeMax), operandValue, maxOverflows))) return zv::Val();
30413042
if (minOverflows || maxOverflows) {
30423043
return newIntegerType();
30433044
}

‎turbo-ext/tests/type-family.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7741,6 +7741,13 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu
77417741
}
77427742
}
77437743
}
7744+
foreach ([[0, null], [1, null], [null, 0], [null, -1], [null, 10], [-10, null]] as $i => [$min, $max]) {
7745+
foreach ([0, 1, 8, 12] as $shift) {
7746+
foreach (['getShiftLeftType', 'getShiftRightType'] as $method) {
7747+
$r["$method open range $i shift $shift"] = $catching(static fn () => $resolver->$method($te($range($min, $max)), $te($int($shift)), $getType));
7748+
}
7749+
}
7750+
}
77447751
foreach ($operands as $leftName => $left) {
77457752
foreach ($operands as $rightName => $right) {
77467753
$r["resolveIdenticalType $leftName $rightName"] = $catching(static fn () => $viewResult($resolver->resolveIdenticalType($left, $right)));

0 commit comments

Comments
 (0)