From 1ec3d60fab6017f30dd1761d51a5fc6ce3d666a5 Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Wed, 26 Aug 2026 20:30:48 +0545 Subject: [PATCH 1/4] Fix GH-19320: Prevent FPM UID and GID overflow (#22986) --- NEWS | 3 + sapi/fpm/fpm/fpm_unix.c | 85 +++++++++++++++++++++---- sapi/fpm/fpm/fpm_worker_pool.h | 9 ++- sapi/fpm/tests/gh19320-id-overflow.phpt | 54 ++++++++++++++++ 4 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 sapi/fpm/tests/gh19320-id-overflow.phpt diff --git a/NEWS b/NEWS index c6ca2040030d..df8a5c5c51c9 100644 --- a/NEWS +++ b/NEWS @@ -88,6 +88,9 @@ PHP NEWS - LibXML: . Fixed bug GH-22752 (Build failure with libxml 2.15). (David Carlier) +- FPM: + . Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel) + - MBString: . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative offset in a non-UTF-8 encoding). (Eyüp Can Akman) diff --git a/sapi/fpm/fpm/fpm_unix.c b/sapi/fpm/fpm/fpm_unix.c index b2f0e71d8331..a58e248b6666 100644 --- a/sapi/fpm/fpm/fpm_unix.c +++ b/sapi/fpm/fpm/fpm_unix.c @@ -2,6 +2,9 @@ #include "fpm_config.h" +#include +#include +#include #include #include #include @@ -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); @@ -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) @@ -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) @@ -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) { @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; } } diff --git a/sapi/fpm/fpm/fpm_worker_pool.h b/sapi/fpm/fpm/fpm_worker_pool.h index efb8640cd32f..aa06f6109bc7 100644 --- a/sapi/fpm/fpm/fpm_worker_pool.h +++ b/sapi/fpm/fpm/fpm_worker_pool.h @@ -3,6 +3,8 @@ #ifndef FPM_WORKER_POOL_H #define FPM_WORKER_POOL_H 1 +#include + #include "fpm_conf.h" #include "fpm_shm.h" @@ -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; diff --git a/sapi/fpm/tests/gh19320-id-overflow.phpt b/sapi/fpm/tests/gh19320-id-overflow.phpt new file mode 100644 index 000000000000..fa0c708dd3f2 --- /dev/null +++ b/sapi/fpm/tests/gh19320-id-overflow.phpt @@ -0,0 +1,54 @@ +--TEST-- +FPM: Reject out-of-range numeric user and group IDs +--SKIPIF-- + +--FILE-- +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-- + From ab4139d44593e1edbfc793f06f6272459251a924 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 26 Aug 2026 16:47:12 +0200 Subject: [PATCH 2/4] [ci skip] NEWS --- NEWS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index df8a5c5c51c9..e4c3889e292d 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,9 @@ PHP NEWS . Fixed bug GH-23457 (imagebmp() is extremely slow when writing to a file). (Lazizbek Ergashev) +- FPM: + . Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel) + - Intl: . Fixed a double-free when IntlGregorianCalendar construction fails after the ICU constructor adopts the TimeZone. (iliaal) @@ -88,9 +91,6 @@ PHP NEWS - LibXML: . Fixed bug GH-22752 (Build failure with libxml 2.15). (David Carlier) -- FPM: - . Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel) - - MBString: . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative offset in a non-UTF-8 encoding). (Eyüp Can Akman) From 12b1f654db632e1420142dd87f190a93a44fb88e Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 27 Aug 2026 00:09:11 +0800 Subject: [PATCH 3/4] Fix GH-23418: UAF when accessing mounted Phar subdirectories (#23442) Here we passes a properly null-terminated copy of the shortened path to `phar_mount_entry()` instead and keep `test` alive until error formatting and manifest lookup have completed. --- NEWS | 4 ++++ ext/phar/tests/gh23418.phpt | 32 ++++++++++++++++++++++++++++++++ ext/phar/util.c | 13 ++++++++----- 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 ext/phar/tests/gh23418.phpt diff --git a/NEWS b/NEWS index e4c3889e292d..935b31234898 100644 --- a/NEWS +++ b/NEWS @@ -52,6 +52,10 @@ PHP NEWS . Fixed a leak when a persistent connection failed a liveness check with no other live PDO handle. (iliaal) +- Phar: + . Fixed bug GH-23418 (Use-after-free when looking up mounted directories). + (Weilin Du) + - Standard: . Fixed a memory leak in array_merge_recursive() when the recursive merge of an object converted to an array fails. (David Carlier) diff --git a/ext/phar/tests/gh23418.phpt b/ext/phar/tests/gh23418.phpt new file mode 100644 index 000000000000..d7ebebcb9ecb --- /dev/null +++ b/ext/phar/tests/gh23418.phpt @@ -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-- +addFromString('x.txt', 'x'); +$p->setStub(''); +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-- + +--EXPECT-- +PharFileInfo, isDir=true diff --git a/ext/phar/util.c b/ext/phar/util.c index d3bdf3d52a78..f4de9922f899 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -1382,7 +1382,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; @@ -1425,22 +1425,25 @@ 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); if (NULL == (entry = zend_hash_str_find_ptr(&phar->manifest, path, path_len))) { 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(); From 0c87849da5a67a6f4cac4b6225cf026a7db5377b Mon Sep 17 00:00:00 2001 From: Julien Voisin Date: Wed, 26 Aug 2026 18:31:00 +0200 Subject: [PATCH 4/4] Validate large run map entries instead of assuming them (#23366) Three places dispatch on the page map entry of a pointer, and reach the large-run case by elimination, with the assumption written down as a comment rather than checked: if (EXPECTED(info & ZEND_MM_IS_SRUN)) { ... } else /* if (info & ZEND_MM_IS_LRUN) */ { The assumption does not always hold: when ZEND_MM_IS_FRUN is 0 and zend_mm_free_pages_ex() zeroes chunk->map[page_num], a pointer to a large run that has already been freed has info == 0, so it fails the SRUN test, and falls into the large-run branch. There, ZEND_MM_LRUN_PAGES(0) is 0, and the three callers quietly degrade: - zend_mm_free_heap() frees a run of zero pages, i.e. a double free of a large block is accepted and does nothing at all. - zend_mm_size() reports a block size of 0. - zend_mm_realloc_heap() takes old_size 0 and reallocates from there. A large-block double free or a use of a freed pointer is silently absorbed by the allocator instead of being a hard failure. This commit promotes the comment to a real ZEND_MM_CHECK() in all three. The value is already in a register at that point, so it costs a test and a branch. This was checked under GDB by allocating a large block, freeing it, and then reusing the pointer. Before, _efree() returned normally, _zend_mem_block_size() returned 0 and _erealloc() returned a new pointer. After this commit, each of the three aborts with "zend_mm_heap corrupted". Amusingly, the two comments naming ZEND_MM_IS_LARGE_RUN referred to a macro that does not exist: the real name is ZEND_MM_IS_LRUN. --- Zend/zend_alloc.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index fc7bc1f4d9d4..575b54b11a24 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -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); } @@ -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 @@ -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) {