From 1447e5590670ebb273af9d4011246595b5c1cf16 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 29 Mar 2017 18:10:20 +0200 Subject: 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. (cherry picked from commit a57bcd38b49bfe9d048b12de3c839bc72b298d2e) --- nixos/modules/config/update-users-groups.pl | 70 ++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) (limited to 'nixos') diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl index 4ca8a83554add..ef5e6346f02e7 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); -- cgit 1.4.1