[GIT-PULLS] [php-src] PR #23507: Fix live ranges for ZEND_JMP_SET and ZEND_COALESCE
[email protected] (Mrmaxmeier)
| Newsgroups | php.git-pulls |
|---|---|
| Message-ID | <[email protected]> |
Pull Request: https://github.com/php/php-src/pull/23507
Author: Mrmaxmeier
Hi,
we ran into a use of an uninitialized value bug `fuzzer-function-jit` fuzzing target:
```php
<?php
try {
$a ?: match (true) { 1 => 1 };
} catch (UnhandledMatchError $e) {
}
```
and the `??` variant:
```php
<?php
try {
$a ?? match (true) { 1 => 1 };
} catch (UnhandledMatchError $e) {
}
```
Note: MSAN would be the right tool to detect this bug, but oss-fuzz flags this harness as ASAN-only because the JIT doesn't work with MSAN.
<details>
<summary>ASAN backtrace for reproducer (<code>?:</code> / ZEND_JMP_SET)</summary>
```
/out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each.
Running: /testcase
AddressSanitizer:DEADLYSIGNAL
=================================================================
==14==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x55e832e30ebf bp 0x7ffeab63ebd0 sp 0x7ffeab63eb30 T0)
==14==The signal is caused by a READ memory access.
==14==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used.
SCARINESS: 20 (wild-addr-read)
#0 0x55e832e30ebf in zend_gc_delref /src/php-src/Zend/zend_types.h:835:2
#1 0x55e832e30ebf in zval_delref_p /src/php-src/Zend/zend_types.h:1406:9
#2 0x55e832e30ebf in zval_ptr_dtor_nogc /src/php-src/Zend/zend_variables.h:34:35
#3 0x55e832e30ebf in cleanup_live_vars /src/php-src/Zend/zend_execute.c:4948:6
#4 0x55e833170b00 in zend_dispatch_try_catch_finally_helper_SPEC /src/php-src/Zend/zend_vm_execute.h:3334:4
#5 0x55e8333140bb in fuzzer_execute_ex /src/php-src/sapi/fuzzer/fuzzer-execute-common.h:65:12
#6 0x55e832e3813d in zend_execute /src/php-src/Zend/zend_vm_execute.h:115989:2
#7 0x55e8333153bf in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:293:5
#8 0x55e833313954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2
[..]
DEDUP_TOKEN: zend_gc_delref--zval_delref_p--zval_ptr_dtor_nogc
SUMMARY: AddressSanitizer: SEGV /src/php-src/Zend/zend_types.h:835:2 in zend_gc_delref
==14==ABORTING
```
</details>
<details>
<summary>ASAN backtrace for the <code>??</code> / ZEND_COALESCE variant</summary>
```
/out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each.
Running: /testcase
AddressSanitizer:DEADLYSIGNAL
=================================================================
==14==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x55eb3a830ebf bp 0x7ffd7f0cf250 sp 0x7ffd7f0cf1b0 T0)
==14==The signal is caused by a READ memory access.
==14==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used.
SCARINESS: 20 (wild-addr-read)
#0 0x55eb3a830ebf in zend_gc_delref /src/php-src/Zend/zend_types.h:835:2
#1 0x55eb3a830ebf in zval_delref_p /src/php-src/Zend/zend_types.h:1406:9
#2 0x55eb3a830ebf in zval_ptr_dtor_nogc /src/php-src/Zend/zend_variables.h:34:35
#3 0x55eb3a830ebf in cleanup_live_vars /src/php-src/Zend/zend_execute.c:4948:6
#4 0x55eb3ab70b00 in zend_dispatch_try_catch_finally_helper_SPEC /src/php-src/Zend/zend_vm_execute.h:3334:4
#5 0x55eb3ad140bb in fuzzer_execute_ex /src/php-src/sapi/fuzzer/fuzzer-execute-common.h:65:12
#6 0x55eb3a83813d in zend_execute /src/php-src/Zend/zend_vm_execute.h:115989:2
#7 0x55eb3ad153bf in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:293:5
#8 0x55eb3ad13954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2
[..]
DEDUP_TOKEN: zend_gc_delref--zval_delref_p--zval_ptr_dtor_nogc
SUMMARY: AddressSanitizer: SEGV /src/php-src/Zend/zend_types.h:835:2 in zend_gc_delref
==14==ABORTING
```
</details>
`ZEND_JMP_SET` (`?:`) and `ZEND_COALESCE` (`??`) both write their result *only* on the branch they take. `zend_calc_live_ranges()` started their live range right behind the definition, so the range also covered the fall-through path, on which the temporary was never written at all.
Normally this is harmless because another opcode redefines the result on the fall-through path, and the backward walk stops at that later definition, hiding the bogus range. Once the optimizer removes that redefinition the range shows up. In the reproducer the `QM_ASSIGN` of the else branch is gone because the `match` always throws, so unwinding the `UnhandledMatchError` makes `cleanup_live_vars()` destroy uninitialized stack memory.
The fix starts the live range at the jump target instead of behind the definition. On the branch that is taken control transfers there directly, so that is exactly the region in which the result is defined. If the result is freed right at the jump target the range is empty and no range is emitted at all.
Thanks!
---
_Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses._