Subject: bug#77862: guix-daemon run as non-root sets up /etc/group incorrectly in build container

From: Ludovic Courtès <[email-masked]>
Date: Mon, 02 Jun 2025 23:06:08 +0200

Hey Reepca,

Reepca Russelstein <[email-masked]> writes:

> Ludovic Courtès <[email-masked]> writes:

[...]

>> The attached patch tries to do that, by calling out to ‘newuidmap’, and
>> under the assumption that /etc/subgid allows mapping the ‘kvm’ group.
>>
>> It does the job (a build process can chown to ‘kvm’), but I couldn’t get
>> the GID mapping preserved across the ‘unshare’ call (the call that is
>> made to “lock” mounts), hence the “#if 0” there.
>>
>> The problem is that when we call ‘unshare’, the ‘newgidmap’ setuid
>> binary is not longer accessible because we’re already in a chroot, so it
>> seems that we cannot preserve the GID map.
>
> ... and even if we had the setuid binary accessible (for example via a
> saved file descriptor that could be used with execveat, or a
> bind-mount), it wouldn't be of any use at this point because (man
> user_namespaces):
>
> "if either the user or the group ID of the file has no mapping inside
> the namespace, the set-user-ID (set-group- ID) bit is silently ignored:
> the new program is executed, but the process's effective user (group) ID
> is left unchanged."
>
> Naturally, uid 0 isn't going to be mapped!  In fact, more generally,
> newuidmap and newgidmap can't ever be used from within an uninitialized
> user namespace, since by definition uid 0 isn't yet mapped in it.
>
> So it falls to the parent process to do the initialization - that is, it
> now has to do the initialization twice.  Of course, it's going to need
> some way of knowing when the second user namespace has been created, and
> the child is going to need some way of knowing when it's been
> initialized, so we'll need to either use two pipes or switch to using a
> socketpair.

That makes sense, thanks for explaining!

However, I think part of the rules still don’t fit in my head.  Here’s
what I have now (parent is 6929):

--8<---------------cut here---------------start------------->8---
6929  openat(AT_FDCWD, "/proc/6938/uid_map", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 17
6929  write(17, "30001 1000 1", 12)     = 12
6929  close(17)                         = 0
6929  openat(AT_FDCWD, "/proc/6938/setgroups", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 17
6929  write(17, "deny", 4)              = 4
6929  close(17)                         = 0
6929  openat(AT_FDCWD, "/proc/6938/gid_map", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 17
6929  write(17, "30000 998 1", 11)      = 11
6929  close(17)                         = 0
[…]
6938  unshare(CLONE_NEWNS|CLONE_NEWUSER) = 0
6938  write(20, "reinit\n", 7)          = 7
6929  <... read resumed>"reinit\n", 20) = 7
6938  read(17,  <unfinished ...>
6929  write(4, "gmlo\0\0\0\0+\0\0\0\0\0\0\0|   reinitializing UID/GID mapping of 6938\n\0\0\0\0\0", 64) = 64
6929  getgid()                          = 998
6929  getuid()                          = 1000
6929  openat(AT_FDCWD, "/proc/6938/uid_map", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 17
6929  write(17, "30001 1000 1", 12)     = -1 EPERM (Operation not permitted)
--8<---------------cut here---------------end--------------->8---

Why oh why would we get EPERM the second time?

These restrictions appear to be respected:

   •  The data written to uid_map (gid_map) must consist of a sin‐
      gle  line  that maps the writing process's effective user ID
      (group ID) in the parent user namespace to a user ID  (group
      ID) in the user namespace.

   •  The  writing process must have the same effective user ID as
      the process that created the user namespace.

Or is the user namespace of the parent (6929) not the same as “the
parent user namespace”?

Thanks for your patience.  :-)

Ludo’.

PS: The more I use it, the less I can stand this user namespace soup
    presented as an “API”.