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

Code before:
// SupportsUnprivilegedUsernsCreation returns true if creating user namespaces
// as an unprivileged user is supported
func SupportsUnprivilegedUsernsCreation() bool {
	unprivilegedUsernsSupportedOnce.Do(func() {
		if err := checkUnprivilegedUsernsCreation(); err != nil {
			unprivilegedUsernsSupported = false
			return
		}
		unprivilegedUsernsSupported = true
	})
	return unprivilegedUsernsSupported
}

// checkUnprivilegedUsernsCreation tests if we can create a user namespace
// as an unprivileged user. This can fail on systems that deny unprivileged
// user namespaces through various means like AppArmor
func checkUnprivilegedUsernsCreation() error {
	// Assume nobody user is unprivileged
	nobodyUID := 65534
	var pidfd int

	// CLONE_NEWIPC is a random namespace to unshare to, we just need to verify
	// that we can indeed make a user namespace, then unshare into another namespace
	// as the target user without hitting any sort of apparmor restrictions
	_, pidfd, err := startProcessWithUserNamespace(nobodyUID, syscall.CLONE_NEWIPC)
	if err != nil {
		return fmt.Errorf("user namespace creation as unprivileged user failed: %w", err)
	}

	unix.PidfdSendSignal(pidfd, unix.SIGKILL, nil, 0)
	pidfdWaitid(pidfd)
	unix.Close(pidfd)

	return nil
}

// UnshareAfterEnterUserns allows to disassociate parts of its execution context
// within a user namespace.
func UnshareAfterEnterUserns(uidMap, gidMap string, unshareFlags uintptr, f func(pid int) error) (retErr error) {
	if unshareFlags&syscall.CLONE_NEWUSER == syscall.CLONE_NEWUSER {
		return fmt.Errorf("unshare flags should not include user namespace")
	}

	if !SupportsPidFD() {
		return fmt.Errorf("kernel doesn't support pidfd")
	}

	uidMaps, err := parseIDMapping(uidMap)
	if err != nil {
		return err
	}

Review comment (bug):
@rata -- the last CI run (where there was no `syscall.CLONE_NEWIPC`) failed on ubuntu 24.04. The dmesg output was truncated so I think I missed the apparmor message unfortunately, but I think this is going to be necessary.

Code after:

	if !SupportsPidFD() {
		return fmt.Errorf("kernel doesn't support pidfd")
	}

	uidMaps, err := parseIDMapping(uidMap)
	if err != nil {
		return err
	}

	gidMaps, err := parseIDMapping(gidMap)
	if err != nil {
		return err
	}

	targetUID := uidMaps[0].HostID
	pid, pidfd, err := startProcessWithUserNamespace(targetUID, unshareFlags, uidMaps, gidMaps)
	if err != nil {
		return err
	}

	defer unix.Close(pidfd)
	defer func() {
		derr := unix.PidfdSendSignal(pidfd, unix.SIGKILL, nil, 0)
		if derr != nil {
			if !errors.Is(derr, unix.ESRCH) {
				retErr = derr
			}
			return
		}
		pidfdWaitid(pidfd)
	}()

	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, ":")