[Pkg-shadow-devel] [Git][debian/adduser][debian/latest] 8 commits: move firstlastuidgid away for rewritten version

Marc Haber (@zugschlus) gitlab at salsa.debian.org
Wed Jul 22 05:59:33 BST 2026



Marc Haber pushed to branch debian/latest at Debian / adduser


Commits:
382556ca by Marc Haber at 2026-06-28T14:58:35+02:00
move firstlastuidgid away for rewritten version

Git-Dch: ignore

- - - - -
b3684896 by Marc Haber at 2026-07-13T13:45:10+02:00
add range_intersection(), first_avail_?id

Git-Dch: ignore

- - - - -
cdac3790 by Marc Haber at 2026-07-13T13:49:58+02:00
try hard to align UID and GID of user and usergroup

This is a big re-work of the uid and gid selection mechanism.

Thanks: C. Gatzemeier, Ian Jackson
Closes: #583976, #453086

- - - - -
8ef59659 by Marc Haber at 2026-07-13T13:58:31+02:00
rename autopkgtest suite for more clarity in logs

Git-Dch: ignore

- - - - -
6a25889a by Marc Haber at 2026-07-13T14:01:51+02:00
adapt test to new uid/gid selection behavior

Git-Dch: ignore

- - - - -
7106013b by Marc Haber at 2026-07-13T14:02:21+02:00
add regression tests for the new uid/gid functions

Those tests are the reason for the functions moving to AdduserCommon so
that they can simply be use'd here.

Git-Dch: ignore

- - - - -
d1d8792f by Marc Haber at 2026-07-13T14:04:42+02:00
disable tests that don't fit any more

This file is just being kept around to be sure that the new programmed
tests have the same coverage, it will go away eventually.

Git-Dch: ignore

- - - - -
06ac8af4 by Marc Haber at 2026-07-13T14:05:24+02:00
completely rewritten test for firstlastuidgid

Git-Dch: ignore

- - - - -


8 changed files:

- AdduserCommon.pm
- adduser
- debian/tests/control
- debian/tests/f/firstlastuidgid.t
- + debian/tests/f/firstlastuidgid_orig.t
- + debian/tests/f/uidgid_functions.t
- debian/tests/f/uidgidpool.t
- doc/adduser.8


Changes:

=====================================
AdduserCommon.pm
=====================================
@@ -153,6 +153,10 @@ use constant {
     'LOGMSGDEFLEVEL',
     'existing_user_status',
     'existing_group_status',
+    'range_intersection',
+    'first_avail_uid',
+    'first_avail_gid',
+    'first_avail_uid_gid',
 );
 
 sub sanitize_string {
@@ -195,6 +199,120 @@ sub sanitize_string {
     }
 }
 
+# range_intersection: given two closed integer ranges [min1,max1] and
+# [min2,max2], return the closed range that is their intersection, as
+# (floor, ceiling). The floor is the higher of the two starts, the
+# ceiling is the lower of the two ends. If the ranges do not overlap,
+# floor > ceiling is returned; callers must check for this themselves
+# (callers in adduser pass the result straight to first_avail_uid_gid,
+# whose scan loop naturally returns -1 when min > max).
+# parameters:
+#   min1, max1: first range
+#   min2, max2: second range
+# return values:
+#   (floor, ceiling) of the intersection
+sub range_intersection {
+    my ($min1, $max1, $min2, $max2) = @_;
+    my $floor   = ($min1 > $min2) ? $min1 : $min2;
+    my $ceiling = ($max1 < $max2) ? $max1 : $max2;
+    return ($floor, $ceiling);
+}
+
+# first_avail_uid: return the first available uid in given range
+# parameters:
+#   min, max: the range
+#   pool_id: user id suggested from pool
+#   preferred: an id to try first, if it falls in [min,max] and is
+#     actually free (checked the same way as every id in the range
+#     scan, i.e. honoring the reserved uid pool). Used by --ingroup:
+#     if the group's GID is also free as a UID, prefer it, instead
+#     of always taking the lowest free UID in range regardless of
+#     whether it happens to differ from the GID.
+#   reserved_uid_pool: hashref of ids reserved by other pending
+#     UID_POOL entries, keyed by id. May be undef or omitted, which
+#     is treated the same as an empty hashref.
+# return values:
+#   -1 if no free uid is available
+#  otherwise the choosen uid
+sub first_avail_uid {
+    my ($min, $max, $pool_id, $preferred, $reserved_uid_pool) = @_;
+    $reserved_uid_pool //= {};
+    if (defined ($pool_id)) {
+        return $pool_id if (!defined(getpwuid($pool_id)));
+        return -1;
+    }
+    if (defined($preferred) && $preferred >= $min && $preferred <= $max
+        && !exists($reserved_uid_pool->{$preferred}) && !defined(getpwuid($preferred))) {
+        log_trace( "first_avail_uid: preferred id %d is free, using it", $preferred );
+        return $preferred;
+    }
+    log_info( mtx("Selecting UID from range %d to %d ...\n"),$min,$max);
+
+    my $t = $min;
+    while ($t <= $max) {
+       return $t if (!exists($reserved_uid_pool->{$t}) and !defined(getpwuid($t)));
+       $t++;
+    }
+    return -1; # nothing available
+}
+
+# first_avail_gid: return the first available gid in given range
+# parameters:
+#   min, max: the range
+#   pool_id: group id suggested from pool
+#   reserved_gid_pool: hashref of ids reserved by other pending
+#     GID_POOL entries, keyed by id. May be undef or omitted, which
+#     is treated the same as an empty hashref.
+# return values:
+#   -1 if no free gid is available
+#   otherwise the choosen gid
+sub first_avail_gid {
+    my ($min, $max, $pool_id, $reserved_gid_pool) = @_;
+    $reserved_gid_pool //= {};
+    if (defined ($pool_id)) {
+        return $pool_id if (!defined(getgrgid($pool_id)));
+        return -1;
+    }
+    log_info( mtx("Selecting GID from range %d to %d ..."),$min,$max);
+
+    my $t = $min;
+    while ($t <= $max) {
+       return $t if (!exists($reserved_gid_pool->{$t}) and !defined(getgrgid($t)));
+       $t++;
+    }
+    return -1; # nothing available
+}
+
+# first_avail_uid_gid: return the first available id in given range
+#     that is both available as uid and gid
+# parameters:
+#   min, max: the range
+#   pool_id: user id suggested from pool
+#   reserved_uid_pool, reserved_gid_pool: hashrefs of ids reserved by
+#     other pending UID_POOL/GID_POOL entries, keyed by id. May be
+#     undef or omitted, which is treated the same as an empty hashref.
+# return values:
+#   -1 if no free id is available
+#   otherwise the choosen id
+sub first_avail_uid_gid {
+    my ($min, $max, $pool_id, $reserved_uid_pool, $reserved_gid_pool) = @_;
+    $reserved_uid_pool //= {};
+    $reserved_gid_pool //= {};
+    if (defined ($pool_id)) {
+        return $pool_id if (!defined(getgrgid($pool_id)));
+        return -1;
+    }
+    log_info( mtx("Selecting UID/GID from range %d to %d ..."), $min, $max );
+
+    my $t = $min;
+    while ($t <= $max) {
+       return $t if (!exists($reserved_uid_pool->{$t}) && !exists($reserved_gid_pool->{$t}) &&
+                     !defined(getgrgid($t)) && !defined(getpwuid($t)));
+       $t++;
+    }
+    return -1; # nothing available
+}
+
 
 sub egetgrnam {
     my ($name) = @_;


=====================================
adduser
=====================================
@@ -491,7 +491,8 @@ if ($action eq "addsysgroup") {
         $last_gid = $new_lastgid || $config{"last_system_gid"};
         $gid_option = &first_avail_gid($first_gid,
                            $last_gid,
-                           $gid_pool{$new_name}{'id'});
+                           $gid_pool{$new_name}{'id'},
+                           \%reserved_gid_pool);
         if ($gid_option == -1) {
             log_warn( mtx("No GID is available in the range %d-%d (FIRST_SYS_GID - LAST_SYS_GID)."),
                 $first_gid, $last_gid );
@@ -536,7 +537,8 @@ if ($action eq "addgroup") {
             );
         $gid_option = &first_avail_gid($first_gid,
                            $last_gid,
-                           $gid_pool{$new_name}{'id'});
+                           $gid_pool{$new_name}{'id'},
+                           \%reserved_gid_pool);
 
         if ($gid_option == -1) {
             log_warn( mtx("No GID is available in the range %d-%d (FIRST_GID - LAST_GID)."),
@@ -622,29 +624,53 @@ if ($action eq "addsysuser") {
     check_user_group(1);
 
     if (!defined($new_uid) && $make_group_also) {
-        $new_uid = &first_avail_uid($new_firstuid || $config{"first_system_uid"},
-                                    $new_lastuid || $config{"last_system_uid"},
-                                    $uid_pool{$new_name}{'id'});
+        my $first_sys_uid = $new_firstuid || $config{"first_system_uid"};
+        my $last_sys_uid  = $new_lastuid  || $config{"last_system_uid"};
+        my $first_sys_gid = $new_firstgid || $config{"first_system_gid"};
+        my $last_sys_gid  = $new_lastgid  || $config{"last_system_gid"};
+        my ($first_uidgid, $last_uidgid) = &range_intersection(
+            $first_sys_uid, $last_sys_uid, $first_sys_gid, $last_sys_gid);
+        # TODO: Check what happens when those ranges do not overlap
+        $new_uid = &first_avail_uid_gid( $first_uidgid,
+                                         $last_uidgid,
+                                         $uid_pool{$new_name}{'id'},
+                                         \%reserved_uid_pool,
+                                         \%reserved_gid_pool);
+        log_trace( "sys uidgid=%s, from first_uidgid %s, last_uidgid %s", $new_uid, $first_uidgid, $last_uidgid);
         if ($new_uid == -1) {
             log_warn( mtx("No UID/GID pair is available in the range %d-%d (FIRST_SYS_UID - LAST_SYS_UID)."),
-                $config{"first_system_uid"},
-                $config{"last_system_uid"} );
+                $first_uidgid,
+                $last_uidgid );
             log_fatal( mtx("The user `%s' was not created."), $new_name );
             exit( RET_NO_ID_IN_RANGE );
         }
-        $gid_option = &first_avail_gid($new_firstgid || $config{"first_system_gid"},
-                                       $new_lastgid || $config{"last_system_gid"},
-                                       $gid_pool{$new_name}{'id'});
+        $gid_option = $new_uid;
         $ingroup_name = $new_name;
     }
     elsif (!defined($new_uid) && !$make_group_also) {
-        $new_uid = &first_avail_uid($new_firstuid || $config{"first_system_uid"},
-                                    $new_lastuid || $config{"last_system_uid"},
-                                    $uid_pool{$new_name}{'id'});
+        my $first_sys_uid = $new_firstuid || $config{"first_system_uid"};
+        my $last_sys_uid  = $new_lastuid  || $config{"last_system_uid"};
+        # if the primary group is already known by GID (--gid) or by
+        # name (--ingroup), and that GID happens to also be free as a
+        # UID, prefer it, so the user ends up with matching UID/GID
+        # even though it was not created as a usergroup. Falls back
+        # to the lowest free UID in range if the GID is not free as
+        # a UID (or is outside the UID range).
+        my $preferred_uid;
+        if (defined($gid_option)) {
+            $preferred_uid = $gid_option;
+        } elsif ($ingroup_name) {
+            $preferred_uid = egetgrnam($ingroup_name);
+        }
+        $new_uid = &first_avail_uid($first_sys_uid,
+                                    $last_sys_uid,
+                                    $uid_pool{$new_name}{'id'},
+                                    $preferred_uid,
+                                    \%reserved_uid_pool);
         if ($new_uid == -1) {
             log_warn( mtx("No UID is available in the range %d-%d (FIRST_SYS_UID - LAST_SYS_UID)."),
-                $config{"first_system_uid"},
-                $config{"last_system_uid"} );
+                $first_sys_uid,
+                $last_sys_uid );
             log_fatal( mtx("The user `%s' was not created."), $new_name);
             exit( RET_NO_ID_IN_RANGE );
         }
@@ -839,28 +865,38 @@ if ($action eq "adduser") {
     check_user_group(0);
     $first_uid = $new_firstuid || $config{"first_uid"};
     $last_uid = $new_lastuid || $config{"last_uid"};
-    if ($config{"usergroups"} =~  /yes/i) {
-        $first_gid = $first_uid;
-        $last_gid = $last_uid;
-    } else {
+    #if ($config{"usergroups"} =~  /yes/i) {
+    #    $first_gid = $first_uid;
+    #    $last_gid = $last_uid;
+    #} else {
         $first_gid = $new_firstgid || $config{"first_gid"};
         $last_gid = $new_lastgid || $config{"last_gid"};
-    }
+        #}
     log_trace( "first_uid %s, last_uid %s, first_gid %s, last_gid %s", $first_uid, $last_uid, $first_gid, $last_gid );
     log_info (gtx("Adding user `%s' ..."),$new_name);
 
     if (!defined($new_uid)) {
         if ( defined $ingroup_name ) {
+            # if the requested group's GID happens to also be free as a
+            # UID, prefer it, so the user ends up with matching UID/GID
+            # even though it was not created as a usergroup. Falls back
+            # to the lowest free UID in range if the GID is not free as
+            # a UID (or is outside the UID range).
+            my $ingroup_gid = egetgrnam($ingroup_name);
             $new_uid = &first_avail_uid( $first_uid,
                                              $last_uid,
-                                             $uid_pool{$new_name}{'id'});
+                                             $uid_pool{$new_name}{'id'},
+                                             $ingroup_gid,
+                                             \%reserved_uid_pool);
         } else {
-            my $first_uidgid = ($first_uid, $first_gid)[$first_uid > $first_gid];
-            my $last_uidgid  = ($last_uid, $last_gid)[$last_uid < $last_gid];
+            my ($first_uidgid, $last_uidgid) = &range_intersection(
+                $first_uid, $last_uid, $first_gid, $last_gid);
             # TODO: Check what happens when those ranges do not overlap
             $new_uid = &first_avail_uid_gid( $first_uidgid,
                                              $last_uidgid,
-                                             $uid_pool{$new_name}{'id'});
+                                             $uid_pool{$new_name}{'id'},
+                                             \%reserved_uid_pool,
+                                             \%reserved_gid_pool);
             log_trace( "uidgid=%s, from first_uidgid %s, last_uidgid %s", $new_uid, $first_uidgid, $last_uidgid);
         }
         # TODO: user can specify different UID and GID here.
@@ -1223,77 +1259,6 @@ sub sanitize_name {
     return "";
 }
 
-# first_avail_uid: return the first available uid in given range
-# parameters:
-#   min, max: the range
-#   pool_id: user id suggested from pool
-# return values:
-#   -1 if no free uid is available
-#  otherwise the choosen uid
-sub first_avail_uid {
-    my ($min, $max, $pool_id) = @_;
-    if (defined ($pool_id)) {
-        return $pool_id if (!defined(getpwuid($pool_id)));
-        return -1;
-    }
-    log_info( mtx("Selecting UID from range %d to %d ...\n"),$min,$max);
-
-    my $t = $min;
-    while ($t <= $max) {
-       return $t if (!exists($reserved_uid_pool{$t}) and !defined(getpwuid($t)));
-       $t++;
-    }
-    return -1; # nothing available
-}
-
-# first_avail_gid: return the first available gid in given range
-# parameters:
-#   min, max: the range
-#   pool_id: group id suggested from pool
-# return values:
-#   -1 if no free gid is available
-#   otherwise the choosen gid
-sub first_avail_gid {
-    my ($min, $max, $pool_id) = @_;
-    if (defined ($pool_id)) {
-        return $pool_id if (!defined(getgrgid($pool_id)));
-        return -1;
-    }
-    log_info( mtx("Selecting GID from range %d to %d ..."),$min,$max);
-
-    my $t = $min;
-    while ($t <= $max) {
-       return $t if (!exists($reserved_gid_pool{$t}) and !defined(getgrgid($t)));
-       $t++;
-    }
-    return -1; # nothing available
-}
-
-# first_avail_uid_gid: return the first available id in given range
-#     that is both available as uid and gid
-# parameters:
-#   min, max: the range
-#   pool_id: user id suggested from pool
-# return values:
-#   -1 if no free id is available
-#   otherwise the choosen id
-sub first_avail_uid_gid {
-    my ($min, $max, $pool_id) = @_;
-    if (defined ($pool_id)) {
-        return $pool_id if (!defined(getgrgid($pool_id)));
-        return -1;
-    }
-    log_info( mtx("Selecting UID/GID from range %d to %d ..."), $min, $max );
-
-    my $t = $min;
-    while ($t <= $max) {
-       return $t if (!exists($reserved_uid_pool{$t}) && !exists($reserved_gid_pool{$t}) &&
-                     !defined(getgrgid($t)) && !defined(getpwuid($t)));
-       $t++;
-    }
-    return -1; # nothing available
-}
-
 sub ch_comment {
     my ($name, $comment) = @_;
     my $usermod = &which('usermod');


=====================================
debian/tests/control
=====================================
@@ -4,7 +4,7 @@
 Test-Command: /usr/bin/prove -v debian/tests/f
 Depends: @, cron, perl, login
 Restrictions: needs-root
-Features: test-name=package-test-suite
+Features: test-name=debian-tests-f
 
 Test-Command: /usr/bin/prove -p -v debian/tests/ecryptfs
 Depends: cron, ecryptfs-utils, kmod, perl


=====================================
debian/tests/f/firstlastuidgid.t
=====================================
@@ -21,17 +21,10 @@ my $funame = 'fixedu01';
 my $fgname = 'fixedg01';
 my $user;
 my $userbase;
-my $fuid;
 my $group;
 my $groupbase;
-my $fgid;
 my $prefix;
 
-my $firstuid1;
-my $lastuid1;
-my $firstgid1;
-my $lastgid1;
-
 sub cleanup {
     my $prefix=$_[0] || '';
     foreach $userbase( @unames ) {
@@ -57,546 +50,548 @@ sub cleanup {
 }
 cleanup();
 
-# test group U1A: default config, no command line
-
-$prefix = 'u1a';
-$fuid = 3750;
-$fgid = 3761;
-my %confhash=();
-apply_config_hash(\%confhash);
-
-foreach $groupbase( @gnames ) {
-    $group = $prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet, $group);
-    assert_group_exists($group);
-    if ($gidcount==0) {
-        $gidcount=((getgrnam($group))[2]);
+# ------------------------------------------------------------
+# Helpers for the UID/GID intersection-search and --ingroup/--gid
+# preferred-uid test groups (U5A onwards). These blocks all follow
+# one of three shapes; pulling each shape into a function removes
+# the need to retype the same addgroup/adduser/assert sequence with
+# only the numbers and a couple of flags changed.
+# ------------------------------------------------------------
+
+# assert_intersection_search_success: create @unames as a usergroup
+# (one adduser call per name, each creating its own same-named
+# group) using the given uid/gid ranges, optionally as system users
+# with --group, and assert the resulting uids/gids are sequential
+# starting at expect_first. Used by the U5A/U6A/S5A/S6A-style
+# "the combined search picks the right intersection floor" tests.
+#
+# Args (hashref): prefix, system (bool), firstuid, lastuid,
+#   firstgid, lastgid, expect_first.
+sub assert_intersection_search_success {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my @sysopt = $a->{system} ? ('--system', '--group') : ();
+    my $uidcount = $a->{expect_first};
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
+            '--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid},
+            $user);
+        assert_user_exists($user);
+        assert_group_exists($user);
+        assert_primary_group_membership_exists($user, $user);
+        assert_user_has_uid($user, $uidcount);
+        assert_group_has_gid($user, $uidcount);
+        $uidcount++;
     }
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user = $prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
+    cleanup($prefix);
+}
+
+# assert_intersection_search_failure: same usergroup setup, but with
+# uid/gid ranges that do not overlap at all, so adduser must fail
+# outright and create neither the user nor the group. Used by the
+# U7A/S6A-style "non-overlapping ranges must fail" tests.
+#
+# Args (hashref): prefix, system (bool), firstuid, lastuid,
+#   firstgid, lastgid.
+sub assert_intersection_search_failure {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my @sysopt = $a->{system} ? ('--system', '--group') : ();
+    assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+        @sysopt,
         '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    if ($uidcount==0) {
-        $uidcount=((getpwnam($user))[2]);
+        '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
+        '--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid},
+        $prefix.$unamex);
+    assert_user_does_not_exist($prefix.$unamex);
+    assert_group_does_not_exist($prefix.$unamex);
+    cleanup($prefix);
+}
+
+# assert_preferred_uid: create a group with a fixed gid (system or
+# not), optionally pre-occupy that gid as a uid via a separate fixed
+# user (to provoke the fallback path), then create one new user
+# joining that group either by name (--ingroup) or by numeric id
+# (--gid), within the given uid range, and assert the uid it ends
+# up with. Used by the U8A/U9A/U10A/S7A/S8A/S9A/S10A-style
+# preferred-uid hit/fallback/out-of-range tests.
+#
+# Args (hashref): prefix, system (bool), firstuid, lastuid, fgid,
+#   join_by_gid (bool: false to join with --ingroup GROUPNAME, true
+#     to join with --gid GID -- only meaningful in combination with
+#     system, since only adduser --system supports the --gid form
+#     of this feature),
+#   occupy_gid_as_uid (bool: if true, first create a fixed user
+#     occupying the group's gid as a uid, to provoke the fallback
+#     path),
+#   expect_uid (the uid the new user is expected to end up with).
+sub assert_preferred_uid {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my @sysopt = $a->{system} ? ('--system') : ();
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        @sysopt,
+        '--gid', $a->{fgid},
+        $prefix.$fgname);
+    assert_group_exists($prefix.$fgname);
+    assert_group_has_gid($prefix.$fgname, $a->{fgid});
+    if ($a->{occupy_gid_as_uid}) {
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            '--uid', $a->{fgid}, '--gid', $a->{fgid},
+            $prefix.$funame);
+        assert_user_exists($prefix.$funame);
+        assert_user_has_uid($prefix.$funame, $a->{fgid});
     }
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='u1a2';
-foreach $userbase( @unames ) {
-    $user = $prefix.$userbase;
+    my @joinopt = $a->{join_by_gid} ? ('--gid', $a->{fgid}) : ('--ingroup', $prefix.$fgname);
     assert_command_success('/usr/sbin/adduser', @quiet,
+        @sysopt,
+        @joinopt,
         '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_group_exists($user);
-    if ($uidcount==0) {
-        $uidcount=((getpwnam($user))[2]);
-    }
-    if ($gidcount==0) {
-        $gidcount=((getgrnam($user))[2]);
+        '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
+        $prefix.$unames[0]);
+    assert_user_exists($prefix.$unames[0]);
+    assert_primary_group_membership_exists($prefix.$unames[0], $prefix.$fgname);
+    assert_user_has_uid($prefix.$unames[0], $a->{expect_uid});
+    cleanup($prefix);
+}
+
+# assert_usergroup_loop: create @gnames as plain groups using
+# --firstgid (the last one's gid is captured as $gidcount and
+# returned), plus one fixed-gid group; then create @unames joining
+# the first of those groups via --ingroup, using --firstuid; the
+# first such user may get the group's gid preferred as its uid (if
+# it is free and in range -- it always is here, since no
+# --lastuid/--lastgid is given), the rest get sequential uids from
+# firstuid; finally create one fixed-uid user outside any group
+# loop. Used by the U2A/U3A/U4A/S2A/S3A/S4A-style tests, which only
+# differ in whether the ranges come from the command line, the
+# config file, or both, and in whether --system is used throughout.
+#
+# Args (hashref): prefix, system (bool), firstuid, firstgid, fuid,
+#   fgid, via_cli (bool, defaults to true: whether --firstuid/
+#   --firstgid are actually passed on the command line, vs. relying
+#   on a value already present in the config file).
+sub assert_usergroup_loop {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my $firstuid = $a->{firstuid};
+    my $firstgid = $a->{firstgid};
+    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
+    my @sysopt = $a->{system} ? ('--system') : ();
+    my @gidopt = $via_cli ? ('--firstgid', $firstgid) : ();
+    my @uidopt = $via_cli ? ('--firstuid', $firstuid) : ();
+    my $gidcount = $firstgid;
+    foreach my $groupbase ( @gnames ) {
+        my $group = $prefix.$groupbase;
+        assert_command_success('/usr/sbin/addgroup', @quiet,
+            @sysopt,
+            @gidopt,
+            $group);
+        assert_group_exists($group);
+        assert_group_has_gid($group, $gidcount);
+        $gidcount++;
     }
-    assert_primary_group_membership_exists($user, $user);
-    assert_user_has_uid($user, $uidcount);
-    assert_group_has_gid($user, $gidcount);
-    $uidcount++;
-    $gidcount++;
-}
-cleanup($prefix);
-
-# test group U2A: default config, uid/gid range requested on command line
-
-$prefix='u2a';
-$firstuid1=2000;
-$firstgid1=2100;
-$fuid = 3750;
-$fgid = 3761;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
+    assert_group_does_not_exist($prefix.$fgname);
     assert_command_success('/usr/sbin/addgroup', @quiet,
-       '--firstgid', $firstgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_group_does_not_exist($prefix.$fgname);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--firstgid', $firstgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
+        @sysopt,
+        @gidopt,
+        '--gid', $a->{fgid},
+        $prefix.$fgname);
+    assert_group_exists($prefix.$fgname);
+    assert_group_has_gid($prefix.$fgname, $a->{fgid});
+
+    my $uidcount = $firstuid;
+    my $user_index = 0;
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--ingroup', $prefix.$gnames[0],
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            @uidopt,
+            $user);
+        assert_user_exists($user);
+        assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+        if ($user_index == 0) {
+            # The group's GID is free as a UID and inside the search
+            # range, so adduser prefers it as the new user's UID.
+            # Query the group's actual GID to be robust against it
+            # having landed somewhere other than $firstgid.
+            my $group_gid = (getgrnam($prefix.$gnames[0]))[2];
+            assert_user_has_uid($user, $group_gid);
+            # Seed $uidcount for subsequent users: the preferred GID
+            # is now taken, so adduser falls back to scanning from
+            # $firstuid, skipping anything already occupied.
+            $uidcount = $firstuid;
+            while (defined(getpwuid($uidcount))) {
+                $uidcount++;
+            }
+        } else {
+            assert_user_has_uid($user, $uidcount);
+            $uidcount++;
+            while (defined(getpwuid($uidcount))) {
+                $uidcount++;
+            }
+        }
+        $user_index++;
+    }
     assert_command_success('/usr/sbin/adduser', @quiet,
+        @sysopt,
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        @uidopt,
+        '--uid', $a->{fuid},
+        $prefix.$funame);
+    assert_user_exists($prefix.$funame);
+    assert_user_has_uid($prefix.$funame, $a->{fuid});
+    cleanup($prefix);
+}
+
+# assert_usergroup_loop_to_exhaustion: same shape as
+# assert_usergroup_loop, but with a uid range and a gid range each
+# exactly as wide as @gnames/@unames, so the range fills up exactly
+# and one further attempt (creating $gnamex / $unamex) must fail.
+# Because the gid range and uid range given here are always
+# disjoint by construction, the group's gid is never a candidate
+# for the uid search, so (unlike assert_usergroup_loop) every user
+# in the loop gets a plain sequential uid, with no preferred-uid
+# special case needed for the first one. Used by the
+# U2L/U3L/U4L/S2L/S3L/S4L-style tests.
+#
+# Args (hashref): prefix, system (bool), firstuid, lastuid,
+#   firstgid, lastgid, fuid, fgid, via_cli (bool, defaults to true:
+#   whether --firstuid/--lastuid/--firstgid/--lastgid are actually
+#   passed on the command line, vs. relying on values already
+#   present in the config file).
+sub assert_usergroup_loop_to_exhaustion {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my $system = $a->{system};
+    my $fuid = $a->{fuid};
+    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
+    my @sysopt = $system ? ('--system') : ();
+    my @uidrange = $via_cli ? ('--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid}) : ();
+    my @gidrange = $via_cli ? ('--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid}) : ();
+
+    my $gidcount = $a->{firstgid};
+    foreach my $groupbase ( @gnames ) {
+        my $group = $prefix.$groupbase;
+        assert_command_success('/usr/sbin/addgroup', @quiet,
+            @sysopt, @gidrange,
+            $group);
+        assert_group_exists($group);
+        assert_group_has_gid($group, $gidcount);
+        $gidcount++;
+    }
+    assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+        @sysopt, @gidrange,
+        $prefix.$gnamex);
+    assert_group_does_not_exist($prefix.$gnamex);
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        @sysopt, @gidrange,
+        '--gid', $a->{fgid},
+        $prefix.$fgname);
+    assert_group_exists($prefix.$fgname);
+    assert_group_has_gid($prefix.$fgname, $a->{fgid});
+
+    my $uidcount = $a->{firstuid};
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--ingroup', $prefix.$gnames[0],
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            @uidrange,
+            $user);
+        assert_user_exists($user);
+        assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+        assert_user_has_uid($user, $uidcount);
+        $uidcount++;
+    }
+    assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+        @sysopt,
         '--ingroup', $prefix.$gnames[0],
         '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, 
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, 
-    '--uid', $fuid,
-   $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='u2a2';
-$firstuid1=2000;
-$fuid = 3750;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
+        @uidrange,
+        $prefix.$unamex);
+    assert_user_does_not_exist($prefix.$unamex);
+    if ($system) {
+        assert_command_success_silent('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            @uidrange,
+            '--uid', $fuid,
+            $prefix.$funame);
+    } else {
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            @uidrange,
+            '--uid', $fuid,
+            $prefix.$funame);
+    }
+    assert_user_exists($prefix.$funame);
+    assert_user_has_uid($prefix.$funame, $fuid);
+    cleanup($prefix);
+}
+
+# assert_plain_usergroup_loop: create @unames as plain usergroups
+# (one adduser call per name, no --ingroup, no group loop), and
+# assert sequential matching uid==gid for each, starting at
+# firstuid. By default firstuid is passed via --firstuid on the
+# command line; pass a false via_cli to omit the flag and rely on
+# whatever FIRST_UID is already in the config file instead (used by
+# the U3A2/S3A2-style tests, which deliberately test that a
+# leftover config value, with no command-line override at all,
+# still produces the expected uid). Used by the
+# U2A2/U3A2/U4A2/S2A2/S3A2/S4A2-style tests.
+#
+# Args (hashref): prefix, system (bool), firstuid, via_cli (bool,
+#   defaults to true).
+sub assert_plain_usergroup_loop {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my $system = $a->{system};
+    my $firstuid = $a->{firstuid};
+    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
+    my @sysopt = $system ? ('--system') : ();
+    my @uidopt = $via_cli ? ('--firstuid', $firstuid) : ();
+    my $expected_uidgid = exists($a->{expected_uidgid}) ? $a->{expected_uidgid} : $firstuid;
+    assert_command_success("true plain_usergroup_loop firstuid $firstuid expected_uidgid $expected_uidgid");
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            @uidopt,
+            $user);
+        assert_user_exists($user);
+        if ($system) {
+            assert_primary_group_membership_exists($user, 'nogroup');
+        } else {
+            assert_group_exists($user);
+            assert_primary_group_membership_exists($user, $user);
+            assert_group_has_gid($user, $expected_uidgid);
+        }
+        assert_user_has_uid($user, $expected_uidgid);
+        $expected_uidgid++;
+    }
+    cleanup($prefix);
+}
+
+# assert_usergroup_loop_dynamic: same overall shape as
+# assert_usergroup_loop (group loop + --ingroup user loop + one
+# fixed-uid user), but for the case where no --firstuid/--firstgid
+# is given at all and the config is left at its default: the actual
+# first uid/gid assigned is whatever the system picks, captured
+# from the first created group/user rather than asserted up front.
+# For system users, the default system id ranges are typically
+# densely populated by real system accounts, so each subsequent id
+# is found by skipping forward past any id that turns out to
+# already be taken (mirroring what adduser's own search would do),
+# rather than assuming a plain ++ always lands on a free slot. Used
+# by the U1A/S1A-style tests.
+#
+# Args (hashref): prefix, system (bool), fuid, fgid.
+sub assert_usergroup_loop_dynamic {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my $system = $a->{system};
+    my @sysopt = $system ? ('--system') : ();
+    my $gidcount = 0;
+    foreach my $groupbase ( @gnames ) {
+        my $group = $prefix.$groupbase;
+        assert_command_success('/usr/sbin/addgroup', @quiet, @sysopt, $group);
+        assert_group_exists($group);
+        if ($gidcount == 0) {
+            $gidcount = ((getgrnam($group))[2]);
+        }
+        assert_group_has_gid($group, $gidcount);
+        if ($system) {
+            while (defined(getgrgid($gidcount))) {
+                $gidcount++;
+            }
+        } else {
+            $gidcount++;
+        }
+    }
+    # this is optimistcally assuming that fgid is also free as a uid.
+    # we error out if we were too optimistic.
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        @sysopt,
+        '--gid', $a->{fgid},
+        $prefix.$fgname);
+    assert_group_exists($prefix.$fgname);
+    assert_group_has_gid($prefix.$fgname, $a->{fgid});
+    assert_uid_does_not_exist($a->{fgid});
+
+    my $uidcount = 0;
+    my $first_group_gid = ((getgrnam($prefix.$gnames[0]))[2]);
+    my $user_index = 0;
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--ingroup', $prefix.$fgname,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            $user);
+        assert_user_exists($user);
+        assert_primary_group_membership_exists($user, $prefix.$fgname);
+        if ($user_index == 0) {
+            # the group's gid is free as a uid and inside the default
+            # search range, so it is preferred over the lowest free uid
+            assert_user_has_uid($user, $a->{fgid});
+        } else {
+            if ($uidcount == 0) {
+                # capture the second user's real uid: after the first
+                # user took the preferred gid, the fallback scan
+                # restarts from FIRST_UID, which may not be sequential
+                # with respect to the first user's uid at all
+                $uidcount = ((getpwnam($user))[2]);
+            }
+            assert_user_has_uid($user, $uidcount);
+            if ($system) {
+                while (defined(getpwuid($uidcount))) {
+                    $uidcount++;
+                }
+            } else {
+                $uidcount++;
+            }
+        }
+        $user_index++;
+    }
     assert_command_success('/usr/sbin/adduser', @quiet,
+        @sysopt,
         '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1,
-        $user);
-    assert_user_exists($user);
-    assert_group_exists($user);
-    assert_primary_group_membership_exists($user, $user);
-    assert_user_has_uid($user, $uidcount);
-    assert_group_has_gid($user, $uidcount);
-    $uidcount++;
+        '--uid', $a->{fuid},
+        $prefix.$funame);
+    assert_user_exists($prefix.$funame);
+    assert_user_has_uid($prefix.$funame, $a->{fuid});
+    cleanup($prefix);
+}
+
+
+# assert_plain_usergroup_loop_dynamic: dynamic-capture counterpart
+# of assert_plain_usergroup_loop, for the case where no --firstuid
+# is given at all. Captures the uid and gid of the first created
+# user independently (rather than assuming they are equal from the
+# start, the way assert_plain_usergroup_loop does), since this
+# predates the combined uid/gid search guarantee and was written
+# without relying on it. Used by the U1A2/S1A2-style tests.
+#
+# Args (hashref): prefix, system (bool).
+sub assert_plain_usergroup_loop_dynamic {
+    my ($a) = @_;
+    my $prefix = $a->{prefix};
+    my $system = $a->{system};
+    my @sysopt = $system ? ('--system') : ();
+    my $uidcount = 0;
+    my $gidcount = 0;
+    foreach my $userbase ( @unames ) {
+        my $user = $prefix.$userbase;
+        assert_command_success('/usr/sbin/adduser', @quiet,
+            @sysopt,
+            '--comment', '""', '--disabled-password', '--no-create-home',
+            $user);
+        assert_user_exists($user);
+        if ($uidcount == 0) {
+            $uidcount = ((getpwnam($user))[2]);
+        }
+        if ($system) {
+            assert_primary_group_membership_exists($user, 'nogroup');
+            assert_user_has_uid($user, $uidcount);
+            while (defined(getpwuid($uidcount))) {
+                $uidcount++;
+            }
+        } else {
+            assert_group_exists($user);
+            if ($gidcount == 0) {
+                $gidcount = ((getgrnam($user))[2]);
+            }
+            assert_primary_group_membership_exists($user, $user);
+            assert_user_has_uid($user, $uidcount);
+            assert_group_has_gid($user, $gidcount);
+            $uidcount++;
+            $gidcount++;
+        }
+    }
+    cleanup($prefix);
 }
-cleanup($prefix);
+
+# test group U1A: default config, no command line
+
+my %confhash=();
+apply_config_hash(\%confhash);
+assert_command_success("true u1a: empty config file");
+assert_usergroup_loop_dynamic({prefix => 'u1a', system => 0, fuid => 3750, fgid => 3761});
+
+assert_command_success("true u1a2: empty config file");
+assert_plain_usergroup_loop_dynamic({prefix => 'u1a2', system => 0});
+
+# test group U2A: default config, uid/gid range requested on command line
+
+assert_command_success("true u2a: empty config file");
+assert_usergroup_loop({prefix => 'u2a', system => 0, firstuid => 6000, firstgid => 6050, fuid => 6900, fgid => 6910});
+
+assert_command_success("true u2a2: empty config file");
+assert_plain_usergroup_loop({prefix => 'u2a2', system => 0, firstuid => 2000});
 
 # test group U3A: uid range requested by config, no command line
 
-$prefix='u3a';
-$firstuid1=2000;
-$firstgid1=2100;
-$fuid = 3750;
-$fgid = 3761;
 %confhash=();
-$confhash{'FIRST_UID'}="$firstuid1";
-$confhash{'FIRST_GID'}="$firstgid1";
+$confhash{'FIRST_UID'}="6100";
+$confhash{'FIRST_GID'}="6150";
 apply_config_hash(\%confhash);
+assert_command_success("true u3a: FIRST_UID=6100 FIRST_GID=6150");
+assert_usergroup_loop({prefix => 'u3a', system => 0, firstuid => 6100, firstgid => 6150, fuid => 6920, fgid => 6930, via_cli => 0});
 
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet, $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='u3a2';
-$firstuid1=2000;
-$fuid = 3750;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_group_exists($user);
-    assert_primary_group_membership_exists($user, $user);
-    assert_user_has_uid($user, $uidcount);
-    assert_group_has_gid($user, $uidcount);
-    $uidcount++;
-}
-cleanup($prefix);
+assert_command_success("true u3a2: FIRST_UID=6100 FIRST_GID=6150");
+assert_plain_usergroup_loop({prefix => 'u3a2', system => 0, firstuid => 6100, expected_uidgid => 6150, via_cli => 0});
 
 # test group U4A: ranges requested by config, overriden by command line
 
-$prefix='u4a';
-$firstuid1=2200;
-$firstgid1=2300;
-$fuid = 3750;
-$fgid = 3761;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-      '--firstgid', $firstgid1,
-      $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--firstgid', $firstgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, 
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, 
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='u4a2';
-$firstuid1=2000;
-$fuid = 3750;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1,
-        $user);
-    assert_user_exists($user);
-    assert_group_exists($user);
-    assert_primary_group_membership_exists($user, $user);
-    assert_user_has_uid($user, $uidcount);
-    assert_group_has_gid($user, $uidcount);
-    $uidcount++;
-}
-cleanup($prefix);
+assert_usergroup_loop({prefix => 'u4a', system => 0, firstuid => 6200, firstgid => 6250, fuid => 6940, fgid => 6950});
+
+assert_plain_usergroup_loop({prefix => 'u4a2', system => 0, firstuid => 6200});
 
 %confhash=();
 apply_config_hash(\%confhash);
 
 # test group S1A: default config, no command line
 
-$prefix = 's1a';
-$fuid = 200;
-$fgid = 210;
 %confhash=();
 apply_config_hash(\%confhash);
+assert_usergroup_loop_dynamic({prefix => 's1a', system => 1, fuid => 200, fgid => 210});
 
-foreach $groupbase( @gnames ) {
-    $group = $prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet, '--system', $group);
-    assert_group_exists($group);
-    if ($gidcount==0) {
-        $gidcount=((getgrnam($group))[2]);
-    }
-    assert_group_has_gid($group, $gidcount);
-    while (defined(getgrgid($gidcount))) {
-        $gidcount++;
-    }
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user = $prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    if ($uidcount==0) {
-        $uidcount=((getpwnam($user))[2]);
-    }
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    while (defined(getpwuid($uidcount))) {
-        $uidcount++;
-    }
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='s1a2';
-foreach $userbase( @unames ) {
-    $user = $prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    if ($uidcount==0) {
-        $uidcount=((getpwnam($user))[2]);
-    }
-    assert_primary_group_membership_exists($user, 'nogroup');
-    assert_user_has_uid($user, $uidcount);
-    while (defined(getpwuid($uidcount))) {
-        $uidcount++;
-    }
-}
-cleanup($prefix);
+assert_plain_usergroup_loop_dynamic({prefix => 's1a2', system => 1});
 
 # test group S2A: default config, uid/gid range requested on command line
 
-$prefix='s2a';
-$firstuid1=220;
-$firstgid1=230;
-$fuid = 950;
-$fgid = 960;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-       '--system',
-       '--firstgid', $firstgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_group_does_not_exist($prefix.$fgname);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--firstgid', $firstgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, 
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, 
-    '--uid', $fuid,
-   $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='s2a2';
-$firstuid1=240;
-$fuid = 250;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, 'nogroup');
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-cleanup($prefix);
+assert_usergroup_loop({prefix => 's2a', system => 1, firstuid => 700, firstgid => 720, fuid => 850, fgid => 860});
+
+assert_plain_usergroup_loop({prefix => 's2a2', system => 1, firstuid => 700});
 
 # test group S3A: uid range requested by config, no command line
 
-$prefix='s3a';
-$firstuid1=260;
-$firstgid1=270;
-$fuid = 970;
-$fgid = 980;
 %confhash=();
-$confhash{'FIRST_SYSTEM_UID'}="$firstuid1";
-$confhash{'FIRST_SYSTEM_GID'}="$firstgid1";
+$confhash{'FIRST_SYSTEM_UID'}="730";
+$confhash{'FIRST_SYSTEM_GID'}="750";
 apply_config_hash(\%confhash);
+assert_command_success("true firstsysuid=730 firstsysgid=750");
+assert_usergroup_loop({prefix => 's3a', system => 1, firstuid => 730, firstgid => 750, fuid => 870, fgid => 880, via_cli => 0});
 
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', '--system', @quiet, $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='s3a2';
-$firstuid1=260;
-$fuid = 970;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, 'nogroup');
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-cleanup($prefix);
+assert_plain_usergroup_loop({prefix => 's3a2', system => 1, firstuid => 730, via_cli => 0});
 
 # test group S4A: ranges requested by config, overriden by command line
 
-$prefix='s4a';
-$firstuid1=290;
-$firstgid1=300;
-$fuid = 810;
-$fgid = 820;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-      '--system',
-      '--firstgid', $firstgid1,
-      $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--firstgid', $firstgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, 
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, 
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
-
-$prefix='s4a2';
-$firstuid1=310;
-$fuid = 830;
-$uidcount=$firstuid1;
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, 'nogroup');
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-cleanup($prefix);
+assert_usergroup_loop({prefix => 's4a', system => 1, firstuid => 760, firstgid => 780, fuid => 890, fgid => 900});
+
+assert_plain_usergroup_loop({prefix => 's4a2', system => 1, firstuid => 760});
 
 %confhash=();
 apply_config_hash(\%confhash);
@@ -607,177 +602,22 @@ apply_config_hash(\%confhash);
 # test group U1L: not applicable
 # test group U2L: default config, uid/gid range requested on command line
 
-$prefix='u2l';
-$firstuid1=2090;
-$lastuid1=2093;
-$firstgid1=2190;
-$lastgid1=2193;
-$fuid = 3750;
-$fgid = 3761;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    $prefix.$unamex);
-assert_user_does_not_exist($prefix.$unamex);
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_usergroup_loop_to_exhaustion({prefix => 'u2l', system => 0, firstuid => 2090, lastuid => 2093, firstgid => 2190, lastgid => 2193, fuid => 3750, fgid => 3761});
 
 # test group U3L: uid range requested by config, no command line
 
-$prefix='u3l';
-$firstuid1=2090;
-$lastuid1=2093;
-$firstgid1=2190;
-$lastgid1=2193;
-$fuid = 3750;
-$fgid = 3761;
 %confhash=();
-$confhash{'FIRST_UID'}="$firstuid1";
-$confhash{'FIRST_GID'}="$firstgid1";
-$confhash{'LAST_UID'}="$lastuid1";
-$confhash{'LAST_GID'}="$lastgid1";
+$confhash{'FIRST_UID'}="2090";
+$confhash{'FIRST_GID'}="2190";
+$confhash{'LAST_UID'}="2093";
+$confhash{'LAST_GID'}="2193";
 apply_config_hash(\%confhash);
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    $prefix.$unamex);
-assert_user_does_not_exist($unamex);
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_command_success("true firstuid=2090 firstgid=2190 lastuid=2093 lastgid=2193");
+assert_usergroup_loop_to_exhaustion({prefix => 'u3l', system => 0, firstuid => 2090, lastuid => 2093, firstgid => 2190, lastgid => 2193, fuid => 3750, fgid => 3761, via_cli => 0});
 
 # test group U4L: ranges requested by config, overriden by command line
 
-$prefix='u4l';
-$firstuid1=2290;
-$lastuid1=2293;
-$firstgid1=2390;
-$lastgid1=2393;
-$fuid = 3750;
-$fgid = 3761;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    $prefix.$unamex);
-assert_user_does_not_exist($prefix.$unamex);
-assert_command_success('/usr/sbin/adduser', @quiet,
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_usergroup_loop_to_exhaustion({prefix => 'u4l', system => 0, firstuid => 2290, lastuid => 2293, firstgid => 2390, lastgid => 2393, fuid => 3750, fgid => 3761});
 
 %confhash=();
 apply_config_hash(\%confhash);
@@ -785,199 +625,138 @@ apply_config_hash(\%confhash);
 # test group S1L: not applicable
 # test group S2L: default config, uid/gid range requested on command line
 
-$prefix='s2l';
-$firstuid1=400;
-$lastuid1=403;
-$firstgid1=500;
-$lastgid1=503;
-$fuid = 700;
-$fgid = 750;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-        '--system',
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-        '--system',
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    $prefix.$unamex);
-assert_user_does_not_exist($prefix.$unamex);
-assert_command_success_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_usergroup_loop_to_exhaustion({prefix => 's2l', system => 1, firstuid => 400, lastuid => 403, firstgid => 500, lastgid => 503, fuid => 700, fgid => 750});
 
 # test group S3L: uid range requested by config, no command line
 
-$prefix='s3l';
-$firstuid1=410;
-$lastuid1=413;
-$firstgid1=510;
-$lastgid1=513;
-$fuid = 710;
-$fgid = 760;
 %confhash=();
-$confhash{'FIRST_SYSTEM_UID'}="$firstuid1";
-$confhash{'FIRST_SYSTEM_GID'}="$firstgid1";
-$confhash{'LAST_SYSTEM_UID'}="$lastuid1";
-$confhash{'LAST_SYSTEM_GID'}="$lastgid1";
+$confhash{'FIRST_SYSTEM_UID'}="410";
+$confhash{'FIRST_SYSTEM_GID'}="510";
+$confhash{'LAST_SYSTEM_UID'}="413";
+$confhash{'LAST_SYSTEM_GID'}="513";
 apply_config_hash(\%confhash);
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-       '--system',
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-       '--system',
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    $prefix.$unamex);
-assert_user_does_not_exist($unamex);
-assert_command_success_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_command_success("true firstsysuid=410 firstsysgid=510 lastsysuid=413 lastsysgid=513");
+assert_usergroup_loop_to_exhaustion({prefix => 's3l', system => 1, firstuid => 410, lastuid => 413, firstgid => 510, lastgid => 513, fuid => 710, fgid => 760, via_cli => 0});
 
 # test group S4L: ranges requested by config, overriden by command line
 
-$prefix='s4l';
-$firstuid1=320;
-$lastuid1=323;
-$firstgid1=520;
-$lastgid1=523;
-$fuid = 720;
-$fgid = 770;
-$gidcount=$firstgid1;
-$uidcount=$firstuid1;
-foreach $groupbase( @gnames ) {
-    $group=$prefix.$groupbase;
-    assert_command_success('/usr/sbin/addgroup', @quiet,
-        '--system',
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $group);
-    assert_group_exists($group);
-    assert_group_has_gid($group, $gidcount);
-    $gidcount++;
-}
-assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
-        '--system',
-        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-       $prefix.$gnamex);
-assert_group_does_not_exist($prefix.$gnamex);
-assert_command_success('/usr/sbin/addgroup', @quiet,
-    '--system',
-    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
-    '--gid', $fgid,
-    $prefix.$fgname);
-assert_group_exists($prefix.$fgname);
-assert_group_has_gid($prefix.$fgname, $fgid);
-
-foreach $userbase( @unames ) {
-    $user=$prefix.$userbase;
-    assert_command_success('/usr/sbin/adduser', @quiet,
-        '--system',
-        '--ingroup', $prefix.$gnames[0],
-        '--comment', '""', '--disabled-password', '--no-create-home',
-        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-        $user);
-    assert_user_exists($user);
-    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
-    assert_user_has_uid($user, $uidcount);
-    $uidcount++;
-}
-assert_command_failure_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--ingroup', $prefix.$gnames[0],
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    $prefix.$unamex);
-assert_user_does_not_exist($prefix.$unamex);
-assert_command_success_silent('/usr/sbin/adduser', @quiet,
-    '--system',
-    '--comment', '""', '--disabled-password', '--no-create-home',
-    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
-    '--uid', $fuid,
-    $prefix.$funame);
-assert_user_exists($prefix.$funame);
-assert_user_has_uid($prefix.$funame, $fuid);
-cleanup($prefix);
+assert_usergroup_loop_to_exhaustion({prefix => 's4l', system => 1, firstuid => 320, lastuid => 323, firstgid => 520, lastgid => 523, fuid => 720, fgid => 770});
+
+# ============================================================
+# Regression tests for the UID/GID range-intersection bugfix.
+#
+# first_avail_uid_gid() must search the true intersection of the
+# UID and GID ranges (the higher of the two starts, the lower of
+# the two ends). Before the fix, the expression computing this
+# intersection used ($a, $b)[$a > $b] as a max()/min() idiom; that
+# idiom actually indexes element 0 (i.e. returns $a) whenever
+# "$a > $b" is false, which is the *lower*, not the higher, value
+# in the common case -- so the floor of the search range could end
+# up lower than intended, and the ceiling could end up higher than
+# intended, whenever the configured UID and GID ranges genuinely
+# differed (they are usually equal by default, which is why this
+# went unnoticed). This also affected addsysuser, which used two
+# completely independent first_avail_uid()/first_avail_gid() calls
+# and so never even attempted a combined search.
+# ============================================================
+
+# test group U5A: adduser (regular, usergroup case), asymmetric
+# uid/gid ranges where the uid range starts lower than the gid
+# range and ends lower too. The true intersection floor is
+# firstgid (the higher start); a regression of the original bug
+# would instead start searching at firstuid, which is lower and
+# would be free, producing a UID lower than the asserted value
+# (and, since the ceiling logic is also affected, the wrong end
+# of the range in general).
 
 %confhash=();
 apply_config_hash(\%confhash);
-cleanup();
+assert_intersection_search_success({prefix => 'u5a', system => 0, firstuid => 4000, lastuid => 4100, firstgid => 4050, lastgid => 4150, expect_first => 4050});
+
+# test group U6A: same as U5A but with the gid range starting
+# lower than the uid range, to also exercise the mirror case (the
+# bug was not symmetric between the two halves of the expression:
+# one half picked the lower start instead of the higher one, the
+# other picked the higher end instead of the lower one).
+
+assert_intersection_search_success({prefix => 'u6a', system => 0, firstuid => 4250, lastuid => 4350, firstgid => 4200, lastgid => 4300, expect_first => 4250});
+
+# test group U7A: uid and gid ranges that do not overlap at all
+# must fail outright, never silently assign mismatched uid/gid.
+# A regression of the original bug would compute a search range
+# wider than the true (empty) overlap and could succeed with a
+# mismatched uid/gid pair instead of failing.
+
+assert_intersection_search_failure({prefix => 'u7a', system => 0, firstuid => 4400, lastuid => 4410, firstgid => 4500, lastgid => 4510});
+
+# test group S5A: adduser --system --group must apply the same
+# combined uid/gid intersection search as the regular usergroup
+# case. This was missing entirely before the fix: --system used
+# two fully independent searches and could create a system user
+# whose uid and gid did not match at all.
+
+assert_intersection_search_success({prefix => 's5a', system => 1, firstuid => 920, lastuid => 930, firstgid => 925, lastgid => 935, expect_first => 925});
+
+# test group S6A: --system --group with non-overlapping uid/gid
+# ranges must fail outright, same as the regular-user case.
+
+assert_intersection_search_failure({prefix => 's6a', system => 1, firstuid => 936, lastuid => 940, firstgid => 950, lastgid => 955});
+
+# ============================================================
+# Feature tests: --ingroup now prefers the requested group's GID
+# as the new user's UID, if that number is also free as a UID,
+# instead of unconditionally taking the lowest free UID in range.
+# If the GID is not free as a UID (or falls outside the UID
+# range), behavior falls back to the previous lowest-free-uid
+# scan. This applies to both adduser and adduser --system.
+# ============================================================
+
+# test group U8A: adduser --ingroup GROUP, where GROUP's gid is
+# free as a uid and is *not* the lowest free uid in the requested
+# range (firstuid is deliberately lower and also free) -- so a
+# successful match on the gid proves the preferred-id logic fired,
+# rather than the scan simply reaching that number first anyway.
+
+assert_preferred_uid({prefix => 'u8a', system => 0, firstuid => 4900, lastuid => 4999, fgid => 4950, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 4950});
+
+# test group U9A: same setup, but the group's gid is already
+# taken as a uid by another user, so adduser --ingroup must fall
+# back to the lowest free uid in the requested range instead
+# (firstuid, since that is free and lower than the occupied gid).
+
+assert_preferred_uid({prefix => 'u9a', system => 0, firstuid => 5000, lastuid => 5099, fgid => 5050, join_by_gid => 0, occupy_gid_as_uid => 1, expect_uid => 5000});
+
+# test group S7A: same as U8A, but for adduser --system --ingroup.
+
+assert_preferred_uid({prefix => 's7a', system => 1, firstuid => 941, lastuid => 950, fgid => 945, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 945});
+
+# test group S8A: same as U9A, but for adduser --system --ingroup.
+
+assert_preferred_uid({prefix => 's8a', system => 1, firstuid => 951, lastuid => 960, fgid => 955, join_by_gid => 0, occupy_gid_as_uid => 1, expect_uid => 951});
+
+# test group S9A: same hit-case as S7A, but using --gid GID (a
+# numeric primary group) instead of --ingroup GROUPNAME. addsysuser
+# treats $gid_option and $ingroup_name uniformly for the preferred-
+# uid branch, but this had no dedicated test: the group's gid is
+# free as a uid and inside the search range, and is not the lowest
+# free uid in that range, so a match proves the preferred-id logic
+# fired for the --gid path specifically, not just --ingroup.
+
+assert_preferred_uid({prefix => 's9a', system => 1, firstuid => 961, lastuid => 970, fgid => 965, join_by_gid => 1, occupy_gid_as_uid => 0, expect_uid => 965});
+
+# test group S10A: same fallback-case as S8A, but using --gid GID
+# instead of --ingroup GROUPNAME: the group's gid is already taken
+# as a uid by another user, so adduser --system --gid must fall
+# back to the lowest free uid in the requested range instead.
+
+assert_preferred_uid({prefix => 's10a', system => 1, firstuid => 971, lastuid => 980, fgid => 975, join_by_gid => 1, occupy_gid_as_uid => 1, expect_uid => 971});
+
+# test group U10A: --ingroup preferred-uid logic must respect the
+# requested --firstuid/--lastuid range: if the group's gid lies
+# outside that range, it must not be used even though it is free,
+# and the lowest free uid inside the range must be picked instead.
+
+assert_preferred_uid({prefix => 'u10a', system => 0, firstuid => 5300, lastuid => 5399, fgid => 5450, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 5300});
 
 done_testing();
 


=====================================
debian/tests/f/firstlastuidgid_orig.t
=====================================
@@ -0,0 +1,1020 @@
+#! /usr/bin/perl -Idebian/tests/lib
+
+# check first/last uid/gid functionality
+
+use diagnostics;
+use strict;
+use warnings;
+
+use AdduserTestsCommon;
+use Test::More;
+
+my @quiet=('--stdoutmsglevel=error', '--stderrmsglevel=error');
+my $gidcount;
+my $uidcount;
+
+my @unames = ( 'oflugu01', 'oflugu02', 'oflugu03', 'oflugu04' );
+my $unamex = 'oflugu05';
+my @gnames = ( 'oflugg01', 'oflugg02', 'oflugg03', 'oflugg04' );
+my $gnamex = 'oflugg05';
+my $funame = 'ofixedu01';
+my $fgname = 'ofixedg01';
+my $user;
+my $userbase;
+my $fuid;
+my $group;
+my $groupbase;
+my $fgid;
+my $prefix;
+
+my $firstuid1;
+my $lastuid1;
+my $firstgid1;
+my $lastgid1;
+
+sub cleanup {
+    my $prefix=$_[0] || '';
+    foreach $userbase( @unames ) {
+        $user=$prefix.$userbase;
+        system("/usr/sbin/deluser @quiet --remove-home $user 2>/dev/null");
+        assert_user_does_not_exist($user);
+    }
+    system("/usr/sbin/deluser @quiet --remove-home $prefix$unamex 2>/dev/null");
+    assert_user_does_not_exist($prefix.$unamex);
+    system("/usr/sbin/deluser @quiet --remove-home $prefix$funame 2>/dev/null");
+    assert_user_does_not_exist($prefix.$funame);
+    $uidcount=0;
+    foreach $groupbase( @gnames ) {
+        $group=$prefix.$groupbase;
+        system("/usr/sbin/delgroup @quiet $group 2>/dev/null");
+        assert_group_does_not_exist($group);
+    }
+    system("/usr/sbin/delgroup @quiet $prefix$gnamex 2>/dev/null");
+    assert_group_does_not_exist($prefix.$gnamex);
+    system("/usr/sbin/delgroup @quiet $prefix$fgname 2>/dev/null");
+    assert_user_does_not_exist($prefix.$fgname);
+    $gidcount=0;
+}
+cleanup();
+
+# test group U1A: default config, no command line
+
+$prefix = 'u1a';
+$fuid = 3750;
+$fgid = 3761;
+my %confhash=();
+apply_config_hash(\%confhash);
+
+foreach $groupbase( @gnames ) {
+    $group = $prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet, $group);
+    assert_group_exists($group);
+    if ($gidcount==0) {
+        $gidcount=((getgrnam($group))[2]);
+    }
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user = $prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    if ($uidcount==0) {
+        $uidcount=((getpwnam($user))[2]);
+    }
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='u1a2';
+foreach $userbase( @unames ) {
+    $user = $prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_group_exists($user);
+    if ($uidcount==0) {
+        $uidcount=((getpwnam($user))[2]);
+    }
+    if ($gidcount==0) {
+        $gidcount=((getgrnam($user))[2]);
+    }
+    assert_primary_group_membership_exists($user, $user);
+    assert_user_has_uid($user, $uidcount);
+    assert_group_has_gid($user, $gidcount);
+    $uidcount++;
+    $gidcount++;
+}
+cleanup($prefix);
+
+# test group U2A: default config, uid/gid range requested on command line
+
+$prefix='u2a';
+$firstuid1=2000;
+$firstgid1=2100;
+$fuid = 3750;
+$fgid = 3761;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+       '--firstgid', $firstgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_group_does_not_exist($prefix.$fgname);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--firstgid', $firstgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, 
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    # assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, 
+    '--uid', $fuid,
+   $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='u2a2';
+$firstuid1=2000;
+$fuid = 3750;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1,
+        $user);
+    assert_user_exists($user);
+    assert_group_exists($user);
+    assert_primary_group_membership_exists($user, $user);
+    assert_user_has_uid($user, $uidcount);
+    assert_group_has_gid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+# test group U3A: uid range requested by config, no command line
+
+$prefix='u3a';
+$firstuid1=2000;
+$firstgid1=2100;
+$fuid = 3750;
+$fgid = 3761;
+%confhash=();
+$confhash{'FIRST_UID'}="$firstuid1";
+$confhash{'FIRST_GID'}="$firstgid1";
+apply_config_hash(\%confhash);
+
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet, $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='u3a2';
+$firstuid1=2000;
+$fuid = 3750;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_group_exists($user);
+    assert_primary_group_membership_exists($user, $user);
+    # commented. this behavior has changed. new adduser tries to
+    # honor both the uid and the gid range in this case.
+    # this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    #assert_group_has_gid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+# test group U4A: ranges requested by config, overriden by command line
+
+$prefix='u4a';
+$firstuid1=2200;
+$firstgid1=2300;
+$fuid = 3750;
+$fgid = 3761;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+      '--firstgid', $firstgid1,
+      $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--firstgid', $firstgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, 
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, 
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='u4a2';
+$firstuid1=2000;
+$fuid = 3750;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1,
+        $user);
+    assert_user_exists($user);
+    assert_group_exists($user);
+    assert_primary_group_membership_exists($user, $user);
+    # commented. this behavior has changed. new adduser tries to
+    # honor both the uid and the gid range in this case.
+    # this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    #assert_group_has_gid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+%confhash=();
+apply_config_hash(\%confhash);
+
+# test group S1A: default config, no command line
+
+$prefix = 's1a';
+$fuid = 200;
+$fgid = 210;
+%confhash=();
+apply_config_hash(\%confhash);
+
+foreach $groupbase( @gnames ) {
+    $group = $prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet, '--system', $group);
+    assert_group_exists($group);
+    if ($gidcount==0) {
+        $gidcount=((getgrnam($group))[2]);
+    }
+    assert_group_has_gid($group, $gidcount);
+    while (defined(getgrgid($gidcount))) {
+        $gidcount++;
+    }
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user = $prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    if ($uidcount==0) {
+        $uidcount=((getpwnam($user))[2]);
+    }
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    while (defined(getpwuid($uidcount))) {
+        $uidcount++;
+    }
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='s1a2';
+foreach $userbase( @unames ) {
+    $user = $prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    if ($uidcount==0) {
+        $uidcount=((getpwnam($user))[2]);
+    }
+    assert_primary_group_membership_exists($user, 'nogroup');
+    assert_user_has_uid($user, $uidcount);
+    while (defined(getpwuid($uidcount))) {
+        $uidcount++;
+    }
+}
+cleanup($prefix);
+
+# test group S2A: default config, uid/gid range requested on command line
+
+$prefix='s2a';
+$firstuid1=220;
+$firstgid1=230;
+$fuid = 950;
+$fgid = 960;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+       '--system',
+       '--firstgid', $firstgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_group_does_not_exist($prefix.$fgname);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--firstgid', $firstgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, 
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, 
+    '--uid', $fuid,
+   $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='s2a2';
+$firstuid1=240;
+$fuid = 250;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, 'nogroup');
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+# test group S3A: uid range requested by config, no command line
+
+$prefix='s3a';
+$firstuid1=260;
+$firstgid1=270;
+$fuid = 970;
+$fgid = 980;
+%confhash=();
+$confhash{'FIRST_SYSTEM_UID'}="$firstuid1";
+$confhash{'FIRST_SYSTEM_GID'}="$firstgid1";
+apply_config_hash(\%confhash);
+
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', '--system', @quiet, $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='s3a2';
+$firstuid1=260;
+$fuid = 970;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, 'nogroup');
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+# test group S4A: ranges requested by config, overriden by command line
+
+$prefix='s4a';
+$firstuid1=290;
+$firstgid1=300;
+$fuid = 810;
+$fgid = 820;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+      '--system',
+      '--firstgid', $firstgid1,
+      $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--firstgid', $firstgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, 
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    # commented. this behavior has changed - the first user created
+    # gets the same uid as the group, the following users are one off
+    # the expectation. this test is correctly in the new test file,
+    # hence commented here.
+    #assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, 
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+$prefix='s4a2';
+$firstuid1=310;
+$fuid = 830;
+$uidcount=$firstuid1;
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, 'nogroup');
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+cleanup($prefix);
+
+%confhash=();
+apply_config_hash(\%confhash);
+
+
+# tests with last_ options: we set a range and fill it, last creation must fail
+
+# test group U1L: not applicable
+# test group U2L: default config, uid/gid range requested on command line
+
+$prefix='u2l';
+$firstuid1=2090;
+$lastuid1=2093;
+$firstgid1=2190;
+$lastgid1=2193;
+$fuid = 3750;
+$fgid = 3761;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    $prefix.$unamex);
+assert_user_does_not_exist($prefix.$unamex);
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+# test group U3L: uid range requested by config, no command line
+
+$prefix='u3l';
+$firstuid1=2090;
+$lastuid1=2093;
+$firstgid1=2190;
+$lastgid1=2193;
+$fuid = 3750;
+$fgid = 3761;
+%confhash=();
+$confhash{'FIRST_UID'}="$firstuid1";
+$confhash{'FIRST_GID'}="$firstgid1";
+$confhash{'LAST_UID'}="$lastuid1";
+$confhash{'LAST_GID'}="$lastgid1";
+apply_config_hash(\%confhash);
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    $prefix.$unamex);
+assert_user_does_not_exist($unamex);
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+# test group U4L: ranges requested by config, overriden by command line
+
+$prefix='u4l';
+$firstuid1=2290;
+$lastuid1=2293;
+$firstgid1=2390;
+$lastgid1=2393;
+$fuid = 3750;
+$fgid = 3761;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    $prefix.$unamex);
+assert_user_does_not_exist($prefix.$unamex);
+assert_command_success('/usr/sbin/adduser', @quiet,
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+%confhash=();
+apply_config_hash(\%confhash);
+
+# test group S1L: not applicable
+# test group S2L: default config, uid/gid range requested on command line
+
+$prefix='s2l';
+$firstuid1=400;
+$lastuid1=403;
+$firstgid1=500;
+$lastgid1=503;
+$fuid = 700;
+$fgid = 750;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        '--system',
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+        '--system',
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    $prefix.$unamex);
+assert_user_does_not_exist($prefix.$unamex);
+assert_command_success_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+# test group S3L: uid range requested by config, no command line
+
+$prefix='s3l';
+$firstuid1=410;
+$lastuid1=413;
+$firstgid1=510;
+$lastgid1=513;
+$fuid = 710;
+$fgid = 760;
+%confhash=();
+$confhash{'FIRST_SYSTEM_UID'}="$firstuid1";
+$confhash{'FIRST_SYSTEM_GID'}="$firstgid1";
+$confhash{'LAST_SYSTEM_UID'}="$lastuid1";
+$confhash{'LAST_SYSTEM_GID'}="$lastgid1";
+apply_config_hash(\%confhash);
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+       '--system',
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+       '--system',
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    $prefix.$unamex);
+assert_user_does_not_exist($unamex);
+assert_command_success_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+# test group S4L: ranges requested by config, overriden by command line
+
+$prefix='s4l';
+$firstuid1=320;
+$lastuid1=323;
+$firstgid1=520;
+$lastgid1=523;
+$fuid = 720;
+$fgid = 770;
+$gidcount=$firstgid1;
+$uidcount=$firstuid1;
+foreach $groupbase( @gnames ) {
+    $group=$prefix.$groupbase;
+    assert_command_success('/usr/sbin/addgroup', @quiet,
+        '--system',
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $group);
+    assert_group_exists($group);
+    assert_group_has_gid($group, $gidcount);
+    $gidcount++;
+}
+assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
+        '--system',
+        '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+       $prefix.$gnamex);
+assert_group_does_not_exist($prefix.$gnamex);
+assert_command_success('/usr/sbin/addgroup', @quiet,
+    '--system',
+    '--firstgid', $firstgid1, '--lastgid', $lastgid1,
+    '--gid', $fgid,
+    $prefix.$fgname);
+assert_group_exists($prefix.$fgname);
+assert_group_has_gid($prefix.$fgname, $fgid);
+
+foreach $userbase( @unames ) {
+    $user=$prefix.$userbase;
+    assert_command_success('/usr/sbin/adduser', @quiet,
+        '--system',
+        '--ingroup', $prefix.$gnames[0],
+        '--comment', '""', '--disabled-password', '--no-create-home',
+        '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+        $user);
+    assert_user_exists($user);
+    assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
+    assert_user_has_uid($user, $uidcount);
+    $uidcount++;
+}
+assert_command_failure_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--ingroup', $prefix.$gnames[0],
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    $prefix.$unamex);
+assert_user_does_not_exist($prefix.$unamex);
+assert_command_success_silent('/usr/sbin/adduser', @quiet,
+    '--system',
+    '--comment', '""', '--disabled-password', '--no-create-home',
+    '--firstuid', $firstuid1, '--lastuid', $lastuid1,
+    '--uid', $fuid,
+    $prefix.$funame);
+assert_user_exists($prefix.$funame);
+assert_user_has_uid($prefix.$funame, $fuid);
+cleanup($prefix);
+
+%confhash=();
+apply_config_hash(\%confhash);
+cleanup();
+
+done_testing();
+
+# vim: tabstop=4 shiftwidth=4 expandtab


=====================================
debian/tests/f/uidgid_functions.t
=====================================
@@ -0,0 +1,204 @@
+#! /usr/bin/perl
+
+# Unit tests for the UID/GID selection helpers in Debian::AdduserCommon:
+# range_intersection, first_avail_uid, first_avail_gid, and
+# first_avail_uid_gid.
+#
+# Unlike the other tests in this directory, this file does not spawn
+# adduser/addgroup as external commands. It imports the real
+# Debian::AdduserCommon module that adduser itself uses, and calls
+# the functions directly, with getpwuid/getgrgid replaced by
+# in-process fakes. This lets every branch (the uid/gid range
+# intersection, the --ingroup/--gid preferred-uid logic, and the
+# various pool_id/reserved-pool short circuits) be checked exactly,
+# including cases -- like a non-overlapping uid/gid range, or an id
+# free as a uid but taken as a gid -- that are awkward to set up
+# reliably against the real system passwd/group databases.
+#
+# getpwuid/getgrgid are builtins, so a plain "sub getpwuid {...}" in
+# this file's own package would not be seen by code running inside
+# Debian::AdduserCommon's package: Perl resolves a bare builtin call
+# in the package it is compiled in, not the caller's package. The
+# override has to go through the CORE::GLOBAL:: slot, which applies
+# to every package, including Debian::AdduserCommon.
+
+use diagnostics;
+use strict;
+use warnings;
+use Test::More;
+
+our %fake_passwd; # uid => 1 if taken
+our %fake_group;  # gid => 1 if taken
+
+BEGIN {
+    *CORE::GLOBAL::getpwuid = sub { my ($u) = @_; return $main::fake_passwd{$u} ? "x" : undef; };
+    *CORE::GLOBAL::getgrgid = sub { my ($g) = @_; return $main::fake_group{$g}  ? "x" : undef; };
+}
+
+use Debian::AdduserCommon;
+
+sub reset_fakes {
+    %fake_passwd = ();
+    %fake_group  = ();
+}
+
+# ============================================================
+# range_intersection(min1, max1, min2, max2)
+#
+# Regression coverage for a bug where the intersection floor/ceiling
+# were computed with a ($a, $b)[$a > $b] idiom that, for the common
+# case of $a <= $b, indexes element 0 and so returns the *lower* of
+# the two values where the higher one (the true intersection floor)
+# was wanted -- and the mirror expression for the ceiling had the
+# same problem in the other direction. This went unnoticed because
+# the two ranges passed in are usually equal.
+# ============================================================
+
+subtest 'range_intersection' => sub {
+    my ($f, $l) = range_intersection(100, 200, 100, 200);
+    is($f, 100, 'equal ranges: floor');
+    is($l, 200, 'equal ranges: ceiling');
+
+    # uid range starts lower and ends lower than the gid range:
+    # floor must be the higher start (the gid range's), ceiling
+    # must be the lower end (the uid range's)
+    ($f, $l) = range_intersection(100, 200, 150, 300);
+    is($f, 150, 'asymmetric A: floor is the higher start');
+    is($l, 200, 'asymmetric A: ceiling is the lower end');
+
+    # mirror: gid range starts lower and ends lower
+    ($f, $l) = range_intersection(150, 300, 100, 200);
+    is($f, 150, 'asymmetric B: floor is the higher start');
+    is($l, 200, 'asymmetric B: ceiling is the lower end');
+
+    # non-overlapping ranges: floor ends up above ceiling, which
+    # first_avail_uid_gid()'s scan loop naturally turns into "no
+    # candidate found" without needing a special case
+    ($f, $l) = range_intersection(100, 199, 300, 399);
+    ok($f > $l, 'non-overlapping ranges: floor > ceiling');
+};
+
+# ============================================================
+# first_avail_uid_gid(min, max, pool_id, reserved_uid_pool, reserved_gid_pool)
+#
+# Exercises the combined search over a range computed by
+# range_intersection(), including the case that the original bug
+# would get wrong: an id that is free as a uid but already taken as
+# a gid (or vice versa) must be skipped entirely, not returned.
+# ============================================================
+
+subtest 'first_avail_uid_gid over a computed intersection' => sub {
+    reset_fakes();
+    %fake_group = (200 => 1, 201 => 1); # gid 200,201 taken
+    my ($f, $l) = range_intersection(100, 999, 200, 999);
+    is($f, 200, 'intersection floor');
+    my $uid = first_avail_uid_gid($f, $l, undef);
+    is($uid, 202, 'first id free in both, within the intersection');
+
+    reset_fakes();
+    %fake_group = (100 => 1); # 100 free as uid, taken as gid
+    ($f, $l) = range_intersection(100, 999, 100, 999);
+    $uid = first_avail_uid_gid($f, $l, undef);
+    is($uid, 101, 'id free-as-uid-but-taken-as-gid is skipped');
+
+    reset_fakes();
+    ($f, $l) = range_intersection(100, 199, 300, 399);
+    $uid = first_avail_uid_gid($f, $l, undef);
+    is($uid, -1, 'non-overlapping ranges yield -1, not a false match');
+};
+
+subtest 'first_avail_uid_gid: reserved pools and pool_id' => sub {
+    reset_fakes();
+    my %reserved_uid_pool = (500 => 1);
+    my $uid = first_avail_uid_gid(498, 502, undef, \%reserved_uid_pool, {});
+    is($uid, 498, 'scan skips ids reserved by a pending UID_POOL entry');
+
+    reset_fakes();
+    %fake_group = (777 => 1); # pool_id target already taken as a gid
+    $uid = first_avail_uid_gid(100, 999, 777);
+    is($uid, -1, 'pool_id short-circuits and fails if taken as a gid');
+
+    reset_fakes();
+    $uid = first_avail_uid_gid(100, 999, 777);
+    is($uid, 777, 'pool_id short-circuits and succeeds if free as a gid');
+};
+
+# ============================================================
+# first_avail_uid(min, max, pool_id, preferred, reserved_uid_pool)
+#
+# preferred is the --ingroup/--gid feature: prefer a candidate id
+# (the requested group's gid) if it is free as a uid, before
+# falling back to the lowest free uid in range.
+# ============================================================
+
+subtest 'first_avail_uid: preferred-id parameter' => sub {
+    reset_fakes();
+    my $uid = first_avail_uid(100, 999, undef, 500);
+    is($uid, 500, 'free preferred id is used directly, even though a lower free id exists');
+
+    reset_fakes();
+    %fake_passwd = (500 => 1); # preferred id already taken as a uid
+    $uid = first_avail_uid(100, 999, undef, 500);
+    is($uid, 100, 'taken preferred id falls back to the normal scan');
+
+    reset_fakes();
+    $uid = first_avail_uid(100, 199, undef, 9000);
+    is($uid, 100, 'out-of-range preferred id is ignored, not just deprioritized');
+
+    reset_fakes();
+    my %reserved_uid_pool = (500 => 1); # preferred id reserved by a pending UID_POOL entry
+    $uid = first_avail_uid(100, 999, undef, 500, \%reserved_uid_pool);
+    is($uid, 100, 'reserved preferred id falls back to the normal scan');
+
+    reset_fakes();
+    $uid = first_avail_uid(100, 999, undef, undef);
+    is($uid, 100, 'no preferred id given: behaves like the plain scan');
+};
+
+subtest 'first_avail_uid: pool_id takes priority over preferred' => sub {
+    reset_fakes();
+    %fake_passwd = (777 => 1); # pool_id target already taken -> pool path fails
+    my $uid = first_avail_uid(100, 999, 777, 500);
+    is($uid, -1, 'pool_id short-circuits and ignores preferred entirely');
+
+    reset_fakes();
+    $uid = first_avail_uid(100, 999, 777, 500);
+    is($uid, 777, 'pool_id short-circuits and succeeds if free, ignoring preferred');
+};
+
+# ============================================================
+# first_avail_gid(min, max, pool_id, reserved_gid_pool)
+#
+# Same shape as first_avail_uid, minus the preferred-id parameter
+# (--ingroup/--gid only ever influences uid selection, never gid
+# selection, since the gid in that case is already given).
+# ============================================================
+
+subtest 'first_avail_gid' => sub {
+    reset_fakes();
+    my $gid = first_avail_gid(100, 999, undef);
+    is($gid, 100, 'plain scan returns the lowest free gid');
+
+    reset_fakes();
+    %fake_group = (100 => 1, 101 => 1);
+    $gid = first_avail_gid(100, 999, undef);
+    is($gid, 102, 'scan skips taken gids');
+
+    reset_fakes();
+    my %reserved_gid_pool = (100 => 1);
+    $gid = first_avail_gid(100, 999, undef, \%reserved_gid_pool);
+    is($gid, 101, 'scan skips ids reserved by a pending GID_POOL entry');
+
+    reset_fakes();
+    %fake_group = (777 => 1);
+    $gid = first_avail_gid(100, 999, 777);
+    is($gid, -1, 'pool_id short-circuits and fails if taken');
+
+    reset_fakes();
+    $gid = first_avail_gid(100, 999, 777);
+    is($gid, 777, 'pool_id short-circuits and succeeds if free');
+};
+
+done_testing();
+
+# vim: tabstop=4 shiftwidth=4 expandtab


=====================================
debian/tests/f/uidgidpool.t
=====================================
@@ -54,6 +54,7 @@ my @uidlist = (
 $cl_tree->add(qw(/home/pool101 /home/pool202));
 $cl_user->add(qw(pooluid101 pooluid202));
 my $firstuid = (sort map {$_->{id}} @uidlist)[0];
+assert_command_success("true firstuid $firstuid");
 
 my @uidreserved = (
    {
@@ -120,7 +121,7 @@ cleanup();
 my $fh;
 open ($fh, ">>", $uidpoolfile) or die "Failed to open file $uidpoolfile for writing";
 foreach my $idset( @uidlist ) {
-    print $fh $idset->{name}. ":". $idset->{id}. ":". $idset->{comment}. ":". $idset->{home}. ":". $idset->{shell}. "\n"
+    print $fh $idset->{name}. ":". $idset->{id}. ":". $idset->{comment}. ":". $idset->{home}. ":". $idset->{shell}. "\n";
 }
 
 open ($fh, ">>", $gidpoolfile) or die "Failed to open file $gidpoolfile for writing";
@@ -136,6 +137,7 @@ apply_config_hash(\%confhash);
 
 # test creating user/group with uidpool set
 
+assert_command_success("true uidpool set - files");
 foreach my $group( @gidlist ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -208,6 +210,7 @@ apply_config_hash(\%confhash);
 
 # test not reserved uid in pool
 
+assert_command_success("true not reserved uid in pool - files");
 foreach my $group( @gidreserved ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -224,6 +227,7 @@ foreach my $group( @gidreserved ) {
 
 foreach my $user( @uidreserved ) {
     assert_command_success('/usr/sbin/adduser', @quiet,
+      '--ingroup', 'nogroup',
       '--comment', '""', '--disabled-password', $user->{name});
     assert_user_exists($user->{name});
     assert_user_has_uid($user->{name}, $user->{id});
@@ -241,6 +245,7 @@ apply_config_hash(\%confhash);
 
 # test reserved uid in pool
 
+assert_command_success("true reserved uid in pool - files");
 foreach my $group( @gidreserved ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -280,6 +285,7 @@ apply_config_hash(\%confhash);
 
 # test not reserved uid in pool
 
+assert_command_success("true not reserved uid in pool - 2 - files");
 foreach my $group( @gidreserved ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -296,6 +302,7 @@ foreach my $group( @gidreserved ) {
 
 foreach my $user( @uidreserved ) {
     assert_command_success('/usr/sbin/adduser', @quiet,
+      '--ingroup', 'nogroup',
       '--comment', '""', '--disabled-password', $user->{name});
     assert_user_exists($user->{name});
     assert_user_has_uid($user->{name}, $user->{id});
@@ -313,6 +320,7 @@ apply_config_hash(\%confhash);
 
 # test reserved uid in pool
 
+assert_command_success("true reserved uid in pool - 2 - files");
 foreach my $group( @gidreserved ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -369,6 +377,7 @@ apply_config_hash(\%confhash);
 
 # test creating user/group with uidpool set
 
+assert_command_success("true creating user/group with uidpool set - dirs");
 foreach my $group( @gidlist ) {
     assert_command_success('/usr/sbin/addgroup', @quiet,
       $group->{name});
@@ -433,6 +442,9 @@ foreach my $user( @uidlist ) {
 $cl_user->finalize();
 $cl_tree->finalize();
 
+unlink($uidpoolfile);
+unlink($gidpoolfile);
+
 done_testing();
 
 # vim: tabstop=4 shiftwidth=4 expandtab


=====================================
doc/adduser.8
=====================================
@@ -182,7 +182,9 @@ The range may be overridden with the
 \fB\-\-firstuid\fP and \fB\-\-lastuid\fP options.
 Finally, the UID can be set fully manually with the \fB\-\-uid\fP option.
 .PP
-By default, each user is given a corresponding group with the same name.
+By default, each user is given a corresponding group with the same name
+and the same numeric ID as the user,
+unless \fB\-\-ingroup\fP is used (see below).
 This is commonly called
 \fIUsergroups\fP
 and allows group writable directories to be easily maintained
@@ -190,14 +192,45 @@ by placing the appropriate users in the new group,
 setting the set-group-ID bit in the directory,
 and ensuring that all users use a umask of 002.
 .PP
-For a usergroup,
-\fBadduser\fP will choose the first available GID
-from the range specified by
-\fBFIRST_GID\fP and \fBLAST_GID\fP
-in the configuration file.
-The range may be overridden with the
-\fB\-\-firstgid\fP and \fB\-\-lastgid\fP options.
-Finally, the GID can be set fully manually with the \fB\-\-gid\fP option.
+For this usergroup case,
+\fBadduser\fP does not pick a UID and a GID independently.
+Instead, it searches for the first number that is simultaneously
+available as a UID and as a GID,
+and assigns that single number to both the new user and the new group.
+The range searched is the
+.I intersection
+of the UID range
+(\fBFIRST_UID\fP to \fBLAST_UID\fP,
+or \fB\-\-firstuid\fP / \fB\-\-lastuid\fP if given)
+and the GID range
+(\fBFIRST_GID\fP to \fBLAST_GID\fP,
+or \fB\-\-firstgid\fP / \fB\-\-lastgid\fP if given):
+that is, the search starts at the higher of the two range minimums
+and ends at the lower of the two range maximums.
+If the configured UID and GID ranges do not overlap at all,
+or no number free in both ranges can be found,
+\fBadduser\fP fails outright;
+it does not fall back to assigning the user and group different numbers.
+.PP
+If \fB\-\-ingroup\fP is given
+to add the new user to an already existing group
+instead of a new usergroup,
+this combined search does not apply.
+Instead,
+if the requested group's GID happens to also be free as a UID
+within the configured UID range,
+\fBadduser\fP prefers that number for the new user's UID,
+so the user still ends up with a UID matching the group's GID
+even though the group was not newly created as a usergroup.
+If the GID is not free as a UID,
+or falls outside the UID range,
+\fBadduser\fP falls back to choosing
+the first available UID from the UID range,
+as it always did.
+.PP
+Finally, the UID and/or GID can each be set fully manually
+with the \fB\-\-uid\fP and \fB\-\-gid\fP options,
+which bypass the search(es) above for the value(s) given.
 .PP
 The interaction between
 \fBUSERS_GID\fP, \fBUSERS_GROUP\fP, and \fBUSERGROUPS\fP
@@ -256,9 +289,41 @@ This can be overridden with the \fB\-\-uid\fP option.
 By default, system users are assigned \fBnogroup\fP as primary group.
 To assign an already existing group as primary group,
 use the \fB\-\-gid\fP or \fB\-\-ingroup\fP options.
+.PP
 If the \fB\-\-group\fP option is given
 and the identically named group does not already exist,
-it is created with the same ID.
+it is created with the same numeric ID as the user,
+following the same logic as for a non-system usergroup
+(see \fBAdd a regular (non-system) user\fP above):
+\fBadduser\fP searches for the first number that is simultaneously
+available as a UID and as a GID,
+within the intersection of the UID range
+(\fBFIRST_SYSTEM_UID\fP to \fBLAST_SYSTEM_UID\fP,
+or \fB\-\-firstuid\fP / \fB\-\-lastuid\fP if given)
+and the GID range
+(\fBFIRST_SYSTEM_GID\fP to \fBLAST_SYSTEM_GID\fP,
+or \fB\-\-firstgid\fP / \fB\-\-lastgid\fP if given).
+As with a regular usergroup,
+if the configured UID and GID ranges do not overlap at all,
+or no number free in both ranges can be found,
+\fBadduser\fP fails outright
+rather than assigning the user and group different numbers.
+.PP
+If instead \fB\-\-gid\fP or \fB\-\-ingroup\fP is used
+to assign an already existing group as primary group,
+this combined search does not apply.
+Instead,
+if that group's GID happens to also be free as a UID
+within the configured UID range,
+\fBadduser\fP prefers that number for the new system user's UID.
+Note that for a non-system user (see above),
+only \fB\-\-ingroup\fP triggers this preference;
+\fB\-\-gid\fP alone does not.
+If the GID is not free as a UID,
+or falls outside the UID range,
+\fBadduser\fP falls back to choosing
+the first available UID from the UID range,
+as it always did.
 .PP
 If no home directory is specified,
 the default home directory for a new system user
@@ -400,10 +465,27 @@ in the range that the uid is chosen from
 \fBFIRST_SYSTEM_UID\fP, \fBLAST_SYSTEM_UID\fP,
 \fBFIRST_SYSTEM_GID\fP and \fBLAST_SYSTEM_GID\fP
 in the configuration file).
-If a group is created as a usergroup,
-\fB\-\-firstgid\fP and \fB\-\-lastgid\fP
-are ignored.
-The group gets the same ID as the user.
+If a group is created as a usergroup
+(that is, for \fBadduser\fP without \fB\-\-ingroup\fP,
+or for \fBadduser \-\-system \-\-group\fP without \fB\-\-ingroup\fP),
+\fB\-\-firstgid\fP and \fB\-\-lastgid\fP are
+.B not
+ignored:
+they bound the GID side of the combined UID/GID search
+described under
+\fBAdd a regular (non-system) user\fP
+above,
+so a narrower or shifted \fB\-\-firstgid\fP\^/\fB\-\-lastgid\fP\^
+range can change which number is chosen,
+or cause the search to fail
+if it no longer overlaps the UID range at all.
+The resulting group always gets the same numeric ID as the user.
+If \fB\-\-ingroup\fP is used instead,
+\fB\-\-firstgid\fP and \fB\-\-lastgid\fP have no effect;
+\fB\-\-firstuid\fP and \fB\-\-lastuid\fP still bound
+whether the requested group's GID
+can be preferred as the new user's UID
+(see \fBAdd a regular (non-system) user\fP above).
 Valid modes: \fBadduser\fP, \fBadduser \-\-system\fP,
 for \fP\-\-firstgid\fP and \fB\-\-lastgid\fR also
 \fBaddgroup\fP.
@@ -421,6 +503,14 @@ this option sets the group ID number of the new group to \fIGID\fP.
 When creating a user,
 this option sets the primary group ID number of the new user
 to \fIGID\fP.
+For \fBadduser \-\-system\fP specifically,
+if the chosen UID is not also given explicitly with \fB\-\-uid\fP,
+and \fIGID\fP happens to also be free as a UID
+within the UID range in effect,
+\fBadduser\fP prefers that number for the new user's UID
+(see \fB\-\-ingroup\fP).
+This does not apply to a non-system \fBadduser\fP:
+there, \fB\-\-gid\fP alone never influences UID selection this way.
 Valid modes: \fBadduser\fP, \fBadduser \-\-system\fP,
 \fBaddgroup\fP, \fBaddgroup \-\-system\fP.
 .TP
@@ -453,6 +543,14 @@ to the GID of the named group.
 Unlike with the \fB\-\-gid\fP option,
 the group is specified here by name rather than by numeric ID number.
 The group must already exist.
+If the chosen UID is not also given explicitly with \fB\-\-uid\fP,
+and \fIGROUP\fP's GID happens to also be free as a UID
+within the UID range in effect
+(see \fB\-\-firstuid\fP / \fB\-\-lastuid\fP),
+\fBadduser\fP prefers that number for the new user's UID,
+so the user ends up with a UID matching the group's GID
+even though the group itself was not newly created.
+Otherwise, the first available UID in range is used, as before.
 Valid modes: \fBadduser\fP, \fBadduser \-\-system\fP.
 .TP
 .BI \-\-lastuid " ID"



View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/875caf3ec61ef6000e267daba0e149cafbc2892a...06ac8af4bddb5648219cf319a72c0ee173dae483

-- 
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/875caf3ec61ef6000e267daba0e149cafbc2892a...06ac8af4bddb5648219cf319a72c0ee173dae483
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-shadow-devel/attachments/20260722/6d72a16c/attachment-0001.htm>


More information about the Pkg-shadow-devel mailing list