Skip to content
Open
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
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ PHP NEWS
MariaDB PAM account). (lazerg)

- Opcache:
. Fixed bug GH-23859 (Map pointer growth after OPcache resets under concurrent
traffic). (aasul)
. Fixed tracing JIT returning a wrapped integer instead of a float on
integer overflow after an integer was compared with a float or
numeric-string literal. (Ilia Alshanetsky)
Expand Down
11 changes: 11 additions & 0 deletions ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -4844,6 +4844,8 @@ static zend_result accel_finish_startup_preload(bool in_child)
bool old_reset_signals = SIGG(reset);
#endif

ZCG(preloading) = true;

sapi_module.activate = NULL;
sapi_module.deactivate = NULL;
sapi_module.register_server_variables = NULL;
Expand Down Expand Up @@ -4926,6 +4928,8 @@ static zend_result accel_finish_startup_preload(bool in_child)
sapi_module.ub_write = orig_ub_write;
sapi_module.flush = orig_flush;

ZCG(preloading) = false;

sapi_activate();

return ret;
Expand Down Expand Up @@ -5066,6 +5070,13 @@ static zend_result accel_finish_startup(void)
}

static void accel_activate(void) {
/* zend_post_startup() saves the permanent boundary after preloading.
* Discard only request slots before synchronizing with SHM for this request.
* The permanent boundary is not available during preloading itself. */
if (!ZCG(preloading)) {
zend_map_ptr_reset();
Comment thread
Copilot marked this conversation as resolved.
Resolved
}

if (ZCG(preloaded_internal_run_time_cache)) {
memset(ZCG(preloaded_internal_run_time_cache), 0, ZCG(preloaded_internal_run_time_cache_size));
}
Expand Down
1 change: 1 addition & 0 deletions ext/opcache/ZendAccelerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ typedef struct _zend_accel_globals {
* strings by zend_accel_pending_ce_cache_commit() once ZCSG(map_ptr_last)
* covers them. NULL outside a persist window. */
HashTable *pending_ce_cache;
bool preloading;
} zend_accel_globals;

typedef struct _zend_string_table {
Expand Down
149 changes: 149 additions & 0 deletions sapi/fpm/tests/gh23859-preload.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
--TEST--
FPM: GH-23859 (Preloaded map pointers survive compilation and OPcache restarts)
--EXTENSIONS--
opcache
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php

require_once "tester.inc";

$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[worker]
listen = {{ADDR[worker]}}
pm = static
pm.max_children = 1
[reset]
listen = {{ADDR[reset]}}
pm = static
pm.max_children = 1
EOT;

$code = <<<'EOT'
<?php
if (PreloadedChild::$value !== 7 || PreloadedChild::$items !== ['initial']
|| PreloadedChild::step() !== 8 || preloaded_step() !== 13
|| PreloadedEnum::cases() !== [PreloadedEnum::A]
|| (new ReflectionClass(PreloadedMutable::class))->getDefaultProperties() !== ['item' => 11]) {
throw new Exception('Preloaded state was not reset');
}
PreloadedChild::$value = 23;
PreloadedChild::$items[] = 'changed';

/* Persist new code and load a saved preload script after filling preload slots. */
require __DIR__ . '/gh23859-preload.compile.inc';
require __DIR__ . '/gh23859-preload.saved.inc';
if (PreloadedChild::$value !== 23 || PreloadedChild::$items !== ['initial', 'changed']
|| PreloadedChild::step() !== 25 || preloaded_step() !== 15
|| PreloadedChild::compute() !== 13 || RequestChild::run() !== 20
|| request_step() !== 1 || saved_step() !== 1
|| PreloadedEnum::cases() !== [PreloadedEnum::A]) {
throw new Exception('Compilation corrupted preloaded state');
}
echo 'ok';
EOT;

$tester = new FPM\Tester($cfg, $code);
$preloadFile = $tester->getPrefixedFile('preload.inc');
file_put_contents($preloadFile, <<<'EOT'
<?php
trait PreloadedTrait {
public static function step(): int {
static $n = 0;
return ++$n + self::$value;
}
}
class PreloadedBase {
public static int $value = 7;
public const VALUE = 11;
}
class PreloadedChild extends PreloadedBase {
use PreloadedTrait;
public static array $items = ['initial'];
public static function compute(): int {
return self::VALUE + count(self::$items);
}
}
function preloaded_step(): int {
static $n = 0;
return ++$n + PreloadedChild::compute();
}
enum PreloadedEnum { case A; }
trait PreloadedMutable { public $item = PreloadedBase::VALUE; }
opcache_compile_file(__DIR__ . '/gh23859-preload.saved.inc');
PreloadedChild::step();
preloaded_step();
PreloadedChild::$value = 99;
EOT);
file_put_contents($tester->getPrefixedFile('compile.inc'), <<<'EOT'
<?php
class RequestChild extends PreloadedChild {
public static int $other = 19;
public static function run(): int { static $n = 0; return ++$n + self::$other; }
}
function request_step(): int { static $n = 0; return ++$n; }
EOT);
file_put_contents($tester->getPrefixedFile('saved.inc'), <<<'EOT'
<?php
static $n = 0;
if (++$n !== 1) {
throw new Exception('Saved script static variables were not reset');
}
if (!function_exists('saved_step')) {
function saved_step(): int { static $n = 0; return ++$n; }
}
EOT);
$resetFile = $tester->getPrefixedFile('reset.src.php');
file_put_contents($resetFile, <<<'EOT'
<?php
if (isset($_GET['reset'])) {
var_dump(opcache_reset());
} else {
echo opcache_get_status(false)['opcache_statistics']['manual_restarts'];
}
EOT);

$opcache = ini_get('extension_dir') . DIRECTORY_SEPARATOR . 'opcache.' . PHP_SHLIB_SUFFIX;
$extraArgs = is_file($opcache) ? ['-dzend_extension=' . $opcache] : [];
$iniEntries = [
'opcache.enable' => '1',
'opcache.file_update_protection' => '0',
'opcache.preload' => $preloadFile,
];
if (getenv('TEST_FPM_RUN_AS_ROOT')) {
$iniEntries['opcache.preload_user'] = 'root';
}
$tester->start($extraArgs, iniEntries: $iniEntries);
$tester->expectLogStartNotices();
$tester->request(address: '{{ADDR[reset]}}', scriptFilename: $resetFile)->expectBody('0');
$tester->request(address: '{{ADDR[worker]}}')->expectBody('ok');
$tester->request(address: '{{ADDR[worker]}}')->expectBody('ok');

for ($i = 1; $i <= 3; $i++) {
$tester->request(query: 'reset=1', address: '{{ADDR[reset]}}', scriptFilename: $resetFile)
->expectBody('bool(true)');
$tester->request(address: '{{ADDR[reset]}}', scriptFilename: $resetFile)->expectBody((string) $i);
$tester->request(address: '{{ADDR[worker]}}')->expectBody('ok');
$tester->request(address: '{{ADDR[worker]}}')->expectBody('ok');
}

$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();

?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
foreach (['preload', 'compile', 'saved'] as $file) {
@unlink(__DIR__ . '/gh23859-preload.' . $file . '.inc');
}
@unlink(__DIR__ . '/gh23859-preload.reset.src.php');
?>
87 changes: 87 additions & 0 deletions sapi/fpm/tests/gh23859.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--TEST--
FPM: GH-23859 (Map pointers grow after OPcache restarts in another worker)
--EXTENSIONS--
opcache
zend_test
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php

require_once "tester.inc";

$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[worker]
listen = {{ADDR[worker]}}
pm = static
pm.max_children = 1
[reset]
listen = {{ADDR[reset]}}
pm = static
pm.max_children = 1
EOT;

$code = <<<'EOT'
<?php
class Test {
public static function run() {
static $i = 0;
return ++$i;
}
}
if (Test::run() !== 1) {
throw new Exception('Static variables were not reset');
}
echo zend_get_map_ptr_last();
EOT;

$tester = new FPM\Tester($cfg, $code);
$resetFile = $tester->getPrefixedFile('reset.src.php');
file_put_contents($resetFile, <<<'EOT'
<?php
if (isset($_GET['reset'])) {
var_dump(opcache_reset());
} else {
echo opcache_get_status(false)['opcache_statistics']['manual_restarts'];
}
EOT);

$opcache = ini_get('extension_dir') . DIRECTORY_SEPARATOR . 'opcache.' . PHP_SHLIB_SUFFIX;
$extraArgs = is_file($opcache) ? ['-dzend_extension=' . $opcache] : [];
$tester->start($extraArgs, extensions: ['zend_test'], iniEntries: [
'opcache.enable' => '1',
'opcache.file_update_protection' => '0',
]);
$tester->expectLogStartNotices();
$tester->request(address: '{{ADDR[reset]}}', scriptFilename: $resetFile)->expectBody('0');
$before = $tester->request(address: '{{ADDR[worker]}}')->getBody();
if (!is_numeric($before)) {
throw new Exception('Invalid map pointer count');
}
$tester->request(address: '{{ADDR[worker]}}')->expectBody($before);

/* Complete each restart in the other pool before the stale worker persists again. */
for ($i = 1; $i <= 3; $i++) {
$tester->request(query: 'reset=1', address: '{{ADDR[reset]}}', scriptFilename: $resetFile)
->expectBody('bool(true)');
$tester->request(address: '{{ADDR[reset]}}', scriptFilename: $resetFile)->expectBody((string) $i);
$tester->request(address: '{{ADDR[worker]}}')->expectBody($before);
$tester->request(address: '{{ADDR[worker]}}')->expectBody($before);
}

$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();

?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
@unlink(__DIR__ . '/gh23859.reset.src.php');
?>
Loading