Skip to content

Deterministic subid calculation#1571

Open
jcpunk wants to merge 13 commits intoshadow-maint:masterfrom
jcpunk:deterministic-subid
Open

Deterministic subid calculation#1571
jcpunk wants to merge 13 commits intoshadow-maint:masterfrom
jcpunk:deterministic-subid

Conversation

@jcpunk
Copy link
Copy Markdown
Contributor

@jcpunk jcpunk commented Mar 9, 2026

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 -S as 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.

@jcpunk jcpunk force-pushed the deterministic-subid branch 3 times, most recently from d725670 to b4d445d Compare March 10, 2026 16:59
@jcpunk
Copy link
Copy Markdown
Contributor Author

jcpunk commented Mar 10, 2026

I think I've reworked this into a more linear patch set. Hopefully I've gotten the overflow checks clearly documented.

@jcpunk jcpunk force-pushed the deterministic-subid branch from b4d445d to 3353237 Compare March 10, 2026 17:02
Comment on lines +177 to +190
/*
* 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;
}

Copy link
Copy Markdown
Collaborator

@alejandro-colomar alejandro-colomar Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we want an unsafe mode?

Copy link
Copy Markdown
Contributor Author

@jcpunk jcpunk Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jcpunk jcpunk force-pushed the deterministic-subid branch from 3353237 to 1549cdc Compare March 11, 2026 15:13
@alejandro-colomar
Copy link
Copy Markdown
Collaborator

What is the status of this? I'd like to see the main feature merged...

@hallyn

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.

@jcpunk
Copy link
Copy Markdown
Contributor Author

jcpunk commented Mar 29, 2026

Just double checking that nothing is waiting on me. I was out of office and kinda lost track.

@alejandro-colomar
Copy link
Copy Markdown
Collaborator

alejandro-colomar commented Mar 29, 2026

Just double checking that nothing is waiting on me. I was out of office and kinda lost track.

That's correct. I'd like @hallyn and @ikerexxe to review this.

alejandro-colomar and others added 9 commits March 30, 2026 13:16
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>
@jcpunk jcpunk force-pushed the deterministic-subid branch from 01542ac to a317801 Compare March 30, 2026 18:28
@jcpunk
Copy link
Copy Markdown
Contributor Author

jcpunk commented Mar 30, 2026

Rebased off HEAD to clear merge conflicts.

</para>
<para>
<emphasis role="bold">WARNING</emphasis>:
Because <option>UID_MIN</option> is used to calculate the ranges,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is SUB_GID_COUNT. But not UID_MAX (which is listed below), iiuc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added SUB_{U,G}ID_COUNT to the documentation just now. Does that look better?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, but still why is UID_MAX listed? It's not part of the calculation, is it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jcpunk jcpunk force-pushed the deterministic-subid branch from a317801 to 608d3f9 Compare March 31, 2026 13:05
*/
uintmax_t logical_offset = (uintmax_t)uid_offset * (uintmax_t)count;

*range_start = (id_t)(sub_uid_min + (unsigned long)(logical_offset % space));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hallyn
Copy link
Copy Markdown
Member

hallyn commented Apr 1, 2026 via email

@hallyn
Copy link
Copy Markdown
Member

hallyn commented Apr 1, 2026 via email

@jcpunk
Copy link
Copy Markdown
Contributor Author

jcpunk commented Apr 1, 2026

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>
@jcpunk jcpunk force-pushed the deterministic-subid branch from 608d3f9 to 0122328 Compare April 1, 2026 05:13
@jcpunk
Copy link
Copy Markdown
Contributor Author

jcpunk commented Apr 1, 2026

Ok, please make sure that's well documented in the login.defs manpage.

I've got:

      Users with identities less than <option>UID_MIN</option>
      are incompatible with <option>SUB_GID_DETERMINISTIC</option>,
      but can still be set manually.

In the man page now. Should this be changed?

@jcpunk jcpunk force-pushed the deterministic-subid branch from 0122328 to 1526ce3 Compare April 1, 2026 05:52
jcpunk added 3 commits April 1, 2026 01:11
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>
@jcpunk jcpunk force-pushed the deterministic-subid branch from 1526ce3 to 07c9005 Compare April 1, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants