Skip to content

Commit dc5ac8e

Browse files
committed
enhance memory safety
1 parent a15abd6 commit dc5ac8e

35 files changed

Lines changed: 3105 additions & 277 deletions

‎ext/date/php_date.c‎

Lines changed: 255 additions & 95 deletions
Large diffs are not rendered by default.

‎ext/pcntl/pcntl.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "ext/standard/info.h"
3030
#include "ext/standard/php_filestat.h"
3131
#include "php_signal.h"
32+
#include "ext/user_cache/php_user_cache.h"
3233
#include "php_ticks.h"
3334
#include "zend_exceptions.h"
3435
#include "zend_fibers.h"
@@ -271,8 +272,12 @@ PHP_FUNCTION(pcntl_fork)
271272

272273
ZEND_PARSE_PARAMETERS_NONE();
273274

275+
php_ucache_fork_prepare();
276+
274277
id = fork();
275278
if (id == -1) {
279+
php_ucache_fork_cancel();
280+
276281
PCNTL_G(last_error) = errno;
277282
switch (errno) {
278283
case EAGAIN:
@@ -1586,9 +1591,13 @@ PHP_FUNCTION(pcntl_rfork)
15861591
}
15871592
#endif
15881593

1594+
php_ucache_fork_prepare();
1595+
15891596
pid = rfork(flags);
15901597

15911598
if (pid == -1) {
1599+
php_ucache_fork_cancel();
1600+
15921601
PCNTL_G(last_error) = errno;
15931602
switch (errno) {
15941603
case EAGAIN:
@@ -1626,9 +1635,13 @@ PHP_FUNCTION(pcntl_forkx)
16261635
RETURN_THROWS();
16271636
}
16281637

1638+
php_ucache_fork_prepare();
1639+
16291640
pid = forkx(flags);
16301641

16311642
if (pid == -1) {
1643+
php_ucache_fork_cancel();
1644+
16321645
PCNTL_G(last_error) = errno;
16331646
switch (errno) {
16341647
case EAGAIN:

‎ext/spl/spl_array.c‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,10 +1602,15 @@ static bool spl_array_object_copy_ucache_state(
16021602

16031603
static bool spl_array_object_serialize_ucache_state(zval *state, const zval *object)
16041604
{
1605+
const spl_array_object *intern = Z_SPLARRAY_P(object);
16051606
zval *storage;
16061607

16071608
ZVAL_UNDEF(state);
16081609

1610+
if (!(intern->ar_flags & SPL_ARRAY_IS_SELF) && Z_TYPE(intern->array) != IS_ARRAY) {
1611+
return false;
1612+
}
1613+
16091614
spl_array_object_serialize_state((zval *) object, state, /* with_members */ false);
16101615

16111616
if (EG(exception) || Z_TYPE_P(state) != IS_ARRAY) {
@@ -1628,12 +1633,55 @@ static bool spl_array_object_serialize_ucache_state(zval *state, const zval *obj
16281633

16291634
static PHP_UCACHE_HOT bool spl_array_object_unserialize_ucache_state(zval *object, zval *state)
16301635
{
1636+
spl_array_object *intern;
1637+
zend_class_entry *ce_get_iterator = NULL;
1638+
zend_long flags;
1639+
zval *flags_zv, *storage_zv, *iterator_class_zv;
1640+
16311641
if (Z_TYPE_P(state) != IS_ARRAY) {
16321642
return false;
16331643
}
16341644

1635-
return spl_array_object_unserialize_state(object, Z_ARRVAL_P(state), /* with_members */ false)
1636-
&& !EG(exception);
1645+
flags_zv = zend_hash_index_find(Z_ARRVAL_P(state), 0);
1646+
storage_zv = zend_hash_index_find(Z_ARRVAL_P(state), 1);
1647+
iterator_class_zv = zend_hash_index_find(Z_ARRVAL_P(state), 2);
1648+
1649+
if (flags_zv == NULL || storage_zv == NULL || Z_TYPE_P(flags_zv) != IS_LONG) {
1650+
return false;
1651+
}
1652+
1653+
flags = Z_LVAL_P(flags_zv) & SPL_ARRAY_CLONE_MASK;
1654+
if (!(flags & SPL_ARRAY_IS_SELF) && Z_TYPE_P(storage_zv) != IS_ARRAY) {
1655+
return false;
1656+
}
1657+
1658+
if (iterator_class_zv != NULL && Z_TYPE_P(iterator_class_zv) != IS_NULL) {
1659+
if (Z_TYPE_P(iterator_class_zv) != IS_STRING) {
1660+
return false;
1661+
}
1662+
1663+
ce_get_iterator = zend_lookup_class(Z_STR_P(iterator_class_zv));
1664+
if (ce_get_iterator == NULL || EG(exception) || !instanceof_function(ce_get_iterator, spl_ce_ArrayIterator)) {
1665+
return false;
1666+
}
1667+
}
1668+
1669+
intern = Z_SPLARRAY_P(object);
1670+
intern->ar_flags = (intern->ar_flags & ~SPL_ARRAY_CLONE_MASK) | flags;
1671+
1672+
if (ce_get_iterator != NULL) {
1673+
intern->ce_get_iterator = ce_get_iterator;
1674+
}
1675+
1676+
if (flags & SPL_ARRAY_IS_SELF) {
1677+
zval_ptr_dtor(&intern->array);
1678+
1679+
ZVAL_UNDEF(&intern->array);
1680+
} else {
1681+
spl_array_set_array(object, intern, storage_zv, 0L, true);
1682+
}
1683+
1684+
return !EG(exception);
16371685
}
16381686

16391687
static const php_ucache_safe_direct_handlers spl_array_ucache_handlers = {

‎ext/user_cache/php_user_cache.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ ZEND_API php_ucache_partition *php_ucache_partition_create(const char *name);
109109
ZEND_API void php_ucache_partition_set_max_procs(php_ucache_partition *partition, uint32_t max_procs);
110110
ZEND_API bool php_ucache_partition_startup_storage(php_ucache_partition *partition);
111111
ZEND_API void php_ucache_partition_activate(php_ucache_partition *partition);
112+
ZEND_API void php_ucache_partition_detach_all_except(php_ucache_partition *keep);
113+
ZEND_API void php_ucache_fork_prepare(void);
114+
ZEND_API void php_ucache_fork_cancel(void);
115+
ZEND_API void php_ucache_child_init(void);
112116
ZEND_API void php_ucache_activate_boundary_partition_by_id(
113117
const char *sapi_prefix,
114118
const char *boundary,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
--TEST--
2+
FPM: a worker maps only its own pool's partition segment
3+
--SKIPIF--
4+
<?php
5+
include __DIR__ . '/skipif.inc';
6+
if (!is_readable('/proc/self/maps')) {
7+
die('skip requires /proc/self/maps');
8+
}
9+
?>
10+
--FILE--
11+
<?php
12+
13+
require_once __DIR__ . '/tester.inc';
14+
15+
$cfg = <<<EOT
16+
[global]
17+
error_log = {{FILE:LOG}}
18+
[alpha]
19+
listen = {{ADDR[alpha]}}
20+
pm = static
21+
pm.max_children = 1
22+
pm.max_requests = 0
23+
catch_workers_output = yes
24+
php_admin_value[user_cache.shm_size] = 24M
25+
[beta]
26+
listen = {{ADDR[beta]}}
27+
pm = static
28+
pm.max_children = 1
29+
pm.max_requests = 0
30+
catch_workers_output = yes
31+
php_admin_value[user_cache.shm_size] = 20M
32+
EOT;
33+
34+
$code = <<<'PHP'
35+
<?php
36+
$pool = $_GET['pool'];
37+
$cache = UserCache\Cache::getPool('default');
38+
$cache->store('probe', $pool);
39+
$own = UserCache\Cache::getStatus()->getSharedMemorySize();
40+
$sizes = [24 * 1024 * 1024 => 0, 20 * 1024 * 1024 => 0];
41+
foreach (file('/proc/self/maps') as $line) {
42+
if (!preg_match('~^([0-9a-f]+)-([0-9a-f]+) (\S+) ~', $line, $m) || $m[3][3] !== 's') {
43+
continue;
44+
}
45+
$len = hexdec($m[2]) - hexdec($m[1]);
46+
if (isset($sizes[$len])) {
47+
$sizes[$len]++;
48+
}
49+
}
50+
printf("%s own=%dM alpha-sized=%d beta-sized=%d value=%s\n", $pool, $own >> 20, $sizes[24 * 1024 * 1024], $sizes[20 * 1024 * 1024], $cache->fetch('probe'));
51+
PHP;
52+
53+
$tester = new FPM\Tester($cfg, $code);
54+
$tester->start(iniEntries: [
55+
'opcache.file_update_protection' => '0',
56+
]);
57+
$tester->expectLogStartNotices();
58+
59+
foreach (['alpha', 'beta'] as $pool) {
60+
$response = $tester->request(query: 'pool=' . $pool, address: '{{ADDR[' . $pool . ']}}');
61+
echo trim((string) $response->getBody()), "\n";
62+
}
63+
64+
$tester->terminate();
65+
$tester->expectLogTerminatingNotices();
66+
$tester->close();
67+
68+
/* Release builds do not collect cycles at shutdown. */
69+
unset($tester);
70+
gc_collect_cycles();
71+
72+
echo "Done\n";
73+
74+
?>
75+
--EXPECT--
76+
alpha own=24M alpha-sized=1 beta-sized=0 value=alpha
77+
beta own=20M alpha-sized=0 beta-sized=1 value=beta
78+
Done
79+
--CLEAN--
80+
<?php
81+
require_once __DIR__ . '/tester.inc';
82+
FPM\Tester::clean();
83+
?>

‎ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ try {
166166
echo $e->getMessage(), "\n";
167167
}
168168
var_dump($cache->fetch('anonymous-start', 'not stored'));
169+
170+
/* Cursors advanced by an unserialize()d or weekday-relative interval carry that interval's stale relative fields and still restore. */
171+
echo "\ncursor relative state:\n";
172+
$unserializedInterval = unserialize(serialize(new DateInterval('P1D')));
173+
$fromUnserialized = new DatePeriod(new DateTimeImmutable('2026-01-01'), $unserializedInterval, 2);
174+
foreach ($fromUnserialized as $unused) {
175+
}
176+
$cache->store('cursor-unserialized-interval', $fromUnserialized);
177+
$u = $cache->fetch('cursor-unserialized-interval', 'unrestorable');
178+
var_dump($u instanceof DatePeriod && serialize($u) === serialize($fromUnserialized));
179+
var_dump(period_dates($u) === ['2026-01-01', '2026-01-02', '2026-01-03']);
180+
181+
$byWeekday = new DatePeriod(new DateTimeImmutable('2026-01-01'), DateInterval::createFromDateString('next monday'), 2);
182+
foreach ($byWeekday as $unused) {
183+
}
184+
$cache->store('cursor-weekday-interval', $byWeekday);
185+
$w = $cache->fetch('cursor-weekday-interval', 'unrestorable');
186+
var_dump($w instanceof DatePeriod && serialize($w) === serialize($byWeekday));
187+
var_dump(period_dates($w) === period_dates($byWeekday), period_dates($w));
169188
?>
170189
--EXPECT--
171190
bool(true)
@@ -197,3 +216,17 @@ string(3) "PST"
197216
bool(true)
198217
Serialization of 'DateTimeImmutable@anonymous' is not allowed
199218
string(10) "not stored"
219+
220+
cursor relative state:
221+
bool(true)
222+
bool(true)
223+
bool(true)
224+
bool(true)
225+
array(3) {
226+
[0]=>
227+
string(10) "2026-01-01"
228+
[1]=>
229+
string(10) "2026-01-05"
230+
[2]=>
231+
string(10) "2026-01-12"
232+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
UserCache\Cache: a DatePeriod whose start class the fetching process cannot load, or can only load as abstract, is unrestorable
3+
--EXTENSIONS--
4+
pcntl
5+
--INI--
6+
user_cache.enable=1
7+
user_cache.enable_cli=1
8+
user_cache.shm_size=16M
9+
date.timezone=UTC
10+
--FILE--
11+
<?php
12+
/* The storing child declares concrete start classes; the fetching parent sees one of them only as abstract, the other not at all. */
13+
$cache = UserCache\Cache::getPool('dateperiod-start-class');
14+
$cache->clear();
15+
16+
spl_autoload_register(static function (string $class): void {
17+
echo "autoload: $class\n";
18+
if ($class === 'UserCacheAbstractLaterStart') {
19+
eval('abstract class UserCacheAbstractLaterStart extends DateTimeImmutable {}');
20+
}
21+
});
22+
23+
$pid = pcntl_fork();
24+
if ($pid < 0) {
25+
die("fork failed\n");
26+
}
27+
if ($pid === 0) {
28+
eval('class UserCacheAbstractLaterStart extends DateTimeImmutable {}');
29+
eval('class UserCacheMissingLaterStart extends DateTimeImmutable {}');
30+
$stored = $cache->store('abstract-start', new DatePeriod(new UserCacheAbstractLaterStart('2026-01-01'), new DateInterval('P1D'), 1))
31+
&& $cache->store('missing-start', new DatePeriod(new UserCacheMissingLaterStart('2026-01-01'), new DateInterval('P1D'), 1))
32+
&& $cache->store('plain-start', new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 1));
33+
exit($stored ? 0 : 1);
34+
}
35+
pcntl_waitpid($pid, $status);
36+
var_dump(pcntl_wexitstatus($status));
37+
38+
echo "stored by child:\n";
39+
var_dump($cache->has('abstract-start'), $cache->has('missing-start'), $cache->has('plain-start'));
40+
41+
echo "abstract start class:\n";
42+
var_dump($cache->fetch('abstract-start', 'unrestorable'));
43+
var_dump(class_exists('UserCacheAbstractLaterStart', false));
44+
45+
echo "missing start class:\n";
46+
var_dump($cache->fetch('missing-start', 'unrestorable'));
47+
48+
echo "plain start class:\n";
49+
$plain = $cache->fetch('plain-start', 'unrestorable');
50+
var_dump($plain instanceof DatePeriod);
51+
var_dump($plain->getStartDate()->format('Y-m-d'));
52+
53+
echo "unrestorable entries are dropped:\n";
54+
var_dump($cache->has('abstract-start'), $cache->has('missing-start'), $cache->has('plain-start'));
55+
?>
56+
--EXPECT--
57+
int(0)
58+
stored by child:
59+
bool(true)
60+
bool(true)
61+
bool(true)
62+
abstract start class:
63+
autoload: UserCacheAbstractLaterStart
64+
string(12) "unrestorable"
65+
bool(true)
66+
missing start class:
67+
autoload: UserCacheMissingLaterStart
68+
string(12) "unrestorable"
69+
plain start class:
70+
bool(true)
71+
string(10) "2026-01-01"
72+
unrestorable entries are dropped:
73+
bool(false)
74+
bool(false)
75+
bool(true)

0 commit comments

Comments
 (0)