Deterministic subid calculation#1571
Conversation
d725670 to
b4d445d
Compare
|
I think I've reworked this into a more linear patch set. Hopefully I've gotten the overflow checks clearly documented. |
b4d445d to
3353237
Compare
lib/find_new_sub_uids.c
Outdated
| /* | ||
| * UNSAFE_SUB_UID_DETERMINISTIC_WRAP MODE | ||
| * | ||
| * Promote to uintmax_t before multiplying to avoid truncation on | ||
| * 32-bit platforms where unsigned long is 32 bits. The modulo | ||
| * folds the result back into [0, space) before adding min. | ||
| */ | ||
| uintmax_t logical_offset = (uintmax_t)uid_offset * (uintmax_t)count; | ||
|
|
||
| *range_start = (id_t)(sub_uid_min + (unsigned long)(logical_offset % space)); | ||
| *range_count = count; | ||
| return 0; | ||
| } | ||
|
|
There was a problem hiding this comment.
Why would we want an unsafe mode?
There was a problem hiding this comment.
At my site, once a UNIX UID is assigned, that is your ID forever. It is never reused or reassigned. It is expected to be identical across all systems.
In the next 5 years I expect to have more assigned UIDs than will allow me to give each user 65535 subids.
Eventually I'm going to hit a point where my newest users can't get subids because a bunch of subids are assigned to folks who've left the laboratory.
In my systems with the full user list, I'm going to probably just disable user namespaces since there is no safe way for me to give everyone their own big subid space. I don't really like that plan, but I don't have a better one.
My systems with a limited user list are more interesting... On hosts dedicated to experimental development, I'm willing to take more risks. These hosts typically have less than 100 users.
The great thing about strong determinism in subids is that, so long as all the ids fit between SUBID_MIN and SUBID_MAX, I have perfect consistency across the whole site. Once I run out of space things get rough.
On these hosts as the sysadmin responsible for safety and security of the system, I can see that my UIDs are not contiguous. At times I've got a 1,000+ "unallocated" UIDs on a given host because there are no users in that range who have access to the system. I'm willing to "risk" some overlap when I can see that I've zero interactive UIDs under 2000.
"Unsafe mode" is my best compromise. I get the determinism I want so subids are predicable, and, when I run out of space, I at least have an option I can consider for how to approach the problem.
With a UNIX UID being uint_32, and the traditional subid allocation being "each user gets their own uint_16", I'm going to hit that wall and I don't know what else to do.
If I'm reading the defaults correctly, the defaults from /etc/login.defs only have 9148 65536 ranges. After which some sort of plan is required.
3353237 to
1549cdc
Compare
I think the patches look okay. I didn't want to OK the patch set, because I don't know these features enough. But it looks decently reasonable. I didn't check that the algorithms make sense. You should check that. Then there's also the discussion about the design of the configuration variables. That's up to you too. The source code looks reasonable, which is the only thing I reviewed deeply. |
|
Just double checking that nothing is waiting on me. I was out of office and kinda lost track. |
The input to these functions is always an address (&x); that's guaranteed to be non-null. Signed-off-by: Alejandro Colomar <alx@kernel.org>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
GCC has issues with literal -1. Link: <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119011> Signed-off-by: Alejandro Colomar <alx@kernel.org>
This helped find a bug, and doesn't seem to have any false positives here, so let's use it. Signed-off-by: Alejandro Colomar <alx@kernel.org>
It's the natural type for this API, and it's also more consistent with its wrappers. Let's also use literal -1 for the error code, which is safer than unsigned constants, as -1 is sign-extended to fit whatever unsigned type we're using. Signed-off-by: Alejandro Colomar <alx@kernel.org>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
Cc: Serge Hallyn <serge@hallyn.com> Signed-off-by: Alejandro Colomar <alx@kernel.org>
find_free_range() already checks this, and does it better. Signed-off-by: Alejandro Colomar <alx@kernel.org>
Signed-off-by: Pat Riehecky <riehecky@fnal.gov>
01542ac to
a317801
Compare
|
Rebased off |
| </para> | ||
| <para> | ||
| <emphasis role="bold">WARNING</emphasis>: | ||
| Because <option>UID_MIN</option> is used to calculate the ranges, |
There was a problem hiding this comment.
So is SUB_GID_COUNT. But not UID_MAX (which is listed below), iiuc.
There was a problem hiding this comment.
I added SUB_{U,G}ID_COUNT to the documentation just now. Does that look better?
There was a problem hiding this comment.
Yes, thanks, but still why is UID_MAX listed? It's not part of the calculation, is it?
There was a problem hiding this comment.
It was my intent to use UID_MAX as a way of ensuring the ranges are bounded. Since uid_t is uint_32 right now on Linux there is a risk of wrapping. I was expecting that the upper limit on automatic allocation would be a helpful knob to have on hand.
a317801 to
608d3f9
Compare
lib/find_new_sub_uids.c
Outdated
| */ | ||
| uintmax_t logical_offset = (uintmax_t)uid_offset * (uintmax_t)count; | ||
|
|
||
| *range_start = (id_t)(sub_uid_min + (unsigned long)(logical_offset % space)); |
There was a problem hiding this comment.
Does this ensure that range_start + range_count will fit? I'm not sure it does in all cases.
It seems like it would be safer to take (sub_uid_max - sub_uid_min) / count as the number of ranges which fit (call it numfit), and then using uid_offset %= numfit in the regular calculation.
I might not be thinking right, will take another look tonight if I have time.
There was a problem hiding this comment.
serge@sl25:~/test$ cat range.c
#include <stdio.h>
#include <unistd.h>
int main() {
unsigned long count;
unsigned long space;
unsigned long uid_min;
unsigned long uid_offset;
unsigned long sub_uid_max;
unsigned long sub_uid_min;
uid_t uid;
int i, spaces;
uid_t uid_tests[] = { 1000, 1001, 65536, 100000, 0 };
uid_min = 1000UL;
sub_uid_min = 65536UL;
sub_uid_max = 4294967295UL;
count = 65536UL;
space = sub_uid_max - sub_uid_min + 1;
spaces = space / count;
printf("There are %d spaces of size %lu in %lu\n", spaces, count, space);
for (i = 0; uid_tests[i] != 0; i++) {
uid = uid_tests[i];
uid_offset = uid - uid_min;
printf("uid_offset is %lu for uid %d\n", uid_offset, uid);
printf("and so uid %d wraps to %lu\n", uid, uid_offset % spaces);
}
}
serge@sl25:~/test$ gcc -o range range.c
serge@sl25:~/test$ ./range
There are 65535 spaces of size 65536 in 4294901760
uid_offset is 0 for uid 1000
and so uid 1000 wraps to 0
uid_offset is 1 for uid 1001
and so uid 1001 wraps to 1
uid_offset is 64536 for uid 65536
and so uid 65536 wraps to 64536
uid_offset is 99000 for uid 100000
and so uid 100000 wraps to 33465
There was a problem hiding this comment.
I've just switched to the slots/spaces algorithm you've demonstrated here.
| sub_gid_max = getdef_ulong ("SUB_GID_MAX", 4294967295UL); | ||
| count = getdef_ulong ("SUB_GID_COUNT", 65536UL); | ||
|
|
||
| if (uid < uid_min) { |
There was a problem hiding this comment.
But so this is a pretty harsh limitation - if UID_MIN is 1000, and you create a user with uid 999, you can't give it subuids?
Note that right now useradd -f -u 999 will in fact assign an automatic subuid range.
There was a problem hiding this comment.
if UID_MIN is 1000, and you create a user with uid 999, you can't give it subuids?
Close, with uid less than UID_MIN you can't give them subids automatically with this deterministic algorithm. You could manually assign a block of ids.
|
On Tue, Mar 31, 2026 at 08:43:29PM -0700, Pat Riehecky wrote:
@jcpunk commented on this pull request.
> + id_t *range_start,
+ unsigned long *range_count)
+{
+ unsigned long count;
+ 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) {
> if UID_MIN is 1000, and you create a user with uid 999, you can't give it subuids?
Close, with uid less than UID_MIN you can't give them subids automatically with this deterministic algorithm. You could manually assign a block of ids.
Ok, please make sure that's well documented in the login.defs manpage.
|
|
On Tue, Mar 31, 2026 at 08:42:03PM -0700, Pat Riehecky wrote:
@jcpunk commented on this pull request.
> + This ensures the same UID always receives the same subordinate GID
+ range on every system,
+ making it suitable for environments with centralized user management
+ (LDAP, NIS, etc.)
+ or
+ synchronized UIDs across systems.
+ </para>
+ <para>
+ If <option>SUB_GID_DETERMINISTIC</option> is enabled,
+ you can use
+ <command>usermod --add-subgids -S</command>
+ to produce deterministic subgids.
+ </para>
+ <para>
+ <emphasis role="bold">WARNING</emphasis>:
+ Because <option>UID_MIN</option> is used to calculate the ranges,
It was my intent to use UID_MAX as a way of ensuring the ranges are bounded. Since `uid_t` is `uint_32` right now on Linux there is a risk of wrapping. I was expecting that the upper limit on automatic allocation would be a helpful knob to have on hand.
But you're not. UID_MAX doesn't show up in the code.
|
Blast, that must have gotten dropped by mistake when I was moving things around. It probably isn't a useful control anyway. I'll get that removed from the doc. |
They are not active at this commit, but they are documented. Signed-off-by: Pat Riehecky <riehecky@fnal.gov>
608d3f9 to
0122328
Compare
I've got: In the man page now. Should this be changed? |
0122328 to
1526ce3
Compare
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>
They are not active at this commit, but they are documented. Signed-off-by: Pat Riehecky <riehecky@fnal.gov>
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>
1526ce3 to
07c9005
Compare
This patch solves a long standing problem at my site.
We have two main environments: LDAP and "not LDAP".
Our LDAP server doesn't have a schema for subid, so that is local to each system. Users are added to LDAP by a different group and we aren't usually told when a new user appears.
Our "not LDAP" systems are expected to have their UID match whatever is in LDAP, but beyond that we are free to customize. This is typically a lab environment where folks have groups specific to their access within that lab cluster. So here we explicitly want to mangage the subids.
More often then I'd like, users want us to setup an sync job where their home area from the LDAP is synced down to a sub area on the test cluster. So that makes keeping the subids in sync a bit more of an adventure.
Then it gets worse. My site has currently around 63,000 users. Which means if I give every user 65,536 subids to play with, in a few years I'll be out of space since uid_t is u_int_32. Thankfully, my test labs don't the full user list. They generally have about 100 or so users.
Before the merge of 1ed06fe I figured this was a 100% my problem thing since usermod didn't have a "recommend" mode. But now it does.
This patch makes it automatic for me to keep all my "16bit" user subids in perfect sync by just setting up a job to run
usermod -Sas needed.I'm going to need a subid solution for user 75537 (since
UID_MIN==1000) eventually. Since the only place where I'd consider enabling wrap mode is in my heavily curated lab computers, seeding an imperfect solution. I can't shake the feeling that I'm going to need this one day. When LDAP "rolls over" I think we may just drop user namespaces on those hosts...I'll certainly need help with tests.