From d7778413a2aae6ed758fe282e676460a405c0fa0 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 25 Aug 2026 23:21:28 +0500 Subject: [PATCH 1/4] Fix GH-23457: imagebmp() is extremely slow when writing to a file imagebmp() writes its pixel data a byte at a time, and the gd stream context turned each of those bytes into its own php_stream_write() call. PHP streams do no write buffering, so a 1920x1080 truecolor image cost about six million write syscalls. libgd's own FILE context does not show this because stdio buffers for it. Buffering the stream context in 8 KB chunks takes that image from 9.5s to 0.02s here, with byte-identical output. imagewbmp(), imagegd() and imagegd2() go through the same context and were writing per byte too, so they get the same fix. imagexbm() goes through the same context but writes its output via putBuf rather than per-byte putC, so it was not affected by this bug and sees no change from this patch. Close GH-23460 --- NEWS | 2 ++ ext/gd/gd.c | 28 ++++++++++++++++++++++++---- ext/gd/tests/gh23457.phpt | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 ext/gd/tests/gh23457.phpt diff --git a/NEWS b/NEWS index e56e562a4c92..c6ca2040030d 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,8 @@ PHP NEWS - GD: . Fixed imageaffinematrixget() and imageaffinematrixconcat() reporting the wrong argument in error messages. (Weilin Du) + . Fixed bug GH-23457 (imagebmp() is extremely slow when writing to a file). + (Lazizbek Ergashev) - Intl: . Fixed a double-free when IntlGregorianCalendar construction fails after diff --git a/ext/gd/gd.c b/ext/gd/gd.c index c12586522594..92001c4f9967 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -4464,21 +4464,39 @@ static void _php_image_output_ctxfree(struct gdIOCtx *ctx) /* {{{ */ efree(ctx); } /* }}} */ +typedef struct { + gdIOCtx ctx; + size_t buf_len; + unsigned char buf[8192]; +} php_gd_stream_ctx; + +static void _php_image_stream_flush(php_gd_stream_ctx *stream_ctx) /* {{{ */ +{ + if (stream_ctx->buf_len) { + php_stream_write((php_stream *) stream_ctx->ctx.data, (char *) stream_ctx->buf, stream_ctx->buf_len); + stream_ctx->buf_len = 0; + } +} /* }}} */ + static void _php_image_stream_putc(struct gdIOCtx *ctx, int c) /* {{{ */ { - char ch = (char) c; - php_stream * stream = (php_stream *)ctx->data; - php_stream_write(stream, &ch, 1); + php_gd_stream_ctx *stream_ctx = (php_gd_stream_ctx *) ctx; + if (stream_ctx->buf_len == sizeof(stream_ctx->buf)) { + _php_image_stream_flush(stream_ctx); + } + stream_ctx->buf[stream_ctx->buf_len++] = (unsigned char) c; } /* }}} */ static int _php_image_stream_putbuf(struct gdIOCtx *ctx, const void* buf, int l) /* {{{ */ { php_stream * stream = (php_stream *)ctx->data; + _php_image_stream_flush((php_gd_stream_ctx *) ctx); return php_stream_write(stream, (void *)buf, l); } /* }}} */ static void _php_image_stream_ctxfree(struct gdIOCtx *ctx) /* {{{ */ { if(ctx->data) { + _php_image_stream_flush((php_gd_stream_ctx *) ctx); ctx->data = NULL; } efree(ctx); @@ -4487,6 +4505,7 @@ static void _php_image_stream_ctxfree(struct gdIOCtx *ctx) /* {{{ */ static void _php_image_stream_ctxfreeandclose(struct gdIOCtx *ctx) /* {{{ */ { if(ctx->data) { + _php_image_stream_flush((php_gd_stream_ctx *) ctx); php_stream_close((php_stream *) ctx->data); ctx->data = NULL; } @@ -4494,7 +4513,8 @@ static void _php_image_stream_ctxfreeandclose(struct gdIOCtx *ctx) /* {{{ */ } /* }}} */ static gdIOCtx *create_stream_context(php_stream *stream, int close_stream) { - gdIOCtx *ctx = ecalloc(1, sizeof(gdIOCtx)); + php_gd_stream_ctx *stream_ctx = ecalloc(1, sizeof(php_gd_stream_ctx)); + gdIOCtx *ctx = &stream_ctx->ctx; ctx->putC = _php_image_stream_putc; ctx->putBuf = _php_image_stream_putbuf; diff --git a/ext/gd/tests/gh23457.phpt b/ext/gd/tests/gh23457.phpt new file mode 100644 index 000000000000..77a3a61900d1 --- /dev/null +++ b/ext/gd/tests/gh23457.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-23457 (imagebmp() writes to the stream one byte at a time) +--EXTENSIONS-- +gd +--FILE-- + +--EXPECT-- +bool(true) +bool(true) From 1ec3d60fab6017f30dd1761d51a5fc6ce3d666a5 Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Wed, 26 Aug 2026 20:30:48 +0545 Subject: [PATCH 2/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 3/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 4/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();