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
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ PHP NEWS
. Fixed a leak in Locale::getKeywords() when a keyword value cannot be
read. (iliaal)

- Phar:
. Fixed bug GH-23418 (Use-after-free when looking up mounted directories).
(Weilin Du)


27 Aug 2026, PHP 8.6.0beta2

Expand Down
12 changes: 8 additions & 4 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1527,9 +1527,11 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z
ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
} else /* if (info & ZEND_MM_IS_LRUN) */ {
int pages_count = ZEND_MM_LRUN_PAGES(info);
} else {
/* A freed large run has a zeroed map entry, so this also rejects double frees. */
ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");

int pages_count = ZEND_MM_LRUN_PAGES(info);
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
zend_mm_free_large(heap, chunk, page_num, pages_count);
}
Expand Down Expand Up @@ -1557,7 +1559,8 @@ static size_t zend_mm_size(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_
ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
return bin_data_size[ZEND_MM_SRUN_BIN_NUM(info)];
} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
} else {
ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");
return ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE;
}
#endif
Expand Down Expand Up @@ -1752,7 +1755,8 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
return ret;
} while (0);

} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
} else {
ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
old_size = ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE;
if (size > ZEND_MM_MAX_SMALL_SIZE && size <= ZEND_MM_MAX_LARGE_SIZE) {
Expand Down
32 changes: 32 additions & 0 deletions ext/phar/tests/gh23418.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
GH-23418: Access a subdirectory of a mounted directory with a trailing slash
--EXTENSIONS--
phar
--INI--
phar.readonly=0
--FILE--
<?php
$phar = __DIR__ . '/gh23418.phar';
$mount = __DIR__ . '/gh23418';

@mkdir($mount . '/s2', 0777, true);

$p = new Phar($phar);
$p->addFromString('x.txt', 'x');
$p->setStub('<?php __HALT_COMPILER(); ?>');
unset($p);

$p = new Phar($phar);
Phar::mount('phar://' . $phar . '/m', $mount);
$info = $p['m/s2/'];

echo get_class($info), ', isDir=', $info->isDir() ? 'true' : 'false', PHP_EOL;
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/gh23418.phar');
@rmdir(__DIR__ . '/gh23418/s2');
@rmdir(__DIR__ . '/gh23418');
?>
--EXPECT--
PharFileInfo, isDir=true
13 changes: 8 additions & 5 deletions ext/phar/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,7 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si
if (ZSTR_LEN(str_key) >= path_len || strncmp(ZSTR_VAL(str_key), path, ZSTR_LEN(str_key))) {
continue;
} else {
char *test;
char *test, *mount_path;
size_t test_len;
php_stream_statbuf ssb;

Expand Down Expand Up @@ -1316,23 +1316,26 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si
}

/* mount the file just in time */
if (SUCCESS != phar_mount_entry(phar, test, test_len, path, path_len)) {
efree(test);
mount_path = estrndup(path, path_len);
if (SUCCESS != phar_mount_entry(phar, test, test_len, mount_path, path_len)) {
if (error) {
spprintf(error, 4096, "phar error: path \"%s\" exists as file \"%s\" and could not be mounted", path, test);
}
efree(mount_path);
efree(test);
return NULL;
}

efree(test);
efree(mount_path);

entry = zend_hash_str_find_ptr(&phar->manifest, path, path_len);
if (!entry) {
if (error) {
spprintf(error, 4096, "phar error: path \"%s\" exists as file \"%s\" and could not be retrieved after being mounted", path, test);
}
efree(test);
return NULL;
}
efree(test);
return entry;
}
} ZEND_HASH_FOREACH_END();
Expand Down
85 changes: 73 additions & 12 deletions sapi/fpm/fpm/fpm_unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#include "fpm_config.h"

#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <string.h>
#include <sys/time.h>
#include <sys/resource.h>
Expand Down Expand Up @@ -53,6 +56,42 @@ static inline bool fpm_unix_is_id(const char* name)
return strlen(name) == strspn(name, "0123456789");
}

static bool fpm_unix_parse_uid(struct fpm_worker_pool_s *wp, const char *name, uid_t *uid)
{
uintmax_t sentinel = (uintmax_t) ((uid_t) -1);
uintmax_t max = (uid_t) -1 > (uid_t) 0
? (uintmax_t) ((uid_t) -1)
: (UINTMAX_C(1) << (sizeof(uid_t) * CHAR_BIT - 1)) - 1;

errno = 0;
uintmax_t value = strtoumax(name, NULL, 10);
if (errno == ERANGE || value > max || value == sentinel) {
zlog(ZLOG_ERROR, "[pool %s] user ID '%s' is out of range", wp->config->name, name);
return false;
}

*uid = (uid_t) value;
return true;
}

static bool fpm_unix_parse_gid(struct fpm_worker_pool_s *wp, const char *name, gid_t *gid)
{
uintmax_t sentinel = (uintmax_t) ((gid_t) -1);
uintmax_t max = (gid_t) -1 > (gid_t) 0
? (uintmax_t) ((gid_t) -1)
: (UINTMAX_C(1) << (sizeof(gid_t) * CHAR_BIT - 1)) - 1;

errno = 0;
uintmax_t value = strtoumax(name, NULL, 10);
if (errno == ERANGE || value > max || value == sentinel) {
zlog(ZLOG_ERROR, "[pool %s] group ID '%s' is out of range", wp->config->name, name);
return false;
}

*gid = (gid_t) value;
return true;
}

static struct passwd *fpm_unix_get_passwd(struct fpm_worker_pool_s *wp, const char *name, int flags)
{
struct passwd *pwd = getpwnam(name);
Expand Down Expand Up @@ -93,7 +132,14 @@ static inline bool fpm_unix_check_listen_address(struct fpm_worker_pool_s *wp, c

static inline bool fpm_unix_check_passwd(struct fpm_worker_pool_s *wp, const char *name, int flags)
{
return !name || fpm_unix_is_id(name) || fpm_unix_get_passwd(wp, name, flags);
if (!name || !*name) {
return true;
}
if (fpm_unix_is_id(name)) {
uid_t uid;
return fpm_unix_parse_uid(wp, name, &uid);
}
return fpm_unix_get_passwd(wp, name, flags) != NULL;
}

static struct group *fpm_unix_get_group(struct fpm_worker_pool_s *wp, const char *name, int flags)
Expand All @@ -109,7 +155,14 @@ static struct group *fpm_unix_get_group(struct fpm_worker_pool_s *wp, const char

static inline bool fpm_unix_check_group(struct fpm_worker_pool_s *wp, const char *name, int flags)
{
return !name || fpm_unix_is_id(name) || fpm_unix_get_group(wp, name, flags);
if (!name || !*name) {
return true;
}
if (fpm_unix_is_id(name)) {
gid_t gid;
return fpm_unix_parse_gid(wp, name, &gid);
}
return fpm_unix_get_group(wp, name, flags) != NULL;
}

bool fpm_unix_test_config(struct fpm_worker_pool_s *wp)
Expand All @@ -133,8 +186,8 @@ int fpm_unix_resolve_socket_permissions(struct fpm_worker_pool_s *wp) /* {{{ */
/* uninitialized */
wp->socket_acl = NULL;
#endif
wp->socket_uid = -1;
wp->socket_gid = -1;
wp->socket_uid = (uid_t) -1;
wp->socket_gid = (gid_t) -1;
wp->socket_mode = 0660;

if (!c) {
Expand Down Expand Up @@ -252,7 +305,9 @@ int fpm_unix_resolve_socket_permissions(struct fpm_worker_pool_s *wp) /* {{{ */

if (c->listen_owner && *c->listen_owner) {
if (fpm_unix_is_id(c->listen_owner)) {
wp->socket_uid = strtoul(c->listen_owner, 0, 10);
if (!fpm_unix_parse_uid(wp, c->listen_owner, &wp->socket_uid)) {
return -1;
}
} else {
struct passwd *pwd;

Expand All @@ -268,7 +323,9 @@ int fpm_unix_resolve_socket_permissions(struct fpm_worker_pool_s *wp) /* {{{ */

if (c->listen_group && *c->listen_group) {
if (fpm_unix_is_id(c->listen_group)) {
wp->socket_gid = strtoul(c->listen_group, 0, 10);
if (!fpm_unix_parse_gid(wp, c->listen_group, &wp->socket_gid)) {
return -1;
}
} else {
struct group *grp;

Expand Down Expand Up @@ -325,7 +382,7 @@ int fpm_unix_set_socket_permissions(struct fpm_worker_pool_s *wp, const char *pa
/* When listen.users and listen.groups not configured, continue with standard right */
#endif

if (wp->socket_uid != -1 || wp->socket_gid != -1) {
if (wp->socket_uid != (uid_t) -1 || wp->socket_gid != (gid_t) -1) {
if (0 > chown(path, wp->socket_uid, wp->socket_gid)) {
zlog(ZLOG_SYSERROR, "[pool %s] failed to chown() the socket '%s'", wp->config->name, wp->config->listen_address);
return -1;
Expand Down Expand Up @@ -354,7 +411,9 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */
if (is_root) {
if (wp->config->user && *wp->config->user) {
if (fpm_unix_is_id(wp->config->user)) {
wp->set_uid = strtoul(wp->config->user, 0, 10);
if (!fpm_unix_parse_uid(wp, wp->config->user, &wp->set_uid)) {
return -1;
}
pwd = getpwuid(wp->set_uid);
if (pwd) {
wp->set_gid = pwd->pw_gid;
Expand All @@ -378,7 +437,9 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */

if (wp->config->group && *wp->config->group) {
if (fpm_unix_is_id(wp->config->group)) {
wp->set_gid = strtoul(wp->config->group, 0, 10);
if (!fpm_unix_parse_gid(wp, wp->config->group, &wp->set_gid)) {
return -1;
}
} else {
struct group *grp;

Expand Down Expand Up @@ -476,17 +537,17 @@ int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */

if (wp->set_gid) {
if (0 > setgid(wp->set_gid)) {
zlog(ZLOG_SYSERROR, "[pool %s] failed to setgid(%d)", wp->config->name, wp->set_gid);
zlog(ZLOG_SYSERROR, "[pool %s] failed to setgid(%" PRIuMAX ")", wp->config->name, (uintmax_t) wp->set_gid);
return -1;
}
}
if (wp->set_uid) {
if (0 > initgroups(wp->set_user ? wp->set_user : wp->config->user, wp->set_gid)) {
zlog(ZLOG_SYSERROR, "[pool %s] failed to initgroups(%s, %d)", wp->config->name, wp->config->user, wp->set_gid);
zlog(ZLOG_SYSERROR, "[pool %s] failed to initgroups(%s, %" PRIuMAX ")", wp->config->name, wp->config->user, (uintmax_t) wp->set_gid);
return -1;
}
if (0 > setuid(wp->set_uid)) {
zlog(ZLOG_SYSERROR, "[pool %s] failed to setuid(%d)", wp->config->name, wp->set_uid);
zlog(ZLOG_SYSERROR, "[pool %s] failed to setuid(%" PRIuMAX ")", wp->config->name, (uintmax_t) wp->set_uid);
return -1;
}
}
Expand Down
9 changes: 7 additions & 2 deletions sapi/fpm/fpm/fpm_worker_pool.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#ifndef FPM_WORKER_POOL_H
#define FPM_WORKER_POOL_H 1

#include <sys/types.h>

#include "fpm_conf.h"
#include "fpm_shm.h"

Expand All @@ -23,9 +25,12 @@ struct fpm_worker_pool_s {
char *user, *home; /* for setting env USER and HOME */
enum fpm_address_domain listen_address_domain;
int listening_socket;
int set_uid, set_gid; /* config uid and gid */
uid_t set_uid;
gid_t set_gid; /* config uid and gid */
char *set_user; /* config user name */
int socket_uid, socket_gid, socket_mode;
uid_t socket_uid;
gid_t socket_gid;
int socket_mode;

/* runtime */
struct fpm_child_s *children;
Expand Down
54 changes: 54 additions & 0 deletions sapi/fpm/tests/gh19320-id-overflow.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
FPM: Reject out-of-range numeric user and group IDs
--SKIPIF--
<?php
include "skipif.inc";
?>
--FILE--
<?php

require_once "tester.inc";

$id = '18446744073709551615';
$settings = [
"user = $id",
"user = 1\ngroup = $id",
"user = 1\nlisten.owner = $id",
"user = 1\nlisten.group = $id",
];

foreach ($settings as $setting) {
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[unconfined]
listen = {{ADDR:UDS}}
$setting
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
EOT;

$tester = new FPM\Tester($cfg);
$tester->testConfig();
}

?>
Done
--EXPECT--
ERROR: [pool unconfined] user ID '18446744073709551615' is out of range
ERROR: FPM initialization failed
ERROR: [pool unconfined] group ID '18446744073709551615' is out of range
ERROR: FPM initialization failed
ERROR: [pool unconfined] user ID '18446744073709551615' is out of range
ERROR: FPM initialization failed
ERROR: [pool unconfined] group ID '18446744073709551615' is out of range
ERROR: FPM initialization failed
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>