-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
Description
With this PHP code I get a segfault when tracing JIT is enabled:
<?php
function process($data) {
foreach ($data as &$v) {}
}
$data = [
(object) ["" => 1],
(object) ["" => 1],
(object) [],
];
for ($i = 0; $i < 200; $i += 1) {
foreach ($data as $entry) {
process($entry);
}
}Sometimes I had to run it twice or three times in a row to get it to happen.
After quite a few hours spent in gdb, I think I have a bit of an idea of what's going on. This is the first time I've looked at PHP internals, so it's possible I've misunderstood some of this.
The segfault happens in zend_objects_store_del called from generated code for the return of the process function.
#0 zend_objects_store_del (object=0x5555577c6b00) at /home/flo/Projects/php-src/Zend/zend_objects_API.c:178
#1 0x0000555515400e1a in TRACE-5$process$4 () at unknown:1
#2 0x0000555555e8d9cf in zend_execute (op_array=0xdb11980b754e6600, return_value=0x7fffffff7af0) at /home/flo/Projects/php-src/Zend/zend_vm_execute.h:121924
Backtrace stopped: frame did not save the PC
zend_objects_store_del accepts a pointer to a zend_object, but a pointer to a zend_reference is provided.
The opcode dump for the process function looks like this:
process:
; (lines=6, args=1, vars=2, tmps=1)
; (after optimizer)
; /home/flo/Projects/php-src/repro.php:3-5
0000 CV0($data) = RECV 1
0001 V2 = FE_RESET_RW CV0($data) 0004
0002 FE_FETCH_RW V2 CV1($v) 0004
0003 JMP 0002
0004 FE_FREE V2
0005 RETURN null
LIVE RANGES:
2: 0002 - 0004 (loop)
Initially $data is provided as an object, but FE_RESET_RW turns it into a reference.
The problem then appears in trace 5:
---- TRACE 5 start (side trace 3/5) process() /home/flo/Projects/php-src/repro.php:4
0004 FE_FREE V2 ; op1(&object of class stdClass)
0005 RETURN null
---- TRACE 5 stop (return)
---- TRACE 5 TSSA start (side trace 3/5) process() /home/flo/Projects/php-src/repro.php:4
;#2.X2 [ref, rc1, rcn, any]
0004 FE_FREE #2.V2 [ref, rc1, rcn, any] ; op1(&object of class stdClass)
0005 RETURN null
---- TRACE 5 TSSA stop (return)
I appears to happen when compiling the RETURN.
This is the relevant stack where it gets generated:
#0 jit_ZVAL_DTOR (jit=0x7ffffffeaf20, ref=63, op_info=3221225728, opline=0x0) at ext/opcache/jit/zend_jit_ir.c:1808
#1 0x00005555559b1ea4 in jit_ZVAL_PTR_DTOR (jit=0x7ffffffeaf20, addr=20537, op_info=3221225728, gc=true, opline=0x0) at ext/opcache/jit/zend_jit_ir.c:1845
#2 0x00005555559d4cc4 in zend_jit_free_cv (jit=0x7ffffffeaf20, info=3221225728, var=0) at ext/opcache/jit/zend_jit_ir.c:11022
#3 0x0000555555a1e6b6 in zend_jit_trace (trace_buffer=0x7fffffff3620, parent_trace=3, exit_num=5) at ext/opcache/jit/zend_jit_trace.c:5625
#4 0x0000555555a30728 in zend_jit_compile_side_trace (trace_buffer=0x7fffffff3620, parent_num=3, exit_num=5, polymorphism=0) at ext/opcache/jit/zend_jit_trace.c:8371
#5 0x0000555555a31760 in zend_jit_trace_hot_side (execute_data=0x55555776a5e0, parent_num=3, exit_num=5) at ext/opcache/jit/zend_jit_trace.c:8616
#6 0x0000555555a32e3d in zend_jit_trace_exit (exit_num=5, regs=0x7fffffff7790) at ext/opcache/jit/zend_jit_trace.c:8894
#7 0x0000555515400569 in JIT$$trace_exit () at unknown:1
jit_ZVAL_DTOR uses has_concrete_type and concrete_type on (op_info) & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE).
This does not include MAY_BE_REF, which seemed to me to be the problem at one point.
Interestingly the MAY_BE_REF flag is not actually set there, so that can't be the main problem.
(gdb) print op_info & (1 << 10)
$2 = 0
(gdb) print op_info & (1 << 8)
$3 = 256
op_info gets generated earlier in zend_jit_trace_build_tssa.
In ext/opcache/jit/zend_jit_trace.c:1766 the type gets set based on the parent trace, which in this case is trace 3.
---- TRACE 3 start (loop) $main() /home/flo/Projects/php-src/repro.php:14
0010 FE_FETCH_R V3 CV2($entry) 0015 ; op1(packed array) op2(object of class stdClass)
0011 INIT_FCALL 1 128 string("process")
>init process
0012 SEND_VAR CV2($entry) 1 ; op1(object of class stdClass)
0013 DO_UCALL
>enter process
0001 V2 = FE_RESET_RW CV0($data) 0004 ; op1(object of class stdClass)
---- TRACE 3 stop (link to 1)
I'm not sure if the type inferred in trace 3 is wrong since it only becomes a reference in FE_RESET_RW.
It's also possible that there is missing handling for FE_RESET_RW in the switch here.
Maybe it should set the MAY_BE_REF flag on there when processing trace 3?
I also haven't figured out yet what the difference between type and mem_type is.
That might also be relevant.
I hope some of this helpful in figuring out the actual cause.
Configuration
I used variations of this php.ini:
opcache.enable=true
opcache.enable_cli=true
# CRTO. Happens with any setting here where T = 5 (tracing) and O >= 2.
opcache.jit=0052
opcache.jit_buffer_size=1024M
opcache.jit_debug=4294967295
opcache.opt_debug_level=0x20000
I was able to reproduce this problem with the following versions (all on x86_64 Linux):
- The latest master built from source
- PHP 8.4.15 on NixOS
- PHP 8.3.28 from the Remi RPM Repository on AlmaLinux 9
- PHP 8.3.28 built from source
- PHP 8.2.29 on NixOS
- PHP 8.1.32 on NixOS
- PHP 8.0.29 on NixOS
Since I could reproduce it in all those cases I didn't include the full --version output for all of them.
Security Considerations
As far as I can tell this is not exploitable.
On my machine the offset of handlers on zend_object corresponds to sources on zend_reference, which seems to always be null in this case.
If not the dtor field on zend_object_handlers corresponds to doc_comment on zend_property_info which points to a zend_string, which shouldn't point to executable code and should thus also segfault.
On 32 bit systems handlers should correspond to u2 on the zval of the reference (I hope my offset math is right) and I don't see any possible way an attacker could control that to become a valid pointer.
PHP Version
PHP 8.4.15 (cli) (built: Nov 18 2025 17:26:05) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.15, Copyright (c) Zend Technologies
with Zend OPcache v8.4.15, Copyright (c), by Zend Technologies
Operating System
No response