Commit 1ec3d60fab6 for php.net

commit 1ec3d60fab6017f30dd1761d51a5fc6ce3d666a5
Author: Pratik Bhujel <prateekbhujelpb@gmail.com>
Date:   Wed Aug 26 20:30:48 2026 +0545

    Fix GH-19320: Prevent FPM UID and GID overflow (#22986)

diff --git a/NEWS b/NEWS
index c6ca2040030..df8a5c5c51c 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 b2f0e71d833..a58e248b666 100644
--- a/sapi/fpm/fpm/fpm_unix.c
+++ b/sapi/fpm/fpm/fpm_unix.c
@@ -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>
@@ -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 efb8640cd32..aa06f6109bc 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 <sys/types.h>
+
 #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 00000000000..fa0c708dd3f
--- /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--
+<?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();
+?>