Skip to content

Fix block pass dropping ZEND_FAST_CALL OP2 definition - #23508

Open
Mrmaxmeier wants to merge 1 commit into
php:PHP-8.4from
Mrmaxmeier:fix-block-pass-fast-call-op2
Open

Fix block pass dropping ZEND_FAST_CALL OP2 definition#23508
Mrmaxmeier wants to merge 1 commit into
php:PHP-8.4from
Mrmaxmeier:fix-block-pass-fast-call-op2

Conversation

@Mrmaxmeier

Copy link
Copy Markdown
Contributor

Hi,

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

<?php
function test() {
    $x = 1;
    try {
        try {
            return true ? "returned" : "other";
        } finally {
            undef_fn();
        }
    } catch (Error $e) {
        echo $e->getMessage(), "\n";
    }
    return "fallback";
}

var_dump(test());

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
/out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each.
Running: /testcase
AddressSanitizer:DEADLYSIGNAL
=================================================================
==14==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x55b7732f8d13 bp 0x7ffd18a57e80 sp 0x7ffd18a57e60 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 0x55b7732f8d13 in zend_gc_delref /src/php-src/Zend/zend_types.h:835:2
    #1 0x55b7732f8d13 in i_zval_ptr_dtor /src/php-src/Zend/zend_variables.h:43:8
    #2 0x55b7732f8d13 in zval_ptr_dtor /src/php-src/Zend/zend_variables.c:83:2
    #3 0x55b773170d59 in zend_dispatch_try_catch_finally_helper_SPEC /src/php-src/Zend/zend_vm_execute.h:3359:5
    #4 0x55b7733140bb in fuzzer_execute_ex /src/php-src/sapi/fuzzer/fuzzer-execute-common.h:65:12
    #5 0x55b772ecc006 in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER /src/php-src/Zend/zend_vm_execute.h:2131:4
    #6 0x55b7733140bb in fuzzer_execute_ex /src/php-src/sapi/fuzzer/fuzzer-execute-common.h:65:12
    #7 0x55b772e3813d in zend_execute /src/php-src/Zend/zend_vm_execute.h:115989:2
    #8 0x55b7733153bf in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:293:5
    #9 0x55b773313954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2
    [..]

DEDUP_TOKEN: zend_gc_delref--i_zval_ptr_dtor--zval_ptr_dtor
SUMMARY: AddressSanitizer: SEGV /src/php-src/Zend/zend_types.h:835:2 in zend_gc_delref
==14==ABORTING

When a return crosses a finally block, PHP first stores the return value in a temporary. Both the ZEND_FAST_CALL that enters the finally block and the later ZEND_RETURN refer to that temporary. The OP2 reference on ZEND_FAST_CALL is needed during exception unwinding: if the finally block throws, the VM uses it to find and destroy the pending return value.

The block pass did not account for this extra use. After propagating the value into ZEND_RETURN, it considered the QM_ASSIGN that initialized the temporary dead and removed it. This left the optimized op_array with

FAST_CALL finally T3

but no opcode that initialized T3. If the finally block then threw, exception unwinding attempted to destroy the uninitialized temporary.

The fix marks the OP2 temporary as ineligible for this source-removal optimization. This uses the same mechanism already used for operands that must remain available to ZEND_FETCH_LIST_R and similar opcodes.

Thanks!


Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses.

For a return from inside a try block that has a finally, ZEND_FAST_CALL names
the temporary holding the pending return value in its OP2. That operand is a
real use: zend_dispatch_try_catch_finally_helper() destroys the value through
it when the finally block throws, and the RETURN behind the FAST_CALL consumes
it afterwards.

The block pass tracked only a single source per temporary and treated the
RETURN as its sole use, so constant propagation removed the QM_ASSIGN that
defines it:

    $x = 1;
    try {
        try {
            return true ? 1.5 : 2.5;
        } finally {
            undef_fn();
        }
    } catch (Error $e) {
        echo $e->getMessage(), "\n";
    }

The optimized op_array kept "FAST_CALL finally T3" with T3 no longer defined
anywhere, and unwinding through the finally destroyed an uninitialized
temporary.

Clear the recorded source for the OP2 operand of ZEND_FAST_CALL, the same way
the pass already does for the operands that ZEND_FETCH_LIST_R and friends keep
alive.

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

function test_refcounted() {
$x = 1;
try {
try {
return true ? "returned" : "other";
} finally {
undef_fn();
}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
return "fallback";
}

var_dump(test_scalar());
var_dump(test_refcounted());
?>
--EXPECT--
Call to undefined function undef_fn()
string(8) "fallback"
Call to undefined function undef_fn()

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 (Error $e) {
echo $e->getMessage(), "\n";
}
return "fallback";
}
function test_refcounted() {
$x = 1;
try {
try {
return true ? "returned" : "other";
} finally {
undef_fn();
}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
return "fallback";
}
var_dump(test_scalar());
var_dump(test_refcounted());
?>
--EXPECT--
Call to undefined function undef_fn()
string(8) "fallback"
Call to undefined function undef_fn()
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
return "fallback";
}
function test_refcounted() {
$x = 1;
try {
try {
return true ? "returned" : "other";
} finally {
undef_fn();
}
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
return "fallback";
}
var_dump(test_scalar());
var_dump(test_refcounted());
?>
--EXPECT--
Error: Call to undefined function undef_fn()
string(8) "fallback"
Error: Call to undefined function undef_fn()

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

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