]> git.ipfire.org Git - thirdparty/shadow.git/commitdiff
subid: Add deterministic subid ranges
authorPat Riehecky <riehecky@fnal.gov>
Mon, 16 Mar 2026 13:21:30 +0000 (08:21 -0500)
committerSerge Hallyn <serge@hallyn.com>
Fri, 10 Apr 2026 03:20:34 +0000 (22:20 -0500)
This adds two new options to /etc/login.defs:
* SUB_UID_DETERMINISTIC
* SUB_GID_DETERMINISTIC

In a lab where users are created ad hoc subids might drift
from one host to the other. If there is a shared home area,
this drift can create some frustration.

Creating subids deterministically provides one type of solution
to this problem. Use of nonconsecutive UIDs will result in blocks
of unused subids.

The manpages provide documentation on how these can be used.

Signed-off-by: Pat Riehecky <riehecky@fnal.gov>
lib/find_new_sub_gids.c
lib/find_new_sub_uids.c
lib/prototypes.h
src/newusers.c
src/useradd.c
src/usermod.c

index 3e6cc2d64dbe207f7ed44931845bf7f9012c4127..4ae3a77877971529c31b3b788ef3bfebd0b76772 100644 (file)
 #include "shadowlog.h"
 
 #undef NDEBUG
-#include <assert.h>
 
 
+/*
+ * find_new_sub_gids_deterministic - Assign a subordinate GID range by UID.
+ *
+ * Calculates a deterministic subordinate GID range for a given UID based
+ * on its offset from UID_MIN.  Loads SUB_GID_COUNT from login.defs and
+ * writes it back to *range_count on success.
+ *
+ * BASE FORMULA:
+ *   uid_offset     = uid - UID_MIN
+ *   logical_offset = uid_offset * SUB_GID_COUNT
+ *   start_id       = SUB_GID_MIN + logical_offset
+ *   end_id         = start_id + SUB_GID_COUNT - 1
+ *
+ * DETERMINISTIC MODE:
+ *   All arithmetic overflow is a hard error.  The assigned range must fit
+ *   entirely within [SUB_GID_MIN, SUB_GID_MAX].  Allocation is monotonic
+ *   and guaranteed non-overlapping.
+ *
+ * Return 0 on success, -1 if no GIDs are available.
+ */
+static int
+find_new_sub_gids_deterministic(uid_t uid,
+                               id_t *range_start,
+                               unsigned long *range_count)
+{
+       unsigned long  count;
+       unsigned long  slots;
+       unsigned long  space;
+       unsigned long  uid_min;
+       unsigned long  sub_gid_max;
+       unsigned long  sub_gid_min;
+       unsigned long  uid_offset;
+
+       uid_min = getdef_ulong ("UID_MIN", 1000UL);
+       sub_gid_min = getdef_ulong ("SUB_GID_MIN", 65536UL);
+       sub_gid_max = getdef_ulong ("SUB_GID_MAX", 4294967295UL);
+       count = getdef_ulong ("SUB_GID_COUNT", 65536UL);
+
+       if (uid < uid_min) {
+               fprintf(log_get_logfd(),
+                        _("%s: UID %ju is less than UID_MIN %lu,"
+                          " cannot calculate deterministic subordinate GIDs\n"),
+                        log_get_progname(),
+                        (uintmax_t)uid, uid_min);
+               return -1;
+       }
+
+       if (sub_gid_min > sub_gid_max || count == 0) {
+               fprintf(log_get_logfd(),
+                        _("%s: Invalid configuration: SUB_GID_MIN (%lu),"
+                          " SUB_GID_MAX (%lu), SUB_GID_COUNT (%lu)\n"),
+                        log_get_progname(),
+                        sub_gid_min, sub_gid_max, count);
+               return -1;
+       }
+
+       if (__builtin_add_overflow(sub_gid_max - sub_gid_min, 1UL, &space)) {
+               fprintf(log_get_logfd(),
+                       _("%s: SUB_GID range [%lu, %lu] is too large"
+                         " to represent\n"),
+                       log_get_progname(),
+                       sub_gid_min, sub_gid_max);
+               return -1;
+       }
+
+       if (count > space) {
+               fprintf(log_get_logfd(),
+                        _("%s: Not enough space for any subordinate GIDs"
+                          " (SUB_GID_MIN=%lu, SUB_GID_MAX=%lu,"
+                          " SUB_GID_COUNT=%lu)\n"),
+                        log_get_progname(),
+                        sub_gid_min, sub_gid_max, count);
+               return -1;
+       }
+
+       uid_offset = uid - uid_min;
+       slots = space / count;
+
+       if (uid_offset >= slots) {
+               fprintf(log_get_logfd(),
+                        _("%s: Deterministic subordinate GID range"
+                          " for UID %ju exceeds SUB_GID_MAX (%lu)\n"),
+                        log_get_progname(),
+                        (uintmax_t)uid, sub_gid_max);
+               return -1;
+       }
+
+       *range_start = sub_gid_min + uid_offset * count;
+       *range_count = count;
+       return 0;
+}
+
 /*
  * find_new_sub_gids_linear - Find an unused subordinate GID range via
  * linear search.
@@ -55,8 +146,11 @@ find_new_sub_gids_linear(id_t *range_start, unsigned long *range_count)
  * Return 0 on success, -1 if no unused GIDs are available.
  */
 int
-find_new_sub_gids(id_t *range_start, unsigned long *range_count)
+find_new_sub_gids(uid_t uid, id_t *range_start, unsigned long *range_count)
 {
+       if (getdef_bool("SUB_GID_DETERMINISTIC"))
+               return find_new_sub_gids_deterministic(uid, range_start, range_count);
+
        return find_new_sub_gids_linear(range_start, range_count);
 }
 
index 662ed7def5c713f132086fe884ec325b73990054..6966ef7c1a28a5fcb1ecfeb10adbda596e2f80a2 100644 (file)
 #include "shadowlog.h"
 
 #undef NDEBUG
-#include <assert.h>
 
 
+/*
+ * find_new_sub_uids_deterministic - Assign a subordinate UID range by UID.
+ *
+ * Calculates a deterministic subordinate UID range for a given UID based
+ * on its offset from UID_MIN.  Loads SUB_UID_COUNT from login.defs and
+ * writes it back to *range_count on success.
+ *
+ * BASE FORMULA:
+ *   uid_offset     = uid - UID_MIN
+ *   logical_offset = uid_offset * SUB_UID_COUNT
+ *   start_id       = SUB_UID_MIN + logical_offset
+ *   end_id         = start_id + SUB_UID_COUNT - 1
+ *
+ * DETERMINISTIC MODE:
+ *   All arithmetic overflow is a hard error.  The assigned range must fit
+ *   entirely within [SUB_UID_MIN, SUB_UID_MAX].  Allocation is monotonic
+ *   and guaranteed non-overlapping.
+ *
+ * Return 0 on success, -1 if no UIDs are available.
+ */
+static int
+find_new_sub_uids_deterministic(uid_t uid,
+                               id_t *range_start,
+                               unsigned long *range_count)
+{
+       unsigned long  count;
+       unsigned long  slots;
+       unsigned long  space;
+       unsigned long  uid_min;
+       unsigned long  uid_offset;
+       unsigned long  sub_uid_max;
+       unsigned long  sub_uid_min;
+
+       uid_min = getdef_ulong ("UID_MIN", 1000UL);
+       sub_uid_min = getdef_ulong ("SUB_UID_MIN", 65536UL);
+       sub_uid_max = getdef_ulong ("SUB_UID_MAX", 4294967295UL);
+       count = getdef_ulong ("SUB_UID_COUNT", 65536UL);
+
+       if (uid < uid_min) {
+               fprintf(log_get_logfd(),
+                        _("%s: UID %ju is less than UID_MIN %lu,"
+                          " cannot calculate deterministic subordinate UIDs\n"),
+                        log_get_progname(),
+                        (uintmax_t)uid, uid_min);
+               return -1;
+       }
+
+       if (sub_uid_min > sub_uid_max || count == 0) {
+               fprintf(log_get_logfd(),
+                        _("%s: Invalid configuration: SUB_UID_MIN (%lu),"
+                          " SUB_UID_MAX (%lu), SUB_UID_COUNT (%lu)\n"),
+                        log_get_progname(),
+                        sub_uid_min, sub_uid_max, count);
+               return -1;
+       }
+
+       if (__builtin_add_overflow(sub_uid_max - sub_uid_min, 1UL, &space)) {
+               fprintf(log_get_logfd(),
+                       _("%s: SUB_UID range [%lu, %lu] is too large"
+                         " to represent\n"),
+                       log_get_progname(),
+                       sub_uid_min, sub_uid_max);
+               return -1;
+       }
+
+       if (count > space) {
+               fprintf(log_get_logfd(),
+                        _("%s: Not enough space for any subordinate UIDs"
+                          " (SUB_UID_MIN=%lu, SUB_UID_MAX=%lu,"
+                          " SUB_UID_COUNT=%lu)\n"),
+                        log_get_progname(),
+                        sub_uid_min, sub_uid_max, count);
+               return -1;
+       }
+
+       uid_offset = uid - uid_min;
+       slots = space / count;
+
+       if (uid_offset > slots) {
+               fprintf(log_get_logfd(),
+                        _("%s: Deterministic subordinate UID range"
+                          " for UID %ju exceeds SUB_UID_MAX (%lu)\n"),
+                        log_get_progname(),
+                        (uintmax_t)uid, sub_uid_max);
+               return -1;
+       }
+
+       *range_start = sub_uid_min + uid_offset * count;
+       *range_count = count;
+       return 0;
+}
+
 /*
  * find_new_sub_uids_linear - Find an unused subordinate UID range via
  * linear search.
@@ -55,8 +146,11 @@ find_new_sub_uids_linear(id_t *range_start, unsigned long *range_count)
  * Return 0 on success, -1 if no unused UIDs are available.
  */
 int
-find_new_sub_uids(id_t *range_start, unsigned long *range_count)
+find_new_sub_uids(uid_t uid, id_t *range_start, unsigned long *range_count)
 {
+       if (getdef_bool("SUB_UID_DETERMINISTIC"))
+               return find_new_sub_uids_deterministic(uid, range_start, range_count);
+
        return find_new_sub_uids_linear(range_start, range_count);
 }
 
index 411f8d559c98289ee85e6f82954063b6e49b3fdf..9a03e312507947145e607aaa8ae6692e1d45f21f 100644 (file)
@@ -133,10 +133,10 @@ extern int find_new_uid (bool sys_user,
 
 #ifdef ENABLE_SUBIDS
 /* find_new_sub_gids.c */
-extern int find_new_sub_gids (id_t *range_start, unsigned long *range_count);
+extern int find_new_sub_gids(uid_t uid, id_t *range_start, unsigned long *range_count);
 
 /* find_new_sub_uids.c */
-extern int find_new_sub_uids (id_t *range_start, unsigned long *range_count);
+extern int find_new_sub_uids(uid_t uid, id_t *range_start, unsigned long *range_count);
 #endif                         /* ENABLE_SUBIDS */
 
 /* getgr_nam_gid.c */
index 6e61dd76d59c2890907d79dbcc2e0855e8659a1c..5a6cfefefc191939487abb5cf171be214a73e91c 100644 (file)
@@ -1180,7 +1180,7 @@ int main (int argc, char **argv)
                if (is_sub_uid && !local_sub_uid_assigned(fields[0])) {
                        uid_t sub_uid_start = 0;
                        unsigned long sub_uid_count = 0;
-                       if (find_new_sub_uids(&sub_uid_start, &sub_uid_count) != 0)
+                       if (find_new_sub_uids(newpw.pw_uid, &sub_uid_start, &sub_uid_count) != 0)
                        {
                                fprintf (stderr,
                                        _("%s: can't find subordinate user range: %s\n"),
@@ -1204,7 +1204,7 @@ int main (int argc, char **argv)
                if (is_sub_gid && !local_sub_gid_assigned(fields[0])) {
                        gid_t sub_gid_start = 0;
                        unsigned long sub_gid_count = 0;
-                       if (find_new_sub_gids(&sub_gid_start, &sub_gid_count) != 0) {
+                       if (find_new_sub_gids(newpw.pw_uid, &sub_gid_start, &sub_gid_count) != 0) {
                                fprintf (stderr,
                                        _("%s: can't find subordinate group range: %s\n"),
                                        Prog, strerrno());
index 02ab3dc1b7d32f5b5639cda425dc83e96ee0070c..df679d287d0d552056f2611c2dd8774ab8a68bc5 100644 (file)
@@ -2655,7 +2655,7 @@ int main (int argc, char **argv)
 
 #ifdef ENABLE_SUBIDS
        if (is_sub_uid && subuid_count != 0) {
-               if (find_new_sub_uids(&sub_uid_start, &subuid_count) < 0) {
+               if (find_new_sub_uids(user_id, &sub_uid_start, &subuid_count) < 0) {
                        fprintf (stderr,
                                 _("%s: can't create subordinate user IDs: %s\n"),
                                 Prog, strerrno());
@@ -2665,7 +2665,7 @@ int main (int argc, char **argv)
                }
        }
        if (is_sub_gid && subgid_count != 0) {
-               if (find_new_sub_gids(&sub_gid_start, &subgid_count) < 0) {
+               if (find_new_sub_gids(user_id, &sub_gid_start, &subgid_count) < 0) {
                        fprintf (stderr,
                                 _("%s: can't create subordinate group IDs: %s\n"),
                                 Prog, strerrno());
index 108a60adbda8843a11f82b991f98db8353c88548..7c0321a6c180e9560fef5c393c3a7b5959966df1 100644 (file)
@@ -384,14 +384,14 @@ prepend_range(const char *str, struct id_range_list_entry **head)
 }
 
 static int
-find_range(struct id_range_list_entry **head,
-           int (*find_fn)(id_t *range_start, unsigned long *range_count))
+find_range(struct id_range_list_entry **head, uid_t uid,
+           int (*find_fn)(uid_t uid, id_t *range_start, unsigned long *range_count))
 {
        struct id_range_list_entry *entry;
        struct id_range range;
        unsigned long count;
 
-       if (find_fn(&range.first, &count) != 0)
+       if (find_fn(uid, &range.first, &count) != 0)
                return 0;
 
        range.last = range.first + count;
@@ -2248,7 +2248,7 @@ int main (int argc, char **argv)
        }
 #ifdef ENABLE_SUBIDS
        if (Sflg) {
-               if (find_range (&add_sub_uids, find_new_sub_uids) == 0) {
+               if (find_range (&add_sub_uids, user_id, find_new_sub_uids) == 0) {
                        fprintf (stderr,
                                _("%s: unable to find new subordinate uid range: %s\n"),
                                Prog, strerrno());
@@ -2256,7 +2256,7 @@ int main (int argc, char **argv)
                               strerrno());
                        fail_exit (E_SUB_UID_UPDATE, process_selinux);
                }
-               if (find_range (&add_sub_gids, find_new_sub_gids) == 0) {
+               if (find_range (&add_sub_gids, user_id, find_new_sub_gids) == 0) {
                        fprintf (stderr,
                                _("%s: unable to find new subordinate gid range: %s\n"),
                                Prog, strerrno());