Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ PHP NEWS
. Fixed use-after-free when the error handler releases a string while an
"Illegal string offset" warning is raised for its offset in a read, ?? or
nested isset()/empty(). (Jorg Sowa)
. Fixed partial function applications in eval'd or included code, or in
class constants, reusing a stale cached function with wrong bound
arguments. (Jorg Sowa)
. Fixed anonymous class names being truncated at the NUL byte in error
messages, the error log, and HTML and XML-RPC error output. (Jorg Sowa)
. Fixed AST printing dropping readonly on promoted constructor parameters.
Expand All @@ -34,6 +37,9 @@ PHP NEWS
- Curl:
. Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)

- Date:
. Update timelib to 2026.03 (Derick)

- DBA:
. Fixed dba_close() not closing a database opened with dba_popen().
(Ilia Alshanetsky)
Expand Down Expand Up @@ -127,19 +133,6 @@ PHP NEWS
. Fix potential ABI incompatibility when adding fields in patches by adding
padding to globals. (ilutov)

- Date:
. Update timelib to 2026.03 (Derick)

- Fileinfo:
. Fixed bug GH-25051 (TrueType fonts detected as application/octet-stream
since libmagic 5.48). (Weilin Du)
. Fixed incorrect MIME type detection for HWPX files. (Weilin Du)

- Streams:
. Fixed use-after-free of the previous request's default stream context when
a stream error is raised during request startup, such as when a POST body
cannot be buffered to a temporary file. (Go Kudo)

05 Nov 2026, PHP 8.6.0RC5

- Core:
Expand Down
37 changes: 37 additions & 0 deletions Zend/tests/partial_application/cache_freed_declaring_site_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
PFA in class constant: request cache must not outlive the freed declaring AST
--FILE--
<?php
function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }

$n = 0;
for ($round = 0; $round < 2; $round++) {
for ($argc = 1; $argc <= 7; $argc++) {
for ($mask = 1; $mask < (1 << $argc); $mask++) {
$args = [];
$expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
$placeholders = 0;
for ($i = 0; $i < $argc; $i++) {
if ($mask & (1 << $i)) {
$args[] = '?';
$expected[$i] = ++$placeholders;
} else {
$args[] = "'v$i'";
$expected[$i] = "v$i";
}
}
$shape = 'f(' . implode(', ', $args) . ')';
$n++;
eval("class C$n { const X = $shape; }");
$pfa = constant("C$n::X");
$result = $pfa(...range(1, $placeholders));
if ($result !== implode('|', $expected)) {
echo "$shape: got $result\n";
}
}
}
}
echo "Done\n";
?>
--EXPECT--
Done
36 changes: 36 additions & 0 deletions Zend/tests/partial_application/cache_freed_declaring_site_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
PFA in eval(): request cache must not outlive the freed declaring op_array
--FILE--
<?php
function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }

$n = 0;
for ($round = 0; $round < 2; $round++) {
for ($argc = 1; $argc <= 7; $argc++) {
for ($mask = 1; $mask < (1 << $argc); $mask++) {
$args = [];
$expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
$placeholders = 0;
for ($i = 0; $i < $argc; $i++) {
if ($mask & (1 << $i)) {
$args[] = '?';
$expected[$i] = ++$placeholders;
} else {
$args[] = "'v$i'";
$expected[$i] = "v$i";
}
}
$shape = 'f(' . implode(', ', $args) . ')';
$n++;
$pfa = eval("return $shape;");
$result = $pfa(...range(1, $placeholders));
if ($result !== implode('|', $expected)) {
echo "$shape: got $result\n";
}
}
}
}
echo "Done\n";
?>
--EXPECT--
Done
37 changes: 37 additions & 0 deletions Zend/tests/partial_application/cache_freed_declaring_site_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
PFA in constant declared in eval(): request cache must not outlive the freed declaring AST
--FILE--
<?php
function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }

$n = 0;
for ($round = 0; $round < 2; $round++) {
for ($argc = 1; $argc <= 7; $argc++) {
for ($mask = 1; $mask < (1 << $argc); $mask++) {
$args = [];
$expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
$placeholders = 0;
for ($i = 0; $i < $argc; $i++) {
if ($mask & (1 << $i)) {
$args[] = '?';
$expected[$i] = ++$placeholders;
} else {
$args[] = "'v$i'";
$expected[$i] = "v$i";
}
}
$shape = 'f(' . implode(', ', $args) . ')';
$n++;
eval("const K$n = $shape;");
$pfa = constant("K$n");
$result = $pfa(...range(1, $placeholders));
if ($result !== implode('|', $expected)) {
echo "$shape: got $result\n";
}
}
}
}
echo "Done\n";
?>
--EXPECT--
Done
16 changes: 16 additions & 0 deletions Zend/tests/partial_application/cache_freed_declaring_site_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
PFAs in constants declared on the same line must not share a cache entry
--FILE--
<?php
function f($a, $b) { return "$a|$b"; }

class C { const X = f(?, 'x'), Y = f('y', ?); }
const K1 = f(?, 'k1'), K2 = f('k2', ?);

var_dump((C::X)(1), (C::Y)(2), (K1)(3), (K2)(4));
?>
--EXPECT--
string(3) "1|x"
string(3) "y|2"
string(4) "3|k1"
string(4) "k2|4"
42 changes: 42 additions & 0 deletions Zend/tests/partial_application/cache_freed_declaring_site_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
PFA in included file: request cache must not outlive the freed declaring op_array
--INI--
opcache.enable_cli=0
--FILE--
<?php
function f($a = 'A', $b = 'B', $c = 'C', $d = 'D') { return "$a|$b|$c|$d"; }

$file = __DIR__ . '/cache_freed_declaring_site_005.inc';
for ($round = 0; $round < 2; $round++) {
for ($argc = 1; $argc <= 4; $argc++) {
for ($mask = 1; $mask < (1 << $argc); $mask++) {
$args = [];
$expected = ['A', 'B', 'C', 'D'];
$placeholders = 0;
for ($i = 0; $i < $argc; $i++) {
if ($mask & (1 << $i)) {
$args[] = '?';
$expected[$i] = ++$placeholders;
} else {
$args[] = "'v$i'";
$expected[$i] = "v$i";
}
}
$shape = 'f(' . implode(', ', $args) . ')';
file_put_contents($file, "<?php return $shape;");
$pfa = include $file;
$result = $pfa(...range(1, $placeholders));
if ($result !== implode('|', $expected)) {
echo "$shape: got $result\n";
}
}
}
}
echo "Done\n";
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/cache_freed_declaring_site_005.inc');
?>
--EXPECT--
Done
8 changes: 7 additions & 1 deletion Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -1361,10 +1361,16 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
if (uses_variadic_placeholder) {
flags |= ZEND_PARTIAL_USES_VARIADIC_PLACEHOLDER;
}
/* SHM names are shared by same-line PFAs, so they can't be a cache key */
ZEND_ASSERT((flags & ZEND_PARTIAL_CACHEABLE_IN_SHM)
|| !(GC_FLAGS(fcc_ast->name) & IS_STR_PERMANENT));
/* A non-SHM AST may be freed, but the cached PFA keeps its name alive */
const uint32_t *cache_key = (flags & ZEND_PARTIAL_CACHEABLE_IN_SHM)
? &ast->lineno : (const uint32_t *) fcc_ast->name;
zend_partial_create(result, scope, &frame->This, fptr,
ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1),
extra_named_params, named_positions,
fcc_ast->filename, &ast->lineno,
fcc_ast->filename, cache_key, /* declaring_op_array */ NULL,
(void**)cache_slot, fcc_ast->name, flags, /* const_args */ 0);

if (named_positions) {
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -12292,6 +12292,7 @@ static void zend_compile_const_expr_fcc(zend_ast **ast_ptr)

if (!is_fcc) {
fcc->filename = zend_string_copy(CG(active_op_array)->filename);
/* Not unique by content, but its address is a cache key until persisted */
fcc->name = zend_compile_partial_name(CG(active_op_array), fcc->lineno);
}

Expand Down
16 changes: 10 additions & 6 deletions Zend/zend_partial.c
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,8 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
const uint32_t *declaring_lineno_ptr, void **cache_slot,
const uint32_t *declaring_lineno_ptr,
const zend_op_array *declaring_op_array, void **cache_slot,
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

zend_op_array *op_array = NULL;
Expand Down Expand Up @@ -1016,7 +1017,8 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,

/* Takes ownership of closure_ast */
op_array = zend_accel_compile_pfa(closure_ast, declaring_filename,
declaring_lineno_ptr, function, pfa_name, flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);
declaring_lineno_ptr, declaring_op_array, function, pfa_name,
flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);

clean:
zp_names_dtor(var_names, argc);
Expand All @@ -1040,7 +1042,8 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
const uint32_t *declaring_lineno_ptr, void **cache_slot,
const uint32_t *declaring_lineno_ptr,
const zend_op_array *declaring_op_array, void **cache_slot,
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
Expand All @@ -1056,7 +1059,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
if (UNEXPECTED(!op_array)) {
op_array = zp_compile(this_ptr, function, argc, argv,
extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr,
cache_slot, pfa_name, flags, const_args);
declaring_op_array, cache_slot, pfa_name, flags, const_args);
}

if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
Expand Down Expand Up @@ -1135,15 +1138,16 @@ void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr,
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
const uint32_t *declaring_lineno_ptr, void **cache_slot,
const uint32_t *declaring_lineno_ptr,
const zend_op_array *declaring_op_array, void **cache_slot,
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

ZEND_ASSERT(pfa_name);

const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
extra_named_params, named_positions,
declaring_filename, declaring_lineno_ptr,
cache_slot, pfa_name, flags, const_args);
declaring_op_array, cache_slot, pfa_name, flags, const_args);

if (UNEXPECTED(!op_array)) {
ZEND_ASSERT(EG(exception));
Expand Down
6 changes: 4 additions & 2 deletions Zend/zend_partial.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ BEGIN_EXTERN_C()
/* Create a partial application of 'function'
* 'declaring_lineno_ptr' should be a pointer the zend_op.lineno or
* zend_ast.lineno that declares the PFA. The address is used to build a cache
* key. */
* key, so it must not be reused while cached. 'declaring_op_array' is retained
* for that purpose. */
void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function,
uint32_t argc, zval *argv, zend_array *extra_named_params,
const zend_array *named_positions,
zend_string *declaring_filename,
const uint32_t *declaring_lineno_ptr, void **cache_slot,
const uint32_t *declaring_lineno_ptr,
const zend_op_array *declaring_op_array, void **cache_slot,
zend_string *pfa_name, uint32_t flags, uint32_t const_args);

void zend_partial_op_array_dtor(zval *pDest);
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -9923,7 +9923,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM)
(ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ?
call->extra_named_params : NULL,
OP2_TYPE == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL,
EX(func)->op_array.filename, &opline->lineno, cache_slot,
EX(func)->op_array.filename, &opline->lineno, &EX(func)->op_array, cache_slot,
pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args);

if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) {
Expand Down
8 changes: 4 additions & 4 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading