Skip to content

Fix live ranges for ZEND_JMP_SET and ZEND_COALESCE - #23507

Open
Mrmaxmeier wants to merge 1 commit into
php:PHP-8.4from
Mrmaxmeier:fix-live-range-jmp-set-coalesce
Open

Fix live ranges for ZEND_JMP_SET and ZEND_COALESCE#23507
Mrmaxmeier wants to merge 1 commit into
php:PHP-8.4from
Mrmaxmeier:fix-live-range-jmp-set-coalesce

Conversation

@Mrmaxmeier

@Mrmaxmeier Mrmaxmeier commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Hi,

we ran into a use of an uninitialized value bug in the fuzzer-function-jit fuzzing target:

<?php
try {
    $a ?: match (true) { 1 => 1 };
} catch (UnhandledMatchError $e) {
}

and the ?? variant:

<?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.

ASAN backtrace for reproducer (?: / ZEND_JMP_SET)
/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
ASAN backtrace for the ?? / ZEND_COALESCE variant
/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

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.

@Mrmaxmeier
Mrmaxmeier requested a review from dstogov as a code owner August 29, 2026 14:47
Both opcodes write their result only on the branch they take. zend_calc_live_ranges()
started their live range right behind the definition, so it also covered the
fall-through path, on which the temporary was never written.

Normally another opcode redefines the result on the fall-through path, which
hides the problem because the backward walk stops at that later definition. Once
the optimizer has removed it the bogus range shows up:

    try {
        $a ?: match (true) { 1 => 1 };
    } catch (UnhandledMatchError $e) {
    }

Here the QM_ASSIGN of the else branch is gone because the match always throws,
and unwinding the UnhandledMatchError makes cleanup_live_vars() destroy
uninitialized stack memory. With ZEND_COALESCE the temporary slot may be shared
with a preceding opcode whose value has already been freed, which turns this
into a use after free.

Start the live range at the jump target instead. On the branch that is taken
control transfers there directly, so that is exactly the region in which the
result is defined.

Assisted-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +19 to +30
} catch (UnhandledMatchError $e) {
echo $e->getMessage(), "\n";
}

function coalesce($a, $b) {
return $a ?? $b;
}
var_dump(coalesce("x", "y"), coalesce(null, "y"));
echo "OK\n";
?>
--EXPECT--
Unhandled match case true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (UnhandledMatchError $e) {
echo $e->getMessage(), "\n";
}
function coalesce($a, $b) {
return $a ?? $b;
}
var_dump(coalesce("x", "y"), coalesce(null, "y"));
echo "OK\n";
?>
--EXPECT--
Unhandled match case true
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
function coalesce($a, $b) {
return $a ?? $b;
}
var_dump(coalesce("x", "y"), coalesce(null, "y"));
echo "OK\n";
?>
--EXPECT--
UnhandledMatchError: Unhandled match case true

Ref: https://github.com/php/php-src/blob/master/CODING_STANDARDS.md#testing

Comment on lines +16 to +28
} catch (UnhandledMatchError $e) {
echo $e->getMessage(), "\n";
}

function elvis($a, $b) {
return $a ?: $b;
}
var_dump(elvis("x", "y"), elvis("", "y"), elvis(null, "z"));
echo "OK\n";
?>
--EXPECTF--
Warning: Undefined variable $a in %s on line %d
Unhandled match case true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (UnhandledMatchError $e) {
echo $e->getMessage(), "\n";
}
function elvis($a, $b) {
return $a ?: $b;
}
var_dump(elvis("x", "y"), elvis("", "y"), elvis(null, "z"));
echo "OK\n";
?>
--EXPECTF--
Warning: Undefined variable $a in %s on line %d
Unhandled match case true
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
function elvis($a, $b) {
return $a ?: $b;
}
var_dump(elvis("x", "y"), elvis("", "y"), elvis(null, "z"));
echo "OK\n";
?>
--EXPECTF--
Warning: Undefined variable $a in %s on line %d
UnhandledMatchError: Unhandled match case true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants