From 3008e19df8f02fa133639a86695ef897141e5177 Mon Sep 17 00:00:00 2001
From: Bernhard Voelker <mail@bernhard-voelker.de>
Date: Tue, 28 Nov 2023 21:41:10 +0100
Subject: [PATCH] find: allow -user,-group to accept larger integers beyond
 INT_MAX

The number parsing of integer arguments of the -user and -group option
was limited to INT_MAX, although the data types uid_t and gid_t are
larger on many systems, including x86_64 GNU/Linux.

* find/parser.c (UID_T_MAX, GID_T_MAX): Define.
(parse_group): Use xstrtoumax directly instead of safe_atoi, and check
the returned number vs. GID_T_MAX.  Simplify error handling.
While at it, fix the est_success_rate guessing.
(parse_user): Use xstrtoumax directly instead of safe_atoi, and check
the returned number vs. UID_T_MAX.
* find/getlimits.c: Add helper utility to determine platform-local
limits for uid_t and gid_t, based on 'getlimits' of GNU coreutils,
written by Padraig Brady.
* find/Makefile.am (noinst_PROGRAMS): Make 'getlimits' as such.
* cfg.mk (exclude_file_name_regexp--sc_bindtextdomain): Add getlimits.c.
* find/.gitignore (/getlimits): Add entry.
* tests/find/user-group-max.sh: Add test.
* tests/local.mk (all_tests): Reference it.
* NEWS (Bug Fixes): Mention the fix.

Reported by Jocelyn Le Sage in
https://savannah.gnu.org/bugs/?64900
---
 NEWS                         |  4 ++
 cfg.mk                       |  2 +-
 find/.gitignore              |  1 +
 find/Makefile.am             |  2 +
 find/getlimits.c             | 76 ++++++++++++++++++++++++++++++
 find/parser.c                | 90 +++++++++++-------------------------
 tests/find/user-group-max.sh | 51 ++++++++++++++++++++
 tests/local.mk               |  1 +
 8 files changed, 164 insertions(+), 63 deletions(-)
 create mode 100644 find/getlimits.c
 create mode 100755 tests/find/user-group-max.sh

diff --git a/NEWS b/NEWS
index 6deff331..d9d80ab6 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,10 @@ GNU findutils NEWS - User visible changes.      -*- outline -*- (allout)
   The error diagnostic for non-numeric arguments has been improved as well.
   Likewise for -inum, -links and -uid.
 
+  'find -user' and 'find -group' now allow to specify larger UIDs/GIDs.
+  Previously, that was limited to INT_MAX, although the types uid_t and gid_t
+  are larger on many systems, including x86_64 GNU/Linux. [#64900]
+
 ** Improvements
 
   The find predicates -used, -amin, -cmin, -mmin, -atime, -ctime, and -mtime
diff --git a/cfg.mk b/cfg.mk
index 652f55f0..2d4bb1c0 100644
--- a/cfg.mk
+++ b/cfg.mk
@@ -86,7 +86,7 @@ exclude_file_name_regexp--sc_texinfo_acronym = doc/perm\.texi
 
 # List syntax-check exemptions.
 exclude_file_name_regexp--sc_bindtextdomain = \
-  ^(locate/frcode|lib/regexprops|lib/test_splitstring)\.c$$
+  ^(locate/frcode|lib/regexprops|lib/test_splitstring|find/getlimits)\.c$$
 
 # sc_prohibit_strcmp is broken because it gives false positives for
 # cases where neither argument is a string literal.
diff --git a/find/.gitignore b/find/.gitignore
index d34536ca..7315215c 100644
--- a/find/.gitignore
+++ b/find/.gitignore
@@ -3,4 +3,5 @@
 /Makefile
 /Makefile.in
 /find
+/getlimits
 /libfindtools.a
diff --git a/find/Makefile.am b/find/Makefile.am
index 8e516140..d0534e6f 100644
--- a/find/Makefile.am
+++ b/find/Makefile.am
@@ -38,6 +38,8 @@ LDADD = libfindtools.a ../lib/libfind.a ../gl/lib/libgnulib.a $(LIBINTL) $(LIB_C
 
 SUBDIRS = . testsuite
 
+noinst_PROGRAMS = getlimits
+
 dist-hook: findutils-check-manpages
 
 # Clean coverage files generated by running binaries built with
diff --git a/find/getlimits.c b/find/getlimits.c
new file mode 100644
index 00000000..8dc361f6
--- /dev/null
+++ b/find/getlimits.c
@@ -0,0 +1,76 @@
+/* getlimits - print various platform dependent limits.
+   Copyright (C) 2023 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/* Based on 'getlimits' of GNU coreutils, written by Pádraig Brady.
+ * Stripped down to a minimal version by Bernhard Voelker.  */
+
+#include <config.h>             /* sets _FILE_OFFSET_BITS=64 etc. */
+#include <stdio.h>
+#include <sys/types.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "system.h"
+#include "intprops.h"
+
+#ifndef UID_T_MAX
+# define UID_T_MAX TYPE_MAXIMUM (uid_t)
+#endif
+
+#ifndef GID_T_MAX
+# define GID_T_MAX TYPE_MAXIMUM (gid_t)
+#endif
+
+#ifndef MIN
+# define MIN(a,b) (a<b?a:b)
+#endif
+
+/* Add one to the absolute value of the number whose textual
+   representation is BUF + 1.  Do this in-place, in the buffer.
+   Return a pointer to the result, which is normally BUF + 1, but is
+   BUF if the representation grew in size.  */
+static char const *
+decimal_absval_add_one (char *buf)
+{
+  bool negative = (buf[1] == '-');
+  char *absnum = buf + 1 + negative;
+  char *p = absnum + strlen (absnum);
+  absnum[-1] = '0';
+  while (*--p == '9')
+    *p = '0';
+  ++*p;
+  char *result = MIN (absnum, p);
+  if (negative)
+    *--result = '-';
+  return result;
+}
+
+int
+main (int argc, char **argv)
+{
+  char limit[100];
+
+#define print_int(TYPE)                                      \
+  sprintf (limit + 1, "%" "ju", (uintmax_t) TYPE##_MAX);     \
+  printf (#TYPE"_MAX=%s\n", limit + 1);                      \
+  printf (#TYPE"_OFLOW=%s\n", decimal_absval_add_one (limit))
+
+  print_int (INT);
+  print_int (UID_T);
+  print_int (GID_T);
+
+  return EXIT_SUCCESS;
+}
diff --git a/find/parser.c b/find/parser.c
index a6ee520c..b1674152 100644
--- a/find/parser.c
+++ b/find/parser.c
@@ -34,6 +34,7 @@
 /* gnulib headers. */
 #include "fnmatch.h"
 #include "fts_.h"
+#include "intprops.h"
 #include "modechange.h"
 #include "mountlist.h"
 #include "parse-datetime.h"
@@ -73,6 +74,14 @@
 # define endpwent() ((void) 0)
 #endif
 
+#ifndef UID_T_MAX
+# define UID_T_MAX TYPE_MAXIMUM (uid_t)
+#endif
+
+#ifndef GID_T_MAX
+# define GID_T_MAX TYPE_MAXIMUM (gid_t)
+#endif
+
 /* Roll our own isnan rather than using <math.h>.  */
 #ifndef isnan
 # define isnan(x) ((x) != (x))
@@ -1138,61 +1147,33 @@ static bool
 parse_group (const struct parser_table* entry, char **argv, int *arg_ptr)
 {
   const char *groupname;
-  const int saved_argc = *arg_ptr;
 
   if (collect_arg (argv, arg_ptr, &groupname))
     {
-      gid_t gid;
       struct predicate *our_pred;
+      gid_t gid;
       struct group *cur_gr = getgrnam (groupname);
       endgrent ();
-      if (cur_gr)
+      if (cur_gr != NULL)
 	{
 	  gid = cur_gr->gr_gid;
 	}
       else
 	{
-	  const int gid_len = strspn (groupname, "0123456789");
-	  if (gid_len)
+	  uintmax_t num;
+	  if ((xstrtoumax (groupname, NULL, 10, &num, "") != LONGINT_OK)
+                || (GID_T_MAX < num))
 	    {
-	      if (groupname[gid_len] == 0)
-		{
-		  gid = safe_atoi (groupname, options.err_quoting_style);
-		}
-	      else
-		{
-		  /* XXX: no test in test suite for this */
-		  error (EXIT_FAILURE, 0,
-			 _("%s is not the name of an existing group and"
-			   " it does not look like a numeric group ID "
-			   "because it has the unexpected suffix %s"),
-			 quotearg_n_style (0, options.err_quoting_style, groupname),
-			 quotearg_n_style (1, options.err_quoting_style, groupname+gid_len));
-		  *arg_ptr = saved_argc; /* don't consume the invalid argument. */
-		  return false;
-		}
-	    }
-	  else
-	    {
-	      if (*groupname)
-		{
-		  /* XXX: no test in test suite for this */
-		  error (EXIT_FAILURE, 0,
-		         _("%s is not the name of an existing group"),
-		         quotearg_n_style (0, options.err_quoting_style, groupname));
-		}
-	      else
-		{
-		  error (EXIT_FAILURE, 0,
-		         _("argument to -group is empty, but should be a group name"));
-		}
-	      *arg_ptr = saved_argc; /* don't consume the invalid argument. */
-	      return false;
+	      error (EXIT_FAILURE, 0,
+		     _("invalid group name or GID argument to -group: %s"),
+		     quotearg_n_style (0, options.err_quoting_style,
+				       groupname));
 	    }
+	  gid = num;
 	}
       our_pred = insert_primary (entry, groupname);
       our_pred->args.gid = gid;
-      our_pred->est_success_rate = (our_pred->args.numinfo.l_val < 100) ? 0.99 : 0.2;
+      our_pred->est_success_rate = (our_pred->args.gid < 100) ? 0.99 : 0.2;
       return true;
     }
   return false;
@@ -2463,31 +2444,16 @@ parse_user (const struct parser_table* entry, char **argv, int *arg_ptr)
 	}
       else
 	{
-	  const size_t uid_len = strspn (username, "0123456789");
-	  if (uid_len && (username[uid_len]==0))
+	  uintmax_t num;
+	  if ((xstrtoumax (username, NULL, 10, &num, "") != LONGINT_OK)
+                || (UID_T_MAX < num))
 	    {
-	      uid = safe_atoi (username, options.err_quoting_style);
-	    }
-	  else
-	    {
-	      /* This is a fatal error (if we just return false, the caller
-	       * will say "invalid argument `username' to -user", which is
-	       * not as helpful). */
-	      if (username[0])
-		{
-		  error (EXIT_FAILURE, 0,
-		         _("%s is not the name of a known user"),
-		         quotearg_n_style (0, options.err_quoting_style,
-					   username));
-		}
-	      else
-		{
-		  error (EXIT_FAILURE, 0,
-		         _("The argument to -user should not be empty"));
-		}
-	      /*NOTREACHED*/
-	      return false;
+	      error (EXIT_FAILURE, 0,
+		     _("invalid user name or UID argument to -user: %s"),
+		     quotearg_n_style (0, options.err_quoting_style,
+				       username));
 	    }
+	  uid = num;
 	}
       our_pred = insert_primary (entry, username);
       our_pred->args.uid = uid;
diff --git a/tests/find/user-group-max.sh b/tests/find/user-group-max.sh
new file mode 100755
index 00000000..3beaa4c0
--- /dev/null
+++ b/tests/find/user-group-max.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+# Verify -user/-group allow UID/GID values as large as UID_T_MAX/GID_T_MAX
+
+# Copyright (C) 2023 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; fu_path_prepend_
+print_ver_ find
+
+# The tests -user and -group assume their argument to be a user or group name,
+# and fall back to UID/GID if a decimal integer.
+# The following limits apply on a regular x86_64 GNU/Linux system:
+#     INT_MAX=2147483647
+#   UID_T_MAX=4294967295
+#   GID_T_MAX=4294967295
+# Until findutils-4.9, the number parsing was limited to INT_MAX, even if the
+# data types uid_t/gid_t are larger on the actual system.
+# Read the limits of the current system.
+getlimits_
+
+# Verify that -user/-group support UID/GID numbers until UID_T_MAX/GID_T_MAX.
+find -user "$UID_T_MAX" >/dev/null 2>err || fail=1
+compare /dev/null err || fail=1
+
+find -group "$GID_T_MAX" >/dev/null 2>err || fail=1
+compare /dev/null err || fail=1
+
+# Verify that UID/GID numbers larger than UID_T_MAX/GID_T_MAX get rejected.
+echo "find: invalid user name or UID argument to -user: '$UID_T_OFLOW'" >exp || framework_failure_
+returns_ 1 find -user "$UID_T_OFLOW" -name enoent >/dev/null 2>err || fail=1
+sed -i 's/^.*find/find/' err || framework_failure_
+compare exp err || fail=1
+
+echo "find: invalid group name or GID argument to -group: '$GID_T_OFLOW'" >exp || framework_failure_
+returns_ 1 find -group "$GID_T_OFLOW" -name enoent >/dev/null 2>err || fail=1
+sed -i 's/^.*find/find/' err || framework_failure_
+compare exp err || fail=1
+
+Exit $fail
diff --git a/tests/local.mk b/tests/local.mk
index b8bc90a3..ac225e6e 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -122,6 +122,7 @@ all_tests = \
   tests/find/used.sh \
   tests/find/newer.sh \
   tests/find/opt-numeric-arg.sh \
+  tests/find/user-group-max.sh \
   tests/xargs/conflicting_opts.sh \
   tests/xargs/verbose-quote.sh \
   tests/find/arg-nan.sh \
-- 
2.43.0

