summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorEelco Dolstra <edolstra@gmail.com>2017-03-29 18:10:20 +0200
committerEelco Dolstra <edolstra@gmail.com>2017-03-29 18:13:18 +0200
commita57bcd38b49bfe9d048b12de3c839bc72b298d2e (patch)
tree7df36e66f39ad218ca765e74155c89d512449206 /nixos
parentffd29517dd05e92dbc8a71d3dde835862b48f121 (diff)
downloadnixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar.gz
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar.bz2
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar.lz
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar.xz
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.tar.zst
nixlib-a57bcd38b49bfe9d048b12de3c839bc72b298d2e.zip
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.
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/config/update-users-groups.pl70
1 files changed, 53 insertions, 17 deletions
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 4ca8a83554ad..ef5e6346f02e 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -6,6 +6,21 @@ use JSON;
 make_path("/var/lib/nixos", { mode => 0755 });
 
 
+# Keep track of deleted uids and gids.
+my $uidMapFile = "/var/lib/nixos/uid-map";
+my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
+
+my $gidMapFile = "/var/lib/nixos/gid-map";
+my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
+
+
+sub updateFile {
+    my ($path, $contents, $perms) = @_;
+    write_file("$path.tmp", { binmode => ':utf8', perms => $perms // 0644 }, $contents);
+    rename("$path.tmp", $path) or die;
+}
+
+
 sub hashPassword {
     my ($password) = @_;
     my $salt = "";
@@ -18,10 +33,10 @@ sub hashPassword {
 # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in
 # /etc/login.defs.
 sub allocId {
-    my ($used, $idMin, $idMax, $up, $getid) = @_;
+    my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_;
     my $id = $up ? $idMin : $idMax;
     while ($id >= $idMin && $id <= $idMax) {
-        if (!$used->{$id} && !defined &$getid($id)) {
+        if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) {
             $used->{$id} = 1;
             return $id;
         }
@@ -31,23 +46,36 @@ sub allocId {
     die "$0: out of free UIDs or GIDs\n";
 }
 
-my (%gidsUsed, %uidsUsed);
+my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed);
 
 sub allocGid {
-    return allocId(\%gidsUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) });
+    my ($name) = @_;
+    my $prevGid = $gidMap->{$name};
+    if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
+        print STDERR "reviving group '$name' with GID $prevGid\n";
+        $gidsUsed{$prevGid} = 1;
+        return $prevGid;
+    }
+    return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 499, 0, sub { my ($gid) = @_; getgrgid($gid) });
 }
 
 sub allocUid {
-    my ($isSystemUser) = @_;
+    my ($name, $isSystemUser) = @_;
     my ($min, $max, $up) = $isSystemUser ? (400, 499, 0) : (1000, 29999, 1);
-    return allocId(\%uidsUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
+    my $prevUid = $uidMap->{$name};
+    if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
+        print STDERR "reviving user '$name' with UID $prevUid\n";
+        $uidsUsed{$prevUid} = 1;
+        return $prevUid;
+    }
+    return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
 }
 
 
 # Read the declared users/groups.
 my $spec = decode_json(read_file($ARGV[0]));
 
-# Don't allocate UIDs/GIDs that are already in use.
+# Don't allocate UIDs/GIDs that are manually assigned.
 foreach my $g (@{$spec->{groups}}) {
     $gidsUsed{$g->{gid}} = 1 if defined $g->{gid};
 }
@@ -56,6 +84,11 @@ foreach my $u (@{$spec->{users}}) {
     $uidsUsed{$u->{uid}} = 1 if defined $u->{uid};
 }
 
+# Likewise for previously used but deleted UIDs/GIDs.
+$uidsPrevUsed{$_} = 1 foreach values %{$uidMap};
+$gidsPrevUsed{$_} = 1 foreach values %{$gidMap};
+
+
 # Read the current /etc/group.
 sub parseGroup {
     chomp;
@@ -114,16 +147,18 @@ foreach my $g (@{$spec->{groups}}) {
             }
         }
     } else {
-        $g->{gid} = allocGid if !defined $g->{gid};
+        $g->{gid} = allocGid($name) if !defined $g->{gid};
         $g->{password} = "x";
     }
 
     $g->{members} = join ",", sort(keys(%members));
     $groupsOut{$name} = $g;
+
+    $gidMap->{$name} = $g->{gid};
 }
 
 # Update the persistent list of declarative groups.
-write_file($declGroupsFile, { binmode => ':utf8' }, join(" ", sort(keys %groupsOut)));
+updateFile($declGroupsFile, join(" ", sort(keys %groupsOut)));
 
 # Merge in the existing /etc/group.
 foreach my $name (keys %groupsCur) {
@@ -140,8 +175,8 @@ foreach my $name (keys %groupsCur) {
 # Rewrite /etc/group. FIXME: acquire lock.
 my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" }
     (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
-write_file("/etc/group.tmp", { binmode => ':utf8' }, @lines);
-rename("/etc/group.tmp", "/etc/group") or die;
+updateFile($gidMapFile, encode_json($gidMap));
+updateFile("/etc/group", \@lines);
 system("nscd --invalidate group");
 
 # Generate a new /etc/passwd containing the declared users.
@@ -167,7 +202,7 @@ foreach my $u (@{$spec->{users}}) {
             $u->{uid} = $existing->{uid};
         }
     } else {
-        $u->{uid} = allocUid($u->{isSystemUser}) if !defined $u->{uid};
+        $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid};
 
         if (defined $u->{initialPassword}) {
             $u->{hashedPassword} = hashPassword($u->{initialPassword});
@@ -195,10 +230,12 @@ foreach my $u (@{$spec->{users}}) {
 
     $u->{fakePassword} = $existing->{fakePassword} // "x";
     $usersOut{$name} = $u;
+
+    $uidMap->{$name} = $u->{uid};
 }
 
 # Update the persistent list of declarative users.
-write_file($declUsersFile, { binmode => ':utf8' }, join(" ", sort(keys %usersOut)));
+updateFile($declUsersFile, join(" ", sort(keys %usersOut)));
 
 # Merge in the existing /etc/passwd.
 foreach my $name (keys %usersCur) {
@@ -214,8 +251,8 @@ foreach my $name (keys %usersCur) {
 # Rewrite /etc/passwd. FIXME: acquire lock.
 @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" }
     (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
-write_file("/etc/passwd.tmp", { binmode => ':utf8' }, @lines);
-rename("/etc/passwd.tmp", "/etc/passwd") or die;
+updateFile($uidMapFile, encode_json($uidMap));
+updateFile("/etc/passwd", \@lines);
 system("nscd --invalidate passwd");
 
 
@@ -242,5 +279,4 @@ foreach my $u (values %usersOut) {
     push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
 }
 
-write_file("/etc/shadow.tmp", { binmode => ':utf8', perms => 0600 }, @shadowNew);
-rename("/etc/shadow.tmp", "/etc/shadow") or die;
+updateFile("/etc/shadow", \@shadowNew, 0600);