#248500 adduser: do not re-use uids used once

Package:
adduser
Source:
adduser
Submitter:
Marc Haber
Date:
2026-09-10 02:33:02 UTC
Severity:
wishlist
Tags:
#248500#5
Date:
2004-05-11 16:51:13 UTC
From:
To:
|From: Roland Bauerschmidt <rb@debian.org>
|Newsgroups: gmane.linux.debian.devel.general
|Subject: Re: [RFC] adding system users: which is the best way??
|Date: Mon, 1 Dec 2003 09:36:51 +0100
|
|Steve Greenland wrote:
|> If you're not going to re-use the ids (a good idea), then you might as
|> well leave a username associated with it, so that the admin identify
|> *why* it was used.
|>
|> My personal opinion and policy is that user ids should never be re-used.
|> I realize that on very large networks with many users (possibly
|> distributed timewise, like a univesity undergraduate system), this may
|> not be possible, but it ought to be true for at least the "system"
|> users.
|
|That's a good idea. I'll try to add that as an option to adduser.
|
|-- Roland

This is not yet implemented, but should be easy enough to build-

Greetings
Marc

#248500#10
Date:
2004-05-11 17:17:24 UTC
From:
To:
After thinking more about it, the naive approach of simply looking for
the last used UID in the range to be used and taking the first one
after that one is bad since that uid could also have been used and
deleted again.

We'd need to use a persistent state file, which makes the change less
trivial.

Greetings
Marc

#248500#17
Date:
2005-02-05 12:55:19 UTC
From:
To:
tags #248500 confirmed
thanks

#248500#22
Date:
2006-01-11 05:37:28 UTC
From:
To:
Hello, on a local system I wanted to fix the problem talked about in
this bug, and for us, the "naive" approach of selecting a UID one higher
than the highest currently in the range was good enough. I locally
modified adduser on my system to do that, and I created a patch against
3.80 in case it would be at all useful.

The patch uses a "NO_REUSE_IDS" option, which activates the new behavior
if set to "yes". It implements the change in the "first_avail_id()"
routine.

I am curious about the state-file approach, though; if that is
introduced with a new version of adduser, what do you do about "holes"
in the passwd file that existed prior to installation of that new
version? Maybe do something similar to the naive approach once, to
pre-fill the state file?

Cheers,
Aaron

#248500#27
Date:
2006-01-13 21:08:26 UTC
From:
To:
Hi Aaron

Thank you for the patch.

"NO_REUSE_IDS" sounds a bit strange, I'd rather call it "REUSE_IDS"
and set it to "yes" by default.

That would be the only feasible solution.

Jörg

#248500#32
Date:
2006-02-25 22:11:38 UTC
From:
To:
Hast Du den applied? Ich find "reuse" nicht in der Konfigdatei und
auch nicht in adduser.

Grüße
Marc

#248500#37
Date:
2006-02-25 22:24:23 UTC
From:
To:
Errm. That one was not meant to go to the BTS. I apologize.

Greetings
Marc

#248500#42
Date:
2006-02-28 22:29:58 UTC
From:
To:
Hi

We need to have a persistent statefile if we want to avoid to reuse a uid
which was already used by some other user. Think of the following scenario:

adduser foo
-> foo gets uid X

deluser foo
-> uid X is no longer used

adduser bar
-> bar gets uid X

So this uid is reused, even with the "always increment the hightest
already used uid by one and assign it to the new user"-algorithm.


But the whole thing of making this value persistent seems rather ugly to
me. Need to think some time about that, maybe I have a better idea to solve
that issue (a file for a single value).

Joerg

#248500#47
Date:
2009-07-18 13:32:53 UTC
From:
To:
Well, we already have this "state file" in the system, namely
"/etc/passwd". We could create a disabled system user with a system UID
(for example UID 999, and for example named "adduser"), which would be
managed by the adduser scripts, and which would hold in the GECOS field
the highest ever used user ID in the system.

So, every time a new user is added to the system, the adduser scripts
would update the GECOS field of this "adduser" system user.

This "adduser" user could be disabled ("passwd -l adduser") as it is
never meant to login into the system.

This solution keeps compatibility with the old scheme of things, and
needs no special state file, while solves the problem.

What do you think?

#248500#52
Date:
2011-11-23 10:53:25 UTC
From:
To:
*UGH*

I'd prefer a /var/lib/adduser/state over abusing /etc/passwd any time.

Greetings
Marc

#248500#55
Date:
2011-11-23 10:53:25 UTC
From:
To:
*UGH*

I'd prefer a /var/lib/adduser/state over abusing /etc/passwd any time.

Greetings
Marc

#248500#60
Date:
2026-08-31 13:46:38 UTC
From:
To:
Hi,

I’m interested in working on bug #248500 and have been looking into how UID allocation is currently handled by adduser.

The main issue I’m trying to address is the distinction between avoiding currently allocated UIDs and avoiding reuse of UIDs that have been allocated in the past. The latter requires some form of persistent state, since once an account is removed there is no longer an entry in /etc/passwd from which adduser can determine that the UID was previously used.

I also looked at the UID/GID pool functionality introduced in #1067845, in particular UID_POOL and RESERVE_UID_POOL. This provides a mechanism for reserving known UID assignments, but it does not appear to constitute a general history of previously allocated UIDs.

I’m therefore interested in investigating whether the existing UID pool mechanism could be extended or reused to implement the semantics requested by #248500, while considering a few questions:

1. How should UIDs assigned by adduser be persisted so that they remain unavailable after the corresponding account is removed?
2. What should happen when an account is recreated, should it receive its previous UID or should that UID remain permanently unavailable?
3. How should existing systems be handled, where previously used UIDs are no longer present in /etc/passwd and therefore cannot be reconstructed reliably?
4. How should manually specified UIDs (--uid) interact with the mechanism?
5. Should system UIDs and regular user UIDs follow the same policy?
6. How should concurrent adduser invocations be handled to avoid allocating the same UID?

Before starting an implementation, I’d like to check whether this approach matches the intended direction for #248500, and whether there are any previous proposals or design constraints I should take into account.

If the bug is still considered open for development, I’d be happy to investigate this further and prepare a patch with tests. If, however, the issue has already been addressed by recent changes or is no longer considered relevant, I think the bug can be closed.

Thanks,
Juan Munoz
Salsa: Juaesm

#248500#65
Date:
2026-08-31 14:24:21 UTC
From:
To:
Hi Juan,

thanks for helping with adduser!

adduser has grown a state file in the past, it's a simple, persistent
key-value store that could store the last UID being assigned by adduser
and then just increment from there.

This leaves the use case when an account was explicitly created by the
admin with a forced uid and then removed later, adduser will then
happily reuse that uid when the counter reaches that value. I think we
must ignore that since those operations could be done with
useradd/userdel or by directly editing /etc/passwd without adduser
knowing.

I don't think that the pool mechanism is the right means to address
this. pool is more a method to reserve uids, and adduser should actually
avoid assigning uids/gids

See above, you can use a persistent counter.

That uid is then lower than the counter and will not be reused.

I think it would be sane to start the counter at FIRST_*_UID and then
iterate until the first free uid is found. then initialize the counter
and use that uid.

The wish of the local admin wins and turns off the automatism.

Yes, with dedicated counters.

That's a good point, is adduser not yet locking itself against
concurrent execution?

Please let me know if any of those ideas are bad ideas.

I would be happy if you'd work on that. This bug is the next I would
have tackled myself, but I'll happily leave that for you.

Just remember rebasing your debian/latest branch on the main repo's
before you branch.

Greetings
Marc

#248500#70
Date:
2026-08-31 14:34:54 UTC
From:
To:
Hi Marc,

Thanks for the detailed and fast feedback and for the opportunity to work on this.

The persistent counter approach makes sense to me, and I agree that the UID pool mechanism is not the right abstraction for this. I’ll look into the existing state file implementation and how separate counters for regular and system UIDs could fit into it.

I’ll also investigate the current locking behavior of adduser and make sure concurrent invocations cannot result in the same UID being allocated.

For existing installations, I’ll look at initializing the counters by scanning from the configured FIRST_*_UID range until the first available UID is found, as you suggested.

Thanks again. I’ll get started on it.

Greetings,
Juan Muñoz
Debian: Juaesm

#248500#75
Date:
2026-08-31 15:09:57 UTC
From:
To:
Thank you very much for working on that, I always love to close bugs
that are 20 years old. If I can help or comment, let me know.
https://www.incluesion.de/impressum has my messenger data if you need
more direct contact.

Greetings
Marc

#248500#80
Date:
2026-09-03 14:59:09 UTC
From:
To:
Hi Marc,

I have a working implementation of the counter, with tests, and running it raised four questions I would rather settle with you than guess.

1. A single counter does not survive sub-ranges

The naive reading of "store the last uid and increment from there" breaks when a range is requested explicitly:

  # REUSE_IDS=no
  adduser --firstuid 5000 --lastuid 5010 rhigh     # gets 5000, counter=5000
  addgroup --firstgid 2090 --lastgid 2093 rlow
  warn: No GID is available in the range 5001-2093 (FIRST_GID - LAST_GID)
  fatal: The group `rlow' was not created.

The 2090-2093 range is completely empty, but the counter of the default range pushed the floor above its ceiling.

My fix is to make the range part of the state file key, so each range carries its own counter:

  @global:last_id_1000_59999=1006:last_sys_id_100_999=104

The guarantee becomes "an id is not handed out twice within the range it came from". Does that match what you had in mind, or would you rather have a single counter with a different rule for sub-ranges?

2. Should an explicit range turn off the automatism?

You wrote that for --uid "the wish of the local admin wins and turns off the automatism". Asking for --firstuid/--lastuid looks like the same kind of explicit wish to me, and treating it that way would also leave firstlastuidgid.t untouched. But it does mean that an admin who always carves ranges never gets the guarantee. Your call.

3. The default is more expensive than it looks

With the counter always on, 122 assertions fail in firstlastuidgid.t, firstlastuidgid_orig.t, uidgidpool.t and suidsgidpool.t. They are not stale: they encode the current contract, that adduser picks the first free id of the range. After any deletion that stops being true.

So I currently default REUSE_IDS to yes, which leaves every existing installation and the whole test suite untouched, and lets an admin opt in. Flipping it to no is a one-word change plus a NEWS.Debian entry, but it needs those tests adapted. Which do you prefer?

I did not want to rewrite assertions in firstlastuidgid*.t anyway, since you are reworking that file in wip/new-firstlastuidgid.

4. Where should the counter live inside the state file?

The store is keyed by user name and a counter belongs to no user. Rather than storing it under a fake user name, I taught _read_state() and _write_state() about a reserved '@global' record, so delete_state_user() can not take the counters with it. That touches the format of your module, so tell me if you prefer something else.

5. Is a second knob wanted at all?

You never mentioned configuration, so this may be over-engineering on my side: I added ID_COUNTER_INIT=first_free|highest, because starting at the first free id (as you suggested) cannot cover accounts that were deleted before the state file existed, which was Aaron Hall's unanswered question from 2006. If you would rather not have that option, I will drop it.

Unrelated to this bug, I also came across a couple of small things while reading the code (a duplicated exit code value that contradicts the manpage, among others). I will file those separately rather than mixing them in here.

#248500#85
Date:
2026-09-04 12:33:25 UTC
From:
To:
Hi Juan,

I would say "if a range is explicitly requested, that overrides
everything and the accounts gets the first free uid in that range
without state being updated".

That would also help to keep the state file small.

Yes, it should turm off the automatism.

I would offer to adapt the tests to save you from that tedious work. I
think that's worth it.

That rework is done, I am fine with the -old file going away now instead
of spending an hour fixing it. The new test was vibecoded and I kept the
old one around to be sure. I think I am now confident enough that the
new test is fine.

@global, _adduser, _adduser-internal, _internal or any other string that
doesn't constitute a valid user name is fine.

I like a gold plate on code, so if it's already there, go ahead. It's a
bit ugly that we now have a config option that is only used once on
adduser's first run.

Yes, please.

Greetings
Marc

#248500#90
Date:
2026-09-10 02:30:16 UTC
From:
To:
Hi,

I have opened a merge request implementing this, following the decisions
Marc took in this log:

https://salsa.debian.org/debian/adduser/-/merge_requests/144

adduser now keeps the highest uid and gid it has handed out in a reserved
@global record in /var/lib/adduser/state, and does not hand an id to a new
account if it has already been used once. The counter is written inside the
existing lock and only when the run succeeds.