From: Pat Riehecky Date: Fri, 13 Mar 2026 14:25:41 +0000 (-0500) Subject: subid: setup deterministic_wrap mode X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=84ccd38ea51e8c5a101cc13d83d43bdcf9005908;p=thirdparty%2Fshadow.git subid: setup deterministic_wrap mode 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 --- diff --git a/lib/find_new_sub_gids.c b/lib/find_new_sub_gids.c index 4ae3a7787..3971ce922 100644 --- a/lib/find_new_sub_gids.c +++ b/lib/find_new_sub_gids.c @@ -32,11 +32,23 @@ * 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; } diff --git a/lib/find_new_sub_uids.c b/lib/find_new_sub_uids.c index 6966ef7c1..65f681546 100644 --- a/lib/find_new_sub_uids.c +++ b/lib/find_new_sub_uids.c @@ -32,11 +32,23 @@ * 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; }