Skip to content

Commit 5602690

Browse files
committed
Add runtime type inference verification
1 parent 9ba9b8f commit 5602690

File tree

11 files changed

+366
-7
lines changed

11 files changed

+366
-7
lines changed

.github/workflows/push.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ jobs:
6565
fail-fast: false
6666
matrix:
6767
include:
68-
- debug: false
69-
zts: false
70-
asan: false
68+
# - debug: false
69+
# zts: false
70+
# asan: false
7171
- debug: true
7272
zts: true
7373
asan: true
@@ -114,7 +114,7 @@ jobs:
114114
configurationParameters: >-
115115
--${{ matrix.debug && 'enable' || 'disable' }}-debug
116116
--${{ matrix.zts && 'enable' || 'disable' }}-zts
117-
${{ matrix.asan && 'CFLAGS="-fsanitize=undefined,address -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined,address" CC=clang-16 CXX=clang++-16 --disable-opcache-jit' || '' }}
117+
${{ matrix.asan && 'CFLAGS="-fsanitize=undefined,address -DZEND_TRACK_ARENA_ALLOC -DZEND_VERIFY_TYPE_INFERENCE" LDFLAGS="-fsanitize=undefined,address" CC=clang-16 CXX=clang++-16 --disable-opcache-jit' || '' }}
118118
skipSlow: ${{ matrix.asan }}
119119
- name: make
120120
run: make -j$(/usr/bin/nproc) >/dev/null
@@ -136,11 +136,12 @@ jobs:
136136
runTestsParameters: >-
137137
-d zend_extension=opcache.so
138138
-d opcache.enable_cli=1
139-
${{ matrix.asan && '--asan -x' || '' }}
139+
${{ matrix.asan && '--asan -x --verify-type-inference' || '' }}
140140
- name: Verify generated files are up to date
141141
if: ${{ !matrix.asan }}
142142
uses: ./.github/actions/verify-generated-files
143143
MACOS_DEBUG_NTS:
144+
if: false
144145
runs-on: macos-12
145146
steps:
146147
- name: git checkout
@@ -173,6 +174,7 @@ jobs:
173174
- name: Verify generated files are up to date
174175
uses: ./.github/actions/verify-generated-files
175176
WINDOWS:
177+
if: false
176178
name: WINDOWS_X64_ZTS
177179
runs-on: windows-2019
178180
env:
@@ -199,7 +201,8 @@ jobs:
199201
run: .github/scripts/windows/test.bat
200202
BENCHMARKING:
201203
name: BENCHMARKING
202-
if: github.repository_owner == 'php' || github.event_name == 'pull_request'
204+
if: false
205+
# if: github.repository_owner == 'php' || github.event_name == 'pull_request'
203206
runs-on: ubuntu-22.04
204207
steps:
205208
- name: git checkout

Zend/Optimizer/zend_inference.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1974,7 +1974,8 @@ static uint32_t get_ssa_alias_types(zend_ssa_alias_kind alias) {
19741974
if (__type & (MAY_BE_REF|MAY_BE_RCN)) { \
19751975
__type |= MAY_BE_RC1 | MAY_BE_RCN; \
19761976
} \
1977-
if ((__type & MAY_BE_RC1) && (__type & MAY_BE_STRING)) {\
1977+
/* FIXME: Not sure if this is the correct fix */ \
1978+
if ((__type & MAY_BE_RC1) && (__type & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_RESOURCE))) { \
19781979
/* TODO: support for array keys and ($str . "")*/ \
19791980
__type |= MAY_BE_RCN; \
19801981
} \

Zend/Optimizer/zend_optimizer.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,47 @@ static void zend_optimizer_call_registered_passes(zend_script *script, void *ctx
14701470
}
14711471
}
14721472

1473+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
1474+
static void propagate_ssa_inferred_types_to_oplines(zend_op_array *op_array, void *context)
1475+
{
1476+
zend_func_info *func_info = ZEND_FUNC_INFO(op_array);
1477+
if (func_info == NULL) {
1478+
return;
1479+
}
1480+
1481+
zend_ssa *ssa = &func_info->ssa;
1482+
1483+
for (uint32_t i = 0; i < ssa->vars_count; i++) {
1484+
zend_ssa_var *var = &ssa->vars[i];
1485+
zend_ssa_var_info *var_info = &ssa->var_info[i];
1486+
if (var->definition > 0) {
1487+
zend_op *opline = &op_array->opcodes[var->definition];
1488+
opline->result_inferred_type = var_info->type;
1489+
}
1490+
1491+
int use;
1492+
FOREACH_USE(var, use) {
1493+
zend_op *opline = &op_array->opcodes[use];
1494+
zend_ssa_op *ssa_op = &ssa->ops[use];
1495+
if (ssa_op->op1_use == i) {
1496+
if (!opline->swapped_operands) {
1497+
opline->op1_inferred_type = var_info->type;
1498+
} else {
1499+
opline->op2_inferred_type = var_info->type;
1500+
}
1501+
}
1502+
if (ssa_op->op2_use == i) {
1503+
if (!opline->swapped_operands) {
1504+
opline->op2_inferred_type = var_info->type;
1505+
} else {
1506+
opline->op1_inferred_type = var_info->type;
1507+
}
1508+
}
1509+
} FOREACH_USE_END();
1510+
}
1511+
}
1512+
#endif
1513+
14731514
ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_level, zend_long debug_level)
14741515
{
14751516
zend_op_array *op_array;
@@ -1591,6 +1632,10 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
15911632
}
15921633
}
15931634

1635+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
1636+
zend_foreach_op_array(script, propagate_ssa_inferred_types_to_oplines, NULL);
1637+
#endif
1638+
15941639
for (i = 0; i < call_graph.op_arrays_count; i++) {
15951640
ZEND_SET_FUNC_INFO(call_graph.op_arrays[i], NULL);
15961641
}

Zend/zend_compile.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ static void init_op(zend_op *op)
124124
MAKE_NOP(op);
125125
op->extended_value = 0;
126126
op->lineno = CG(zend_lineno);
127+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
128+
op->result_inferred_type = 0;
129+
op->op1_inferred_type = 0;
130+
op->op2_inferred_type = 0;
131+
op->swapped_operands = false;
132+
#endif
127133
}
128134

129135
static zend_always_inline uint32_t get_next_op_number(void)

Zend/zend_compile.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ struct _zend_op {
143143
uint8_t op1_type; /* IS_UNUSED, IS_CONST, IS_TMP_VAR, IS_VAR, IS_CV */
144144
uint8_t op2_type; /* IS_UNUSED, IS_CONST, IS_TMP_VAR, IS_VAR, IS_CV */
145145
uint8_t result_type; /* IS_UNUSED, IS_CONST, IS_TMP_VAR, IS_VAR, IS_CV */
146+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
147+
uint32_t op1_inferred_type;
148+
uint32_t op2_inferred_type;
149+
uint32_t result_inferred_type;
150+
bool swapped_operands;
151+
#endif
146152
};
147153

148154

Zend/zend_execute.c

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4639,6 +4639,10 @@ static void zend_swap_operands(zend_op *op) /* {{{ */
46394639
op->op1_type = op->op2_type;
46404640
op->op2 = tmp;
46414641
op->op2_type = tmp_type;
4642+
4643+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
4644+
op->swapped_operands = !op->swapped_operands;
4645+
#endif
46424646
}
46434647
/* }}} */
46444648
#endif
@@ -5303,14 +5307,158 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint
53035307
# include "zend_vm_trace_map.h"
53045308
#endif
53055309

5310+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
5311+
static bool zend_verify_type_inference(uint32_t type_mask, zval *value)
5312+
{
5313+
if (type_mask == 0) {
5314+
return true;
5315+
}
5316+
5317+
// IS_PTR is used internally, skip for now
5318+
if (Z_TYPE_P(value) == IS_PTR) {
5319+
return true;
5320+
}
5321+
5322+
if (Z_TYPE_P(value) == IS_INDIRECT) {
5323+
if (!(type_mask & MAY_BE_INDIRECT)) {
5324+
return false;
5325+
}
5326+
value = Z_INDIRECT_P(value);
5327+
}
5328+
5329+
if (Z_REFCOUNTED_P(value)) {
5330+
if (Z_REFCOUNT_P(value) == 1 && !(type_mask & MAY_BE_RC1)) {
5331+
return false;
5332+
}
5333+
if (Z_REFCOUNT_P(value) > 1 && !(type_mask & MAY_BE_RCN)) {
5334+
return false;
5335+
}
5336+
}
5337+
5338+
if (Z_TYPE_P(value) == IS_REFERENCE) {
5339+
if (!(type_mask & MAY_BE_REF)) {
5340+
return false;
5341+
}
5342+
value = Z_REFVAL_P(value);
5343+
}
5344+
5345+
uint32_t type = 1u << Z_TYPE_P(value);
5346+
if (!(type_mask & type)) {
5347+
return false;
5348+
}
5349+
5350+
if (Z_TYPE_P(value) == IS_ARRAY) {
5351+
HashTable *ht = Z_ARRVAL_P(value);
5352+
uint32_t num_checked = 0;
5353+
zend_string *str;
5354+
zval *val;
5355+
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, str, val) {
5356+
if (str) {
5357+
if (!(type_mask & MAY_BE_ARRAY_KEY_STRING)) {
5358+
return false;
5359+
}
5360+
} else {
5361+
if (!(type_mask & MAY_BE_ARRAY_KEY_LONG)) {
5362+
return false;
5363+
}
5364+
}
5365+
5366+
uint32_t array_type = 1u << (Z_TYPE_P(val) + MAY_BE_ARRAY_SHIFT);
5367+
if (!(type_mask & array_type)) {
5368+
return false;
5369+
}
5370+
5371+
/* Don't check all elements of large arrays. */
5372+
if (++num_checked > 16) {
5373+
break;
5374+
}
5375+
} ZEND_HASH_FOREACH_END();
5376+
}
5377+
5378+
return true;
5379+
}
5380+
5381+
static void zend_verify_result_type_inference(zend_execute_data *execute_data, const zend_op *opline)
5382+
{
5383+
if (!RETURN_VALUE_USED(opline)
5384+
|| EG(exception)
5385+
|| opline->opcode == ZEND_ROPE_INIT
5386+
|| opline->opcode == ZEND_ROPE_ADD) {
5387+
return;
5388+
}
5389+
5390+
zval *value = EX_VAR(opline->result.var);
5391+
5392+
// Some jump opcode handlers don't set result when it's never read
5393+
if (Z_TYPE_P(value) == IS_UNDEF
5394+
&& (opline->opcode == ZEND_JMP_SET
5395+
|| opline->opcode == ZEND_JMP_NULL
5396+
|| opline->opcode == ZEND_COALESCE
5397+
|| opline->opcode == ZEND_ASSERT_CHECK)) {
5398+
return;
5399+
}
5400+
5401+
if (RETURN_VALUE_USED(opline) && !zend_verify_type_inference(opline->result_inferred_type, value)) {
5402+
fprintf(stderr, "Return type inference verification failed\n");
5403+
fflush(stderr);
5404+
}
5405+
}
5406+
static void zend_verify_op1_type_inference(zend_execute_data *execute_data, const zend_op *opline)
5407+
{
5408+
if (opline->op1_type == IS_UNUSED) {
5409+
return;
5410+
}
5411+
5412+
if (opline->opcode == ZEND_ROPE_ADD || opline->opcode == ZEND_ROPE_END) {
5413+
return;
5414+
}
5415+
5416+
zval *value = opline->op1_type == IS_CONST
5417+
? RT_CONSTANT(opline, opline->op1)
5418+
: EX_VAR(opline->op1.var);
5419+
5420+
if (!zend_verify_type_inference(opline->op1_inferred_type, value)) {
5421+
fprintf(stderr, "OP1 type inference verification failed\n");
5422+
fflush(stderr);
5423+
}
5424+
}
5425+
static void zend_verify_op2_type_inference(zend_execute_data *execute_data, const zend_op *opline)
5426+
{
5427+
if (opline->op2_type == IS_UNUSED) {
5428+
return;
5429+
}
5430+
5431+
zval *value = opline->op2_type == IS_CONST
5432+
? RT_CONSTANT(opline, opline->op2)
5433+
: EX_VAR(opline->op2.var);
5434+
5435+
if (!zend_verify_type_inference(opline->op2_inferred_type, value)) {
5436+
fprintf(stderr, "OP2 type inference verification failed\n");
5437+
fflush(stderr);
5438+
}
5439+
}
5440+
static void zend_verify_operand_type_inference(zend_execute_data *execute_data, const zend_op *opline)
5441+
{
5442+
zend_verify_op1_type_inference(execute_data, opline);
5443+
zend_verify_op2_type_inference(execute_data, opline);
5444+
}
5445+
# define ZEND_VERIFY_RESULT_TYPE_INFERENCE() zend_verify_result_type_inference(execute_data, OPLINE);
5446+
# define ZEND_VERIFY_OPERAND_TYPE_INFERENCE() zend_verify_operand_type_inference(execute_data, OPLINE);
5447+
#else
5448+
# define ZEND_VERIFY_RESULT_TYPE_INFERENCE()
5449+
# define ZEND_VERIFY_OPERAND_TYPE_INFERENCE()
5450+
#endif
5451+
53065452
#define ZEND_VM_NEXT_OPCODE_EX(check_exception, skip) \
5453+
ZEND_VERIFY_RESULT_TYPE_INFERENCE() \
53075454
CHECK_SYMBOL_TABLES() \
53085455
if (check_exception) { \
53095456
OPLINE = EX(opline) + (skip); \
53105457
} else { \
53115458
ZEND_ASSERT(!EG(exception)); \
53125459
OPLINE = opline + (skip); \
53135460
} \
5461+
ZEND_VERIFY_OPERAND_TYPE_INFERENCE() \
53145462
ZEND_VM_CONTINUE()
53155463

53165464
#define ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() \

Zend/zend_opcode.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,10 @@ ZEND_API void pass_two(zend_op_array *op_array)
11781178
opline->result.var = EX_NUM_TO_VAR(op_array->last_var + opline->result.var);
11791179
}
11801180
ZEND_VM_SET_OPCODE_HANDLER(opline);
1181+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
1182+
// Type inference hasn't happened yet and so can determine operand order correctly
1183+
opline->swapped_operands = false;
1184+
#endif
11811185
opline++;
11821186
}
11831187

0 commit comments

Comments
 (0)