Skip to content

Commit a7a96df

Browse files
committed
JIT: Propagate trace too long from zend_jit_trace_record_fake_init_call()
zend_jit_trace_record_fake_init_call() silently truncated its recording when the trace buffer became full: TRACE_RECORD() broke out of the loop and the index, already past the limit, was returned as a success. Report the failure as -(int)ZEND_JIT_TRACE_STOP_TOO_LONG instead. The callers did check for a negative result, but mapped it to ZEND_JIT_TRACE_STOP_BAD_FUNC, which dates back to the time when -1 meant "this pending call can't be traced" (those returns were replaced by "continue recording" in 097edc8). BAD_FUNC is a successful stop reason, so the truncated recording (a start record, a partial fake init call sequence and an end record) is handed to the trace compiler; at trace start this produces a trace that loops forever. Propagate the reported stop reason instead, so that the incomplete recording is discarded as too long. zend_jit_trace_subtrace() now reports its own failure the same way instead of returning a bare -1, which would have been propagated as ZEND_JIT_TRACE_STOP_RECURSIVE_CALL.
1 parent 1053403 commit a7a96df

4 files changed

Lines changed: 139 additions & 8 deletions

File tree

ext/opcache/jit/zend_jit_vm_helpers.c

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ static uint8_t zend_jit_trace_bad_stop_event(const zend_op *opline, int count)
628628

629629
static int zend_jit_trace_record_fake_init_call_ex(zend_execute_data *call, zend_jit_trace_rec *trace_buffer, int idx, uint32_t is_megamorphic, uint32_t init_level)
630630
{
631-
zend_jit_trace_stop stop ZEND_ATTRIBUTE_UNUSED = ZEND_JIT_TRACE_STOP_ERROR;
631+
zend_jit_trace_stop stop = ZEND_JIT_TRACE_STOP_ERROR;
632632

633633
do {
634634
zend_function *func;
@@ -671,15 +671,21 @@ static int zend_jit_trace_record_fake_init_call_ex(zend_execute_data *call, zend
671671
ZEND_ADD_CALL_FLAG(call, ZEND_CALL_MEGAMORPHIC);
672672
}
673673
TRACE_RECORD(ZEND_JIT_TRACE_INIT_CALL, ZEND_JIT_TRACE_FAKE_INFO(init_level), func);
674+
675+
return idx;
674676
} while (0);
675-
return idx;
677+
678+
/* TRACE_RECORD() may jump here */
679+
return -(int)stop;
676680
}
677681

682+
/* Returns the new trace buffer index, or -(int)zend_jit_trace_stop on failure */
678683
static int zend_jit_trace_record_fake_init_call(zend_execute_data *call, zend_jit_trace_rec *trace_buffer, int idx, uint32_t is_megamorphic)
679684
{
680685
return zend_jit_trace_record_fake_init_call_ex(call, trace_buffer, idx, is_megamorphic, 0);
681686
}
682687

688+
/* Returns the new trace buffer index, or -(int)zend_jit_trace_stop on failure */
683689
static int zend_jit_trace_subtrace(zend_execute_data *call, zend_jit_trace_rec *trace_buffer, int start, int end, uint8_t event, const zend_op_array *op_array, const zend_op *opline)
684690
{
685691
int idx;
@@ -692,7 +698,7 @@ static int zend_jit_trace_subtrace(zend_execute_data *call, zend_jit_trace_rec *
692698
}
693699
}
694700
if (idx + (end - start) >= JIT_G(max_trace_length) - 2) {
695-
return -1;
701+
return -(int)ZEND_JIT_TRACE_STOP_TOO_LONG;
696702
}
697703
memmove(trace_buffer + idx, trace_buffer + start, (end - start) * sizeof(zend_jit_trace_rec));
698704
return idx + (end - start);
@@ -810,12 +816,15 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
810816
if (prev_call) {
811817
int ret = zend_jit_trace_record_fake_init_call(prev_call, trace_buffer, idx, is_megamorphic);
812818
if (ret < 0) {
813-
TRACE_END(ZEND_JIT_TRACE_END, ZEND_JIT_TRACE_STOP_BAD_FUNC, opline);
819+
/* The recorded prefix is incomplete (some pending calls are
820+
* missing), so it must not be compiled. */
821+
stop = (zend_jit_trace_stop)-ret;
822+
TRACE_END(ZEND_JIT_TRACE_END, stop, opline);
814823
#ifdef HAVE_GCC_GLOBAL_REGS
815824
execute_data = save_execute_data;
816825
opline = save_opline;
817826
#endif
818-
return ZEND_JIT_TRACE_STOP_BAD_FUNC;
827+
return stop;
819828
}
820829
idx = ret;
821830
}
@@ -1199,7 +1208,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
11991208
if (prev_call) {
12001209
int ret = zend_jit_trace_record_fake_init_call(prev_call, trace_buffer, idx, 0);
12011210
if (ret < 0) {
1202-
stop = ZEND_JIT_TRACE_STOP_BAD_FUNC;
1211+
stop = (zend_jit_trace_stop)-ret;
12031212
break;
12041213
}
12051214
idx = ret;
@@ -1227,7 +1236,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
12271236
if (prev_call) {
12281237
int ret = zend_jit_trace_record_fake_init_call(prev_call, trace_buffer, idx, 0);
12291238
if (ret < 0) {
1230-
stop = ZEND_JIT_TRACE_STOP_BAD_FUNC;
1239+
stop = (zend_jit_trace_stop)-ret;
12311240
break;
12321241
}
12331242
idx = ret;
@@ -1364,7 +1373,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
13641373
int ret = zend_jit_trace_subtrace(EX(call), trace_buffer,
13651374
last_loop, idx, ZEND_JIT_TRACE_START_LOOP, op_array, opline);
13661375
if (ret < 0) {
1367-
stop = ZEND_JIT_TRACE_STOP_TOO_LONG;
1376+
stop = (zend_jit_trace_stop)-ret;
13681377
break;
13691378
}
13701379
idx = ret;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
JIT: trace buffer overflow while recording fake init calls on recursive return
3+
--INI--
4+
opcache.jit_max_trace_length=19
5+
--FILE--
6+
<?php
7+
8+
function id($x) { return $x; }
9+
10+
/* The return trace started after the recursive call of rec() unrolls one
11+
* return. At that point the parent rec() frame still has the two id() calls
12+
* under construction, and recording a fake init call for them overflows the
13+
* trace buffer. */
14+
function rec($n) {
15+
if ($n <= 0) {
16+
return 0;
17+
}
18+
return id(id(rec($n - 1)));
19+
}
20+
21+
$s = 0;
22+
for ($i = 0; $i < 50; $i++) {
23+
$s += rec(10);
24+
}
25+
26+
var_dump($s);
27+
28+
?>
29+
--EXPECT--
30+
int(0)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
JIT: trace buffer overflow while recording fake init calls on side trace return
3+
--INI--
4+
opcache.jit_max_trace_length=55
5+
--FILE--
6+
<?php
7+
8+
function id($x) { return $x; }
9+
10+
/* The side trace started at the guard of $a[$k] records the (rarely taken)
11+
* long branch and returns from g() into outer(), where the two id() calls are
12+
* still under construction. Recording a fake init call for them overflows the
13+
* trace buffer at that point. */
14+
function g(array $a, int $k) {
15+
$x = $a[$k];
16+
if ($x < 0) {
17+
$x = $x + 1;
18+
$x = $x + 2;
19+
$x = $x + 3;
20+
$x = $x + 4;
21+
$x = $x + 5;
22+
$x = $x + 6;
23+
$x = $x + 7;
24+
$x = $x + 8;
25+
$x = $x + 9;
26+
$x = $x + 10;
27+
$x = $x + 11;
28+
$x = $x + 12;
29+
$x = $x + 13;
30+
$x = $x + 14;
31+
$x = $x + 15;
32+
$x = $x + 16;
33+
$x = $x + 17;
34+
$x = $x + 18;
35+
$x = $x + 19;
36+
$x = $x + 20;
37+
$x = $x + 21;
38+
$x = $x + 22;
39+
$x = $x + 23;
40+
$x = $x + 24;
41+
}
42+
return 1;
43+
}
44+
45+
function outer(array $a, int $k) { return id(id(g($a, $k))); }
46+
47+
function driver(array $a) {
48+
$s = 0;
49+
for ($i = 0; $i < 300; $i++) {
50+
$s += outer($a, $i % 11);
51+
}
52+
return $s;
53+
}
54+
55+
var_dump(driver([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1]));
56+
57+
?>
58+
--EXPECT--
59+
int(300)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
JIT: trace buffer overflow while recording fake init calls at trace start
3+
--INI--
4+
opcache.jit_max_trace_length=8
5+
--FILE--
6+
<?php
7+
8+
class C { static function f($x) { return $x + 1; } }
9+
10+
function sink(array $a) { return array_sum($a); }
11+
function id($x) { return $x; }
12+
13+
/* array_map() is compiled to a foreach loop, so the loop header sits between
14+
* the INIT_FCALL of sink()/id() and their DO_UCALL. A root loop trace started
15+
* there has to record a fake init call for each of the 6 pending calls, which
16+
* does not fit into a trace buffer limited to 8 records. The recording must be
17+
* aborted; the partially recorded trace must not be compiled. */
18+
function test(array $a, $o) {
19+
return sink(id(id(id(id(id(array_map($o::f(...), $a)))))));
20+
}
21+
22+
$a = [1, 2, 3];
23+
$o = new C();
24+
25+
for ($i = 0; $i < 5; $i++) {
26+
$r = test($a, $o);
27+
}
28+
29+
var_dump($r);
30+
31+
?>
32+
--EXPECT--
33+
int(9)

0 commit comments

Comments
 (0)