update-users-groups.pl: Keep track of deallocated UIDs/GIDs

When a user or group is revived, this allows it to be allocated the
UID/GID it had before.

A consequence is that UIDs and GIDs are no longer reused.

Fixes #24010.

+53 -17
+53 -17
nixos/modules/config/update-users-groups.pl
··· 6 make_path("/var/lib/nixos", { mode => 0755 }); 7 8 9 sub hashPassword { 10 my ($password) = @_; 11 my $salt = ""; ··· 18 # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in 19 # /etc/login.defs. 20 sub allocId { 21 - my ($used, $idMin, $idMax, $up, $getid) = @_; 22 my $id = $up ? $idMin : $idMax; 23 while ($id >= $idMin && $id <= $idMax) { 24 - if (!$used->{$id} && !defined &$getid($id)) { 25 $used->{$id} = 1; 26 return $id; 27 } ··· 31 die "$0: out of free UIDs or GIDs\n"; 32 } 33 34 - my (%gidsUsed, %uidsUsed); 35 36 sub allocGid { 37 - return allocId(\%gidsUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) }); 38 } 39 40 sub allocUid { 41 - my ($isSystemUser) = @_; 42 my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1); 43 - return allocId(\%uidsUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); 44 } 45 46 47 # Read the declared users/groups. 48 my $spec = decode_json(read_file($ARGV[0])); 49 50 - # Don't allocate UIDs/GIDs that are already in use. 51 foreach my $g (@{$spec->{groups}}) { 52 $gidsUsed{$g->{gid}} = 1 if defined $g->{gid}; 53 } ··· 55 foreach my $u (@{$spec->{users}}) { 56 $uidsUsed{$u->{uid}} = 1 if defined $u->{uid}; 57 } 58 59 # Read the current /etc/group. 60 sub parseGroup { ··· 114 } 115 } 116 } else { 117 - $g->{gid} = allocGid if !defined $g->{gid}; 118 $g->{password} = "x"; 119 } 120 121 $g->{members} = join ",", sort(keys(%members)); 122 $groupsOut{$name} = $g; 123 } 124 125 # Update the persistent list of declarative groups. 126 - write_file($declGroupsFile, { binmode => ':utf8' }, join(" ", sort(keys %groupsOut))); 127 128 # Merge in the existing /etc/group. 129 foreach my $name (keys %groupsCur) { ··· 140 # Rewrite /etc/group. FIXME: acquire lock. 141 my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" } 142 (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); 143 - write_file("/etc/group.tmp", { binmode => ':utf8' }, @lines); 144 - rename("/etc/group.tmp", "/etc/group") or die; 145 system("nscd --invalidate group"); 146 147 # Generate a new /etc/passwd containing the declared users. ··· 167 $u->{uid} = $existing->{uid}; 168 } 169 } else { 170 - $u->{uid} = allocUid($u->{isSystemUser}) if !defined $u->{uid}; 171 172 if (defined $u->{initialPassword}) { 173 $u->{hashedPassword} = hashPassword($u->{initialPassword}); ··· 195 196 $u->{fakePassword} = $existing->{fakePassword} // "x"; 197 $usersOut{$name} = $u; 198 } 199 200 # Update the persistent list of declarative users. 201 - write_file($declUsersFile, { binmode => ':utf8' }, join(" ", sort(keys %usersOut))); 202 203 # Merge in the existing /etc/passwd. 204 foreach my $name (keys %usersCur) { ··· 214 # Rewrite /etc/passwd. FIXME: acquire lock. 215 @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" } 216 (sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); 217 - write_file("/etc/passwd.tmp", { binmode => ':utf8' }, @lines); 218 - rename("/etc/passwd.tmp", "/etc/passwd") or die; 219 system("nscd --invalidate passwd"); 220 221 ··· 242 push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n"; 243 } 244 245 - write_file("/etc/shadow.tmp", { binmode => ':utf8', perms => 0600 }, @shadowNew); 246 - rename("/etc/shadow.tmp", "/etc/shadow") or die;
··· 6 make_path("/var/lib/nixos", { mode => 0755 }); 7 8 9 + # Keep track of deleted uids and gids. 10 + my $uidMapFile = "/var/lib/nixos/uid-map"; 11 + my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {}; 12 + 13 + my $gidMapFile = "/var/lib/nixos/gid-map"; 14 + my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {}; 15 + 16 + 17 + sub updateFile { 18 + my ($path, $contents, $perms) = @_; 19 + write_file("$path.tmp", { binmode => ':utf8', perms => $perms // 0644 }, $contents); 20 + rename("$path.tmp", $path) or die; 21 + } 22 + 23 + 24 sub hashPassword { 25 my ($password) = @_; 26 my $salt = ""; ··· 33 # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in 34 # /etc/login.defs. 35 sub allocId { 36 + my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_; 37 my $id = $up ? $idMin : $idMax; 38 while ($id >= $idMin && $id <= $idMax) { 39 + if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) { 40 $used->{$id} = 1; 41 return $id; 42 } ··· 46 die "$0: out of free UIDs or GIDs\n"; 47 } 48 49 + my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed); 50 51 sub allocGid { 52 + my ($name) = @_; 53 + my $prevGid = $gidMap->{$name}; 54 + if (defined $prevGid && !defined $gidsUsed{$prevGid}) { 55 + print STDERR "reviving group '$name' with GID $prevGid\n"; 56 + $gidsUsed{$prevGid} = 1; 57 + return $prevGid; 58 + } 59 + return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) }); 60 } 61 62 sub allocUid { 63 + my ($name, $isSystemUser) = @_; 64 my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1); 65 + my $prevUid = $uidMap->{$name}; 66 + if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) { 67 + print STDERR "reviving user '$name' with UID $prevUid\n"; 68 + $uidsUsed{$prevUid} = 1; 69 + return $prevUid; 70 + } 71 + return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) }); 72 } 73 74 75 # Read the declared users/groups. 76 my $spec = decode_json(read_file($ARGV[0])); 77 78 + # Don't allocate UIDs/GIDs that are manually assigned. 79 foreach my $g (@{$spec->{groups}}) { 80 $gidsUsed{$g->{gid}} = 1 if defined $g->{gid}; 81 } ··· 83 foreach my $u (@{$spec->{users}}) { 84 $uidsUsed{$u->{uid}} = 1 if defined $u->{uid}; 85 } 86 + 87 + # Likewise for previously used but deleted UIDs/GIDs. 88 + $uidsPrevUsed{$_} = 1 foreach values %{$uidMap}; 89 + $gidsPrevUsed{$_} = 1 foreach values %{$gidMap}; 90 + 91 92 # Read the current /etc/group. 93 sub parseGroup { ··· 147 } 148 } 149 } else { 150 + $g->{gid} = allocGid($name) if !defined $g->{gid}; 151 $g->{password} = "x"; 152 } 153 154 $g->{members} = join ",", sort(keys(%members)); 155 $groupsOut{$name} = $g; 156 + 157 + $gidMap->{$name} = $g->{gid}; 158 } 159 160 # Update the persistent list of declarative groups. 161 + updateFile($declGroupsFile, join(" ", sort(keys %groupsOut))); 162 163 # Merge in the existing /etc/group. 164 foreach my $name (keys %groupsCur) { ··· 175 # Rewrite /etc/group. FIXME: acquire lock. 176 my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" } 177 (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut)); 178 + updateFile($gidMapFile, encode_json($gidMap)); 179 + updateFile("/etc/group", \@lines); 180 system("nscd --invalidate group"); 181 182 # Generate a new /etc/passwd containing the declared users. ··· 202 $u->{uid} = $existing->{uid}; 203 } 204 } else { 205 + $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid}; 206 207 if (defined $u->{initialPassword}) { 208 $u->{hashedPassword} = hashPassword($u->{initialPassword}); ··· 230 231 $u->{fakePassword} = $existing->{fakePassword} // "x"; 232 $usersOut{$name} = $u; 233 + 234 + $uidMap->{$name} = $u->{uid}; 235 } 236 237 # Update the persistent list of declarative users. 238 + updateFile($declUsersFile, join(" ", sort(keys %usersOut))); 239 240 # Merge in the existing /etc/passwd. 241 foreach my $name (keys %usersCur) { ··· 251 # Rewrite /etc/passwd. FIXME: acquire lock. 252 @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" } 253 (sort { $a->{uid} <=> $b->{uid} } (values %usersOut)); 254 + updateFile($uidMapFile, encode_json($uidMap)); 255 + updateFile("/etc/passwd", \@lines); 256 system("nscd --invalidate passwd"); 257 258 ··· 279 push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n"; 280 } 281 282 + updateFile("/etc/shadow", \@shadowNew, 0600);