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
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ PHP NEWS
(ndossche)
. Fixed JIT ignoring exceptions from typed property set hooks. (David Carlier)

- PCNTL:
. Added the $rekey_memory parameter to pcntl_fork(), pcntl_rfork() and
pcntl_forkx() to optionally avoid re-keying inherited allocator lists
(GH-25403). (Alessio Attilio)

- PDO:
. Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
column index. (Ilia Alshanetsky)
Expand Down
22 changes: 20 additions & 2 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2264,8 +2264,16 @@ static void zend_mm_init_key(zend_mm_heap *heap)
zend_mm_refresh_key(heap);
}

ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
static void zend_mm_refresh_key_child_ex(zend_mm_heap *heap, bool rekey)
{
if (!rekey) {
memset(&heap->rand_state, 0, sizeof(heap->rand_state));
#if ZEND_DEBUG
heap->pid = getpid();
#endif
return;
}

uintptr_t old_key = heap->shadow_key;

zend_mm_init_key(heap);
Expand Down Expand Up @@ -2294,6 +2302,11 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
#endif
}

ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
{
zend_mm_refresh_key_child_ex(heap, true);
}

static zend_mm_heap *zend_mm_init(void)
{
zend_mm_chunk *chunk = (zend_mm_chunk*)zend_mm_chunk_alloc_int(ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE);
Expand Down Expand Up @@ -3403,9 +3416,14 @@ ZEND_API void shutdown_memory_manager(bool silent, bool full_shutdown)
zend_mm_shutdown(AG(mm_heap), full_shutdown, silent);
}

ZEND_API void refresh_memory_manager_ex(bool rekey)
{
zend_mm_refresh_key_child_ex(AG(mm_heap), rekey);
}

ZEND_API void refresh_memory_manager(void)
{
zend_mm_refresh_key_child(AG(mm_heap));
refresh_memory_manager_ex(true);
}

ZEND_NORETURN static ZEND_COLD void zend_out_of_memory(void)
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_alloc.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void);
ZEND_API void start_memory_manager(void);
ZEND_API void shutdown_memory_manager(bool silent, bool full_shutdown);
ZEND_API void refresh_memory_manager(void);
ZEND_API void refresh_memory_manager_ex(bool rekey);
ZEND_API bool is_zend_mm(void);
ZEND_API bool is_zend_ptr(const void *ptr);

Expand Down
21 changes: 21 additions & 0 deletions ext/pcntl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Memory re-keying after fork

`pcntl_fork(bool $rekey_memory = true)`,
`pcntl_rfork(int $flags, int $signal = 0, bool $rekey_memory = true)` and
`pcntl_forkx(int $flags, bool $rekey_memory = true)` refresh the allocator's
shadow key in the child by default. This requires traversing and writing the
inherited free lists and cached chunk list. A fragmented heap can therefore
cause substantial child startup time and copy-on-write memory overhead.

Passing `rekey_memory: false` skips that traversal and retains the inherited
shadow key. Allocator links remain protected, but the parent and children share
the key: disclosure of that key in one process compromises this protection in
other processes that retain it. Callers must weigh this reduced isolation
against the startup and memory savings. The default preserves per-child key
isolation.

Both modes reset the allocator's random generator state so future key refreshes
use fresh randomness. Both also update allocator process bookkeeping and perform
the usual child initialization, including timer initialization, stat and realpath
cache clearing, and extension child initialization hooks. The setting applies to
one fork; subsequent forks use their own argument, defaulting to `true`.
21 changes: 15 additions & 6 deletions ext/pcntl/pcntl.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,12 @@ PHP_MINFO_FUNCTION(pcntl)
PHP_FUNCTION(pcntl_fork)
{
pid_t id;
bool rekey_memory = true;

ZEND_PARSE_PARAMETERS_NONE();
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(rekey_memory)
ZEND_PARSE_PARAMETERS_END();

id = fork();
if (id == -1) {
Expand All @@ -295,7 +299,7 @@ PHP_FUNCTION(pcntl_fork)

}
} else if (id == 0) {
php_child_init();
php_child_init_ex(rekey_memory);
}

RETURN_LONG((zend_long) id);
Expand Down Expand Up @@ -1552,11 +1556,13 @@ PHP_FUNCTION(pcntl_rfork)
zend_long flags;
zend_long csignal = 0;
pid_t pid;
bool rekey_memory = true;

ZEND_PARSE_PARAMETERS_START(1, 2)
ZEND_PARSE_PARAMETERS_START(1, 3)
Z_PARAM_LONG(flags)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(csignal)
Z_PARAM_BOOL(rekey_memory)
ZEND_PARSE_PARAMETERS_END();

/* This is a flag to use with great caution in general, preferably not within PHP */
Expand Down Expand Up @@ -1599,7 +1605,7 @@ PHP_FUNCTION(pcntl_rfork)
php_error_docref(NULL, E_WARNING, "Error %d", errno);
}
} else if (pid == 0) {
php_child_init();
php_child_init_ex(rekey_memory);
}

RETURN_LONG((zend_long) pid);
Expand All @@ -1616,9 +1622,12 @@ PHP_FUNCTION(pcntl_forkx)
{
zend_long flags;
pid_t pid;
bool rekey_memory = true;

ZEND_PARSE_PARAMETERS_START(1, 1)
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_LONG(flags)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(rekey_memory)
ZEND_PARSE_PARAMETERS_END();

if (flags < FORK_NOSIGCHLD || flags > FORK_WAITPID) {
Expand All @@ -1644,7 +1653,7 @@ PHP_FUNCTION(pcntl_forkx)
php_error_docref(NULL, E_WARNING, "Error %d", errno);
}
} else if (pid == 0) {
php_child_init();
php_child_init_ex(rekey_memory);
}

RETURN_LONG((zend_long) pid);
Expand Down
6 changes: 3 additions & 3 deletions ext/pcntl/pcntl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@
const PCNTL_ECAPMODE = UNKNOWN;
#endif

function pcntl_fork(): int {}
function pcntl_fork(bool $rekey_memory = true): int {}

/**
* @param int $status
Expand Down Expand Up @@ -1085,11 +1085,11 @@ function pcntl_unshare(int $flags): bool {}
#endif

#ifdef HAVE_RFORK
function pcntl_rfork(int $flags, int $signal = 0): int{}
function pcntl_rfork(int $flags, int $signal = 0, bool $rekey_memory = true): int{}
#endif

#ifdef HAVE_FORKX
function pcntl_forkx(int $flags): int{}
function pcntl_forkx(int $flags, bool $rekey_memory = true): int{}
#endif

#ifdef HAVE_PIDFD_OPEN
Expand Down
10 changes: 7 additions & 3 deletions ext/pcntl/pcntl_arginfo.h

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

8 changes: 4 additions & 4 deletions ext/pcntl/pcntl_decl.h

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

32 changes: 32 additions & 0 deletions ext/pcntl/tests/gh25403_cow.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
GH-25403: opting out of child memory re-keying avoids inherited free-list writes
--EXTENSIONS--
pcntl
--SKIPIF--
<?php
if (PHP_OS !== 'Linux') die('skip Linux minor-fault accounting required');
if (getenv('USE_ZEND_ALLOC') === '0') die('skip Zend allocator required');
?>
--INI--
memory_limit=128M
--FILE--
<?php
$strings = [];
for ($i = 0; $i < 131072; $i++) {
$strings[] = str_repeat('x', 256);
}
foreach ($strings as $i => $value) {
if ($i % 2) unset($strings[$i]);
}
$pid = pcntl_fork(rekey_memory: false);
if ($pid === -1) throw new Exception('Fork failed');
if ($pid === 0) {
var_dump(getrusage()['ru_minflt'] < 2048);
exit(0);
}
if (pcntl_waitpid($pid, $status) !== $pid) throw new Exception('Wait failed');
var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0);
?>
--EXPECT--
bool(true)
bool(true)
48 changes: 48 additions & 0 deletions ext/pcntl/tests/gh25403_fork.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
pcntl_fork() preserves child initialization with optional memory re-keying
--EXTENSIONS--
pcntl
--SKIPIF--
<?php
if (!function_exists('pcntl_fork')) die('skip pcntl_fork unavailable');
?>
--FILE--
<?php
function checkChild(bool $rekey): void {
$objects = [];
for ($i = 0; $i < 8192; $i++) {
$objects[] = str_repeat('x', 128 + $i % 64);
}
foreach ($objects as $i => $value) {
if ($i % 2) unset($objects[$i]);
}
realpath(__FILE__);
$pid = pcntl_fork(rekey_memory: $rekey);
if ($pid === -1) throw new Exception('Fork failed');
if ($pid === 0) {
if (array_key_exists(__FILE__, realpath_cache_get())) exit(1);
for ($i = 0; $i < 8192; $i++) {
$objects[] = str_repeat('y', 128 + $i % 64);
}
gc_mem_caches();
$pid = pcntl_fork(rekey_memory: !$rekey);
if ($pid === -1) exit(1);
if ($pid === 0) {
unset($objects);
gc_mem_caches();
exit(0);
}
if (pcntl_waitpid($pid, $status) !== $pid) exit(1);
exit(pcntl_wifexited($status) ? pcntl_wexitstatus($status) : 1);
}
var_dump(pcntl_waitpid($pid, $status) === $pid);
var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0);
}
checkChild(false);
checkChild(true);
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
48 changes: 48 additions & 0 deletions ext/pcntl/tests/gh25403_forkx.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
pcntl_forkx() preserves child initialization with optional memory re-keying
--EXTENSIONS--
pcntl
--SKIPIF--
<?php
if (!function_exists('pcntl_forkx')) die('skip pcntl_forkx unavailable');
?>
--FILE--
<?php
function checkChild(bool $rekey): void {
$objects = [];
for ($i = 0; $i < 8192; $i++) {
$objects[] = str_repeat('x', 128 + $i % 64);
}
foreach ($objects as $i => $value) {
if ($i % 2) unset($objects[$i]);
}
realpath(__FILE__);
$pid = pcntl_forkx(FORK_WAITPID, rekey_memory: $rekey);
if ($pid === -1) throw new Exception('Fork failed');
if ($pid === 0) {
if (array_key_exists(__FILE__, realpath_cache_get())) exit(1);
for ($i = 0; $i < 8192; $i++) {
$objects[] = str_repeat('y', 128 + $i % 64);
}
gc_mem_caches();
$pid = pcntl_fork(rekey_memory: !$rekey);
if ($pid === -1) exit(1);
if ($pid === 0) {
unset($objects);
gc_mem_caches();
exit(0);
}
if (pcntl_waitpid($pid, $status) !== $pid) exit(1);
exit(pcntl_wifexited($status) ? pcntl_wexitstatus($status) : 1);
}
var_dump(pcntl_waitpid($pid, $status) === $pid);
var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0);
}
checkChild(false);
checkChild(true);
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
Loading
Loading