diff --git a/NEWS b/NEWS index 571d8d82e012..50c3346e17a3 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 48f594663b3e..a6d51ccbce81 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -4440,21 +4440,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); @@ -4463,6 +4481,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; } @@ -4470,7 +4489,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) 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 b93bf6940dea..147ddcc91b9e 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -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; @@ -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(); 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-- +