]> git.ipfire.org Git - thirdparty/shadow.git/commitdiff
subid: setup deterministic_wrap mode master
authorPat Riehecky <riehecky@fnal.gov>
Fri, 13 Mar 2026 14:25:41 +0000 (09:25 -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:
* UNSAFE_SUB_UID_DETERMINISTIC_WRAP
* UNSAFE_SUB_GID_DETERMINISTIC_WRAP

Deterministic subordinate ID allocation ties each user's subid range
directly to their UID, giving consistent, reproducible ranges across all
hosts without a shared database.  This property breaks down when the
subordinate ID space is exhausted.

With a UID space that on Linux extends to 2^32-1 and the traditional
per-user subid allocation of 2^16 ranges, a site with a large UID
population could exhaust the subordinate ID space before all user UIDs
are allocated.

UNSAFE_SUB_UID_DETERMINISTIC_WRAP and UNSAFE_SUB_GID_DETERMINISTIC_WRAP
provide an explicit opt-in to modulo (ring-buffer) wrapping as a
predictable last resort.  This preserves the deterministic allocation
at the risk of subid overlap.

The UNSAFE_ prefix and the required explicit opt-in are intentional.

Overlapping ranges break namespace isolation and can allow container
escapes and privilege escalation between users whose ranges collide.

These options are appropriate only when all of the following hold:
  - Strict subid determinism is require
  - The active UID population on the host is small and well-known
  - The administrator regularly audits the UID distribution and confirms
    no two active users produce overlapping computed ranges

Do not enable these options on hosts with an uncontrolled user population.

Signed-off-by: Pat Riehecky <riehecky@fnal.gov>
lib/find_new_sub_gids.c
lib/find_new_sub_uids.c

index 4ae3a77877971529c31b3b788ef3bfebd0b76772..3971ce922edde8d13ebacb3075ab0a899aeb1578 100644 (file)
  *   start_id       = SUB_GID_MIN + logical_offset
  *   end_id         = start_id + SUB_GID_COUNT - 1
  *
- * DETERMINISTIC MODE:
+ * DETERMINISTIC-SAFE MODE (default):
  *   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.
  *
+ * UNSAFE_SUB_GID_DETERMINISTIC_WRAP MODE:
+ *   Activated with UNSAFE_SUB_GID_DETERMINISTIC_WRAP yes
+ *
+ *   WARNING: SECURITY RISK!
+ *   WARNING: MAY CAUSE RANGE OVERLAPS!
+ *   WARNING: MAY CAUSE CONTAINER ESCAPES!
+ *
+ *   The subordinate GID space is treated as a ring.  Arithmetic overflow
+ *   is normalised via modulo over [SUB_GID_MIN, SUB_GID_MAX].
+ *   This means ranges MAY overlap for large UID populations!
+ *   Intended only for development, testing, or constrained lab environments.
+ *
  * Return 0 on success, -1 if no GIDs are available.
  */
 static int
@@ -44,7 +56,9 @@ find_new_sub_gids_deterministic(uid_t uid,
                                id_t *range_start,
                                unsigned long *range_count)
 {
+       bool           allow_wrap;
        unsigned long  count;
+       unsigned long  slot;
        unsigned long  slots;
        unsigned long  space;
        unsigned long  uid_min;
@@ -56,6 +70,7 @@ find_new_sub_gids_deterministic(uid_t uid,
        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);
+       allow_wrap = getdef_bool ("UNSAFE_SUB_GID_DETERMINISTIC_WRAP");
 
        if (uid < uid_min) {
                fprintf(log_get_logfd(),
@@ -96,17 +111,22 @@ find_new_sub_gids_deterministic(uid_t uid,
 
        uid_offset = uid - uid_min;
        slots = space / count;
+       slot = uid_offset;
 
        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;
+               if (allow_wrap) {
+                       slot = uid_offset % slots;
+               } else {
+                       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_start = sub_gid_min + slot * count;
        *range_count = count;
        return 0;
 }
index 6966ef7c1a28a5fcb1ecfeb10adbda596e2f80a2..65f68154673d64f3b0b31aa71f53ac6f5caca660 100644 (file)
  *   start_id       = SUB_UID_MIN + logical_offset
  *   end_id         = start_id + SUB_UID_COUNT - 1
  *
- * DETERMINISTIC MODE:
+ * DETERMINISTIC-SAFE MODE (default):
  *   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.
  *
+ * UNSAFE_SUB_UID_DETERMINISTIC_WRAP MODE:
+ *   Activated with UNSAFE_SUB_UID_DETERMINISTIC_WRAP yes
+ *
+ *   WARNING: SECURITY RISK!
+ *   WARNING: MAY CAUSE RANGE OVERLAPS!
+ *   WARNING: MAY CAUSE CONTAINER ESCAPES!
+ *
+ *   The subordinate UID space is treated as a ring.  Arithmetic overflow
+ *   is normalised via modulo over [SUB_UID_MIN, SUB_UID_MAX].
+ *   This means ranges MAY overlap for large UID populations!
+ *   Intended only for development, testing, or constrained lab environments.
+ *
  * Return 0 on success, -1 if no UIDs are available.
  */
 static int
@@ -44,7 +56,9 @@ find_new_sub_uids_deterministic(uid_t uid,
                                id_t *range_start,
                                unsigned long *range_count)
 {
+       bool           allow_wrap;
        unsigned long  count;
+       unsigned long  slot;
        unsigned long  slots;
        unsigned long  space;
        unsigned long  uid_min;
@@ -56,6 +70,7 @@ find_new_sub_uids_deterministic(uid_t uid,
        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);
+       allow_wrap = getdef_bool ("UNSAFE_SUB_UID_DETERMINISTIC_WRAP");
 
        if (uid < uid_min) {
                fprintf(log_get_logfd(),
@@ -96,17 +111,22 @@ find_new_sub_uids_deterministic(uid_t uid,
 
        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;
+       slot = uid_offset;
+
+       if (uid_offset >= slots) {
+               if (allow_wrap) {
+                       slot = uid_offset % slots;
+               } else {
+                       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_start = sub_uid_min + slot * count;
        *range_count = count;
        return 0;
 }