Repository: containerd/containerd
File: pkg/sys/unshare_linux.go (Go)

Code before:
	return nil
}

// TODO: Support multiple mappings in future
func parseIDMapping(mapping string) ([]syscall.SysProcIDMap, error) {
	parts := strings.Split(mapping, ":")
	if len(parts) != 3 {
		return nil, fmt.Errorf("user namespace mappings require the format `container-id:host-id:size`")
	}

	cID, err := strconv.Atoi(parts[0])
	if err != nil {
		return nil, fmt.Errorf("invalid container id for user namespace remapping, %w", err)
	}

	hID, err := strconv.Atoi(parts[1])
	if err != nil {
		return nil, fmt.Errorf("invalid host id for user namespace remapping, %w", err)
	}

	size, err := strconv.Atoi(parts[2])
	if err != nil {
		return nil, fmt.Errorf("invalid size for user namespace remapping, %w", err)
	}

	if cID < 0 || hID < 0 || size < 0 {
		return nil, fmt.Errorf("invalid mapping %s, all IDs and size must be positive integers", mapping)
	}

	return []syscall.SysProcIDMap{
		{
			ContainerID: cID,
			HostID:      hID,
			Size:        size,
		},
	}, nil
}

// startProcessWithUserNamespace starts a ptraced dummy process to create
// a user namespace. It runs a goroutine on a single thread as targetHostUID
// when creating the process and user namespace, ensuring that the kernel
// attributes user limits to targetHostUID and not containerd's user. On success
// it returns the pid, pidfd and no error. It is expected that the caller sets
// up the uid_map/gid_map for the pid (this cannot be done as part of os.StartProcess()
// since targetHostUID doesn't have CAP_SETUID)
func startProcessWithUserNamespace(targetHostUID int, unshareFlags uintptr) (int, int, error) {
	type result struct {
		pid   int
		pidfd int
		err   error
	}

Review comment (bug):
Based on doc https://www.man7.org/linux/man-pages/man7/capabilities.7.html#:~:text=Effect%20of%20user,ambient%20capability%0A%20%20%20%20%20%20%20%20%20%20sets., after setresuid, thread's capability will be cleared.

> If one or more of the real, effective, or saved set user IDs
          was previously 0, and as a result of the UID changes all of
          these IDs have a nonzero value, then all capabilities are
          cleared from the permitted, effective, and ambient capability
          sets.

Based on my local test, this change doesn't work. If I understand it correctly, we need to restore capability after retsetuid. For example,


```go
@@ -216,16 +216,28 @@ func startProcessWithUserNamespace(targetHostUID int, unshareFlags uintptr) (int
 func startProcessWithUsernsLocked(targetHostUID int, unshareFlags uintptr) (int, int, error) {
        // -1 means no change in setresuid()
        originalEUID := -1
+       var originalCaps capSnapshot

        if targetHostUID != -1 {
                // We're changing users, so we need to figure out what user to return to
                originalEUID = os.Geteuid()
+               var err error
+               originalCaps, err = getCaps()
+               if err != nil {
+                       return -1, -1, fmt.Errorf("failed to read capabilities: %w", err)
+               }
        }

        if _, _, errno := syscall.RawSyscall(unix.SYS_SETRESUID, ^uintptr(0), uintptr(targetHostUID), ^uintptr(0)); errno != 0 {
                return -1, -1, fmt.Errorf("failed to set effective UID: %w", errno)
        }

+       if targetHostUID != -1 {
+               if err := setCaps(originalCaps); err != nil {
+                       return -1, -1, fmt.Errorf("failed to restore capabilities: %w", err)
+               }
+       }
+
        var pidfd int
```

Code after:
	}()

	res := <-resultChan
	return res.pid, res.pidfd, res.err
}

// startProcessWithUsernsLocked expects the os thread to be locked already. It does
//  1. setresuid() to the targetHostUID user to attribute further user namespace creations to it
//  2. sets the thread's effective capabilities back to the original user's to allow writing
//     to /proc/uid_map as well as to create the user namespace as a "privileged" process in
//     some distro's eyes,
//  3. spawns a new ptraced process in a new user namespace
//  4. unshares into unshareFlags within that user namespace
//
// returns the pid, pidfd, and error information of that process. On error the process is already
// killed, and the thread is *not* setresuid()'ed back to the original user. In this case the
// thread should be killed off by the caller to avoid using a thread in a bad state
func startProcessWithUsernsLocked(targetHostUID int, unshareFlags uintptr, uidMaps, gidMaps []syscall.SysProcIDMap) (int, int, error) {
	originalEUID := os.Geteuid()

	originalCaps, err := getCurrentCaps()
	if err != nil {
		return -1, -1, fmt.Errorf("failed to read current capabilities: %w", err)
	}

	if _, _, errno := syscall.RawSyscall(unix.SYS_SETRESUID, ^uintptr(0), uintptr(targetHostUID), ^uintptr(0)); errno != 0 {
		return -1, -1, fmt.Errorf("failed to set effective UID: %w", errno)
	}

	err = setCurrentCaps(originalCaps)
	if err != nil {
		return -1, -1, fmt.Errorf("failed to restore capabilities: %w", err)
	}

	var pidfd int
	proc, err := os.StartProcess("/proc/self/exe", []string{"UnshareAfterEnterUserns"}, &os.ProcAttr{
		Sys: &syscall.SysProcAttr{
			// clone new user namespace first and then unshare
			Cloneflags:                 unix.CLONE_NEWUSER,
			Unshareflags:               unshareFlags,
			UidMappings:                uidMaps,
			GidMappings:                gidMaps,
			GidMappingsEnableSetgroups: true,
			// NOTE: It's reexec but it's not heavy because subprocess
			// be in PTRACE_TRACEME mode before performing execve.
			Ptrace:    true,
			Pdeathsig: syscall.SIGKILL,
			PidFD:     &pidfd,
		},
	})
	if err != nil {