Skip to content

Commit dcf148c

Browse files
committed
ext/opcache: Fix GH-24088 integer range propagation
Track numeric operand types before range inference with a lightweight worklist. Coercions and symbolic constraints must not reuse bounds that apply only to integer inputs. Guard casts, arithmetic, increment/decrement, and assignments through typed references, then run full type inference using the valid ranges. Fixes GH-24088
1 parent 63b0af4 commit dcf148c

7 files changed

Lines changed: 479 additions & 11 deletions

File tree

‎NEWS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ PHP NEWS
3838
. Fixed field_count not resetting on OK packet. (Kamil Tekiela)
3939

4040
- Opcache:
41+
. Fixed bug GH-24088 (Range inference optimizer bug). (Ilia Alshanetsky)
4142
. Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a
4243
loop-invariant addition). (Ilia Alshanetsky)
4344
. Fixed OSS-Fuzz #545352966 (default value AST of an SHM-persisted partial).

‎Zend/Optimizer/zend_inference.c‎

Lines changed: 206 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,15 @@ static bool zend_inference_calc_binary_op_range(
785785
const zend_op_array *op_array, const zend_ssa *ssa,
786786
const zend_op *opline, const zend_ssa_op *ssa_op, uint8_t opcode, zend_ssa_range *tmp) {
787787
zend_long op1_min, op2_min, op1_max, op2_max, t1, t2, t3, t4;
788+
uint32_t allowed_types = MAY_BE_LONG;
789+
790+
if (opcode == ZEND_ADD || opcode == ZEND_SUB || opcode == ZEND_MUL || opcode == ZEND_DIV) {
791+
allowed_types |= MAY_BE_DOUBLE;
792+
}
793+
if ((OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~allowed_types)
794+
|| (OP2_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~allowed_types)) {
795+
return 0;
796+
}
788797

789798
switch (opcode) {
790799
case ZEND_ADD:
@@ -1066,6 +1075,12 @@ static bool zend_inference_calc_binary_op_range(
10661075
return 0;
10671076
}
10681077

1078+
static bool zend_inference_has_integer_range(const zend_ssa *ssa, int var)
1079+
{
1080+
return ssa->var_info[var].has_range
1081+
&& (ssa->var_info[var].type & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF)) == MAY_BE_LONG;
1082+
}
1083+
10691084
static bool zend_inference_calc_range(const zend_op_array *op_array, const zend_ssa *ssa, int var, int widening, bool narrowing, zend_ssa_range *tmp)
10701085
{
10711086
uint32_t line;
@@ -1164,7 +1179,7 @@ static bool zend_inference_calc_range(const zend_op_array *op_array, const zend_
11641179
tmp->underflow = constraint->range.underflow && tmp->underflow;
11651180
tmp->min = MAX(constraint->range.min, tmp->min);
11661181
#ifdef SYM_RANGE
1167-
} else if (narrowing && ssa->var_info[constraint->min_ssa_var].has_range) {
1182+
} else if (narrowing && zend_inference_has_integer_range(ssa, constraint->min_ssa_var)) {
11681183
tmp->underflow = ssa->var_info[constraint->min_ssa_var].range.underflow && tmp->underflow;
11691184
if (!add_will_overflow(ssa->var_info[constraint->min_ssa_var].range.min, constraint->range.min)) {
11701185
tmp->min = MAX(ssa->var_info[constraint->min_ssa_var].range.min + constraint->range.min, tmp->min);
@@ -1175,7 +1190,7 @@ static bool zend_inference_calc_range(const zend_op_array *op_array, const zend_
11751190
tmp->max = MIN(constraint->range.max, tmp->max);
11761191
tmp->overflow = constraint->range.overflow && tmp->overflow;
11771192
#ifdef SYM_RANGE
1178-
} else if (narrowing && ssa->var_info[constraint->max_ssa_var].has_range) {
1193+
} else if (narrowing && zend_inference_has_integer_range(ssa, constraint->max_ssa_var)) {
11791194
if (!add_will_overflow(ssa->var_info[constraint->max_ssa_var].range.max, constraint->range.max)) {
11801195
tmp->max = MIN(ssa->var_info[constraint->max_ssa_var].range.max + constraint->range.max, tmp->max);
11811196
}
@@ -1187,7 +1202,7 @@ static bool zend_inference_calc_range(const zend_op_array *op_array, const zend_
11871202
tmp->underflow = constraint->range.underflow;
11881203
tmp->min = constraint->range.min;
11891204
#ifdef SYM_RANGE
1190-
} else if (narrowing && ssa->var_info[constraint->min_ssa_var].has_range) {
1205+
} else if (narrowing && zend_inference_has_integer_range(ssa, constraint->min_ssa_var)) {
11911206
if (add_will_overflow(ssa->var_info[constraint->min_ssa_var].range.min, constraint->range.min)) {
11921207
tmp->underflow = 1;
11931208
tmp->min = ZEND_LONG_MIN;
@@ -1204,7 +1219,7 @@ static bool zend_inference_calc_range(const zend_op_array *op_array, const zend_
12041219
tmp->max = constraint->range.max;
12051220
tmp->overflow = constraint->range.overflow;
12061221
#ifdef SYM_RANGE
1207-
} else if (narrowing && ssa->var_info[constraint->max_ssa_var].has_range) {
1222+
} else if (narrowing && zend_inference_has_integer_range(ssa, constraint->max_ssa_var)) {
12081223
if (add_will_overflow(ssa->var_info[constraint->max_ssa_var].range.max, constraint->range.max)) {
12091224
tmp->overflow = 1;
12101225
tmp->max = ZEND_LONG_MAX;
@@ -1269,7 +1284,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
12691284

12701285
case ZEND_BW_NOT:
12711286
if (ssa_op->result_def == var) {
1272-
if (OP1_HAS_RANGE()) {
1287+
if (OP1_HAS_RANGE()
1288+
&& (OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF)) == MAY_BE_LONG) {
12731289
if (OP1_RANGE_UNDERFLOW() ||
12741290
OP1_RANGE_OVERFLOW()) {
12751291
tmp->min = ZEND_LONG_MIN;
@@ -1297,7 +1313,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
12971313
}
12981314
} else if (ssa_op->result_def == var) {
12991315
if (opline->extended_value == IS_LONG) {
1300-
if (OP1_HAS_RANGE()) {
1316+
if (OP1_HAS_RANGE()
1317+
&& (OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF)) == MAY_BE_LONG) {
13011318
tmp->min = OP1_MIN_RANGE();
13021319
tmp->max = OP1_MAX_RANGE();
13031320
return 1;
@@ -1349,7 +1366,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
13491366
break;
13501367
case ZEND_PRE_INC:
13511368
if (ssa_op->op1_def == var || ssa_op->result_def == var) {
1352-
if (OP1_HAS_RANGE()) {
1369+
if (OP1_HAS_RANGE()
1370+
&& !(OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~(MAY_BE_LONG | MAY_BE_DOUBLE))) {
13531371
tmp->min = OP1_MIN_RANGE();
13541372
tmp->max = OP1_MAX_RANGE();
13551373
tmp->underflow = OP1_RANGE_UNDERFLOW();
@@ -1368,7 +1386,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
13681386
break;
13691387
case ZEND_PRE_DEC:
13701388
if (ssa_op->op1_def == var || ssa_op->result_def == var) {
1371-
if (OP1_HAS_RANGE()) {
1389+
if (OP1_HAS_RANGE()
1390+
&& !(OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~(MAY_BE_LONG | MAY_BE_DOUBLE))) {
13721391
tmp->min = OP1_MIN_RANGE();
13731392
tmp->max = OP1_MAX_RANGE();
13741393
tmp->underflow = OP1_RANGE_UNDERFLOW();
@@ -1387,7 +1406,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
13871406
break;
13881407
case ZEND_POST_INC:
13891408
if (ssa_op->op1_def == var || ssa_op->result_def == var) {
1390-
if (OP1_HAS_RANGE()) {
1409+
if (OP1_HAS_RANGE()
1410+
&& !(OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~(MAY_BE_LONG | MAY_BE_DOUBLE))) {
13911411
tmp->min = OP1_MIN_RANGE();
13921412
tmp->max = OP1_MAX_RANGE();
13931413
tmp->underflow = OP1_RANGE_UNDERFLOW();
@@ -1409,7 +1429,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
14091429
break;
14101430
case ZEND_POST_DEC:
14111431
if (ssa_op->op1_def == var || ssa_op->result_def == var) {
1412-
if (OP1_HAS_RANGE()) {
1432+
if (OP1_HAS_RANGE()
1433+
&& !(OP1_INFO() & (MAY_BE_ANY | MAY_BE_UNDEF | MAY_BE_REF) & ~(MAY_BE_LONG | MAY_BE_DOUBLE))) {
14131434
tmp->min = OP1_MIN_RANGE();
14141435
tmp->max = OP1_MAX_RANGE();
14151436
tmp->underflow = OP1_RANGE_UNDERFLOW();
@@ -1445,7 +1466,8 @@ ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, cons
14451466
break;
14461467
case ZEND_ASSIGN:
14471468
if (ssa_op->op1_def == var || ssa_op->op2_def == var || ssa_op->result_def == var) {
1448-
if (OP2_HAS_RANGE()) {
1469+
if (OP2_HAS_RANGE()
1470+
&& (ssa_op->op2_def == var || !(OP1_INFO() & MAY_BE_REF))) {
14491471
tmp->min = OP2_MIN_RANGE();
14501472
tmp->max = OP2_MAX_RANGE();
14511473
tmp->underflow = OP2_RANGE_UNDERFLOW();
@@ -4879,6 +4901,173 @@ static void zend_mark_cv_references(const zend_op_array *op_array, const zend_sc
48794901
free_alloca(worklist, use_heap);
48804902
}
48814903

4904+
static uint32_t zend_inference_numeric_operand_type(
4905+
const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline,
4906+
uint8_t op_type, znode_op op, int use)
4907+
{
4908+
if (op_type == IS_CONST) {
4909+
uint8_t type = Z_TYPE_P(CRT_CONSTANT(op));
4910+
return type == IS_CONSTANT_AST ? MAY_BE_ANY : 1U << type;
4911+
}
4912+
return get_ssa_var_info(ssa, use) & (MAY_BE_ANY | MAY_BE_UNDEF);
4913+
}
4914+
4915+
static uint32_t zend_inference_numeric_type(zend_type type)
4916+
{
4917+
uint32_t mask = ZEND_TYPE_PURE_MASK(type);
4918+
return !ZEND_TYPE_IS_COMPLEX(type) && mask && !(mask & ~(MAY_BE_LONG | MAY_BE_DOUBLE))
4919+
? mask : MAY_BE_ANY;
4920+
}
4921+
4922+
static uint32_t zend_inference_calc_numeric_type(
4923+
const zend_op_array *op_array, const zend_ssa *ssa, int var, zend_long optimization_level)
4924+
{
4925+
const zend_ssa_var *ssa_var = &ssa->vars[var];
4926+
if (ssa_var->definition_phi) {
4927+
const zend_ssa_phi *phi = ssa_var->definition_phi;
4928+
if (phi->pi >= 0) {
4929+
uint32_t type = ssa->var_info[phi->sources[0]].type;
4930+
return phi->has_range_constraint ? type : type & phi->constraint.type.type_mask;
4931+
}
4932+
uint32_t type = 0;
4933+
for (uint32_t i = 0; i < ssa->cfg.blocks[phi->block].predecessors_count; i++) {
4934+
type |= ssa->var_info[phi->sources[i]].type;
4935+
}
4936+
return type;
4937+
}
4938+
if (ssa_var->definition < 0) {
4939+
return MAY_BE_ANY;
4940+
}
4941+
4942+
const zend_op *opline = &op_array->opcodes[ssa_var->definition];
4943+
const zend_ssa_op *ssa_op = &ssa->ops[ssa_var->definition];
4944+
uint32_t t1 = zend_inference_numeric_operand_type(
4945+
op_array, ssa, opline, opline->op1_type, opline->op1, ssa_op->op1_use);
4946+
uint32_t t2 = zend_inference_numeric_operand_type(
4947+
op_array, ssa, opline, opline->op2_type, opline->op2, ssa_op->op2_use);
4948+
4949+
switch (opline->opcode) {
4950+
case ZEND_ADD:
4951+
case ZEND_SUB:
4952+
case ZEND_MUL:
4953+
case ZEND_DIV:
4954+
case ZEND_MOD:
4955+
case ZEND_SL:
4956+
case ZEND_SR:
4957+
case ZEND_BW_OR:
4958+
case ZEND_BW_AND:
4959+
case ZEND_BW_XOR:
4960+
if (ssa_op->result_def == var) {
4961+
return binary_op_result_type(ssa, opline->opcode, t1, t2, -1, optimization_level);
4962+
}
4963+
break;
4964+
case ZEND_BW_NOT:
4965+
if (ssa_op->result_def == var && !(t1 & (MAY_BE_STRING | MAY_BE_OBJECT))) {
4966+
return MAY_BE_LONG;
4967+
}
4968+
break;
4969+
case ZEND_ASSIGN_OP:
4970+
if (opline->extended_value != ZEND_CONCAT && opline->extended_value != ZEND_POW
4971+
&& !(OP1_INFO() & MAY_BE_REF)
4972+
&& (ssa_op->op1_def == var || ssa_op->result_def == var)) {
4973+
return binary_op_result_type(ssa, opline->extended_value, t1, t2, -1, optimization_level);
4974+
}
4975+
break;
4976+
case ZEND_CAST:
4977+
if (ssa_op->result_def == var) {
4978+
return 1U << opline->extended_value;
4979+
}
4980+
ZEND_FALLTHROUGH;
4981+
case ZEND_QM_ASSIGN:
4982+
case ZEND_JMP_SET:
4983+
case ZEND_COALESCE:
4984+
case ZEND_COPY_TMP:
4985+
case ZEND_SEND_VAR:
4986+
case ZEND_UNSET_DIM:
4987+
case ZEND_UNSET_OBJ:
4988+
case ZEND_OP_DATA:
4989+
return t1;
4990+
case ZEND_POST_INC:
4991+
case ZEND_POST_DEC:
4992+
if (ssa_op->result_def == var) {
4993+
return t1;
4994+
}
4995+
ZEND_FALLTHROUGH;
4996+
case ZEND_PRE_INC:
4997+
case ZEND_PRE_DEC:
4998+
return (t1 & ~(MAY_BE_LONG | MAY_BE_DOUBLE)) ? MAY_BE_ANY
4999+
: t1 ? t1 | MAY_BE_DOUBLE : 0;
5000+
case ZEND_ASSIGN:
5001+
if (ssa_op->op2_def == var || !(OP1_INFO() & MAY_BE_REF)) {
5002+
return t2;
5003+
}
5004+
break;
5005+
case ZEND_RECV:
5006+
case ZEND_RECV_INIT:
5007+
if (op_array->arg_info && opline->op1.num <= op_array->num_args) {
5008+
return zend_inference_numeric_type(op_array->arg_info[opline->op1.num - 1].type);
5009+
}
5010+
break;
5011+
case ZEND_STRLEN:
5012+
case ZEND_COUNT:
5013+
case ZEND_FUNC_NUM_ARGS:
5014+
return MAY_BE_LONG;
5015+
case ZEND_DO_FCALL:
5016+
case ZEND_DO_ICALL:
5017+
case ZEND_DO_UCALL:
5018+
case ZEND_DO_FCALL_BY_NAME:
5019+
case ZEND_FRAMELESS_ICALL_0:
5020+
case ZEND_FRAMELESS_ICALL_1:
5021+
case ZEND_FRAMELESS_ICALL_2:
5022+
case ZEND_FRAMELESS_ICALL_3: {
5023+
const zend_func_info *func_info = ZEND_FUNC_INFO(op_array);
5024+
if (ssa_op->result_def != var || !func_info || !func_info->call_map) {
5025+
break;
5026+
}
5027+
const zend_call_info *call_info = func_info->call_map[ssa_var->definition];
5028+
if (!call_info || call_info->is_prototype) {
5029+
break;
5030+
}
5031+
const zend_function *func = call_info->callee_func;
5032+
if ((func->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE)
5033+
&& !(func->common.fn_flags & (ZEND_ACC_RETURN_REFERENCE | ZEND_ACC_GENERATOR))) {
5034+
return zend_inference_numeric_type(func->common.arg_info[-1].type);
5035+
}
5036+
break;
5037+
}
5038+
default:
5039+
break;
5040+
}
5041+
return MAY_BE_ANY;
5042+
}
5043+
5044+
static void zend_infer_numeric_types(
5045+
const zend_op_array *op_array, const zend_ssa *ssa, zend_long optimization_level)
5046+
{
5047+
uint32_t len = zend_bitset_len(ssa->vars_count);
5048+
ALLOCA_FLAG(use_heap);
5049+
zend_bitset worklist = ZEND_BITSET_ALLOCA(len, use_heap);
5050+
zend_bitset_clear(worklist, len);
5051+
for (int i = op_array->last_var; i < ssa->vars_count; i++) {
5052+
zend_bitset_incl(worklist, i);
5053+
}
5054+
5055+
int var;
5056+
WHILE_WORKLIST(worklist, len, var) {
5057+
if (ssa->var_info[var].type & MAY_BE_REF) {
5058+
continue;
5059+
}
5060+
uint32_t type = ssa->vars[var].alias ? MAY_BE_ANY
5061+
: zend_inference_calc_numeric_type(op_array, ssa, var, optimization_level);
5062+
type = (type & (MAY_BE_ANY | MAY_BE_UNDEF)) | ssa->var_info[var].type;
5063+
if (type != ssa->var_info[var].type) {
5064+
ssa->var_info[var].type = type;
5065+
add_usages(op_array, ssa, worklist, var);
5066+
}
5067+
} WHILE_WORKLIST_END();
5068+
free_alloca(worklist, use_heap);
5069+
}
5070+
48825071
ZEND_API zend_result zend_ssa_inference(zend_arena **arena, const zend_op_array *op_array, const zend_script *script, zend_ssa *ssa, zend_long optimization_level) /* {{{ */
48835072
{
48845073
zend_ssa_var_info *ssa_var_info;
@@ -4910,7 +5099,13 @@ ZEND_API zend_result zend_ssa_inference(zend_arena **arena, const zend_op_array
49105099

49115100
zend_mark_cv_references(op_array, script, ssa);
49125101

5102+
zend_infer_numeric_types(op_array, ssa, optimization_level);
49135103
zend_infer_ranges(op_array, ssa);
5104+
for (i = op_array->last_var; i < ssa->vars_count; i++) {
5105+
if (!(ssa_var_info[i].type & MAY_BE_REF)) {
5106+
ssa_var_info[i].type = 0;
5107+
}
5108+
}
49145109

49155110
if (zend_infer_types(op_array, script, ssa, optimization_level) == FAILURE) {
49165111
return FAILURE;

‎ext/opcache/tests/opt/gh24088.phpt‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
GH-24088 (Integer casts must not reuse ranges inferred for other types)
3+
--EXTENSIONS--
4+
opcache
5+
--INI--
6+
opcache.enable=1
7+
opcache.enable_cli=1
8+
opcache.optimization_level=-1
9+
--FILE--
10+
<?php
11+
function bounded($value) {
12+
if ($value > 5 && $value < 7) {
13+
return match ((int) $value) {
14+
6 => 'six',
15+
default => 'not six',
16+
};
17+
}
18+
}
19+
20+
function equal($value) {
21+
if ($value == 6) {
22+
return (int) $value;
23+
}
24+
}
25+
26+
function copied($value, $copy) {
27+
if ($value == 6) {
28+
if ($copy) {
29+
$result = $value;
30+
} else {
31+
$result = 6;
32+
}
33+
return (int) $result;
34+
}
35+
}
36+
37+
echo 'float: ', bounded(5.5), "\n";
38+
echo 'numeric string: ', bounded('5.5'), "\n";
39+
echo 'integer: ', bounded(6), "\n";
40+
echo 'boolean: ', equal(true), "\n";
41+
echo 'copied boolean: ', copied(true, true), "\n";
42+
echo 'joined integer: ', copied(true, false), "\n";
43+
?>
44+
--EXPECT--
45+
float: not six
46+
numeric string: not six
47+
integer: six
48+
boolean: 1
49+
copied boolean: 1
50+
joined integer: 6

0 commit comments

Comments
 (0)