Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autogen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ CFLAGS="$CFLAGS -Werror=implicit-function-declaration"
CFLAGS="$CFLAGS -Werror=implicit-int"
CFLAGS="$CFLAGS -Werror=incompatible-pointer-types"
CFLAGS="$CFLAGS -Werror=int-conversion"
CFLAGS="$CFLAGS -Werror=sign-compare"
CFLAGS="$CFLAGS -Werror=overflow"
CFLAGS="$CFLAGS -Werror=sizeof-pointer-div"
CFLAGS="$CFLAGS -Werror=unused-but-set-parameter"
CFLAGS="$CFLAGS -Werror=unused-function"
Expand Down
62 changes: 62 additions & 0 deletions etc/login.defs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ SUB_UID_COUNT 65536
# using the numeric user ID rather than the username.
#SUB_UID_STORE_BY_UID no

#
# Enable deterministic subordinate UID allocation based on the user's UID.
# When set to "yes", subordinate UID ranges are calculated using a
# deterministic formula instead of searching for the next free range:
#
# WARNING: Do not mix deterministic and linear (default) allocation
# on the same system or across systems sharing /etc/subuid. Mixing
# methods will cause subordinate ID range conflicts and overlaps.
#
# Default: no
#
#SUB_UID_DETERMINISTIC no

#
# Allow deterministic subordinate UID calculation to wrap around using
# modulo arithmetic when a UID would overflow the configured subordinate
# ID space. Only effective when SUB_UID_DETERMINISTIC is "yes".
#
# When disabled (default), any arithmetic overflow is a hard error,
# ensuring non-overlapping monotonic allocation.
#
# WARNING: SECURITY RISK - MAY CAUSE RANGE OVERLAPS AND PRIVILEGE ESCALATION!
#
# When enabled (WRAP MODE), the subordinate ID space is treated as a ring
# buffer. Uses modulo arithmetic to handle overflow. May cause range
# overlaps between users.
#
# Default: no
#
#UNSAFE_SUB_UID_DETERMINISTIC_WRAP no
Comment thread
alejandro-colomar marked this conversation as resolved.

#
# Min/max values for automatic gid selection in groupadd(8)
#
Expand All @@ -261,6 +292,37 @@ SUB_GID_COUNT 65536
# using the numeric user ID rather than the username.
#SUB_GID_STORE_BY_UID no

#
# Enable deterministic subordinate GID allocation based on the user's UID.
# When set to "yes", subordinate GID ranges are calculated using a
# deterministic formula instead of searching for the next free range:
#
# WARNING: Do not mix deterministic and linear (default) allocation
# on the same system or across systems sharing /etc/subgid. Mixing
# methods will cause subordinate ID range conflicts and overlaps.
#
# Default: no
#
#SUB_GID_DETERMINISTIC no
Comment thread
hallyn marked this conversation as resolved.

#
# Allow deterministic subordinate GID calculation to wrap around using
# modulo arithmetic when a UID would overflow the configured subordinate
# ID space. Only effective when SUB_GID_DETERMINISTIC is "yes".
#
# When disabled (default), any arithmetic overflow is a hard error,
# ensuring non-overlapping monotonic allocation.
#
# WARNING: SECURITY RISK - MAY CAUSE RANGE OVERLAPS AND PRIVILEGE ESCALATION!
#
# When enabled (WRAP MODE), the subordinate ID space is treated as a ring
# buffer. Uses modulo arithmetic to handle overflow. May cause range
# overlaps between users.
#
# Default: no
#
#UNSAFE_SUB_GID_DETERMINISTIC_WRAP no

#
# Max number of login(1) retries if password is bad
#
Expand Down
158 changes: 135 additions & 23 deletions lib/find_new_sub_gids.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,163 @@
#include "shadowlog.h"

#undef NDEBUG
#include <assert.h>


/*
* find_new_sub_gids - Find a new unused range of GIDs.
* find_new_sub_gids_deterministic - Assign a subordinate GID range by UID.
*
* If successful, find_new_sub_gids provides a range of unused
* user IDs in the [SUB_GID_MIN:SUB_GID_MAX] range.
* 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-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
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;
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);
Comment thread
alejandro-colomar marked this conversation as resolved.
allow_wrap = getdef_bool ("UNSAFE_SUB_GID_DETERMINISTIC_WRAP");

if (uid < uid_min) {
Comment thread
hallyn marked this conversation as resolved.
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);
Comment thread
alejandro-colomar marked this conversation as resolved.
Comment thread
alejandro-colomar marked this conversation as resolved.
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);
Comment thread
alejandro-colomar marked this conversation as resolved.
return -1;
}
Comment thread
alejandro-colomar marked this conversation as resolved.

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"),
Comment thread
alejandro-colomar marked this conversation as resolved.
log_get_progname(),
sub_gid_min, sub_gid_max, count);
return -1;
}
Comment thread
alejandro-colomar marked this conversation as resolved.

uid_offset = uid - uid_min;
slots = space / count;
slot = uid_offset;

if (uid_offset >= slots) {
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 + slot * count;
*range_count = count;
return 0;
}

/*
* find_new_sub_gids_linear - Find an unused subordinate GID range via
* linear search.
*
* Loads SUB_GID_COUNT from login.defs and writes the allocated count back
* to *range_count on success.
*
* Return 0 on success, -1 if no unused GIDs are available.
*/
int find_new_sub_gids (id_t *range_start, unsigned long *range_count)
static int
find_new_sub_gids_linear(id_t *range_start, unsigned long *range_count)
{
unsigned long min, max;
unsigned long count;
id_t start;

assert (range_start != NULL);
Comment thread
hallyn marked this conversation as resolved.
assert (range_count != NULL);

min = getdef_ulong ("SUB_GID_MIN", 100000UL);
max = getdef_ulong ("SUB_GID_MAX", 600100000UL);
count = getdef_ulong ("SUB_GID_COUNT", 65536);

if (min > max || count >= max || (min + count - 1) > max) {
(void) fprintf (log_get_logfd(),
_("%s: Invalid configuration: SUB_GID_MIN (%lu),"
" SUB_GID_MAX (%lu), SUB_GID_COUNT (%lu)\n"),
log_get_progname(), min, max, count);
return -1;
}

start = sub_gid_find_free_range(min, max, count);
if (start == (id_t)-1) {
fprintf (log_get_logfd(),
_("%s: Can't get unique subordinate GID range\n"),
log_get_progname());
SYSLOG(LOG_WARN, "no more available subordinate GIDs on the system");
if (start == -1)
Comment thread
hallyn marked this conversation as resolved.
return -1;
}

*range_start = start;
*range_count = count;
return 0;
}

/*
* find_new_sub_gids - Find a new unused range of subordinate GIDs.
*
* Return 0 on success, -1 if no unused GIDs are available.
*/
int
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);
}

#else /* !ENABLE_SUBIDS */
extern int ISO_C_forbids_an_empty_translation_unit;
#endif /* !ENABLE_SUBIDS */

Loading
Loading