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

Code before:
			return
		}
		pidfdWaitid(pidfd)
	}()

	// Now we can write the uid/gid mappings
	uidMapPath := fmt.Sprintf("/proc/%d/uid_map", pid)
	uidMapContent := fmt.Sprintf("%d %d %d\n", uidMaps[0].ContainerID, uidMaps[0].HostID, uidMaps[0].Size)
	if err := os.WriteFile(uidMapPath, []byte(uidMapContent), 0644); err != nil {
		return fmt.Errorf("failed to write UID mapping: %w", err)
	}

	gidMapPath := fmt.Sprintf("/proc/%d/gid_map", pid)
	gidMapContent := fmt.Sprintf("%d %d %d\n", gidMaps[0].ContainerID, gidMaps[0].HostID, gidMaps[0].Size)
	if err := os.WriteFile(gidMapPath, []byte(gidMapContent), 0644); err != nil {
		return fmt.Errorf("failed to write GID mapping: %w", err)
	}

	if f != nil {
		if err := f(pid); err != nil {
			return err
		}
	}

	// Ensure the child process is still alive. If the err is ESRCH, we
	// should return error because the pid could be reused. It's safe to
	// return error and retry.
	if err := unix.PidfdSendSignal(pidfd, 0, nil, 0); err != nil {
		return fmt.Errorf("failed to ensure child process is alive: %w", err)
	}
	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])

Review comment (suggestion):
I think the following might be simpler:
* Here we start an anonymous function in a go-routine like this line is doing
* The function just locks the thread, calls `startProcessWithUsernsLocked()`
* If the result is an error, it just never unlocks the thread. If there is no error, it does the unlock
* Then, it sends the result on a channel
* The outer function is waiting on this channel, of course.

And then a new function `startProcessWithUsernsLocked()` just does the 3 steps (change the user, start the usrens, restore the user), sequentially, returns an error as soon as it happens, it has no defers, no undos, no anything.

I think that might be simpler to reason about.


Code after:
		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.
func startProcessWithUserNamespace(targetHostUID int, unshareFlags uintptr, uidMaps, gidMaps []syscall.SysProcIDMap) (int, int, error) {
	type result struct {
		pid   int
		pidfd int
		err   error
	}
	resultChan := make(chan result)

	go func() {
		runtime.LockOSThread()
		pid, pidfd, err := startProcessWithUsernsLocked(targetHostUID, unshareFlags, uidMaps, gidMaps)

		// If this errored out let the go runtime reap the thread by not unlocking
		if err == nil {
			runtime.UnlockOSThread()
		}
		resultChan <- result{pid, pidfd, err}
	}()

	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