Skip to content

Commit dea69c3

Browse files
committed
Add runtime type inference verification
Co-authored-by: Dmitry Stogov <dmitry@zend.com> Closes GH-12930
1 parent d7d0d19 commit dea69c3

File tree

6 files changed

+199
-20
lines changed

6 files changed

+199
-20
lines changed

.github/nightly_matrix.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function get_matrix_include(array $branches) {
6363
'branch' => $branch,
6464
'debug' => true,
6565
'zts' => true,
66-
'configuration_parameters' => "CFLAGS='-DZEND_RC_DEBUG=1 -DPROFITABILITY_CHECKS=0 -DZEND_VERIFY_FUNC_INFO=1'",
66+
'configuration_parameters' => "CFLAGS='-DZEND_RC_DEBUG=1 -DPROFITABILITY_CHECKS=0 -DZEND_VERIFY_FUNC_INFO=1 -DZEND_VERIFY_TYPE_INFERENCE'",
6767
'timeout_minutes' => 360,
6868
'test_function_jit' => true,
6969
'asan' => false,

.github/workflows/nightly.yml

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,16 @@ jobs:
349349
fail-fast: false
350350
matrix:
351351
branch: ${{ fromJson(needs.GENERATE_MATRIX.outputs.branches) }}
352-
name: "${{ matrix.branch.name }}_COMMUNITY"
352+
type: ['asan', 'verify_type_inference']
353+
# These branches don't include type verification
354+
exclude:
355+
- { branch: { name: 'PHP-8.1', ref: 'PHP-8.1', major: 8, minor: 1 }, type: 'verify_type_inference' }
356+
- { branch: { name: 'PHP-8.2', ref: 'PHP-8.2', major: 8, minor: 2 }, type: 'verify_type_inference' }
357+
- { branch: { name: 'PHP-8.3', ref: 'PHP-8.3', major: 8, minor: 3 }, type: 'verify_type_inference' }
358+
name: "${{ matrix.branch.name }}_COMMUNITY_${{ matrix.type }}"
353359
runs-on: ubuntu-${{ matrix.branch.version.minor >= 3 && '22.04' || '20.04' }}
354360
env:
361+
ASAN_OPTIONS: exitcode=139
355362
UBSAN_OPTIONS: print_stacktrace=1
356363
USE_ZEND_ALLOC: 0
357364
USE_TRACKED_ALLOC: 1
@@ -365,11 +372,11 @@ jobs:
365372
- name: ./configure
366373
uses: ./.github/actions/configure-x64
367374
with:
375+
# CFLAGS removes O2, so we have to add it again...
368376
configurationParameters: >-
369-
--enable-debug
370377
--enable-zts
371-
CFLAGS='-fsanitize=undefined,address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC'
372-
LDFLAGS='-fsanitize=undefined,address'
378+
${{ matrix.type == 'asan' && '--enable-debug CFLAGS="-fsanitize=undefined,address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined,address"' || '' }}
379+
${{ matrix.type == 'verify_type_inference' && 'CFLAGS="-DZEND_VERIFY_TYPE_INFERENCE -O2"' || '' }}
373380
- name: make
374381
run: make -j$(/usr/bin/nproc) >/dev/null
375382
- name: make install
@@ -379,12 +386,19 @@ jobs:
379386
sudo service mysql start
380387
mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test"
381388
mysql -uroot -proot -e "SET GLOBAL local_infile = true"
382-
- name: Enable Opcache and JIT
389+
- name: Enable Opcache
383390
run: |
391+
echo memory_limit=-1 >> /etc/php.d/opcache.ini
384392
echo zend_extension=opcache.so > /etc/php.d/opcache.ini
385393
echo opcache.enable_cli=1 >> /etc/php.d/opcache.ini
386394
echo opcache.enable=1 >> /etc/php.d/opcache.ini
387-
echo opcache.protect_memory=1 >> /etc/php.d/opcache.ini
395+
echo opcache.memory_consumption=256M >> /etc/php.d/opcache.ini
396+
echo opcache.file_update_protection=0 >> /etc/php.d/opcache.ini
397+
echo opcache.interned_strings_buffer=64 >> /etc/php.d/opcache.ini
398+
echo opcache.max_accelerated_files=100000 >> /etc/php.d/opcache.ini
399+
- name: Enable JIT
400+
if: matrix.type != 'verify_type_inference'
401+
run: |
388402
echo opcache.jit=tracing >> /etc/php.d/opcache.ini
389403
echo opcache.jit_buffer_size=1G >> /etc/php.d/opcache.ini
390404
echo opcache.jit_max_root_traces=100000 >> /etc/php.d/opcache.ini
@@ -394,11 +408,6 @@ jobs:
394408
echo opcache.jit_hot_func=1 >> /etc/php.d/opcache.ini
395409
echo opcache.jit_hot_return=1 >> /etc/php.d/opcache.ini
396410
echo opcache.jit_hot_side_exit=1 >> /etc/php.d/opcache.ini
397-
echo opcache.file_update_protection=0 >> /etc/php.d/opcache.ini
398-
echo opcache.memory_consumption=256M >> /etc/php.d/opcache.ini
399-
echo opcache.interned_strings_buffer=64 >> /etc/php.d/opcache.ini
400-
echo opcache.max_accelerated_files=100000 >> /etc/php.d/opcache.ini
401-
echo memory_limit=-1 >> /etc/php.d/opcache.ini
402411
php -v
403412
- name: Test AMPHP
404413
run: |
@@ -410,7 +419,6 @@ jobs:
410419
cd "amphp-$repository"
411420
git rev-parse HEAD
412421
php /usr/bin/composer install --no-progress --ignore-platform-reqs
413-
export ASAN_OPTIONS=exitcode=139
414422
vendor/bin/phpunit || EXIT_CODE=$?
415423
if [ ${EXIT_CODE:-0} -gt 128 ]; then
416424
X=1;
@@ -426,7 +434,6 @@ jobs:
426434
php /usr/bin/composer install --no-progress --ignore-platform-reqs
427435
# Hack to disable a test that hangs
428436
php -r '$c = file_get_contents("tests/Filesystem/FilesystemTest.php"); $c = str_replace("public function testSharedGet()", "#[\\PHPUnit\\Framework\\Attributes\\Group('"'"'skip'"'"')]\n public function testSharedGet()", $c); file_put_contents("tests/Filesystem/FilesystemTest.php", $c);'
429-
export ASAN_OPTIONS=exitcode=139
430437
php vendor/bin/phpunit --exclude-group skip || EXIT_CODE=$?
431438
if [ ${EXIT_CODE:-0} -gt 128 ]; then
432439
exit 1
@@ -441,7 +448,6 @@ jobs:
441448
cd "reactphp-$repository"
442449
git rev-parse HEAD
443450
php /usr/bin/composer install --no-progress --ignore-platform-reqs
444-
export ASAN_OPTIONS=exitcode=139
445451
vendor/bin/phpunit || EXIT_CODE=$?
446452
if [ $[EXIT_CODE:-0} -gt 128 ]; then
447453
X=1;
@@ -455,7 +461,6 @@ jobs:
455461
cd event-loop
456462
git rev-parse HEAD
457463
php /usr/bin/composer install --no-progress --ignore-platform-reqs
458-
export ASAN_OPTIONS=exitcode=139
459464
vendor/bin/phpunit || EXIT_CODE=$?
460465
if [ ${EXIT_CODE:-0} -gt 128 ]; then
461466
exit 1
@@ -471,7 +476,6 @@ jobs:
471476
php -r '$c = file_get_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php"); $c = str_replace("public function testSanitizeDeepNestedString()", "/** @group skip */\n public function testSanitizeDeepNestedString()", $c); file_put_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php", $c);'
472477
# Buggy FFI test in Symfony, see https://github.com/symfony/symfony/issues/47668
473478
php -r '$c = file_get_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php"); $c = str_replace("*/\n public function testCastNonTrailingCharPointer()", "* @group skip\n */\n public function testCastNonTrailingCharPointer()", $c); file_put_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php", $c);'
474-
export ASAN_OPTIONS=exitcode=139
475479
export SYMFONY_DEPRECATIONS_HELPER=max[total]=999
476480
X=0
477481
for component in $(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n'); do
@@ -487,7 +491,6 @@ jobs:
487491
git clone https://github.com/sebastianbergmann/phpunit.git --branch=main --depth=1
488492
cd phpunit
489493
git rev-parse HEAD
490-
export ASAN_OPTIONS=exitcode=139
491494
php /usr/bin/composer install --no-progress --ignore-platform-reqs
492495
php ./phpunit || EXIT_CODE=$?
493496
if [ ${EXIT_CODE:-0} -gt 128 ]; then
@@ -506,7 +509,6 @@ jobs:
506509
git clone https://github.com/WordPress/wordpress-develop.git wordpress --depth=1
507510
cd wordpress
508511
git rev-parse HEAD
509-
export ASAN_OPTIONS=exitcode=139
510512
php /usr/bin/composer install --no-progress --ignore-platform-reqs
511513
cp wp-tests-config-sample.php wp-tests-config.php
512514
sed -i 's/youremptytestdbnamehere/test/g' wp-tests-config.php

.github/workflows/push.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
configurationParameters: >-
122122
--${{ matrix.debug && 'enable' || 'disable' }}-debug
123123
--${{ matrix.zts && 'enable' || 'disable' }}-zts
124-
${{ matrix.asan && 'CFLAGS="-fsanitize=undefined,address -fno-sanitize=pointer-overflow -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined,address -fno-sanitize=pointer-overflow" CC=clang-16 CXX=clang++-16' || '' }}
124+
${{ matrix.asan && 'CFLAGS="-fsanitize=undefined,address -fno-sanitize=pointer-overflow -DZEND_TRACK_ARENA_ALLOC -DZEND_VERIFY_TYPE_INFERENCE" LDFLAGS="-fsanitize=undefined,address -fno-sanitize=pointer-overflow" CC=clang-16 CXX=clang++-16' || '' }}
125125
skipSlow: ${{ matrix.asan }}
126126
- name: make
127127
run: make -j$(/usr/bin/nproc) >/dev/null

Zend/zend_compile.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ 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->op1_use_type = 0;
129+
op->op2_use_type = 0;
130+
op->result_use_type = 0;
131+
op->op1_def_type = 0;
132+
op->op2_def_type = 0;
133+
op->result_def_type = 0;
134+
#endif
127135
}
128136

129137
static zend_always_inline uint32_t get_next_op_number(void)

Zend/zend_compile.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ 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_use_type;
148+
uint32_t op2_use_type;
149+
uint32_t result_use_type;
150+
uint32_t op1_def_type;
151+
uint32_t op2_def_type;
152+
uint32_t result_def_type;
153+
#endif
146154
};
147155

148156

Zend/zend_execute.c

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4639,6 +4639,16 @@ 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+
uint32_t tmp_info;
4645+
tmp_info = op->op1_use_type;
4646+
op->op1_use_type = op->op2_use_type;
4647+
op->op2_use_type = tmp_info;
4648+
tmp_info = op->op1_def_type;
4649+
op->op1_def_type = op->op2_def_type;
4650+
op->op2_def_type = tmp_info;
4651+
#endif
46424652
}
46434653
/* }}} */
46444654
#endif
@@ -5303,14 +5313,165 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint
53035313
# include "zend_vm_trace_map.h"
53045314
#endif
53055315

5316+
#ifdef ZEND_VERIFY_TYPE_INFERENCE
5317+
5318+
#define ZEND_VERIFY_TYPE_INFERENCE_ERROR(msg, ...) \
5319+
do { \
5320+
fprintf(stderr, "Inference verification failed at %04d %s (" msg ")\n", (int)(opline - EX(func)->op_array.opcodes), operand, __VA_ARGS__); \
5321+
exit(139); \
5322+
} while (0)
5323+
5324+
static void zend_verify_type_inference(zval *value, uint32_t type_mask, uint8_t op_type, zend_execute_data *execute_data, const zend_op *opline, const char *operand)
5325+
{
5326+
if (type_mask == MAY_BE_CLASS) {
5327+
return;
5328+
}
5329+
5330+
if (Z_TYPE_P(value) == IS_INDIRECT) {
5331+
if (!(type_mask & MAY_BE_INDIRECT)) {
5332+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x mising MAY_BE_INDIRECT", type_mask);
5333+
}
5334+
value = Z_INDIRECT_P(value);
5335+
}
5336+
5337+
/* Verifying RC inference is currently not possible because type information is based on the SSA
5338+
* built without ZEND_SSA_RC_INFERENCE, which is missing various definitions for RC-modifying
5339+
* operations. Support could be added by repeating SSA-construction and type inference with the
5340+
* given flag. */
5341+
// if (Z_REFCOUNTED_P(value)) {
5342+
// if (Z_REFCOUNT_P(value) == 1 && !(type_mask & MAY_BE_RC1)) {
5343+
// ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_RC1", type_mask);
5344+
// }
5345+
// if (Z_REFCOUNT_P(value) > 1 && !(type_mask & MAY_BE_RCN)) {
5346+
// ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_RCN", type_mask);
5347+
// }
5348+
// }
5349+
5350+
if (Z_TYPE_P(value) == IS_REFERENCE) {
5351+
if (!(type_mask & MAY_BE_REF)) {
5352+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_REF", type_mask);
5353+
}
5354+
value = Z_REFVAL_P(value);
5355+
}
5356+
5357+
if (!(type_mask & (1u << Z_TYPE_P(value)))) {
5358+
if (Z_TYPE_P(value) == IS_UNUSED && op_type == IS_VAR && (type_mask & MAY_BE_NULL)) {
5359+
/* FETCH_OBJ_* for typed property may return IS_UNDEF. This is an exception. */
5360+
} else {
5361+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing type %d", type_mask, Z_TYPE_P(value));
5362+
}
5363+
}
5364+
5365+
if (Z_TYPE_P(value) == IS_ARRAY) {
5366+
HashTable *ht = Z_ARRVAL_P(value);
5367+
uint32_t num_checked = 0;
5368+
zend_string *str;
5369+
zval *val;
5370+
if (HT_IS_INITIALIZED(ht)) {
5371+
if (HT_IS_PACKED(ht) && !MAY_BE_PACKED(type_mask)) {
5372+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_ARRAY_PACKED", type_mask);
5373+
}
5374+
if (!HT_IS_PACKED(ht) && !MAY_BE_HASH(type_mask)) {
5375+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_ARRAY_HASH", type_mask);
5376+
}
5377+
} else {
5378+
if (!(type_mask & MAY_BE_ARRAY_EMPTY)) {
5379+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_ARRAY_EMPTY", type_mask);
5380+
}
5381+
}
5382+
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, str, val) {
5383+
if (str) {
5384+
if (!(type_mask & MAY_BE_ARRAY_KEY_STRING)) {
5385+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_ARRAY_KEY_STRING", type_mask);
5386+
break;
5387+
}
5388+
} else {
5389+
if (!(type_mask & MAY_BE_ARRAY_KEY_LONG)) {
5390+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing MAY_BE_ARRAY_KEY_LONG", type_mask);
5391+
break;
5392+
}
5393+
}
5394+
5395+
uint32_t array_type = 1u << (Z_TYPE_P(val) + MAY_BE_ARRAY_SHIFT);
5396+
if (!(type_mask & array_type)) {
5397+
ZEND_VERIFY_TYPE_INFERENCE_ERROR("mask 0x%x missing array type %d", type_mask, Z_TYPE_P(val));
5398+
break;
5399+
}
5400+
5401+
/* Don't check all elements of large arrays. */
5402+
if (++num_checked > 16) {
5403+
break;
5404+
}
5405+
} ZEND_HASH_FOREACH_END();
5406+
}
5407+
}
5408+
5409+
static void zend_verify_inference_use(zend_execute_data *execute_data, const zend_op *opline)
5410+
{
5411+
if (opline->op1_use_type
5412+
&& (opline->op1_type & (IS_TMP_VAR|IS_VAR|IS_CV))
5413+
&& opline->opcode != ZEND_ROPE_ADD
5414+
&& opline->opcode != ZEND_ROPE_END) {
5415+
zend_verify_type_inference(EX_VAR(opline->op1.var), opline->op1_use_type, opline->op1_type, execute_data, opline, "op1_use");
5416+
}
5417+
if (opline->op2_use_type
5418+
&& (opline->op2_type & (IS_TMP_VAR|IS_VAR|IS_CV))) {
5419+
zend_verify_type_inference(EX_VAR(opline->op2.var), opline->op2_use_type, opline->op2_type, execute_data, opline, "op2_use");
5420+
}
5421+
if (opline->result_use_type
5422+
&& (opline->result_type & (IS_TMP_VAR|IS_VAR|IS_CV))) {
5423+
zend_verify_type_inference(EX_VAR(opline->result.var), opline->result_use_type, opline->result_type, execute_data, opline, "result_use");
5424+
}
5425+
}
5426+
5427+
static void zend_verify_inference_def(zend_execute_data *execute_data, const zend_op *opline)
5428+
{
5429+
if (EG(exception)) {
5430+
return;
5431+
}
5432+
if (opline->op1_def_type
5433+
&& (opline->op1_type & (IS_TMP_VAR|IS_VAR|IS_CV))
5434+
// array is actually changed by the the following instruction(s)
5435+
&& opline->opcode != ZEND_FETCH_DIM_W
5436+
&& opline->opcode != ZEND_FETCH_DIM_RW
5437+
&& opline->opcode != ZEND_FETCH_DIM_FUNC_ARG
5438+
&& opline->opcode != ZEND_FETCH_LIST_W) {
5439+
zend_verify_type_inference(EX_VAR(opline->op1.var), opline->op1_def_type, opline->op1_type, execute_data, opline, "op1_def");
5440+
}
5441+
if (opline->op2_def_type
5442+
&& (opline->op2_type & (IS_TMP_VAR|IS_VAR|IS_CV))) {
5443+
zend_verify_type_inference(EX_VAR(opline->op2.var), opline->op2_def_type, opline->op2_type, execute_data, opline, "op2_def");
5444+
}
5445+
if (opline->result_def_type
5446+
&& (opline->result_type & (IS_TMP_VAR|IS_VAR|IS_CV))
5447+
&& opline->opcode != ZEND_ROPE_INIT
5448+
&& opline->opcode != ZEND_ROPE_ADD
5449+
// Some jump opcode handlers don't set result when it's never read
5450+
&& opline->opcode != ZEND_JMP_SET
5451+
&& opline->opcode != ZEND_JMP_NULL
5452+
&& opline->opcode != ZEND_COALESCE
5453+
&& opline->opcode != ZEND_ASSERT_CHECK) {
5454+
zend_verify_type_inference(EX_VAR(opline->result.var), opline->result_def_type, opline->result_type, execute_data, opline, "result_def");
5455+
}
5456+
}
5457+
5458+
# define ZEND_VERIFY_INFERENCE_USE() zend_verify_inference_use(execute_data, OPLINE);
5459+
# define ZEND_VERIFY_INFERENCE_DEF() zend_verify_inference_def(execute_data, OPLINE);
5460+
#else
5461+
# define ZEND_VERIFY_INFERENCE_USE()
5462+
# define ZEND_VERIFY_INFERENCE_DEF()
5463+
#endif
5464+
53065465
#define ZEND_VM_NEXT_OPCODE_EX(check_exception, skip) \
5466+
ZEND_VERIFY_INFERENCE_DEF() \
53075467
CHECK_SYMBOL_TABLES() \
53085468
if (check_exception) { \
53095469
OPLINE = EX(opline) + (skip); \
53105470
} else { \
53115471
ZEND_ASSERT(!EG(exception)); \
53125472
OPLINE = opline + (skip); \
53135473
} \
5474+
ZEND_VERIFY_INFERENCE_USE() \
53145475
ZEND_VM_CONTINUE()
53155476

53165477
#define ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() \

0 commit comments

Comments
 (0)